from datetime import date, datetime from typing import Literal from pydantic import BaseModel, ConfigDict, EmailStr, Field, model_validator class UserCreate(BaseModel): email: EmailStr password: str = Field(min_length=6) class UserLogin(BaseModel): email: str password: str class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int email: EmailStr is_admin: bool = False class Token(BaseModel): access_token: str refresh_token: str | None = None token_type: str = "bearer" expires_in: int | None = None user: UserOut | None = None class SetupStatus(BaseModel): setup_required: bool GoalMetric = Literal[ "wpm", "filler_rate", "overall_score", "practice_days_per_week", "session_duration_minutes", "eye_contact_score", "delivery_score", ] GoalComparison = Literal["gte", "lte", "between"] class UserGoalCreate(BaseModel): metric_name: GoalMetric comparison: GoalComparison target_value: float = Field(ge=0) target_upper: float | None = Field(default=None, ge=0) is_active: bool = True @model_validator(mode="after") def validate_range(self): if self.comparison == "between": if self.target_upper is None or self.target_upper <= self.target_value: raise ValueError("between goals require target_upper greater than target_value") elif self.target_upper is not None: raise ValueError("target_upper is only valid for between goals") return self class UserGoalUpdate(BaseModel): comparison: GoalComparison | None = None target_value: float | None = Field(default=None, ge=0) target_upper: float | None = Field(default=None, ge=0) is_active: bool | None = None class UserGoalOut(BaseModel): id: int metric_name: GoalMetric comparison: GoalComparison target_value: float target_upper: float | None is_active: bool current_value: float | None achieved: bool created_at: datetime updated_at: datetime class PresenterProfileUpdate(BaseModel): display_name: str | None = Field(default=None, max_length=120) primary_role: str | None = Field(default=None, max_length=120) focus_track: Literal[ "tech_storytelling", "keynote_clarity", "camera_confidence", "executive_pitch", "tech_presenter", "tech_news_creator", "engineering_leader", ] | None = None experience_level: Literal["beginner", "intermediate", "advanced"] | None = None target_wpm_min: int | None = Field(default=None, ge=80, le=200) target_wpm_max: int | None = Field(default=None, ge=90, le=220) max_filler_rate: float | None = Field(default=None, ge=0, le=20) min_eye_contact: float | None = Field(default=None, ge=0, le=1) daily_minutes_goal: int | None = Field(default=None, ge=5, le=120) weaknesses: list[str] | None = None speaking_goal_statement: str | None = Field(default=None, max_length=500) reset_start_date: bool = False class PresenterProfileOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int display_name: str primary_role: str focus_track: str experience_level: str target_wpm_min: int target_wpm_max: int max_filler_rate: float min_eye_contact: float daily_minutes_goal: int weaknesses: list[str] = Field(default_factory=list) speaking_goal_statement: str | None = None curriculum_start_date: date class CurriculumCompleteRequest(BaseModel): session_id: int | None = None score: float | None = None reflection_notes: str | None = Field(default=None, max_length=2000) class StoryboardRequest(BaseModel): headline: str = Field(min_length=3, max_length=300) summary: str = Field(default="", max_length=4000) source: str | None = Field(default=None, max_length=120) url: str | None = Field(default=None, max_length=1000) category: str | None = Field(default="AI & Systems", max_length=80) framework: Literal["briefing_60s", "explainer_2m", "contrarian_take", "war_story"] = "briefing_60s" personal_angle: str | None = Field(default=None, max_length=1000) class WeeklyNoteUpdate(BaseModel): week_start_date: date wins_note: str | None = Field(default=None, max_length=2000) focus_next_week: str | None = Field(default=None, max_length=2000) class TranscriptUpdateRequest(BaseModel): transcript_segments: list["TranscriptSegmentCreate"] = Field(default_factory=list) class TranscriptionRequest(BaseModel): backend: str | None = None model: str | None = None class TranscriptionModelDownloadRequest(BaseModel): backend: str model: str class InferenceEngineCheckRequest(BaseModel): engine_id: str base_url: str | None = None class InferenceEngineStatusOut(BaseModel): id: str label: str base_url: str live: bool detail: str available_models: list[str] = Field(default_factory=list) class TranscriptionModelOut(BaseModel): id: str name: str repo_id: str description: str languages: str available: bool = False local_path: str | None = None class TranscriptionBackendOut(BaseModel): id: str label: str supported: bool live: bool detail: str models: list[TranscriptionModelOut] = Field(default_factory=list) default_model: str | None = None class TranscriptionSettingsOut(BaseModel): default_backend: str backends: list[TranscriptionBackendOut] = Field(default_factory=list) text_inference_engines: list[InferenceEngineStatusOut] = Field(default_factory=list) class TranscriptSegmentCreate(BaseModel): start_ms: int end_ms: int text: str class TranscriptSegmentOut(TranscriptSegmentCreate): model_config = ConfigDict(from_attributes=True) id: int class DocumentBlockOut(BaseModel): index: int text: str class SessionDocumentOut(BaseModel): name: str mime_type: str blocks: list[DocumentBlockOut] = Field(default_factory=list) class MetricsPointCreate(BaseModel): t: int wpm: float = 0 rms: float = 0 eye_contact: float = 0 smile: float = 0 yaw: float = 0 pitch: float = 0 roll: float = 0 filler_count: int = 0 pause_ms: int = 0 class ScoreCreate(BaseModel): data: dict class ScoreOut(ScoreCreate): model_config = ConfigDict(from_attributes=True) id: int class SessionCreate(BaseModel): title: str | None = None mode: str | None = None prompt: str | None = None goal: str | None = None curriculum_day: int | None = Field(default=None, ge=1, le=30) exercise_id: str | None = None story_framework: str | None = None target_keywords: list[str] = Field(default_factory=list) teleprompter_script: str | None = None started_at: datetime | None = None ended_at: datetime | None = None duration_ms: int = 0 wpm_avg: float = 0 filler_count: int = 0 eye_contact_pct: float = 0 tags: list[str] = Field(default_factory=list) transcript_segments: list[TranscriptSegmentCreate] = Field(default_factory=list) metrics: list[MetricsPointCreate] = Field(default_factory=list) score: dict | None = None class SessionOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int title: str | None mode: str | None prompt: str | None goal: str | None curriculum_day: int | None = None exercise_id: str | None = None story_framework: str | None = None target_keywords: list[str] = Field(default_factory=list) teleprompter_script: str | None = None started_at: datetime | None ended_at: datetime | None duration_ms: int wpm_avg: float filler_count: int eye_contact_pct: float video_path: str | None transcription_status: str created_at: datetime tags: list[str] = Field(default_factory=list) transcript_segments: list[TranscriptSegmentOut] = Field(default_factory=list) metrics: list[MetricsPointCreate] = Field(default_factory=list) score: dict | None = None document: SessionDocumentOut | None = None class SessionListItem(BaseModel): model_config = ConfigDict(from_attributes=True) id: int title: str | None mode: str | None curriculum_day: int | None = None duration_ms: int wpm_avg: float filler_count: int eye_contact_pct: float created_at: datetime transcription_status: str video_available: bool = False score: dict | None = None class UploadResponse(BaseModel): session_id: int status: str video_path: str | None = None class ExportResponse(BaseModel): zip_path: str