Commit Graph

209 Commits

Author SHA1 Message Date
BenStullsBets 5382995931 feat(sim): play chosen morph, lock clip per altitude, drop secondary swap
Task 7. advance() threads the current clip as from_clip_id, plays the
server-resolved chained morphs (each already lands on its destination clip),
then locks activeClipId until the next altitude change — no fade-to-black
secondary swap, no re-roll while parked. Builds a (from->to) morph lookup
from the ring; preloads only the morphs reachable from the locked clip,
refreshed per landing (154 morphs is too many to preload up front). Drops
reverseFile + fadeThroughBlack.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:15:03 -07:00
BenStullsBets 7855718f74 feat(ring,api): pick destination clip then resolve matching chained morph
Tasks 3-6. Transition is now a directed clip-pair morph; ScaleRing.morph_for
looks up (from,to)->file. advance_ring returns structural steps (no file);
resolve_move picks each crossed altitude's clip THEN its morph, chained, and
reports the locked target. Fast spin keeps the whole chain (blended) instead
of collapsing. /api/ring/advance takes from_clip_id, draws picks, returns the
resolved chain. Dropped ring_move_to_dict + _rev_file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:12:25 -07:00
BenStullsBets cc469b5298 feat(pipeline): per-clip-pair morph manifest + member-pair baking (154 clips)
Tasks 1-2 of the altitude clip-pair morph plan. Manifest emits a morph
entry for every video pair in adjacent altitudes, both directions
(<src>__<dst>.mp4 zoom-in + .rev zoom-out); generate_media() bakes them
all via the existing xfade=zoomin recipe keyed by clip member.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:04:29 -07:00
BenStullsBets 3b3d30e8e0 plan(0024): add zoom-out E2E case; resolve review-gate open items
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:17:42 -07:00
BenStullsBets 441645ae7a plan(0024): altitude clip-pair morph + lock per altitude
Choose the destination clip before transitioning, play the matching
member->member morph (154 clips, all adjacent video pairs, both
directions), chain through each crossed altitude, and lock the clip
until the altitude is changed (no secondary swap, no re-roll).

Session 0024 (writing-plans). Stops at the review gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:09:06 -07:00
BenStullsBets 6427ab4a49 chore(audio): tuck audio diagnostics (status + native player) under Dev Mode
The 'no sound' debugging is resolved (stale-server resilience). Move the audio
status readout + native test player into the Dev Mode panel — kept for future
'can't hear' debugging, out of the installation UI. 7 E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 18:58:22 -07:00
BenStullsBets 56d08d5c60 fix(video): client tolerates a stale server (old /api/alteration shape + payload)
The operator's video wouldn't turn on: their stale uvicorn (old Python) requires a
7-way 'content' and 422s the {visual,audio} payload, and returns {content:{video}}
not {render:{video:{shown}}}. So the alteration POST failed and video never updated
(audio works because it's client-driven). Fix: controls() also sends a back-compat
'content' field (current server ignores it; old one ignores visual/audio), and
update() reads video-shown from either response shape. Verified end-to-end against a
simulated fully-stale server: video shows + audio plays, zero errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:46:49 -07:00
BenStullsBets 97d2ddd573 fix(audio): client soundtrack fallback when /api/ring lacks audio (stale server)
Root cause of 'no sound', found via the live readout: the operator's /api/ring
returned the ring WITHOUT the per-scale audio field (a server process predating the
audio code) → soundtrackUrl() was null → the element never got a src (readyState 0,
no error). The native test player proved the file itself plays. Fix: client falls
back to a scale-id→soundtrack map (mirrors build_pool_manifest.SCALE_AUDIO) so audio
works without a server restart. Verified: stale-ring sim now plays; audio-first-then-
video leaves both on. +2 E2E regression tests (7 total). Chromium + WebKit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:42:12 -07:00
BenStullsBets 56e352fe65 diag(audio): status shows resolved url + ring source + scale (pinpoints null soundtrack url)
The operator's 'PAUSED readyState 0' (no error) means soundtrackUrl() returned null
— the running app's /api/ring has no audio for the scale, the signature of a STALE
uvicorn server (Python predates the audio code; static reload doesn't restart it).
Status now shows url=<…|NONE>, ring=server|SYNTH, scale id so it's unambiguous.
Fresh server verified: ▶ playing rs=4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:34:16 -07:00
BenStullsBets 2564168ac3 Merge origin/main (0022 transcript) into 0023 diagnostics 2026-06-26 13:28:37 -07:00
BenStullsBets deb7c34df9 Merge session-0023: audio diagnostics (status readout + native test player) 2026-06-26 13:28:19 -07:00
BenStullsBets 625d32c0f3 diag(audio): live audio-status readout + native test player; stop swallowing play errors
Three blind fixes failed because headless browsers relax autoplay (can't reproduce)
and play() errors were swallowed. Instrument instead:
- #audio-status: live readout (playing/paused/blocked/media-error) under the toggle
- surface the play() rejection reason + element error (was .catch(()=>{}))
- 'Audio check' fieldset with a native <audio controls> on the soundtrack file, to
  test the file directly with zero app code in the path
Diagnostic only — no behavior change. 37 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:28:17 -07:00
BenStullsBets 58a6ee4644 add sessions/0022/SESSION-0022.0-TRANSCRIPT-2026-06-26T08-40--2026-06-26T08-50.md + replace placeholder/variant SESSION-0022.0-TRANSCRIPT-2026-06-26T08-40--INPROGRESS.md 2026-06-26 13:20:59 -07:00
BenStullsBets fd1da791bf claim human-experience-filter-art session 0022 (placeholder) + sessions.json entry 2026-06-26 13:20:20 -07:00
BenStullsBets 4ac39c9034 Merge session-0022: Loading Universe splash + Safari-safe single-element audio + video/audio coupling
Removes the tap-to-start wall (Loading Universe splash instead), fixes audio still
silent on real Safari (single <audio> played synchronously in the toggle gesture),
both toggles default off, first Video-on couples Audio. Verified Chromium + WebKit.
290 tests + 5 Playwright E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:19:39 -07:00
BenStullsBets 30ec0c9c26 fix(audio): Loading Universe splash; single-element in-gesture audio (Safari); first-video-on couples audio
Operator follow-ups:
- Remove the tap-to-start wall; show a 'Loading Universe…' splash until all media
  preloaded (main() awaits preloadAllMedia + a progress bar), then reveal the app.
- Both Video + Audio toggles default OFF (black + silent at start).
- FIX audio still silent on real Safari: the A/B-crossfade-after-gesture didn't
  unlock on Safari (priming srcless elements doesn't unlock them). Use ONE <audio>
  element played SYNCHRONOUSLY inside the toggle's click — the reliable Safari
  unlock; later altitude swaps reuse the unlocked element.
- First time Video turns on, also turn Audio on (one flip = full experience), set+
  played in the same gesture; Audio-on-first stays audio-only.
- Soundtrack follows Altitude via the single element (brief fade swap).
- Verified in headless Chromium AND WebKit: coupling works, soundtrack plays,
  video-off blanks, zero JS errors. 290 tests + 5 Playwright E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:19:15 -07:00
BenStullsBets 3d46b96ecb add sessions/0021/SESSION-0021.0-TRANSCRIPT-2026-06-26T08-12--2026-06-26T08-20.md + replace placeholder/variant SESSION-0021.0-TRANSCRIPT-2026-06-26T08-12--INPROGRESS.md 2026-06-26 12:57:38 -07:00
BenStullsBets 3b19d3eb92 claim human-experience-filter-art session 0021 (placeholder) + sessions.json entry 2026-06-26 12:56:57 -07:00
BenStullsBets fddb6d65d4 Merge session-0021: off/on Audio+Video toggles; Safari autoplay + GPU video-off fixes
Operator-driven revision of the audio feature: Dev-Mode-style Video/Audio toggles,
white-noise deferred (Audio = off/soundtrack), and fixes for two real-browser bugs
(video-off not blanking GPU-composited layers; no soundtrack on Safari/iOS).
Verified in headless Chromium + WebKit. 288 tests + 3 Playwright E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:56:20 -07:00
BenStullsBets 2722949805 fix(audio): off/on Audio + Video toggles; fix Safari autoplay + GPU video-off
Operator-driven live-UI revision:
- Video + Audio are now Dev-Mode-style on/off toggles (Audio on = soundtrack);
  white-noise deferred (removed from the live control; pure machinery kept).
- FIX video-off not blanking: the <video>/<canvas> are GPU-composited and could
  paint above the auto-z #black on a real GPU — give #black z-index + its own
  layer AND hide the video layers (opacity 0) on video-off.
- FIX no soundtrack on Safari/iOS: Safari unlocks <audio> only when play() runs
  INSIDE the user gesture; the start gesture now primes both A/B elements
  synchronously, so the async crossfades are allowed to play.
- Toggles wired on 'change' (matches the Dev Mode switch).
- player/audio.py AUDIO_SOURCES -> {off, soundtrack}; tests + E2E updated.
- Verified in headless Chromium AND WebKit: audio plays, video-off blanks both
  layers, zero JS errors. 288 tests + 3 Playwright E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 12:56:07 -07:00
BenStullsBets d446238b79 add sessions/0020/SESSION-0020.0-TRANSCRIPT-2026-06-26T06-47--2026-06-26T07-58.md + replace placeholder/variant SESSION-0020.0-TRANSCRIPT-2026-06-26T06-47--INPROGRESS.md 2026-06-26 08:06:49 -07:00
BenStullsBets c51fdc422a update sessions/0020/SESSION-0020.0-TRANSCRIPT-2026-06-26T06-47--INPROGRESS.md 2026-06-26 08:06:25 -07:00
BenStullsBets aa76cd1c9f Merge session-0020: Audio + separated Visual/Audio controls
Splits bundled Content into orthogonal Visual×Audio; per-altitude soundtracks +
synthesized pink white-noise; server-resolved render.audio; client A/B crossfade
layer; control-surface spec reframed. 288 tests + 3 Playwright E2E pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 08:02:45 -07:00
BenStullsBets c07067492b test(audio): Playwright E2E (3 tests, skip-if-no-browser) + client robustness
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>
2026-06-26 08:00:44 -07:00
BenStullsBets fd0aad6610 docs(audio): reframe control-surface spec content->visual+audio; record §12 decisions + roadmap
- networked-control-surface §3/§4/§5/§6: Content -> Visual + Audio; transitions.content
  -> transitions.visual; render.content -> render.video + render.audio; SSE notes
- audio spec §12: resolved (audio=live ~0.6s; noise=pink; tap-to-start overlay);
  status -> in implementation
- ROADMAP: session-0020 audio increment; music layer deferred

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:50:34 -07:00
BenStullsBets 68f9d7342a feat(audio): client — split Content into Visual+Audio controls + audio crossfade layer
- index.html: Content <select> -> Visual toggle + Audio source selector; A/B <audio>
- app.js: controls() emits visual+audio; update() posts altitude_index and reads
  render.video/render.audio; A/B gain-crossfade applyAudio(); one-time tap-to-start
  gesture (autoplay policy); mediaUrl() passes absolute /media urls through
- style.css: #start-gesture overlay
- smoke: page serves controls + audio elements; soundtrack url couples to altitude;
  noise + soundtrack media serve 200

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:48:03 -07:00
BenStullsBets 29c6d822bd feat(audio): Visual×Audio Python contract — resolver, controls, state, API
- player/audio.py: pure resolve_visual / resolve_audio_url / resolve_audio (§4/§6)
- retire player/content.py (7-way table) + its test
- player/controls.py: serial contract content -> visual + audio
- player/state.py: Playback carries video:bool + audio_source:str
- simulator/app.py: /api/alteration validates visual+audio, returns render.video
  + server-resolved render.audio.url from altitude_index; NOISE_URL constant
- tests reframed; full suite 285 passed, 2 skipped

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:46:22 -07:00
BenStullsBets 1f9b2a0bec feat(audio): Scale.audio field + per-scale soundtrack wiring through the manifest
Scale gains audio:str='' (back-compat). clips.py reads/serializes it; ring_to_dict
exposes it; build_pool_manifest emits SCALE_AUDIO per scale. Manifest regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:42:40 -07:00
BenStullsBets 39fb9108db feat(audio): ffmpeg production pass — loop/loudnorm builders, runner, build script
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>
2026-06-26 07:41:34 -07:00
BenStullsBets 062a283de4 docs(plan): audio+video separated Visual/Audio controls implementation plan
Session 0020. Anchored on the audio-video-separated-controls Solution Design.
Scope: build into the current single-page simulator (simulator-first directive);
reframe the unbuilt control-surface spec on paper. 10 tasks, all testing tiers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 07:01:06 -07:00
BenStullsBets adc39fe648 claim human-experience-filter-art session 0020 (placeholder) + sessions.json entry 2026-06-26 06:48:09 -07:00
BenStullsBets 99a751cb46 Merge origin/main (session 0018/0019 transcripts) into audio-controls merge
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 06:39:28 -07:00
BenStullsBets 6f2d1c4f7b Merge feat/audio-video-controls: audio soundtracks + separated audio/visual controls spec
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 06:38:25 -07:00
BenStullsBets a3ec09af7e feat(audio): per-altitude soundtracks + separated audio/visual controls spec
Sound for the experience, plus the design to control it.

Soundtrack layer (ambience), all license-clean (PD/CC0), recorded in
docs/audio-candidate-pool.md (media gitignored; re-sourceable):
- cosmos: Pillars of Creation sonification (NASA, PD)
- orbit:  station room-tone hum (CC0)
- coast:  sea waves + terns (CC0)
- reef:   6-layer NOAA soundscape + CC0 submerged bed (PD + CC0)
- abyss:  blue-whale moan (NOAA, PD)

Solution Design (proposed): split the bundled Content selector into two
orthogonal dials, Visual (on/off) x Audio (off/soundtrack/white-noise),
unlocking white-noise+video. Soundtrack follows the single Altitude dial;
white noise is altitude-independent. Reframes the networked-control-surface
spec SS3/SS5 (still proposed/unbuilt -> clean pre-impl edit). Music layer deferred.

Dev panel: links to the clip + audio review galleries; sticky stage/panel layout.
gitignore: audio media, review_audio.html, .DS_Store.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 06:37:56 -07:00
BenStullsBets e0b29c032c add sessions/0018/SESSION-0018.0-TRANSCRIPT-2026-06-25T09-42--2026-06-26T05-56.md + replace placeholder/variant SESSION-0018.0-TRANSCRIPT-2026-06-25T09-42--INPROGRESS.md 2026-06-26 05:57:52 -07:00
BenStullsBets 1b96e7065b add sessions/0019/SESSION-0019.0-TRANSCRIPT-2026-06-26T04-47--2026-06-26T05-47.md + replace placeholder/variant SESSION-0019.0-TRANSCRIPT-2026-06-26T04-47--INPROGRESS.md 2026-06-26 05:48:00 -07:00
BenStullsBets f28a06cc7f Merge feat/networked-control-surface: networked control surface spec (session 0019) 2026-06-26 05:46:25 -07:00
benstull 229af60f1f Merge pull request 'feat(content): add cosmos_miri pool member (Carina mid-infrared pan)' (#25) from feat/cosmos-miri-pool-member into main
feat(content): add cosmos_miri pool member (Carina mid-infrared pan) (#25)
2026-06-26 12:39:34 +00:00
BenStullsBets c389e2a834 docs(spec): networked control surface (renderer/controller split) — session 0019
v1 hardware step: split the simulator into a laptop RENDERER and a separate
touch CONTROLLER over wifi/LAN. Server-authoritative SessionState + SSE push;
one /api/control contract any "control source" speaks (iPad web twin now,
Arduino later). Live knobs (Left/Right/Mood) vs transitional controls
(Altitude, Content) with per-knob transition state + renderer settled-ack.

Reframes ROADMAP sub-project 4 and absorbs sub-project 3's serial-input slice.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 05:39:12 -07:00
BenStullsBets 32e74ca446 fix(sim): make sim-local use the venv interpreter
`make sim-local` called bare `python`, which isn't on this box's PATH (only
python3 / .venv/bin/python exist) — so it failed instantly and "couldn't connect"
to the server. Resolve PYTHON to the venv interpreter, falling back to
python3 then python.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 05:28:02 -07:00
BenStullsBets e3a9b7cff6 feat(content): add cosmos_miri pool member (Carina mid-infrared pan)
Operator wanted more Cosmic Cliffs footage and a different-color rendering.
Keeps the NIRCam flythrough as the cosmos primary and adds ESA/Webb weic2205c
("Pan of the Carina Nebula, combined NIRCam + MIRI") as a new cosmos pool
member — the same Carina towers in the muted gray/teal/gold MIRI mid-infrared
palette, distinct from the iconic orange/blue flythrough.

- POOLS["cosmos"] grows 4 -> 5 (cosmos, cosmos_miri, galaxies, hudf, xdf);
  primary unchanged.
- META + LABELS authored (nebula/star/distance, salience-gated); affect inherits
  the cosmos scale vocabulary.
- manifest.json regenerated (19 -> 20 clips).
- Source: 4K master, text-free/human-free, trim 4-26s (clears the fade-from-
  black), 1 s crossfade-loop (~21 s) -> 1080p base + 4K master (gitignored).
- docs/content-candidate-pool.md updated (cosmos pool of 5 + sourcing note).

271 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 05:07:13 -07:00
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
BenStullsBets 2eb752b5bc fix(sim): content-hash media URLs + fade pool-landing clip swap
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>
2026-06-25 13:03:56 -07:00
benstull aa56dfe145 Merge pull request 'fix(sim): bust HTTP cache on media preload so re-baked clips show' (#22) from fix/preload-cache-bust into main 2026-06-25 19:40:50 +00:00
BenStullsBets ed267c554c fix(sim): bust HTTP cache on media preload so re-baked clips show
The preload fetched each clip with the default HTTP cache, so a clip re-baked
under the same path (e.g. the cosmos base re-sourced to the Webb Cosmic Cliffs
flythrough) kept showing stale footage after a reload — worst case forever, when
a pre-`no-cache` build had pinned it `immutable, max-age=1y` (a plain reload never
revalidates an immutable entry). Fetch the preload with `cache: "reload"`, which
bypasses the HTTP cache, pulls fresh bytes, and updates the cache entry; the
in-memory blob cache still gives instant in-session altitude swaps. One fetch per
(re)load, so no steady-state cost.

269 passed, 2 skipped. app.js syntax-checked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:40:34 -07:00
benstull 7da7446740 Merge pull request 'feat(sim): real ring zoom transitions + Dev Mode + cosmos → Webb Cosmic Cliffs' (#21) from feature/ring-transitions-real into main 2026-06-25 19:31:22 +00:00
BenStullsBets d24f829276 feat(content): swap cosmos primary to the Webb "Cosmic Cliffs" flythrough
Operator found the Orion SVS 30957 flythrough merely good and asked for a more
dramatic nebula. Replaces the cosmos-scale primary with STScI's "Exploring the
Cosmic Cliffs in 3D" (Carina Nebula, Webb NIRCam — NASA SVS 31348), sourced from
its 4K master (Clifs-3d-STScI.mp4, 3840x2160) via the content pipeline:
trim 10-34s (clears the title card ~8s and end-credit card ~102s) + crossfade-loop
-> 4K master + supersampled 1080p base (5.5 Mbps, crisper than the old 2.9 Mbps
1080p-native Orion encode).

- build_pool_manifest META: cosmos -> SVS 31348 Cosmic Cliffs, new CCBY_WEBB
  credit (NASA, ESA, CSA, STScI). Labels retuned for the Carina framing
  (emission nebula / protostar; redshift -> distance ~7,500 ly).
- Regenerated manifest.json (also corrects stale JPL-Orion provenance left when
  129bb23 updated META but not the manifest) + all ring-edge transitions from the
  new base (cosmos-orbit / abyss-cosmos now zoom from the Cosmic Cliffs).
- docs/content-candidate-pool.md: record the swap + history.

269 passed, 2 skipped. New base verified text-free + full-frame by-eye across the
loop; final look to be confirmed by the operator (no Chrome on the box).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:48:44 -07:00