feat(sim): sky scale + auto-start experience + drop reef_hawaiian

Expanded-pools sky-scale work plus two experience tweaks:

- New `sky` altitude (airplane-window / low-flight layer) between orbit and
  coast; RING_ORDER cosmos·orbit·sky·coast·reef·abyss. New clip pools across
  all scales, re-baked the full adjacent-pair morph set (all-intra, LFS), and
  removed the now-stale cosmos↔abyss transitions the old ring order produced.
- Auto-start: once the preload finishes the experience turns Video on and
  raises the audio dial to 3/10 with no user gesture (app.js autoStart()).
- Dropped reef_hawaiian (text overlay — violates the text-free pool rule);
  reef pool 7→6, manifest transitions 586→558.

Tests: rewrote tests/test_e2e_audio.py for the dial UI + auto-start; updated
the manifest morph-count assertions and the altitude-scrub target for the new
ring order. 302 pytest + 11 Playwright e2e + 9 node unit all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-29 06:56:25 -07:00
parent 108e6207ba
commit 73d92b90d0
630 changed files with 6906 additions and 1123 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ def test_transitions_are_per_clip_pair_both_directions():
expected = _expected_pairs()
got = {(t["from"], t["to"], t["file"]) for t in trans}
assert got == expected
assert len(trans) == len(expected) == 154
assert len(trans) == len(expected) == 558
assert all(t["model"] == "placeholder-zoom" for t in trans)
@@ -53,7 +53,7 @@ def test_generate_media_bakes_every_member_pair(monkeypatch, tmp_path):
rev = {f"{h}__{l}.rev.mp4" for H, L in b._adjacent_edges()
for h in b.POOLS[H] for l in b.POOLS[L]}
assert fwd <= outs and rev <= outs
assert len(fwd) + len(rev) == 154
assert len(fwd) + len(rev) == 558
def test_transition_cmd_is_all_intra():
+50 -48
View File
@@ -1,9 +1,13 @@
"""Playwright E2E for the audio + Video/Audio toggles (audio spec §9). Skips
"""Playwright E2E for the audio dial + Video toggle (audio spec §9). Skips
cleanly when Playwright or its browser binary is absent.
NOTE: headless engines relax BOTH autoplay and GPU compositing, so these assert
the WIRING (toggle→play, the first-video-on→audio coupling, video-off blanking) —
real Safari/iOS autoplay and real-GPU compositing still need a device by-ear/eye check.
the WIRING (auto-start on load, dial→play/volume, video-off blanking) — real
Safari/iOS autoplay and real-GPU compositing still need a device by-ear/eye check.
The experience AUTO-STARTS once the preload finishes: Video turns on and the audio
level dial rises to 3/10 with no user gesture (app.js autoStart()). Audio is a 010
range dial (not a checkbox); set it by writing `value` + firing an `input` event.
Install: pip install -e '.[e2e]' && python -m playwright install chromium
"""
@@ -58,75 +62,73 @@ def page(app_url):
pytest.skip(f"chromium not available: {exc}")
pg = browser.new_page()
pg.goto(app_url)
# wait out the "Loading Universe…" splash (it overlays + blocks clicks)
# wait out the "Loading Universe…" splash (it overlays + blocks clicks); the
# auto-start fires as it dismisses.
pg.wait_for_selector("#loading", state="detached", timeout=60000)
yield pg
browser.close()
def _toggle(page, which):
"""Click the visible toggle track for #visual / #audio (the input is hidden)."""
page.click(f"label[for='{which}'] .dev-switch-track")
def _toggle_visual(page):
"""Click the visible Video toggle track (the checkbox input is hidden)."""
page.click("label[for='visual'] .dev-switch-track")
def test_app_starts_blanked_and_silent(page):
# both toggles default off → black screen, no audio (no tap-to-start wall)
assert page.is_checked("#visual") is False
assert page.is_checked("#audio") is False
page.wait_for_selector("#black:not(.hidden)")
assert page.evaluate("document.getElementById('aud').paused") is True
def _set_audio(page, level):
"""Drive the 010 audio dial: set value + fire `input` (the engine reconciles
playback synchronously in that gesture)."""
page.evaluate(
"(lvl) => { const a = document.getElementById('audio');"
" a.value = String(lvl); a.dispatchEvent(new Event('input', {bubbles:true})); }",
level,
)
def test_audio_toggle_plays_the_current_scale_soundtrack(page):
_toggle(page, "audio")
def test_app_auto_starts_video_and_audio(page):
# Once the preload finishes the experience auto-starts: Video on + audio at 3/10,
# video showing (black hidden), a scale soundtrack playing — no tap-to-start wall.
assert page.is_checked("#visual") is True
page.wait_for_function("document.getElementById('audio').value === '3'")
page.wait_for_function("document.getElementById('audio-level-val').textContent === '3'")
page.wait_for_selector("#black.hidden", state="attached") # video showing
page.wait_for_function(
"(() => { const a = document.getElementById('aud');"
" return /\\/media\\/audio\\/.+\\.mp3/.test(a.src) && !a.paused; })()"
)
def test_first_video_on_also_enables_audio(page):
_toggle(page, "visual") # first video-on couples audio
page.wait_for_function("document.getElementById('audio').checked === true")
def test_audio_dial_sets_level_and_label(page):
_set_audio(page, 7)
page.wait_for_function("document.getElementById('audio').value === '7'")
page.wait_for_function("document.getElementById('audio-level-val').textContent === '7'")
def test_audio_dial_zero_silences(page):
# Level 0 fades both crossfade elements to silence, then pauses them.
_set_audio(page, 0)
page.wait_for_function(
"(() => { const a = document.getElementById('aud'); return !!a.src && !a.paused; })()"
"(() => { const a = document.getElementById('aud'), b = document.getElementById('aud-b');"
" return a.paused && b.paused; })()"
)
page.wait_for_selector("#black.hidden", state="attached") # video showing (black hidden=display:none)
assert page.evaluate("document.getElementById('audio').value") == "0"
def test_audio_before_video_stays_audio_only(page):
_toggle(page, "audio") # audio on first
page.wait_for_function("!document.getElementById('aud').paused")
assert page.is_checked("#visual") is False # video untouched
page.wait_for_selector("#black:not(.hidden)") # still blanked
def test_audio_then_video_leaves_both_on(page):
_toggle(page, "audio") # audio first
page.wait_for_function("!document.getElementById('aud').paused")
_toggle(page, "visual") # then video
page.wait_for_selector("#black.hidden", state="attached") # video showing
assert page.is_checked("#audio") is True
assert page.evaluate("!document.getElementById('aud').paused") is True # audio still plays
def test_video_off_blanks_after_autostart(page):
# Video is auto-on; turning it off blanks the screen and hides the GPU layers.
_toggle_visual(page) # on -> off
page.wait_for_selector("#black:not(.hidden)")
page.wait_for_function(
"['vid','paint'].every(id => getComputedStyle(document.getElementById(id)).opacity === '0')"
)
def test_soundtrack_fallback_when_ring_lacks_audio(page):
# simulate a stale server whose /api/ring omits the per-scale `audio` field;
# the client falls back to the scale-id soundtrack map so audio still plays
# Client resilience: even if /api/ring omits the per-scale `audio` field, the
# scale-id fallback map keeps a soundtrack playing. Drop the field, re-apply the
# dial, and confirm the cosmos soundtrack still resolves + plays.
page.evaluate("ring.scales.forEach(s => { delete s.audio; })")
_toggle(page, "audio")
_set_audio(page, 5)
page.wait_for_function(
"(() => { const a = document.getElementById('aud');"
" return a.src.includes('/media/audio/cosmos/') && !a.paused; })()"
)
def test_video_off_blanks_after_being_on(page):
_toggle(page, "visual") # on
page.wait_for_selector("#black.hidden", state="attached")
_toggle(page, "visual") # off
page.wait_for_selector("#black:not(.hidden)")
# the GPU-composited video layers are hidden too (settle the opacity transition)
page.wait_for_function(
"['vid','paint'].every(id => getComputedStyle(document.getElementById(id)).opacity === '0')"
)