feat: add generation trace and partial-ready workflow status

This commit is contained in:
2026-04-18 21:53:55 +08:00
parent 96dfc677e2
commit e99a7fbe14
36 changed files with 2597 additions and 144 deletions

View File

@@ -10,6 +10,7 @@ class StoryGenerationStatus(str, Enum):
"""Overall story generation lifecycle."""
NARRATIVE_READY = "narrative_ready"
PARTIAL_READY = "partial_ready"
ASSETS_GENERATING = "assets_generating"
COMPLETED = "completed"
DEGRADED_COMPLETED = "degraded_completed"
@@ -30,7 +31,10 @@ class StoryLike(Protocol):
story_text: str | None
pages: list[dict] | None
cover_prompt: str | None
image_url: str | None
generation_status: str
text_status: str
image_status: str
audio_status: str
last_error: str | None
@@ -55,6 +59,37 @@ def has_narrative_content(story: StoryLike) -> bool:
return bool(story.story_text) or bool(story.pages)
def _has_retryable_image(story: StoryLike, image_status: StoryAssetStatus) -> bool:
if image_status in {StoryAssetStatus.READY, StoryAssetStatus.GENERATING}:
return False
pages = story.pages or []
has_missing_page_image = any(
isinstance(page, dict)
and page.get("image_prompt")
and not page.get("image_url")
for page in pages
)
return bool(story.cover_prompt and not story.image_url) or has_missing_page_image
def _has_pending_assets(
story: StoryLike,
*,
image_status: StoryAssetStatus,
audio_status: StoryAssetStatus,
) -> bool:
"""Whether readable content still has optional assets to complete."""
if _has_retryable_image(story, image_status):
return True
return bool(story.story_text) and audio_status not in {
StoryAssetStatus.READY,
StoryAssetStatus.GENERATING,
}
def resolve_story_generation_status(story: StoryLike) -> StoryGenerationStatus:
"""Derive the overall status from narrative and asset states."""
@@ -70,6 +105,9 @@ def resolve_story_generation_status(story: StoryLike) -> StoryGenerationStatus:
if StoryAssetStatus.FAILED in (image_status, audio_status):
return StoryGenerationStatus.DEGRADED_COMPLETED
if _has_pending_assets(story, image_status=image_status, audio_status=audio_status):
return StoryGenerationStatus.PARTIAL_READY
if (
image_status == StoryAssetStatus.NOT_REQUESTED
and audio_status == StoryAssetStatus.NOT_REQUESTED
@@ -105,6 +143,12 @@ def sync_story_status(
if last_error is not _ERROR_UNSET:
story.last_error = last_error
story.text_status = (
StoryAssetStatus.READY.value
if has_narrative_content(story)
else StoryAssetStatus.FAILED.value
)
generation_status = resolve_story_generation_status(story)
story.generation_status = generation_status.value