E2E asserts in a real headless Chromium: white_noise loads the noise asset,
soundtrack loads the current scale's asset, and Visual=off fades video while
audio keeps playing. Driving it out surfaced + fixed three client issues:
- fadeVolume uses setInterval (rAF stalls when the tab is backgrounded)
- start gesture drops srcless priming (a click grants Chrome autoplay
document-wide; priming empty <audio> only hung the handler / polluted state)
- applyAudio doesn't await play() (awaiting a srcless/buffering play can hang)
pyproject: [e2e] optional dep (pytest-playwright). Full suite 288 passed, 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure arg builders (audio_ops) + thin runner (audio_run) + build_audio_media.py.
Produces the 5 normalized soundtrack loops + a synthesized pink-noise bed under
the gitignored sample_media/audio tree. 6 tests pass (white-noise integration
runs against bundled imageio_ffmpeg).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Two fixes to the simulator's media handling, both surfaced by the cosmos clip swap.
1. Permanent cache-bust via content hash. New `GET /api/media-versions` returns a
short sha1 token per served file (clip bases + ring transitions + reverses),
cached by (mtime, size) so a re-bake is picked up without a restart. The client
fetches it at boot and appends `?v=<hash>` to every /media URL, so a clip
re-baked under the same path (e.g. a re-sourced cosmos base) gets a fresh URL
the browser can't serve stale — fixing the recurring stale-clip problem at the
root (supersedes the `cache: "reload"` belt-and-suspenders, which stays).
2. Fade the pool-landing swap. The baked ring transition zooms into the scale
PRIMARY, but the rotating pool picks a random member on landing — so a
non-primary pick hard-cut from the primary to the chosen clip (e.g. coast
birdrock -> surfgrass, the artifact the operator saw). On landing, when the pick
differs from the primary, mask the swap behind a short fade through the existing
#black overlay (CSS opacity transition); same-clip landings still do a plain
(re)load. General across all pooled scales.
271 passed, 2 skipped (+2: media-version content-hash + absent-file omission).
app.js syntax-checked; endpoint verified live (29 files; token matches a manual
sha1 of the bytes).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `immutable, max-age=1y` media header pinned stale footage in the browser for
a year, so a re-sourced or re-cropped clip (e.g. the de-captioned cosmos) never
showed without a manual cache clear. Switch /media to `no-cache` (store, but
revalidate every load): instant altitude swaps already come from the in-memory
blob preload, so the HTTP cache only matters on (re)load — where a conditional
request is a cheap 304 when unchanged but picks up edited clips immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each altitude change reset the single <video> element's src and re-fetched
the scale's footage over the network, compounded by the dev no-store policy
applying to /media too — so every scale change paid a full fetch+decode.
- simulator/app.py: split the cache middleware — app shell/JS/CSS stay
no-store (by-eye dev iteration), but /media is now cacheable
(public, max-age, immutable) since the clips are static.
- simulator/static/app.js: preload every ring clip (19 base + 5 transition,
~350 MB) into in-memory blob object URLs at startup — non-blocking, the
first scale plays immediately while the rest stream in, ordered current
scale outward. mediaUrl() serves the cached blob when ready, else falls
back to the network. A small progress chip self-removes when done.
- tests: assert app assets stay no-store and /media is cacheable.
269 passed, 2 skipped. Frontend verified by-eye by the operator (no headless
browser in repo yet).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content-pipeline Increment 2, part 2 (the tooling). Implements design §11.5
(docs/superpowers/specs/2026-06-24-content-pipeline-design.md):
- tools/pipeline/track.py — the stage-5 geometry pass. Classical path: a
hand-seeded normalized box propagated by OpenCV Lucas-Kanade optical flow
(CSRT used instead when a contrib build provides it), sampled to a SPARSE
loop-normalized keyframed track + an appear/disappear window. Optional ML
detect+track path lazy-imports ultralytics (clear ImportError if absent;
base install needs only cv2). Pure helpers (normalize/denormalize, loop_t,
infer_window, sample_track, track_to_annotation) are unit-tested; the cv2
propagation is an opt-in integration test on real abyss_wow footage.
Semantics are never produced here — geometry only.
- Author mode — /author.html + author.js/.css reuse the preview stage: pick a
pool clip, scrub, drag a seed box, Run tracker (or Add as static box), author
the LEFT detail tiers (general -> scientific+fact) + salience, shift-click to
place affect anchors with RIGHT emotion tiers, and Save to manifest. Backend:
POST /api/author/track (runs track_seed on the clip's base) + POST
/api/author/clip (idempotent upsert via tools.pipeline.manifest — keeps media
+ provenance, replaces only authored content, reloads in place). The tracker
propagates box geometry; all strings/scientific names/facts are hand-typed.
- Repointed the pipeline integration test off the retired forest base to the
cosmos pool primary. USER_GUIDE simulator section brought current (pools,
coast, 3-knob Mood, real-time dream, progressive tiers, author mode).
267 passed / 2 skipped (+ track pure + opt-in real-footage + author endpoint
tests). Author UI by-eye review deferred to the operator (no Chrome on this box).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content-pipeline Increment 2, part 1 (the experience). Implements design §11
(docs/superpowers/specs/2026-06-24-content-pipeline-design.md):
- Rotating pool (§11.1): each ring scale references a POOL of vetted clips
(docs/content-candidate-pool.md); the player picks a random member on landing.
player.ring.Scale gains `pool` + pure `pick_clip_id(scale, r)` (injected
randomness); the pick happens at the API boundary (random.random()), a delta=0
advance is the initial/re-roll pick. clips.py parses/exposes the pool; the
client lands on move.target_clip_id. Back-compat: a scale with only clip_id is
a pool of one.
- Rename ring scale forest -> coast (new land<->sea scale, orbit..reef); ring
order cosmos -> orbit -> coast -> reef -> abyss -> wrap. Cleanup: dropped the
leftover local media (cosmos_traverse, orbit_westcoast, old forest/reef, the
Increment-1 orbit/abyss single bases); new coast-edge transition placeholders.
- Time-windowed tracked labels (§11.2): annotations gain appear/disappear
(loop-normalized, wraps across the seam); a label shows only while on screen
and its box tracks the subject. abyss + reef pools authored as test material.
- Progressive LEFT detail tiers (§11.3): per-object `salience` (first shows at
Left 5-salience) + tiered strings (general -> specific -> scientific -> +fact)
escalating with the Left knob; replaces flat min_level gating. Strings may be a
tier list or a static string (back-compat). No engine change (client reads
overlay.level).
- Progressive RIGHT emotion tiers (§11.4): affect vocabulary escalates basic ->
compound with the Right knob (dream.strength); appearance still gated by
min(left,right). No engine change.
New simulator/build_pool_manifest.py holds the hand-authored content and emits
the 19-clip pool manifest (the reproducible baseline). setup_scales_media.py
marked superseded. 251 passed / 4 skipped (+ ring pool + API pool/window tests).
By-eye visual review deferred to the operator (no Chrome on this box).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- run.py: add resolve_ffmpeg() mirroring setup_scales_media.py; change
process_clip ff param to None and resolve at call time
- test_pipeline_integration.py: gate on resolve_ffmpeg() not PATH ffmpeg;
verify proxy dims with cv2 instead of ffprobe
- ffmpeg_ops.py: add fps= to trim segments in crossfade_loop_args so xfade
receives CFR input (xfade rejects 1/0 rate from raw trim output)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the pre-baked SD restyle variants (too fluid — the painting re-rolled
every keyframe and the flow-warp melted frames) with a deterministic, real-time
dream that holds STILL across the loop; only the footage's own motion moves.
Design: docs/superpowers/specs/2026-06-22-right-axis-deterministic-dream-design.md
Engine (player/alteration.py): Restyle(variant) -> Dream(strength, intensity);
Calibration.right_variant_map -> dream_gain. player/state.py: a Right change is a
LIVE_UPDATE now, not a crossfade (only a clip swap crossfades). Plan dict
restyle -> dream. Tests migrated; 233 green. (No new Python behavior for the
client-render swap below.)
Renderer (simulator front-end): the Right dream is a WebGL2 Kuwahara shader on a
<canvas> over the live video — edge-preserving, so motion/structure stay crisp;
the dream is the same footage, just stylized (no blur). Ramped by the knob; max
goes "trippy" (vivid saturation + ~45deg hue drift + posterize, concentrated near
max via t=intensity^2). Phase-1 luminous-haze (CSS+bloom) was built then retired
by eye (blur read as out-of-focus video). Mood grade rides as a CSS filter on the
canvas; bloom layer removed.
Dev ergonomics that fell out of the iteration:
- /dev/version + a 1s client poll = live-reload (open tab never runs stale
renderer code; reloads on my edits AND server restarts).
- no-cache on the dev server; an on-screen error banner + crash-proof render so a
silent throw can't masquerade as "nothing is changing".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Left-brain HUD look-and-feel pass (judged by eye in the sim):
- corner-bracket targeting reticles (capped arms) replace plain boxes
- translucent label chips, legible over any frame
- two sensor channels: cyan detections (with deterministic confidence
scores) vs amber measurements (center crosshair)
- bbox-sized "ANALYSIS . L{n} . {k} OBJ" status tag, escalating with Left
Affect channel (emotions in the HUD), per
docs/superpowers/specs/2026-06-22-affect-channel-hud-design.md:
- surfaces only when BOTH knobs up; strength = min(left, right)
- stable per-scene palette, more words appear with combined strength
- soft glowing violet words on their own opacity layer (the dream
leaking into the machine's read); math in player/alteration.py
- forest/cosmos/abyss palettes in the manifest; 5 new unit tests
Simulator no-cache middleware so by-eye iteration never serves a stale
app.js / api response (fixes the latent "plan has affect but clip.affect
empty" stale-/api/clips bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right strength curve (level 1-4 -> increasing img2img keyframe strength) is pure
and unit-tested; the restyle engine is a verbatim port of the session-0008 POC
flow_restyle.py (SD img2img keyframes + Farneback optical-flow tweens = calm,
no boiling). torch/diffusers imported lazily so the module + curve test stay light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A multi-detent encoder spin previously chained one full transition per scale
crossed (e.g. +5 ≈ 5 placeholder morphs ≈ 12-15s), which feels sluggish for a
fast spin. Design §3 anticipates this: "transitions chain, or past a speed
threshold a faster blended pass is used."
`player.ring.advance_ring` gains an opt-in `fast_spin_threshold` (canonical
default `DEFAULT_FAST_SPIN_THRESHOLD = 3`). A spin batches its detents into one
advance() call, so `abs(delta)` is the input layer's proxy for spin speed; at or
above the threshold the move collapses to a single blended pass — one
`TransitionStep` (the arrival edge, marked `blended`, landing straight on the
destination, `RingMove.fast=True`) instead of the full chain. Landing index,
seam-crossing (`wrapped`), and arrival edge/direction are exactly the full
chain's; only the in-between transitions are dropped.
The policy is opt-in (default off) because the pure function cannot observe
wall-clock spin speed — the simulator/firmware, which can, enables it. This keeps
all existing complete-chain behavior and tests intact. The simulator passes the
default threshold and plays the blended step at 2.5× (FAST_BLEND_RATE); a
dedicated fast-blend clip can replace the accelerated arrival-edge placeholder
when real transition media exists.
- player/ring.py: TransitionStep.blended, RingMove.fast, threshold param + policy
- simulator/clips.py: serialize fast/blended in ring_move_to_dict
- simulator/app.py: apply DEFAULT_FAST_SPIN_THRESHOLD at /api/ring/advance
- simulator/static/app.js: play a blended step at FAST_BLEND_RATE
- docs: scales design §3, USER_GUIDE, ROADMAP slice-3 note
Tests: +9 (ring policy, serializer, API). Suite 224 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Tuning the look by eye in the simulator (design §8) surfaced that the dark
mood pole rendered a full-frame hue-rotate(-200deg) — rock orange, trees
purple: the disorienting look rejected in session 0008, not the peaceful POC
dark_frame. Replace it with darken + slight desaturate on the video filter
plus a multiply-blended deep-blue wash (#tint, below the SVG HUD so the
overlay stays legible). Dark now reads cool/somber with natural greens, like
the approved POC look; light/left/right unchanged and confirmed peaceful.
With full tilt now tasteful on every axis, LOCK DEFAULT_CALIBRATION to unity
gains + linear variant map as a deliberate by-eye choice (not placeholders),
closing the open session-0006 knob->strength decision: knobs run 0=off..4=max,
equal Dark/Light = identity, the 5 notches map 1:1 to the 5 discrete Right
bakes. Guard the locked constants with test_default_calibration_is_locked.
Resolves design §8 open questions (calibration curve shape; grade-vs-overlay
ordering) and records the dark-grade fix. Full suite 193 passed / 2 skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 13 / spec §10 test 8. Hermetic e2e: fake fetcher ->
ingest_candidate -> proposed record; approve flips it; reload + validate_catalog;
select(approved_only=True) finds it. Opt-in real-ffprobe/ffmpeg tests generate a
lavfi clip and assert mode/duration/resolution + dominant color, skipped when the
binaries are absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 12 / spec §8.2. Walks proposed records (fields +
coords + rationale + best-effort ffmpeg preview), prompts accept/edit/skip/quit,
and persists each approval via save_catalog rewrite. I/O seams (input_fn/now_fn/
out) and --no-preview make the walk testable hermetically; decision logic lives
in the tested review core.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 11 / spec §8.1. proposed_records filter and approve()
return an approved copy via dataclasses.replace (no in-place mutation), with
optional coordinate/rationale override and an injected reviewed_at timestamp.
Pure, no I/O, no clock.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 9 / spec §6.4. Three keyless first-ship fetchers
parse documented JSON APIs via an injected HttpClient; license/attribution go
through tools.licensing; NASA third-party + IA no-license cases are flagged in
notes for review. musopen/fma/freesound are explicit deferred stubs raising
NotImplementedError; freesound documents FREESOUND_API_TOKEN (secret, env-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 8 / spec §6. ingest_candidate dedupes, downloads,
mechanically tags, drafts coordinates, builds a proposed Record and appends it;
ingest_search loops over fetcher hits. All boundaries (download/prober/color_fn)
injectable, so the pipeline is fully hermetic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 7 / spec §7. HeuristicProposer seeds the brain plane
from archive priors and the mood plane from title/description keyword nudges,
clamps to 0..4, and emits a one-line rationale. Deterministic, no I/O, no ML
(honors design §11) — only a DRAFT a human reviews.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 6 / spec §5.3. normalize_license maps CC URLs /
identifiers and public-domain markers to the LICENSES vocab, builds non-empty
attribution for cc_by/cc_by_nc, and rejects unmappable licenses at ingest.
librivox_license() helper returns public_domain.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 5 / spec §5.2. ffmpeg-only dominant color (no image
library), opt-in by design; extract_frame for review previews. Runners injectable
so the unit suite never shells out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 4 / spec §5.1. derive_tags(Probe) -> Tags with the
attached_pic cover-art guard so an art-bearing audio file stays mode=audio.
Duration falls back to the longest stream when format.duration is absent, and
rounds half-up so 12.5s -> 13s (Python's round() would give 12 via banker's
rounding) to match the spec's stated behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 3 / spec §5.1. Parses ffprobe JSON into a Probe
(streams + format); subprocess runner injectable so tests never shell out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per sub-project-2 plan Task 2 / spec §3. Purely additive to hef.catalog;
no existing symbol changes. validate_catalog runs per-record validate() then
asserts id uniqueness; index_by_id gives by-id lookup the player also wants.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>