feat(sim+player): scale-ring navigation (endless encoder) + cosmos/abyss scales

Add scale-ring navigation to the simulator per the scales-library design §3:
an endless rotary-encoder control (relative, vs the absolute 0-4 experience
knobs) walks a closed ring of neutral "scales of nature" clips, with placeholder
AI zoom/warp transitions between adjacent scales and a small->large wrap (the
infinite-zoom payoff).

- player/ring.py (new, canonical pure logic): ScaleRing + advance_ring; one
  transition clip per edge, played forward zooming inward / reversed outward;
  modulo wrap both ways; chained steps for a multi-detent spin. Mirrors
  player/content.py conventions (frozen dataclasses, pure functions).
- simulator: load_ring + ring_to_dict/ring_move_to_dict; GET /api/ring and
  POST /api/ring/advance (Python owns step/wrap/transition math; the browser
  only plays the returned transition(s) then settles on the target scale).
- frontend: endless-encoder control (zoom in/out buttons + stage scroll),
  current-scale readout, multi-clip active-scale selection, transition playback.
- media: two cheap true-PD scales so the ring is demonstrable -- cosmos
  (NASA/Hubble) + abyss (NOAA Ocean Exploration), provenance recorded in the
  manifest, bytes generated as labelled placeholders by setup_scales_media.py.
  The expensive multi-strength flow-stabilized Right re-bake is DEFERRED (new
  scales carry a raw base only) until the ring is liked; Pi renderer +
  serial/firmware remain deferred (simulator-first).
- docs: ROADMAP slice-3 done; USER_GUIDE scale-ring gesture; plan doc.

Verified: pytest 215 passed / 2 skipped (+22 new); sim boots, /api/ring +
/api/ring/advance correct incl. wrap, media served, and a headless-Chrome CDP
drive walked cosmos -> forest -> abyss -> (wrap) -> cosmos with the active clip
reloading correctly.

Spec: docs/superpowers/specs/2026-06-07-scales-library-and-right-axis-pipeline-design.md (§3)
Plan: docs/superpowers/plans/2026-06-07-scale-ring-navigation.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-07 23:40:21 -07:00
parent 6cf1ae08ff
commit 7a50ae41bb
16 changed files with 951 additions and 25 deletions
+20 -4
View File
@@ -139,6 +139,23 @@ Design:
plan:
[`2026-06-07-reconciled-simulator-alteration-slice.md`](./superpowers/plans/2026-06-07-reconciled-simulator-alteration-slice.md).
**Slice 3 — scale-ring navigation (simulator) ✅ Done.** Merged to `main`
(session 0011). Adds the **endless rotary-encoder** control — *relative* (vs the
absolute 04 experience knobs) — that walks a **closed ring** of neutral
"scales of nature" clips, with placeholder AI zoom/warp **transitions** between
adjacent scales and a small→large **wrap** (the infinite-zoom payoff, scales
design §3). The navigation math is canonical Python (`player/ring.py`:
`ScaleRing` + `advance_ring`, one transition clip per edge, played forward
inward / reversed outward); the sim exposes `GET /api/ring` + `POST
/api/ring/advance` and the browser only plays the returned transition(s) then
settles on the target scale, keeping the live knob alteration on top. Two cheap
**true-PD** scales were added so the ring is demonstrable — `cosmos` (NASA/Hubble)
and `abyss` (NOAA Ocean Exploration), provenance recorded in the manifest, bytes
generated as labelled placeholders by `simulator/setup_scales_media.py`. The
expensive real multi-strength flow-stabilized Right re-bake is **deferred** (new
scales carry a raw base only) until the ring is liked. Plan:
[`2026-06-07-scale-ring-navigation.md`](./superpowers/plans/2026-06-07-scale-ring-navigation.md).
**Remaining slices (not started):**
- **Runtime renderer** — drive the single panoramic projector via mpv/ffmpeg;
@@ -152,10 +169,9 @@ plan:
- **Offline v2v variant pipeline** — author the pre-baked Right restyle variants
via the local flow-stabilized SD pipeline (now ~free, not a paid API — see the
scales-library/right-axis design §1/§4); a real multi-strength flow-stabilized
re-bake per base clip + the multilingual label/string tables + TTS.
- **Scale-ring navigation** — the endless rotary encoder + short pre-baked AI
zoom/warp transitions between neutral "scales of nature" clips on a closed ring
(scales-library design §3); a new control + offline pipeline element.
re-bake per base clip + the multilingual label/string tables + TTS. The
**scale-ring transitions** (forward/reverse per-edge morphs) are part of this
offline pipeline too — the simulator currently uses placeholders.
- **Catalog model changes** — audio *source* + "neutral base" vs "altered
variant" flag (sub-project 2 territory, design §13).
+15 -4
View File
@@ -298,12 +298,16 @@ piece moved from *selecting* clips to *altering* them.)
**One-time setup — populate the sample footage** (look-tuning only; not shipped
content):
python simulator/setup_sample_media.py
python simulator/setup_sample_media.py # the forest scale (real POC base + Right variants)
python simulator/setup_scales_media.py # the cosmos + abyss scales + ring transitions
This copies the session-0008 POC artifacts (`~/hef-poc/out/`) into
`simulator/sample_media/forest/` — the neutral base clip and the real
`setup_sample_media.py` copies the session-0008 POC artifacts (`~/hef-poc/out/`)
into `simulator/sample_media/forest/` — the neutral base clip and the real
flow-stabilized Right restyle — and generates placeholder intermediate Right
strengths. The `.mp4` binaries are gitignored.
strengths. `setup_scales_media.py` makes the scale **ring** demonstrable: cheap
synthetic placeholder bases for the two true-PD scales (`cosmos` = NASA/Hubble,
`abyss` = NOAA Ocean Exploration) plus the per-edge zoom/warp transition clips.
The `.mp4` binaries are gitignored.
**Run it (Docker):**
@@ -320,6 +324,13 @@ then open http://localhost:8000.
- **Content dial** — picks audio/video channel; "off" and audio-only positions go
to black walls.
- **Scale ring (endless encoder)**`⊖ out` / `in ⊕` (or scroll the stage) walk a
*closed ring* of neutral "scales of nature" clips — cosmos → forest → abyss and
back around (diving past the smallest **wraps** to the largest). It is *relative*
(an endless encoder), distinct from the absolute 04 knobs: each step plays a
short placeholder zoom/warp **transition**, then settles on the next scale, with
the current knob alteration still applied. The current scale is named beside the
buttons (`name (i/N)`).
- **Four experience knobs (04):**
- **Dark / Light** — a live runtime color grade (cool/dark ↔ warm/bright; equal
or zero = the raw footage).
@@ -0,0 +1,97 @@
# Scale-Ring Navigation (Simulator) — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans or
> superpowers:subagent-driven-development to implement task-by-task. Steps use
> checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add scale-ring navigation to the simulator — an endless rotary-encoder
control (relative, vs the absolute 04 experience knobs) that walks a *closed
ring* of neutral "scales of nature" clips, with placeholder AI zoom/warp
transitions between adjacent scales. Add 12 cheap, true-PD neutral base clips
(NASA/Hubble cosmos + NOAA Ocean Exploration deep sea) so the ring is
demonstrable. **Defer** the expensive real multi-strength flow-stabilized Right
re-bake until the ring is liked; keep deferring the Pi renderer + serial/firmware.
**Spec:** `docs/superpowers/specs/2026-06-07-scales-library-and-right-axis-pipeline-design.md` §3 (scale navigation & zoom transitions), §2 (scales library), §2.1 (strict-PD sourcing map).
**Architecture:** Python stays the single source of truth. The ring *navigation
math* — step, wrap, which transition clip to play and in which direction — lives
in a new canonical pure module `player/ring.py` (mirrors `player/content.py` /
`player/alteration.py`: frozen dataclasses, pure functions). The simulator's data
layer (`simulator/clips.py`) parses a `ring` section of the manifest into the
ring types; the API exposes the ring and a stateless `advance` move; the browser
holds the current ring index and only *renders* (plays the returned transition
file, then the target scale's clip, with the existing per-knob alteration on top).
**Ordering convention (canonical):** ring index 0 = the *largest* scale (cosmos);
increasing index zooms **inward** toward the smallest. `+1` detent = zoom in,
`-1` = zoom out — both wrap (past the smallest wraps to the largest, the
infinite-zoom payoff, §3). Edge `i` connects `scales[i] → scales[(i+1) % N]`; the
last edge is the small→large wrap seam. One transition clip per edge plays
**forward** when zooming inward and **reversed** when zooming outward, so N scales
need only N transition clips (§3: "one clip per ring edge").
**Deferral, made explicit:** the new cosmos/abyss scale clips carry a raw base
only (no pre-baked Right variants); the `Clip` model already falls back any
unauthored strength to the raw base, so Right on a new scale is a no-op until the
multi-strength flow-stabilized re-bake happens (deliberately deferred). Forest
keeps its existing POC variants.
**Tech Stack:** Python 3.13, FastAPI + pydantic, pytest, frozen dataclasses;
vanilla JS + CSS/SVG for the browser; ffmpeg (system or `imageio-ffmpeg`) for
placeholder media.
---
## File Structure
**Engine (create):**
- `player/ring.py``Scale`, `Transition`, `ScaleRing`, `RingMove`,
`TransitionStep`, `scale_at`, `advance_ring`. Pure logic, no I/O/JSON.
**Simulator (modify):**
- `simulator/clips.py` — add `load_ring(manifest)` building a `ScaleRing` from the
manifest `ring` section + `ring_to_dict` / `ring_move_to_dict` serializers.
- `simulator/app.py``GET /api/ring`, `POST /api/ring/advance`.
- `simulator/static/{index.html,app.js,style.css}` — endless-encoder control
(zoom-in/zoom-out buttons + mouse-wheel on the stage), current-scale readout,
multi-clip active-clip selection, transition playback.
- `simulator/sample_media/manifest.json` (committed) — add cosmos + abyss clips
and the `ring` section; media binaries stay gitignored.
- `simulator/setup_sample_media.py` (or sibling) — generate cheap cosmos/abyss
base placeholders + per-edge transition placeholders (ffmpeg); record true-PD
provenance in the manifest. Real PD fetch is a best-effort `--fetch` bonus.
**Tests (create/extend):**
- `tests/test_player_ring.py` (create) — ring navigation logic (step, chain, wrap,
direction/reverse, degenerate N≤1).
- `tests/test_clips.py` (extend) — `load_ring`, serializers.
- `tests/test_simulator_api.py` (extend) — `/api/ring`, `/api/ring/advance`.
**Docs:**
- `docs/ROADMAP.md` — scale-ring status.
- `docs/USER_GUIDE.md` — simulator "scale ring" gesture (if present).
---
## Task 1: Engine — `player/ring.py` (pure ring navigation)
- [ ] TDD `Scale`/`Transition`/`ScaleRing` construction + validation (N edges for N scales; ≥1 scale; degenerate N≤1 ring).
- [ ] TDD `scale_at` (mod indexing) and `advance_ring(ring, from_index, delta)``RingMove` (to_index by mod; per-unit `TransitionStep` list; `reversed` on outward; `wrapped` flag on crossing the seam; chained steps for |delta|>1).
## Task 2: Simulator data layer — `load_ring` + serializers
- [ ] TDD `load_ring(manifest)` parsing the `ring` section into a `ScaleRing`.
- [ ] TDD `ring_to_dict` / `ring_move_to_dict`.
## Task 3: API — `/api/ring`, `/api/ring/advance`
- [ ] TDD `GET /api/ring` (scales + transitions, with each scale's title) and `POST /api/ring/advance` (validates from_index/delta; returns the move + target clip_id).
## Task 4: Media — cosmos + abyss bases + transitions; manifest
- [ ] Add cosmos + abyss clips and the `ring` section to the committed manifest with true-PD provenance.
- [ ] Extend the media-setup script to generate cheap base + transition placeholders (and optional real PD fetch).
## Task 5: Frontend — endless encoder + multi-clip + transition playback
- [ ] Active-clip selection by ring index; zoom-in/out buttons + wheel; current-scale readout; play returned transition file(s), then settle on target clip; keep live alteration on top.
## Task 6: Verify + docs
- [ ] Full `pytest -q` green; boot the sim and exercise the ring by eye/screenshot.
- [ ] Update ROADMAP / USER_GUIDE.