c3aa419c53
The affect channel previously gated on BOTH knobs (strength = min(left, right)), so the analytical Left brain co-controlled the emotion words. Per operator direction, emotions are now driven by the Right (dreamlike) knob ALONE — Left no longer gates them. This sharpens the split: Left = measurement (detections + measurements), Right = the dream AND the feeling it evokes. - player/alteration.py: `_affect_strength(right) = right` (was min(left,right)); `_affect_intensity` keyed off Right. AffectOverlay + is_identity docs updated (is_identity still holds: right>0 ⇒ dream.strength>0). - simulator/static/app.js: renderAffect comments (gating is Right via `strength`); the client logic already keyed min_level/tier off the passed strength + right. - Affect design spec: revision note + every min(left,right) → right. - Tests rewritten: affect is Right-driven (full Right + Left 0 surfaces affect; Right 0 + Left 4 surfaces none). 271 passed, 2 skipped. Live API verified: left=0/right=4 → strength 4; left=4/ right=0 → strength 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
188 lines
6.9 KiB
Python
188 lines
6.9 KiB
Python
"""The alteration engine: a knob vector -> a layered RenderPlan (design §4, §5).
|
|
|
|
Right axis (reframed session 0013): a DETERMINISTIC, real-time dream applied live
|
|
to the base footage — a STILL altered state, not the old pre-baked SD variant that
|
|
churned across the loop (Right-axis dream design,
|
|
docs/superpowers/specs/2026-06-22-right-axis-deterministic-dream-design.md). The
|
|
Left axis carries its knob LEVEL so a runtime annotation track can pick which
|
|
labels show, and a frozen `Calibration` parameterizes the knob->strength gains so
|
|
they can be tuned by eye in the simulator and baked into DEFAULT_CALIBRATION.
|
|
|
|
Layers compose per §4.2:
|
|
- Substrate: ColorGrade (Dark/Light mood, center = identity §5) + the Right Dream
|
|
(deterministic luminous haze, applied client-side from `dream.intensity`).
|
|
- Overlay: AnalyticalOverlay (Left) + the affect channel, composited on top.
|
|
Left and Right stack (different layers); Dark/Light are the two poles of one
|
|
mood grade.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from hef.selection import Coordinate
|
|
|
|
KNOB_MAX = 4 # knob full-scale (0..4)
|
|
|
|
|
|
def _clamp(x: float, lo: float, hi: float) -> float:
|
|
return max(lo, min(hi, x))
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Calibration:
|
|
"""Tunable knob->strength curves (settled by eye in the sim, then baked).
|
|
|
|
- mood_gain: scales the signed Dark/Light tone (result clamped to [-1, 1]).
|
|
- overlay_gain: scales the Left overlay (and affect) intensity (clamped [0, 1]).
|
|
- dream_gain: scales the Right deterministic-dream intensity (clamped [0, 1]).
|
|
"""
|
|
|
|
mood_gain: float = 1.0
|
|
overlay_gain: float = 1.0
|
|
dream_gain: float = 1.0
|
|
|
|
|
|
# The LOCKED calibration (session 0010, 2026-06-07) — settled by eye in the
|
|
# simulator, closing the open session-0006 knob->strength decision. Convention:
|
|
# every experience knob runs 0 = off .. 4 = max, and equal Dark/Light = identity
|
|
# (raw footage); there is no "centered at 2 = no push" coordinate.
|
|
# - mood_gain = 1.0: full Dark/Light knob reaches the full mood grade. By-eye
|
|
# evidence (POC renders + sim, after the dark-grade fix this session) shows
|
|
# full tilt is peaceful on every axis, so no softening is warranted.
|
|
# - overlay_gain = 1.0: full Left = opacity 1.0; the HUD is legible, not
|
|
# overwhelming, sitting above the grade.
|
|
# - dream_gain = 1.0: full Right = dream intensity 1.0; the deterministic
|
|
# luminous haze (Right-axis dream design, session 0013) reaches full strength.
|
|
# These are unity by deliberate choice, not as placeholders. The gains stay
|
|
# parameterized so a different feel is one edit away; test_default_calibration_is_locked
|
|
# guards the values from drifting silently.
|
|
DEFAULT_CALIBRATION = Calibration(
|
|
mood_gain=1.0,
|
|
overlay_gain=1.0,
|
|
dream_gain=1.0,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ColorGrade:
|
|
"""Mood-axis grade (§5). `tone` is signed: >0 warm yellow->white (light),
|
|
<0 cool blue->black (dark), 0 = identity (raw, ungraded)."""
|
|
|
|
tone: float
|
|
|
|
@property
|
|
def is_identity(self) -> bool:
|
|
return self.tone == 0.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AnalyticalOverlay:
|
|
"""Left axis (§4.1): analytical HUD/annotation, composited on top at runtime.
|
|
`level` is the Left knob (0..4); a runtime annotation track uses it to pick
|
|
which labels appear. `intensity` 0..1 is the overlay opacity/strength."""
|
|
|
|
level: int
|
|
intensity: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AffectOverlay:
|
|
"""Affect channel (RIGHT brain): emotion-words surfaced by the Right (dreamlike)
|
|
knob alone — the analytical Left knob does NOT gate them. `strength` is the Right
|
|
knob (0..4) — a runtime affect track picks which feeling words appear by it;
|
|
`intensity` 0..1 is their opacity. Feeling is the right brain's output, surfaced
|
|
as the machine's felt reading (affect-channel design, session 0013; Right-only
|
|
since session 0018)."""
|
|
|
|
strength: int
|
|
intensity: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Dream:
|
|
"""Right axis: a deterministic, real-time dream applied live to the base
|
|
footage — a STILL altered state (Right-axis dream design, session 0013),
|
|
NOT the old pre-baked SD variant that churned across the loop. `strength` is
|
|
the Right knob (0..4); `intensity` 0..1 is the dream's strength (the client
|
|
maps it to the luminous-haze look, the way it maps `grade.tone` to a filter)."""
|
|
|
|
strength: int
|
|
intensity: float
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RenderPlan:
|
|
"""The full layered alteration for one knob vector (§4.2)."""
|
|
|
|
grade: ColorGrade
|
|
overlay: AnalyticalOverlay
|
|
affect: AffectOverlay
|
|
dream: Dream
|
|
|
|
@property
|
|
def is_identity(self) -> bool:
|
|
"""True when the plan leaves the neutral base un-altered. Affect needs no
|
|
clause: `affect.strength == right`, and `right > 0` already makes
|
|
`dream.strength > 0`, so an active-affect plan is non-identity via the
|
|
dream check below."""
|
|
return (
|
|
self.grade.is_identity
|
|
and self.overlay.level == 0
|
|
and self.dream.strength == 0
|
|
)
|
|
|
|
|
|
def _overlay_intensity(left: int, cal: Calibration) -> float:
|
|
return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0)
|
|
|
|
|
|
def _affect_strength(right: int) -> int:
|
|
"""Affect is a RIGHT-brain output: the emotion channel is driven by the Right
|
|
(dreamlike) knob alone — the analytical Left knob no longer gates it."""
|
|
return right
|
|
|
|
|
|
def _affect_intensity(right: int, cal: Calibration) -> float:
|
|
return _clamp(cal.overlay_gain * _affect_strength(right) / KNOB_MAX, 0.0, 1.0)
|
|
|
|
|
|
def _dream_intensity(right: int, cal: Calibration) -> float:
|
|
return _clamp(cal.dream_gain * right / KNOB_MAX, 0.0, 1.0)
|
|
|
|
|
|
def _mood_tone(dark: int, light: int, cal: Calibration) -> float:
|
|
return _clamp(cal.mood_gain * (light - dark) / KNOB_MAX, -1.0, 1.0)
|
|
|
|
|
|
def plan_alteration(
|
|
coord: Coordinate, calibration: Calibration = DEFAULT_CALIBRATION
|
|
) -> RenderPlan:
|
|
"""Map a knob vector to its layered RenderPlan (design §4)."""
|
|
return RenderPlan(
|
|
grade=ColorGrade(tone=_mood_tone(coord.dark, coord.light, calibration)),
|
|
overlay=AnalyticalOverlay(
|
|
level=coord.left,
|
|
intensity=_overlay_intensity(coord.left, calibration),
|
|
),
|
|
affect=AffectOverlay(
|
|
strength=_affect_strength(coord.right),
|
|
intensity=_affect_intensity(coord.right, calibration),
|
|
),
|
|
dream=Dream(
|
|
strength=coord.right,
|
|
intensity=_dream_intensity(coord.right, calibration),
|
|
),
|
|
)
|
|
|
|
|
|
def render_plan_to_dict(plan: RenderPlan) -> dict:
|
|
"""JSON-serializable form for the simulator API."""
|
|
return {
|
|
"grade": {"tone": plan.grade.tone},
|
|
"overlay": {"level": plan.overlay.level, "intensity": plan.overlay.intensity},
|
|
"affect": {"strength": plan.affect.strength, "intensity": plan.affect.intensity},
|
|
"dream": {"strength": plan.dream.strength, "intensity": plan.dream.intensity},
|
|
"is_identity": plan.is_identity,
|
|
}
|