"""Story generation status helpers.""" from __future__ import annotations from enum import Enum from typing import Protocol class StoryGenerationStatus(str, Enum): """Overall story generation lifecycle.""" NARRATIVE_READY = "narrative_ready" ASSETS_GENERATING = "assets_generating" COMPLETED = "completed" DEGRADED_COMPLETED = "degraded_completed" FAILED = "failed" class StoryAssetStatus(str, Enum): """Asset generation state for image and audio.""" NOT_REQUESTED = "not_requested" GENERATING = "generating" READY = "ready" FAILED = "failed" class StoryLike(Protocol): """Protocol for story-like objects used by status helpers.""" story_text: str | None pages: list[dict] | None generation_status: str image_status: str audio_status: str last_error: str | None _ERROR_UNSET = object() def _normalize_asset_status(value: str | None) -> StoryAssetStatus: if not value: return StoryAssetStatus.NOT_REQUESTED try: return StoryAssetStatus(value) except ValueError: return StoryAssetStatus.NOT_REQUESTED def has_narrative_content(story: StoryLike) -> bool: """Whether the story already has readable content.""" return bool(story.story_text) or bool(story.pages) def resolve_story_generation_status(story: StoryLike) -> StoryGenerationStatus: """Derive the overall status from narrative and asset states.""" if not has_narrative_content(story): return StoryGenerationStatus.FAILED image_status = _normalize_asset_status(story.image_status) audio_status = _normalize_asset_status(story.audio_status) if StoryAssetStatus.GENERATING in (image_status, audio_status): return StoryGenerationStatus.ASSETS_GENERATING if StoryAssetStatus.FAILED in (image_status, audio_status): return StoryGenerationStatus.DEGRADED_COMPLETED if ( image_status == StoryAssetStatus.NOT_REQUESTED and audio_status == StoryAssetStatus.NOT_REQUESTED ): return StoryGenerationStatus.NARRATIVE_READY return StoryGenerationStatus.COMPLETED def has_failed_assets(story: StoryLike) -> bool: """Whether any persisted asset is still in a failed state.""" image_status = _normalize_asset_status(story.image_status) audio_status = _normalize_asset_status(story.audio_status) return StoryAssetStatus.FAILED in (image_status, audio_status) def sync_story_status( story: StoryLike, *, image_status: StoryAssetStatus | None = None, audio_status: StoryAssetStatus | None = None, last_error: str | None | object = _ERROR_UNSET, ) -> None: """Update asset statuses and refresh overall generation status.""" if image_status is not None: story.image_status = image_status.value if audio_status is not None: story.audio_status = audio_status.value if last_error is not _ERROR_UNSET: story.last_error = last_error generation_status = resolve_story_generation_status(story) story.generation_status = generation_status.value if last_error is _ERROR_UNSET and not has_failed_assets(story): story.last_error = None