Files
dreamweaver/backend/app/schemas/story_schemas.py

231 lines
6.2 KiB
Python

"""Story-related Pydantic schemas."""
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, Field
MAX_DATA_LENGTH = 2000
MAX_EDU_THEME_LENGTH = 200
MAX_TTS_LENGTH = 4000
class StoryStatusMixin(BaseModel):
"""Shared generation status fields returned by story APIs."""
generation_status: str
text_status: str
image_status: str
audio_status: str
last_error: str | None = None
retryable_assets: list[Literal["image", "audio"]] = Field(default_factory=list)
class GenerateRequest(BaseModel):
"""Story generation request."""
type: Literal["keywords", "full_story"]
data: str = Field(..., min_length=1, max_length=MAX_DATA_LENGTH)
education_theme: str | None = Field(default=None, max_length=MAX_EDU_THEME_LENGTH)
child_profile_id: str | None = None
universe_id: str | None = None
class GenerationRequest(BaseModel):
"""Unified generation request for story and storybook outputs."""
output_mode: Literal["story", "storybook"] = Field(default="story")
type: Literal["keywords", "full_story"] = Field(default="keywords")
data: str = Field(..., min_length=1, max_length=MAX_DATA_LENGTH)
education_theme: str | None = Field(default=None, max_length=MAX_EDU_THEME_LENGTH)
generate_images: bool = Field(default=True)
page_count: int = Field(default=6, ge=4, le=12)
child_profile_id: str | None = None
universe_id: str | None = None
class StoryResponse(StoryStatusMixin):
"""Story generation response."""
id: int
title: str
story_text: str
cover_prompt: str | None
image_url: str | None
mode: str
child_profile_id: str | None = None
universe_id: str | None = None
class StoryListItem(StoryStatusMixin):
"""Story list item."""
id: int
title: str
image_url: str | None
created_at: datetime
mode: str
class FullStoryResponse(StoryStatusMixin):
"""Full story response with asset status."""
id: int
title: str
story_text: str
cover_prompt: str | None
image_url: str | None
audio_ready: bool
mode: str
errors: dict[str, str | None] = Field(default_factory=dict)
child_profile_id: str | None = None
universe_id: str | None = None
class StorybookRequest(BaseModel):
"""Storybook generation request."""
keywords: str = Field(..., min_length=1, max_length=200)
page_count: int = Field(default=6, ge=4, le=12)
education_theme: str | None = Field(default=None, max_length=MAX_EDU_THEME_LENGTH)
generate_images: bool = Field(default=False, description="Whether to generate images too.")
child_profile_id: str | None = None
universe_id: str | None = None
class StorybookPageResponse(BaseModel):
"""One storybook page."""
page_number: int
text: str
image_prompt: str
image_url: str | None = None
class StorybookResponse(StoryStatusMixin):
"""Storybook generation response."""
id: int | None = None
title: str
main_character: str
art_style: str
pages: list[StorybookPageResponse]
cover_prompt: str
cover_url: str | None = None
class GenerationResponse(StoryStatusMixin):
"""Unified generation response for the target workflow API."""
id: int
generation_job_id: str | None = None
title: str
mode: str
story_text: str | None = None
pages: list[StorybookPageResponse] | None = None
cover_prompt: str | None = None
image_url: str | None = None
cover_url: str | None = None
audio_ready: bool = False
errors: dict[str, str | None] = Field(default_factory=dict)
main_character: str | None = None
art_style: str | None = None
child_profile_id: str | None = None
universe_id: str | None = None
class StoryDetailResponse(StoryStatusMixin):
"""Story detail response for both stories and storybooks."""
id: int
title: str
story_text: str | None = None
pages: list[StorybookPageResponse] | None = None
cover_prompt: str | None
image_url: str | None
mode: str
child_profile_id: str | None = None
universe_id: str | None = None
class StoryImageResponse(StoryStatusMixin):
"""Cover image generation response."""
image_url: str | None
class StoryAssetRetryRequest(BaseModel):
"""Retry selected generated assets for a story."""
assets: list[Literal["image", "audio"]] = Field(..., min_length=1)
class GenerationJobEventResponse(BaseModel):
"""One persisted event emitted by a generation job."""
id: int
job_id: str
story_id: int | None = None
event_type: str
status: str
message: str | None = None
event_metadata: dict[str, Any] = Field(default_factory=dict)
created_at: datetime
class GenerationJobSummaryResponse(BaseModel):
"""Generation job summary for progress lists."""
id: str
story_id: int | None = None
output_mode: str
input_type: str
status: str
current_step: str
progress_percent: int
progress_label: str
is_terminal: bool
result_snapshot: dict[str, Any] = Field(default_factory=dict)
error_message: str | None = None
created_at: datetime
updated_at: datetime
class GenerationJobDetailResponse(GenerationJobSummaryResponse):
"""Generation job detail with append-only workflow events."""
request_payload: dict[str, Any] = Field(default_factory=dict)
events: list[GenerationJobEventResponse] = Field(default_factory=list)
class GenerationProviderStatResponse(BaseModel):
"""Aggregated provider call stats for one adapter/capability pair."""
capability: str
adapter: str
call_count: int
success_count: int
failure_count: int
avg_latency_ms: float | None = None
estimated_cost_usd: float = 0.0
class GenerationProviderStatsResponse(BaseModel):
"""Provider call stats aggregated from generation job events."""
story_id: int
total_calls: int
successful_calls: int
failed_calls: int
avg_latency_ms: float | None = None
estimated_cost_usd: float = 0.0
by_provider: list[GenerationProviderStatResponse] = Field(default_factory=list)
class AchievementItem(BaseModel):
"""Achievement item returned for a story."""
type: str
description: str
obtained_at: str | None = None