Compare commits

..

4 Commits

Author SHA1 Message Date
BenStullsBets 7a2f37af7e claim human-experience-filter-art session 0019 (placeholder) + sessions.json entry 2026-06-26 04:47:47 -07:00
benstull d0ad9a9ac9 Merge pull request 'feat(sim): emotions are a right-brain output (Right knob alone)' (#24) from feat/affect-right-brain-only into main 2026-06-26 11:29:35 +00:00
BenStullsBets c3aa419c53 feat(sim): emotions are a right-brain output (Right knob alone)
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>
2026-06-26 04:29:21 -07:00
benstull 5bb762ae53 Merge pull request 'fix(sim): content-hash media URLs + fade pool-landing clip swap' (#23) from fix/media-versions-and-pool-landing into main 2026-06-25 20:04:15 +00:00
6 changed files with 99 additions and 56 deletions
@@ -1,27 +1,36 @@
# Affect channel — emotions in the Left-brain HUD
**Status:** approved (session 0013, 2026-06-22)
**Status:** approved (session 0013, 2026-06-22); **revised session 0018, 2026-06-26**
**Surface:** simulator preview (`simulator/`, `player/alteration.py`)
**Builds on:** the machine-altered-perception design SPEC; the Left HUD look-and-feel
pass (same session).
> **Revision (session 0018) — affect is a RIGHT-brain output.** The original design
> gated emotions on *both* knobs (`strength = min(left, right)`). Per operator
> direction, **emotions are now controlled by the Right (dreamlike) knob alone** —
> the analytical Left knob no longer gates them. This sharpens the left/right split:
> Left = measurement (detections + measurements), Right = the dream **and** the
> feeling it evokes. Every `min(left, right)` below now reads `right`. The data
> shape, rendering, and per-word `min_level` mechanic are unchanged — only the knob
> the strength is keyed off.
## Idea
A third HUD channel beside the cyan *detections* and amber *measurements*: soft,
glowing **emotion-words** that surface the feelings the experience may invoke.
They appear only when *both* the analytical (Left) and dreamlike (Right) knobs are
up — the dream leaking into the machine's reading. This is the uncanny edge of the
thesis: the machine trying to quantify not just what is there, but how you are
meant to feel.
They are a **right-brain output: driven by the dreamlike Right knob alone** (the
analytical Left knob does not gate them) — the dream surfacing as the machine's
felt reading. This is the uncanny edge of the thesis: the machine trying to
quantify not just what is there, but how you are meant to feel.
## Behaviour
- **Trigger / intensity.** Affect strength = `min(left, right)` (04). Either knob
at 0 → no emotions at all. Opacity scales with that strength × `overlay_gain`.
- **Escalation.** A **stable emotional palette per scene** — higher combined
strength reveals *more* words at higher opacity. Each word carries a `min_level`
(the same mechanic the detection channel uses), but compared against the
*combined* `min(left, right)` strength rather than Left alone.
- **Trigger / intensity.** Affect strength = `right` (04). Right at 0 → no
emotions at all, regardless of Left. Opacity scales with that strength ×
`overlay_gain`.
- **Escalation.** A **stable emotional palette per scene** — higher Right reveals
*more* words at higher opacity. Each word carries a `min_level` (the same
mechanic the detection channel uses), compared against the `right` strength.
- **Rendering.** Floating words at authored scene positions — **no boxes or
reticles**, lowercase, semi-transparent with a soft glow, in a distinct **violet**
register so they read as affective and clearly *softer* than the hard clinical
@@ -33,15 +42,15 @@ Consistent with the existing split (alteration math in Python, label-selection i
the client):
- `player/alteration.py` gains an `AffectOverlay(strength, intensity)` on the
`RenderPlan`. `strength = min(left, right)`; `intensity = clamp(overlay_gain *
min(left,right) / KNOB_MAX, 0, 1)`. Returned in `render_plan_to_dict` under
`"affect"`. Pure, unit-tested.
`RenderPlan`. `strength = right`; `intensity = clamp(overlay_gain * right /
KNOB_MAX, 0, 1)`. Returned in `render_plan_to_dict` under `"affect"`. Pure,
unit-tested.
- The simulator client picks *which* words to show by comparing each affect
entry's `min_level` to `plan.affect.strength`, and renders them at
`plan.affect.intensity` opacity.
`is_identity` is unaffected: `min(left,right) > 0` implies `left > 0`, which already
makes `overlay.level > 0`.
`is_identity` is unaffected: `affect.strength == right`, and `right > 0` already
makes `dream.strength > 0`.
## Data
+17 -14
View File
@@ -88,11 +88,12 @@ class AnalyticalOverlay:
@dataclass(frozen=True)
class AffectOverlay:
"""Affect channel (Left x Right): emotion-words surfaced only when BOTH the
analytical (Left) and dreamlike (Right) knobs are up. `strength` is
`min(left, right)` (0..4) — a runtime affect track picks which feeling words
appear by it; `intensity` 0..1 is their opacity. The dream leaking into the
machine's reading (affect-channel design, session 0013)."""
"""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
@@ -122,8 +123,9 @@ class RenderPlan:
@property
def is_identity(self) -> bool:
"""True when the plan leaves the neutral base un-altered. Affect needs no
clause: `min(left,right) > 0` implies `left > 0`, so `overlay.level > 0`
already makes a plan with active affect non-identity."""
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
@@ -135,13 +137,14 @@ def _overlay_intensity(left: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * left / KNOB_MAX, 0.0, 1.0)
def _affect_strength(left: int, right: int) -> int:
"""Affect surfaces only when BOTH knobs are up — gated by the smaller one."""
return min(left, right)
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(left: int, right: int, cal: Calibration) -> float:
return _clamp(cal.overlay_gain * _affect_strength(left, right) / KNOB_MAX, 0.0, 1.0)
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:
@@ -163,8 +166,8 @@ def plan_alteration(
intensity=_overlay_intensity(coord.left, calibration),
),
affect=AffectOverlay(
strength=_affect_strength(coord.left, coord.right),
intensity=_affect_intensity(coord.left, coord.right, calibration),
strength=_affect_strength(coord.right),
intensity=_affect_intensity(coord.right, calibration),
),
dream=Dream(
strength=coord.right,
@@ -0,0 +1,25 @@
# Session 0019.0 — Transcript
> App: human-experience-filter-art
> Start: 2026-06-26T04-47 (PST)
> Type: brainstorming
> Posture: careful
> Claude-Session: d6ed1203-92b6-471f-80a4-4c773b645ca1
> Status: **PLACEHOLDER — claimed at session start; finalized at session end.**
>
> This file reserves session ID 0019 for human-experience-filter-art. The driver replaces this
> body with the full transcript and renames the file to its final
> SESSION-0019.0-TRANSCRIPT-2026-06-26T04-47--<end>.md form at session end.
## Launch prompt
```
Let's start talking about hardware. Let's assume for the first version I'm ok having my computer or laptop do the rendering. We'd still want a controller but it could be connected to the computer via wifi, USB, or bluetooth and would send commands to the software we've already written that would adjust left brain, right brain, etc. We'd prototype this by having a controller simulator run on another device, such as an ipad (just riffing here) but eventually move to a physical remote running on an Arduino or similar
```
## Deferred decisions
_Autonomous-mode low-confidence calls the driver made and would have
liked operator input on. Appended as the session runs; surfaced at
finalize. Empty if none._
+3
View File
@@ -52,5 +52,8 @@
},
"0018": {
"title": ""
},
"0019": {
"title": ""
}
}
+6 -5
View File
@@ -456,10 +456,11 @@ function renderOverlay(level, intensity) {
lastOverlay = { level, intensity };
}
// Affect channel: soft, glowing emotion-words surfaced only when BOTH knobs are
// up. `strength` = min(left, right); `intensity` is the layer opacity. Words are
// placed at authored scene points (no boxes — feelings are scene-level) and read
// softer than the clinical reticles — the dream leaking into the machine's read.
// Affect channel: soft, glowing emotion-words — a RIGHT-brain output, surfaced by
// the Right (dreamlike) knob alone (`strength` = right; the Left analytical knob no
// longer gates them). `intensity` is the layer opacity. Words are placed at authored
// scene points (no boxes — feelings are scene-level) and read softer than the
// clinical reticles — feeling, not measurement.
function renderAffect(strength, intensity, right) {
const clip = activeClip();
if (!affectLayer) return; // tolerate a stale page missing the affect layer
@@ -472,7 +473,7 @@ function renderAffect(strength, intensity, right) {
const [x, y] = f.at.map((n) => n * 100);
const t = svg("text", { x, y, "text-anchor": "middle", class: "hud-affect" }, affectLayer);
// RIGHT emotion tier: the vocabulary escalates basic -> compound as the Right
// knob rises (appearance still gated by min(left,right) above).
// knob rises (appearance is gated by the Right knob via `strength` above).
const raw = strings[f.key];
t.textContent = pickTier(raw !== undefined ? raw : f.key, clamp(right || 0, 1, tierCount(raw)));
}
+23 -21
View File
@@ -55,42 +55,44 @@ def test_left_and_right_stack_not_cancel():
assert plan.dream.intensity == 1.0
def test_affect_needs_both_knobs_up():
# affect-channel design (session 0013): emotion-words surface only when BOTH
# the Left and Right knobs are up — either at 0 means no affect at all.
def test_affect_is_driven_by_the_right_knob_alone():
# Right-brain affect (session 0018): emotion-words are controlled by the Right
# knob alone — Right at 0 means no affect regardless of Left, and Left no longer
# gates the channel (full Right with Left at 0 still surfaces affect).
assert plan_alteration(_coord(left=4, right=0)).affect.strength == 0
assert plan_alteration(_coord(left=0, right=4)).affect.strength == 0
assert plan_alteration(_coord(left=4, right=0)).affect.intensity == 0.0
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 0.0
assert plan_alteration(_coord(left=0, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0
def test_affect_strength_is_the_smaller_knob():
# strength = min(left, right)the smaller knob gates the channel.
assert plan_alteration(_coord(left=2, right=4)).affect.strength == 2
def test_affect_strength_is_the_right_knob():
# strength = right — Left is irrelevant to the affect channel.
assert plan_alteration(_coord(left=2, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=4, right=2)).affect.strength == 2
assert plan_alteration(_coord(left=4, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=3, right=1)).affect.strength == 1
assert plan_alteration(_coord(left=0, right=3)).affect.strength == 3
assert plan_alteration(_coord(left=4, right=1)).affect.strength == 1
def test_affect_intensity_scales_with_strength_over_full_scale():
for lo in range(5):
plan = plan_alteration(_coord(left=lo, right=4))
assert plan.affect.intensity == pytest.approx(lo / 4)
assert plan_alteration(_coord(left=4, right=4)).affect.intensity == 1.0
def test_affect_intensity_scales_with_right_over_full_scale():
for r in range(5):
plan = plan_alteration(_coord(left=0, right=r))
assert plan.affect.intensity == pytest.approx(r / 4)
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0
def test_affect_intensity_honors_overlay_gain():
# affect rides the same overlay_gain as the analytical HUD it belongs to.
# affect text rides the overlay-opacity gain, but is keyed off the Right knob.
cal = Calibration(overlay_gain=0.5)
assert plan_alteration(_coord(left=4, right=4), cal).affect.intensity == pytest.approx(0.5)
assert plan_alteration(_coord(left=0, right=4), cal).affect.intensity == pytest.approx(0.5)
clamp = Calibration(overlay_gain=10.0)
assert plan_alteration(_coord(left=4, right=4), clamp).affect.intensity == 1.0
assert plan_alteration(_coord(left=0, right=4), clamp).affect.intensity == 1.0
def test_affect_active_implies_non_identity():
# min(left,right) > 0 implies left > 0, so an active affect plan is never identity.
plan = plan_alteration(_coord(left=2, right=3))
assert plan.affect.strength == 2
# affect.strength == right, and right > 0 makes dream.strength > 0, so an active
# affect plan (even with Left at 0) is never identity.
plan = plan_alteration(_coord(left=0, right=3))
assert plan.affect.strength == 3
assert not plan.is_identity