"""Pydantic schemas for voice co-creation sessions.""" from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field MAX_VOICE_TRANSCRIPT_LENGTH = 1000 MAX_VOICE_ABORT_REASON_LENGTH = 200 MAX_VOICE_TURN_DURATION_MS = 90_000 class VoiceSessionCreateRequest(BaseModel): """Create one draft voice co-creation session.""" child_profile_id: str | None = None universe_id: str | None = None target_mode: Literal["story"] = Field(default="story") class VoiceTurnCreateFallbackRequest(BaseModel): """Create one voice turn using text fallback instead of uploaded audio.""" transcript_text: str = Field(..., min_length=1, max_length=MAX_VOICE_TRANSCRIPT_LENGTH) duration_ms: int | None = Field(default=None, ge=1, le=MAX_VOICE_TURN_DURATION_MS) class VoiceTurnUploadAcceptedResponse(BaseModel): """Accepted response for one uploaded-audio voice turn.""" turn_id: str session_id: str status: str transcription_provider: str | None = None class VoiceSessionFinalizeRequest(BaseModel): """Finalize one voice session into a persisted story.""" save_story: bool = True generate_cover: bool = True generate_final_audio: bool = False class VoiceSessionAbandonRequest(BaseModel): """Explicitly abandon one in-progress session.""" reason: str | None = Field(default=None, max_length=MAX_VOICE_ABORT_REASON_LENGTH) class VoiceSessionEventResponse(BaseModel): """One persisted session event.""" id: int session_id: str turn_id: str | None = None event_type: str status: str message: str | None = None event_metadata: dict[str, Any] = Field(default_factory=dict) created_at: datetime class VoiceTurnSummaryResponse(BaseModel): """One summarized voice session turn.""" id: str session_id: str turn_index: int status: str user_transcript: str | None = None transcript_confidence: float | None = None transcription_provider: str | None = None detected_intent: str intent_confidence: float | None = None assistant_text: str | None = None assistant_audio_ready: bool = False assistant_audio_url: str | None = None user_audio_ready: bool = False user_audio_url: str | None = None error_message: str | None = None created_at: datetime updated_at: datetime class VoiceSessionSummaryResponse(BaseModel): """One summarized voice co-creation session.""" id: str child_profile_id: str | None = None universe_id: str | None = None final_story_id: int | None = None target_mode: str status: str current_turn_index: int total_turns: int = 0 working_title: str | None = None story_state: dict[str, Any] = Field(default_factory=dict) latest_user_transcript: str | None = None latest_assistant_text: str | None = None latest_detected_intent: str | None = None latest_assistant_audio_ready: bool = False last_turn_status: str | None = None can_continue: bool = False can_finalize: bool = False last_error: str | None = None created_at: datetime updated_at: datetime class VoiceSessionDetailResponse(VoiceSessionSummaryResponse): """Detailed voice session payload with recent turns and events.""" recent_turns: list[VoiceTurnSummaryResponse] = Field(default_factory=list) events: list[VoiceSessionEventResponse] = Field(default_factory=list) class VoiceTurnAcceptedResponse(BaseModel): """Accepted response for one asynchronously processed turn.""" turn_id: str session_id: str status: str class VoiceSessionFinalizeResponse(BaseModel): """Finalize response after a session is converted into a story.""" session_id: str status: str story_id: int | None = None generation_job_id: str | None = None