"""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 VoiceTurnConfirmRequest(BaseModel): """Resolve one pending confirmation before the story continues.""" action: Literal["accept", "retry_recording", "switch_to_text"] 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 understanding_summary: str | None = None requires_confirmation: bool = False confirmation_state: str = "not_needed" confirmation_reason: str | None = None confirmation_message: str | None = None safety_flags: list[str] = Field(default_factory=list) safety_blocked: bool = False safety_message: str | 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_understanding_summary: str | None = None latest_requires_confirmation: bool = False latest_confirmation_state: str | None = None latest_confirmation_message: str | None = None latest_safety_flags: list[str] = Field(default_factory=list) latest_safety_message: str | None = None latest_assistant_audio_ready: bool = False last_turn_status: str | None = None transcription_mode_hint: 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 VoiceSessionAnalyticsResponse(BaseModel): """Aggregated voice co-creation analytics for one user.""" window_days: int | None = None total_sessions: int = 0 active_sessions: int = 0 finalized_sessions: int = 0 abandoned_sessions: int = 0 total_turns: int = 0 successful_turns: int = 0 failed_turns: int = 0 asr_failures: int = 0 tts_failures: int = 0 low_confidence_turns: int = 0 safety_interventions: int = 0 turn_success_rate: float = 0.0 finalize_conversion_rate: float = 0.0 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