TurboSkillSlug / scenegraph /scene_graph.py
legendarydragontamer's picture
deploy
51a9974
Raw
History Blame Contribute Delete
5.32 kB
"""
SceneGraph: the deterministic semantic layer of a TurboSkillSlug session.
This is the CONTRACT that every renderer reads. It is built once, deterministically,
from a session extraction, and it is renderer-agnostic. The four lenses (character,
3D shell, generative atmosphere, full diorama) all consume the SAME SceneGraph, so:
- every visible element still traces to real session data (the core promise),
- renderers are independent and individually degradable,
- a new renderer can be added without touching extraction or the others.
Design rules:
- NOTHING here renders. No SVG, no canvas, no shader. Only structured meaning.
- Every field is DERIVED from the extraction (or a stable default), so the graph
is reproducible: same session -> same SceneGraph.
- Values are normalized and renderer-friendly (0..1 scalars, named enums, hex
colors) so each renderer can map them without re-interpreting raw extraction.
- The graph is versioned. Renderers declare which SCHEMA_VERSION they support.
"""
from __future__ import annotations
import colorsys
import hashlib
from dataclasses import dataclass, field, asdict
from typing import Any
SCHEMA_VERSION = "1.0"
# ---- canonical sentiment vocabulary (extraction may use freeform; we map it) ----
# Each sentiment maps to (valence -1..1, energy 0..1) so renderers can blend.
_SENTIMENT_VA = {
"frustrated": (-0.7, 0.7), "stuck": (-0.6, 0.3), "exhausted": (-0.4, 0.1),
"anxious": (-0.5, 0.6), "confused": (-0.4, 0.4), "focused": (0.1, 0.6),
"curious": (0.4, 0.6), "determined": (0.3, 0.7), "calm": (0.3, 0.3),
"relieved": (0.6, 0.3), "satisfied": (0.7, 0.4), "resolved": (0.7, 0.5),
"joyful": (0.9, 0.8), "delighted": (0.9, 0.7), "triumphant": (1.0, 0.9),
"neutral": (0.0, 0.4),
}
def _norm_sentiment(name: str | None) -> str:
if not name:
return "neutral"
n = str(name).strip().lower()
if n in _SENTIMENT_VA:
return n
# nearest by substring, else neutral
for k in _SENTIMENT_VA:
if k in n or n in k:
return k
return "neutral"
def _va(name: str) -> tuple[float, float]:
return _SENTIMENT_VA[_norm_sentiment(name)]
def _clamp01(x: float) -> float:
return max(0.0, min(1.0, float(x)))
# ---------------------------------------------------------------------------
# Sub-structures
# ---------------------------------------------------------------------------
@dataclass
class SlugState:
"""How the slug itself looks and feels β€” for the CHARACTER renderer."""
mood: str # canonical end sentiment
valence: float # -1..1 (sad -> happy)
energy: float # 0..1 (drained -> lively)
scars: int # = number of dead ends (visible marks on the slug)
expression: str # enum: weary|wary|focused|hopeful|elated
pose: str # enum: slumped|neutral|alert|triumphant
eye_state: str # enum: heavy|narrow|open|bright
@dataclass
class ShellState:
"""The nautilus geometry parameters β€” used by ALL renderers (SVG/3D/guide)."""
turns: float # number of spiral revolutions (from duration)
growth_curve: str # enum: gentle|steady|steep (pacing of the session)
knots: list[dict] # [{t:0..1, severity:0..1}] dead ends along the arm
jewels: list[dict] # [{t:0..1}] gotchas along the rim
aperture: dict # {t:0..1, intensity:0..1} the breakthrough
palette: dict # {start_hex, end_hex, accent_hex}
iridescence: float # 0..1 how strongly nacre shifts (for 3D shader)
@dataclass
class ArcState:
"""The emotional timeline β€” for the SCORE and ATMOSPHERE renderers."""
start: str
end: str
beats: list[dict] # ordered [{t:0..1, kind, valence, energy, label}]
tension_curve: list[float] # sampled 0..1 tension over the session (for music)
@dataclass
class BattleState:
"""The byobu battle cast β€” for the painted/animated battle layers."""
general: dict # {present:bool}
adversary: dict # {present:bool, ferocity:0..1}
fallen: list[dict] # [{t:0..1}] one per dead end
archers: list[dict] # [{t:0..1}] one per gotcha
dragon: dict # {present:bool, t:0..1, scale:0..1} breakthrough
@dataclass
class SceneEnv:
"""The surrounding scene mood β€” for the BACKDROP/diffusion renderer."""
time_of_day: str # dawn|day|dusk|night (from arc shape)
weather: str # clear|overcast|rain|storm (from struggle level)
mood_tags: list[str] # prompt-ready descriptors for generation
palette: dict # {sky_hex, ground_hex, accent_hex}
@dataclass
class SceneGraph:
schema_version: str
session_id: str # stable hash of the extraction (repro + cache key)
duration_minutes: float
themes: list[str]
slug: SlugState
shell: ShellState
arc: ArcState
battle: BattleState
env: SceneEnv
# raw escape hatch: renderers that want a field we didn't surface can read this
extraction: dict = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)