| from __future__ import annotations
|
|
|
| import base64
|
| from dataclasses import dataclass, field
|
| from datetime import datetime
|
| import json
|
| from pathlib import Path
|
| import re
|
| from typing import Any
|
| from uuid import uuid4
|
|
|
| from time_machine.domain.events import (
|
| AudioEvent,
|
| ConversationTextEvent,
|
| DestinationEvent,
|
| ErrorEvent,
|
| MachineStateEvent,
|
| NarrationEvent,
|
| PersonaEvent,
|
| PortraitEvent,
|
| SceneEvent,
|
| SouvenirEvent,
|
| TranscriptEvent,
|
| TemporalEvent,
|
| VisualArtifactEvent,
|
| )
|
| from time_machine.domain.models import Destination, Persona, Souvenir
|
|
|
|
|
| def new_session_state() -> dict[str, Any]:
|
| return {
|
| "session_id": str(uuid4()),
|
| "encounter_id": None,
|
| "persona_name": "Contact",
|
| "machine_state": "dormant",
|
| "target_year": None,
|
| "destination_place": "",
|
| "destination_mode": "",
|
| "destination_atmosphere": "",
|
| "destination_visual_preset": "",
|
| "destination_motifs": [],
|
| "persona_role": "",
|
| "persona_situation": "",
|
| "scene": None,
|
| "portrait": None,
|
| "narration": None,
|
| "artifact": None,
|
| "image_generation_ready": False,
|
| "image_generation_warning": None,
|
| "ambient_key": "temporal_storm",
|
| "transcript_lines": [],
|
| "status": "Machine dormant.",
|
| "destination_md": "",
|
| "persona_md": "",
|
| "souvenir_md": "",
|
| "audio_path": None,
|
| "_partial_character_active": False,
|
| }
|
|
|
|
|
| @dataclass
|
| class UiRenderState:
|
| session_state: dict[str, Any]
|
| status: str = "Machine dormant."
|
| destination_md: str = ""
|
| persona_md: str = ""
|
| transcript_lines: list[str] = field(default_factory=list)
|
| souvenir_md: str = ""
|
| audio_path: str | None = None
|
|
|
| @classmethod
|
| def from_state(cls, session_state: dict[str, Any] | None) -> "UiRenderState":
|
| state = session_state or new_session_state()
|
| return cls(
|
| session_state=state,
|
| status=state.get("status", "Machine dormant."),
|
| destination_md=state.get("destination_md", ""),
|
| persona_md=state.get("persona_md", ""),
|
| transcript_lines=list(state.get("transcript_lines", [])),
|
| souvenir_md=state.get("souvenir_md", ""),
|
| audio_path=state.get("audio_path"),
|
| )
|
|
|
| @classmethod
|
| def reset_for_launch(cls, session_state: dict[str, Any] | None) -> "UiRenderState":
|
| state = session_state or new_session_state()
|
| state["encounter_id"] = None
|
| state["persona_name"] = "Contact"
|
| state["machine_state"] = "launching"
|
| state["target_year"] = None
|
| state["destination_place"] = ""
|
| state["destination_mode"] = ""
|
| state["destination_atmosphere"] = ""
|
| state["destination_visual_preset"] = ""
|
| state["destination_motifs"] = []
|
| state["persona_role"] = ""
|
| state["persona_situation"] = ""
|
| state["scene"] = None
|
| state["portrait"] = None
|
| state["narration"] = None
|
| state["artifact"] = None
|
| state["ambient_key"] = "temporal_storm"
|
| state["transcript_lines"] = []
|
| state["destination_md"] = ""
|
| state["persona_md"] = ""
|
| state["souvenir_md"] = ""
|
| state["audio_path"] = None
|
| state["_partial_character_active"] = False
|
| return cls.from_state(state)
|
|
|
| def apply(self, event: TemporalEvent) -> None:
|
| encounter_id = event.metadata.get("encounter_id")
|
| if encounter_id:
|
| self.session_state["encounter_id"] = encounter_id
|
|
|
| if isinstance(event, MachineStateEvent):
|
| self.session_state["machine_state"] = event.state
|
| self.status = event.message or event.state
|
| elif isinstance(event, DestinationEvent):
|
| self.destination_md = render_destination(event.destination)
|
| self._apply_destination(event.destination)
|
| self.session_state["machine_state"] = "destination"
|
| self.status = event.message or "Destination resolved."
|
| elif isinstance(event, PersonaEvent):
|
| self.persona_md = render_persona(event.persona)
|
| self.session_state["persona_name"] = event.persona.name
|
| self.session_state["persona_role"] = event.persona.role
|
| self.session_state["persona_situation"] = event.persona.immediate_situation
|
| self.session_state["machine_state"] = "persona"
|
| self.status = event.message or "Signal locked."
|
| elif isinstance(event, TranscriptEvent):
|
| self.transcript_lines.append(f"**You:** {event.transcript.text}")
|
| self.status = event.message or "Transcript decoded."
|
| elif isinstance(event, ConversationTextEvent):
|
| name = self.session_state.get("persona_name", "Contact")
|
| line = f"**{name}:** {event.text}"
|
| if event.is_final:
|
| if self.session_state.get("_partial_character_active") and self.transcript_lines:
|
| self.transcript_lines[-1] = line
|
| else:
|
| self.transcript_lines.append(line)
|
| self.session_state["_partial_character_active"] = False
|
| elif self.session_state.get("_partial_character_active") and self.transcript_lines:
|
| self.transcript_lines[-1] = line
|
| else:
|
| self.transcript_lines.append(line)
|
| self.session_state["_partial_character_active"] = True
|
| self.status = event.message or "Character replied."
|
| elif isinstance(event, AudioEvent):
|
| self.audio_path = event.audio.path
|
| self.status = event.message or event.audio.description or "Audio ready."
|
| elif isinstance(event, SceneEvent):
|
| self.session_state["scene"] = event.scene.model_dump(mode="json")
|
| self.session_state["ambient_key"] = event.scene.ambient_key
|
| self.status = event.message or "World image stabilized."
|
| elif isinstance(event, PortraitEvent):
|
| self.session_state["portrait"] = event.portrait.model_dump(mode="json")
|
| self.status = event.message or "Character image resolved."
|
| elif isinstance(event, NarrationEvent):
|
| self.session_state["narration"] = event.narration.model_dump(mode="json")
|
| self.status = event.message or "Narrator signal prepared."
|
| elif isinstance(event, VisualArtifactEvent):
|
| self.session_state["artifact"] = event.artifact.model_dump(mode="json")
|
| self.status = event.message or "Temporal artifact materialized."
|
| elif isinstance(event, SouvenirEvent):
|
| self.souvenir_md = render_souvenir(event.souvenir)
|
| self.status = event.message or "Souvenir ready."
|
| elif isinstance(event, ErrorEvent):
|
| self.status = event.message or "Recoverable error."
|
|
|
| timing_summary = render_timing_summary(event.metadata)
|
| if timing_summary:
|
| self.status = f"{self.status} ({timing_summary})"
|
|
|
| self._sync_state()
|
|
|
| def set_error(self, message: str) -> None:
|
| self.status = message
|
| self._sync_state()
|
|
|
| def to_outputs(self) -> tuple:
|
| self._sync_state()
|
| return (
|
| self.session_state,
|
| self.status,
|
| self.destination_md,
|
| self.persona_md,
|
| "\n\n".join(self.transcript_lines),
|
| self.souvenir_md,
|
| self.audio_path,
|
| render_realtime_config(self.session_state),
|
| render_immersive_payload(self.session_state),
|
| )
|
|
|
| def _apply_destination(self, destination: Destination) -> None:
|
| self.session_state["target_year"] = _parse_year(destination.year)
|
| self.session_state["destination_place"] = destination.place
|
| self.session_state["destination_mode"] = destination.mode
|
| self.session_state["destination_atmosphere"] = destination.atmosphere
|
| self.session_state["destination_visual_preset"] = destination.visual_preset_key
|
| self.session_state["destination_motifs"] = [motif.label for motif in destination.visual_motifs]
|
|
|
| def _sync_state(self) -> None:
|
| self.session_state["status"] = self.status
|
| self.session_state["destination_md"] = self.destination_md
|
| self.session_state["persona_md"] = self.persona_md
|
| self.session_state["transcript_lines"] = list(self.transcript_lines)
|
| self.session_state["souvenir_md"] = self.souvenir_md
|
| self.session_state["audio_path"] = self.audio_path
|
|
|
|
|
| def render_destination(destination: Destination) -> str:
|
| motifs = ", ".join(motif.label for motif in destination.visual_motifs) or "none"
|
| oddity = destination.temporal_oddity or "No visible anomaly."
|
| return (
|
| f"### {destination.place}, {destination.year}\n"
|
| f"**Mode:** {destination.mode}\n\n"
|
| f"{destination.atmosphere}\n\n"
|
| f"**Visual preset:** `{destination.visual_preset_key}`\n\n"
|
| f"**Motifs:** {motifs}\n\n"
|
| f"**Oddity:** {oddity}"
|
| )
|
|
|
|
|
| def render_persona(persona: Persona) -> str:
|
| constraints = "\n".join(f"- {item}" for item in persona.worldview_constraints)
|
| return (
|
| f"### {persona.name}\n"
|
| f"**Role:** {persona.role}\n\n"
|
| f"**Situation:** {persona.immediate_situation}\n\n"
|
| f"**Concern:** {persona.daily_concern}\n\n"
|
| f"**Theory about you:** {persona.voice_theory}\n\n"
|
| f"**Speaking style:** {persona.speaking_style}\n\n"
|
| f"**Worldview constraints:**\n{constraints}"
|
| )
|
|
|
|
|
| def render_souvenir(souvenir: Souvenir) -> str:
|
| return (
|
| f"### {souvenir.stamp_name}\n"
|
| f"**Destination:** {souvenir.destination}\n\n"
|
| f"**Contact:** {souvenir.contact}\n\n"
|
| f"**Quote:** \"{souvenir.quote}\"\n\n"
|
| f"**Artifact:** {souvenir.artifact}\n\n"
|
| f"{souvenir.summary}"
|
| )
|
|
|
|
|
| def render_timing_summary(metadata: dict[str, Any]) -> str:
|
| timings = metadata.get("timings")
|
| if not isinstance(timings, list):
|
| return ""
|
|
|
| parts: list[str] = []
|
| for timing in timings:
|
| if not isinstance(timing, dict):
|
| continue
|
| stage = timing.get("stage")
|
| elapsed_ms = timing.get("elapsed_ms")
|
| if not isinstance(stage, str) or not isinstance(elapsed_ms, int | float):
|
| continue
|
| parts.append(f"{stage}: {elapsed_ms / 1000:.1f}s")
|
| return ", ".join(parts)
|
|
|
|
|
| def render_realtime_config(session_state: dict[str, Any]) -> str:
|
| return json.dumps(
|
| {
|
| "session_id": session_state.get("session_id"),
|
| "encounter_id": session_state.get("encounter_id"),
|
| "persona_name": session_state.get("persona_name", "Contact"),
|
| "ready": bool(session_state.get("encounter_id")),
|
| "path": "/realtime/voice",
|
| },
|
| ensure_ascii=True,
|
| )
|
|
|
|
|
| def render_immersive_payload(session_state: dict[str, Any]) -> str:
|
| current_year = datetime.now().year
|
| target_year = session_state.get("target_year")
|
| direction = "hold"
|
| if isinstance(target_year, int):
|
| direction = "rewind" if target_year < current_year else "fast_forward" if target_year > current_year else "hold"
|
|
|
| return json.dumps(
|
| {
|
| "session_id": session_state.get("session_id"),
|
| "encounter_id": session_state.get("encounter_id"),
|
| "machine_state": session_state.get("machine_state", "dormant"),
|
| "current_year": current_year,
|
| "target_year": target_year,
|
| "direction": direction,
|
| "destination": {
|
| "place": session_state.get("destination_place", ""),
|
| "mode": session_state.get("destination_mode", ""),
|
| "atmosphere": session_state.get("destination_atmosphere", ""),
|
| "visual_preset": session_state.get("destination_visual_preset", ""),
|
| "motifs": session_state.get("destination_motifs", []),
|
| },
|
| "persona": {
|
| "name": session_state.get("persona_name", "Contact"),
|
| "role": session_state.get("persona_role", ""),
|
| "situation": session_state.get("persona_situation", ""),
|
| },
|
| "scene": session_state.get("scene"),
|
| "portrait": session_state.get("portrait"),
|
| "narration": _render_narration_payload(session_state.get("narration")),
|
| "artifact": session_state.get("artifact"),
|
| "image_generation": _render_image_generation_payload(session_state),
|
| "ambient_key": session_state.get("ambient_key", "temporal_storm"),
|
| },
|
| ensure_ascii=True,
|
| )
|
|
|
|
|
| def _render_image_generation_payload(session_state: dict[str, Any]) -> dict[str, Any]:
|
| scene = session_state.get("scene")
|
| portrait = session_state.get("portrait")
|
| return {
|
| "ready": bool(session_state.get("image_generation_ready")),
|
| "warning": session_state.get("image_generation_warning") or "",
|
| "scene_image_ready": _has_png_image(scene),
|
| "portrait_image_ready": _has_png_image(portrait),
|
| }
|
|
|
|
|
| def _has_png_image(value: object) -> bool:
|
| if not isinstance(value, dict):
|
| return False
|
| return bool(value.get("image_b64")) and value.get("mime_type") == "image/png"
|
|
|
|
|
| def _render_narration_payload(value: object) -> dict[str, Any] | None:
|
| if not isinstance(value, dict):
|
| return None
|
| audio = value.get("audio")
|
| audio_path = None
|
| mime_type = "audio/wav"
|
| if isinstance(audio, dict):
|
| mime_type = str(audio.get("mime_type") or mime_type)
|
| raw_path = audio.get("path")
|
| if isinstance(raw_path, str) and raw_path:
|
| audio_path = _audio_data_url(raw_path, mime_type)
|
| return {
|
| "text": value.get("text", ""),
|
| "audio_path": audio_path,
|
| "mime_type": mime_type,
|
| }
|
|
|
|
|
| def _audio_data_url(path_value: str, mime_type: str) -> str | None:
|
| path = Path(path_value)
|
| if not path.exists():
|
| return None
|
| encoded = base64.b64encode(path.read_bytes()).decode("ascii")
|
| return f"data:{mime_type};base64,{encoded}"
|
|
|
|
|
| def _parse_year(value: str) -> int | None:
|
| match = re.search(r"-?\d{1,5}", value)
|
| if not match:
|
| return None
|
| return int(match.group(0))
|
|
|