ai-time-machine / src /time_machine /ui /view_models.py
manikandanj's picture
Prepare AI Time Machine hackathon Space
5862322 verified
Raw
History Blame Contribute Delete
14.7 kB
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))