from __future__ import annotations from datetime import datetime, timezone from typing import Any, Literal from uuid import uuid4 from pydantic import BaseModel, ConfigDict, Field, computed_field def utc_now() -> datetime: return datetime.now(timezone.utc) class StrictModel(BaseModel): model_config = ConfigDict(extra="forbid", validate_assignment=True) class VisualMotif(StrictModel): key: str label: str palette: list[str] = Field(default_factory=list) motion: str | None = None class Destination(StrictModel): year: str place: str mode: Literal["past", "future", "strange"] atmosphere: str visual_preset_key: str visual_motifs: list[VisualMotif] = Field(default_factory=list) temporal_oddity: str | None = None class Persona(StrictModel): name: str local_identifier: str | None = None role: str immediate_situation: str daily_concern: str secret_or_desire: str worldview_constraints: list[str] = Field(default_factory=list) voice_theory: str speaking_style: str safety_notes: list[str] = Field(default_factory=list) class VoiceProfile(StrictModel): voice_id: str description: str pace: Literal["slow", "medium", "fast"] = "medium" emotion: str = "curious" accent_hint: str | None = None class ConversationTurn(StrictModel): role: Literal["user", "character", "machine"] text: str audio_path: str | None = None metadata: dict[str, Any] = Field(default_factory=dict) created_at: datetime = Field(default_factory=utc_now) class ConversationHistory(StrictModel): turns: list[ConversationTurn] = Field(default_factory=list) def append(self, turn: ConversationTurn) -> None: self.turns.append(turn) def character_turns(self) -> list[ConversationTurn]: return [turn for turn in self.turns if turn.role == "character"] class ConversationResponse(StrictModel): text: str prosody_hint: str | None = None ui_signal_event: str | None = None class Transcript(StrictModel): text: str confidence: float | None = Field(default=None, ge=0, le=1) language: str | None = None is_final: bool = True class AudioResult(StrictModel): path: str | None = None mime_type: str = "audio/wav" duration_seconds: float | None = Field(default=None, ge=0) description: str | None = None class ImmersiveScene(StrictModel): prompt: str image_b64: str | None = None mime_type: str = "image/png" ambient_key: str = "temporal_storm" visual_style: str = "surreal cinematic time portal" description: str | None = None class CharacterPortrait(StrictModel): prompt: str image_b64: str | None = None mime_type: str = "image/png" description: str | None = None class Narration(StrictModel): text: str audio: AudioResult | None = None voice_profile: VoiceProfile | None = None class VisualArtifact(StrictModel): title: str kind: str description: str prompt: str image_b64: str | None = None mime_type: str = "image/png" class Souvenir(StrictModel): destination: str contact: str quote: str artifact: str stamp_name: str summary: str class EncounterSession(StrictModel): session_id: str encounter_id: str = Field(default_factory=lambda: str(uuid4())) destination: Destination | None = None persona: Persona | None = None voice_profile: VoiceProfile | None = None scene: ImmersiveScene | None = None portrait: CharacterPortrait | None = None narration: Narration | None = None artifact: VisualArtifact | None = None history: ConversationHistory = Field(default_factory=ConversationHistory) souvenir: Souvenir | None = None created_at: datetime = Field(default_factory=utc_now) updated_at: datetime = Field(default_factory=utc_now) def touch(self) -> None: self.updated_at = utc_now() class SavedEncounter(StrictModel): encounter_id: str session_id: str path: str saved_at: datetime = Field(default_factory=utc_now) class ModelSpec(StrictModel): role: str provider: str model_id: str parameters_billion: float = Field(ge=0) enabled: bool = True runtime: str license: str | None = None notes: str | None = None class ModelBudget(StrictModel): models: list[ModelSpec] = Field(default_factory=list) parameter_limit_billion: float = 32.0 @computed_field @property def enabled_parameters_billion(self) -> float: return round( sum(model.parameters_billion for model in self.models if model.enabled), 3, ) @computed_field @property def largest_enabled_parameters_billion(self) -> float: enabled = [model.parameters_billion for model in self.models if model.enabled] return round(max(enabled, default=0.0), 3) @computed_field @property def is_within_limit(self) -> bool: return all( model.parameters_billion <= self.parameter_limit_billion for model in self.models if model.enabled )