"""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 | None = None generation_job_id: str | None = None title: str | None = None 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 StoryAudioStatusResponse(StoryStatusMixin): """Audio cache status for one story.""" story_id: int audio_ready: bool cache_exists: bool cache_size_bytes: int | None = None cache_updated_at: datetime | None = 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 can_cancel: bool = False can_retry: bool = False 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 GenerationProviderFailureReasonResponse(BaseModel): """Aggregated failed provider call reason.""" reason: str count: int class GenerationProviderStatsResponse(BaseModel): """Provider call stats aggregated from generation job events.""" story_id: int window_days: int | None = None capability: str | None = None 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) failure_reasons: list[GenerationProviderFailureReasonResponse] = Field(default_factory=list) class GenerationProviderAnalyticsResponse(BaseModel): """Provider call stats aggregated across one user's generation history.""" window_days: int | None = None capability: str | None = None total_calls: int successful_calls: int failed_calls: int avg_latency_ms: float | None = None estimated_cost_usd: float = 0.0 job_count: int story_count: int by_provider: list[GenerationProviderStatResponse] = Field(default_factory=list) failure_reasons: list[GenerationProviderFailureReasonResponse] = Field(default_factory=list) class GenerationRecentFailureResponse(BaseModel): """One recent failed generation task for operations summary.""" job_id: str story_id: int | None = None story_title: str | None = None output_mode: str current_step: str error_message: str | None = None failure_label: str updated_at: datetime class GenerationOpsSummaryResponse(BaseModel): """Recent generation health summary for one user.""" window_hours: int stale_threshold_minutes: int active_jobs: int stale_running_jobs: int failed_jobs: int degraded_jobs: int asset_retry_jobs: int recent_failures: list[GenerationRecentFailureResponse] = Field(default_factory=list) class AchievementItem(BaseModel): """Achievement item returned for a story.""" type: str description: str obtained_at: str | None = None