From 12a8177793507571261df3199781aba80480da1d Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Sun, 7 Jun 2026 22:53:19 -0700 Subject: [PATCH] =?UTF-8?q?feat(player):=20discrete=20Right=20variant=20+?= =?UTF-8?q?=20Left=20level=20+=20Calibration=20(slice=20design=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- player/alteration.py | 103 +++++++++++++++++++++----------- tests/test_player_alteration.py | 62 +++++++++++++++---- 2 files changed, 117 insertions(+), 48 deletions(-) diff --git a/player/alteration.py b/player/alteration.py index 297f3d9..4a1b045 100644 --- a/player/alteration.py +++ b/player/alteration.py @@ -1,22 +1,18 @@ """The alteration engine: a knob vector -> a layered RenderPlan (design §4, §5). -Replaces the 2026-06-04 nearest-match *selection* with a *transformation* of a -neutral base clip. Given the four experience knobs, it produces three layers -that compose per §4.2: +Reconciled slice (2026-06-07): the Right axis is a DISCRETE selection of a +pre-baked, flow-stabilized restyle variant (not a continuous blend), 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 curves so they +can be tuned by eye in the simulator and baked into DEFAULT_CALIBRATION. - - Substrate transforms (alter pixels, blend with each other): - * Restyle -- the Right axis: a pre-baked generative v2v dreamlike restyle. - * ColorGrade -- the mood axis (Dark/Light): a deterministic color grade. - - Overlay (composited on top of the substrate): - * AnalyticalOverlay -- the Left axis: HUD/labels/measurement. - -Left and Right are NOT opposites; they live on different layers and stack -(§4.2). Dark and Light are the two poles of one mood grade whose center is the -identity (§5). - -Calibration note: the knob->strength curves below are the single source of -truth for how a 0..4 position maps to a transform strength. See the session -0006 transcript Deferred decisions for the §3-vs-§4.2/§5 reconciliation. +Layers compose per §4.2: + - Substrate: ColorGrade (Dark/Light mood, center = identity §5) + a pre-baked + Right restyle variant. + - Overlay: AnalyticalOverlay (Left), composited on top at runtime. +Left and Right stack (different layers); Dark/Light are the two poles of one +mood grade. See docs/superpowers/specs/2026-06-07-reconciled-simulator- +alteration-slice-design.md. """ from __future__ import annotations @@ -28,6 +24,27 @@ 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 intensity (clamped to [0, 1]). + - right_variant_map: knob value (0..4) -> pre-baked Right variant index. + """ + + mood_gain: float = 1.0 + overlay_gain: float = 1.0 + right_variant_map: tuple = (0, 1, 2, 3, 4) + + +DEFAULT_CALIBRATION = Calibration() + + @dataclass(frozen=True) class ColorGrade: """Mood-axis grade (§5). `tone` is signed: >0 warm yellow->white (light), @@ -42,18 +59,20 @@ class ColorGrade: @dataclass(frozen=True) class AnalyticalOverlay: - """Left axis (§4.1): analytical HUD/annotation/labels, composited on top. - `intensity` 0..1 (0 = no overlay).""" + """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 Restyle: - """Right axis (§4.1): pre-baked generative v2v dreamlike substrate. - `blend` 0..1 (0 = raw substrate, no restyle).""" + """Right axis (§4.1): selects a pre-baked, flow-stabilized restyle variant. + `variant` is a discrete index (0 = raw base, no restyle).""" - blend: float + variant: int @dataclass(frozen=True) @@ -69,30 +88,42 @@ class RenderPlan: """True when the plan leaves the neutral base un-altered.""" return ( self.grade.is_identity - and self.overlay.intensity == 0.0 - and self.restyle.blend == 0.0 + and self.overlay.level == 0 + and self.restyle.variant == 0 ) -def _overlay_intensity(left: int) -> float: - """Left knob -> analytical-overlay intensity (0..1).""" - return left / KNOB_MAX +def _overlay_intensity(left: int, cal: Calibration) -> float: + return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0) -def _restyle_blend(right: int) -> float: - """Right knob -> v2v restyle blend (0..1).""" - return right / KNOB_MAX +def _right_variant(right: int, cal: Calibration) -> int: + return cal.right_variant_map[right] -def _mood_tone(dark: int, light: int) -> float: - """(dark, light) -> signed mood grade in [-1, 1]; equal -> 0 identity (§5).""" - return (light - dark) / KNOB_MAX +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) -> RenderPlan: +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)), - overlay=AnalyticalOverlay(intensity=_overlay_intensity(coord.left)), - restyle=Restyle(blend=_restyle_blend(coord.right)), + grade=ColorGrade(tone=_mood_tone(coord.dark, coord.light, calibration)), + overlay=AnalyticalOverlay( + level=coord.left, + intensity=_overlay_intensity(coord.left, calibration), + ), + restyle=Restyle(variant=_right_variant(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}, + "restyle": {"variant": plan.restyle.variant}, + "is_identity": plan.is_identity, + } diff --git a/tests/test_player_alteration.py b/tests/test_player_alteration.py index 0897f2b..01509f4 100644 --- a/tests/test_player_alteration.py +++ b/tests/test_player_alteration.py @@ -2,11 +2,14 @@ import pytest from hef.selection import Coordinate from player.alteration import ( + DEFAULT_CALIBRATION, AnalyticalOverlay, + Calibration, ColorGrade, RenderPlan, Restyle, plan_alteration, + render_plan_to_dict, ) @@ -17,30 +20,32 @@ def _coord(left=0, right=0, dark=0, light=0): def test_all_zero_knobs_is_the_unaltered_base(): plan = plan_alteration(_coord()) assert plan.is_identity + assert plan.overlay.level == 0 assert plan.overlay.intensity == 0.0 - assert plan.restyle.blend == 0.0 + assert plan.restyle.variant == 0 assert plan.grade.tone == 0.0 assert plan.grade.is_identity def test_left_drives_the_analytical_overlay_only(): plan = plan_alteration(_coord(left=4)) + assert plan.overlay.level == 4 assert plan.overlay.intensity == 1.0 - assert plan.restyle.blend == 0.0 # Left does not touch the substrate + assert plan.restyle.variant == 0 # Left does not touch the substrate assert plan.grade.tone == 0.0 -def test_right_drives_the_restyle_substrate_only(): +def test_right_selects_a_discrete_restyle_variant_only(): plan = plan_alteration(_coord(right=2)) - assert plan.restyle.blend == 0.5 - assert plan.overlay.intensity == 0.0 # Right does not add overlay + assert plan.restyle.variant == 2 + assert plan.overlay.level == 0 # Right does not add overlay def test_left_and_right_stack_not_cancel(): # design §4.2: whole-brain corner = dreamlike substrate WITH labels on top plan = plan_alteration(_coord(left=4, right=4)) - assert plan.overlay.intensity == 1.0 - assert plan.restyle.blend == 1.0 + assert plan.overlay.level == 4 + assert plan.restyle.variant == 4 def test_light_pole_grades_warm_positive_tone(): @@ -50,8 +55,7 @@ def test_light_pole_grades_warm_positive_tone(): def test_dark_pole_grades_cool_negative_tone(): - plan = plan_alteration(_coord(dark=4)) - assert plan.grade.tone == -1.0 + assert plan_alteration(_coord(dark=4)).grade.tone == -1.0 def test_equal_dark_and_light_is_identity_grade(): @@ -66,14 +70,48 @@ def test_dark_minus_light_sets_intermediate_tone(): def test_whole_brain_dark_corner_stacks_grade_substrate_and_overlay(): - # design §4.2 "Dark + analytical": cold measurement over a melancholy scene plan = plan_alteration(_coord(left=4, right=2, dark=4, light=0)) - assert plan.overlay.intensity == 1.0 - assert plan.restyle.blend == 0.5 + assert plan.overlay.level == 4 + assert plan.restyle.variant == 2 assert plan.grade.tone == -1.0 assert not plan.is_identity +def test_default_calibration_is_behavior_preserving(): + # DEFAULT_CALIBRATION must reproduce the original three helpers exactly. + for left in range(5): + assert plan_alteration(_coord(left=left)).overlay.intensity == pytest.approx(left / 4) + for right in range(5): + assert plan_alteration(_coord(right=right)).restyle.variant == right + for dark in range(5): + for light in range(5): + expected = (light - dark) / 4 + assert plan_alteration(_coord(dark=dark, light=light)).grade.tone == pytest.approx(expected) + + +def test_custom_calibration_scales_mood_and_overlay(): + cal = Calibration(mood_gain=0.5, overlay_gain=0.5, right_variant_map=(0, 0, 1, 1, 2)) + assert plan_alteration(_coord(light=4), cal).grade.tone == pytest.approx(0.5) + assert plan_alteration(_coord(left=4), cal).overlay.intensity == pytest.approx(0.5) + assert plan_alteration(_coord(right=3), cal).restyle.variant == 1 + + +def test_calibration_gain_is_clamped_to_unit_range(): + cal = Calibration(mood_gain=10.0, overlay_gain=10.0) + assert plan_alteration(_coord(light=4), cal).grade.tone == 1.0 # clamped, not 10 + assert plan_alteration(_coord(left=4), cal).overlay.intensity == 1.0 + + +def test_render_plan_to_dict_round_trips_the_numbers(): + d = render_plan_to_dict(plan_alteration(_coord(left=4, right=2, dark=4, light=0))) + assert d == { + "grade": {"tone": -1.0}, + "overlay": {"level": 4, "intensity": 1.0}, + "restyle": {"variant": 2}, + "is_identity": False, + } + + def test_render_plan_is_frozen(): plan = plan_alteration(_coord()) with pytest.raises(Exception):