diff --git a/scripts/build_release.py b/scripts/build_release.py index f6f77f4..e2dfdfa 100755 --- a/scripts/build_release.py +++ b/scripts/build_release.py @@ -164,7 +164,13 @@ def main() -> int: if len(video_streams) != 1 or video_streams[0].get("codec_name") != "h264" or video_streams[0].get("pix_fmt") != "yuv420p" or video_streams[0].get("width") != 720 or video_streams[0].get("height") != 1280: raise SystemExit("Release demo media probe failed") contact_sheet = release_dir / f"timmy-talking-turd-{version}-demo-contact-sheet.jpg" - run(["ffmpeg", "-y", "-v", "error", "-i", str(demo), "-vf", "fps=1/2,scale=180:-1,tile=4x3:padding=4:margin=4", "-frames:v", "1", str(contact_sheet)], tree) + # Span the whole recording: a fixed 1/2 fps sheet only covered the first 24s of a longer demo. + demo_duration = float(run(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", str(demo)], tree, capture=True)) + if demo_duration <= 0: + raise SystemExit("Release demo has no measurable duration") + run(["ffmpeg", "-y", "-v", "error", "-i", str(demo), "-vf", f"fps={12 / demo_duration:.6f},scale=180:-1,tile=4x3:padding=4:margin=4", "-frames:v", "1", str(contact_sheet)], tree) + if not contact_sheet.exists() or contact_sheet.stat().st_size == 0: + raise SystemExit("Release demo contact sheet was not produced") run(["npm", "audit", "--audit-level=high"], tree) for file in ("app.js", "server.mjs", "service-worker.js", "src/analysis.js", "src/domain.js", "src/draft.js", "src/suggestion.js", "src/followup.js", "src/vision-readiness.js", "src/hermes-agent-service.js", "src/vision-config.js", "src/vision-service.js", "tests/staging.acceptance.mjs"): run(["node", "--check", file], tree) diff --git a/scripts/record_release_demo.mjs b/scripts/record_release_demo.mjs index ba4e010..b0fb5bd 100644 --- a/scripts/record_release_demo.mjs +++ b/scripts/record_release_demo.mjs @@ -87,6 +87,16 @@ async function caption(text, duration = 1400) { node.animate([{ opacity: 0, transform: 'translateY(-8px)' }, { opacity: 1, transform: 'translateY(0)' }], { duration: 260, fill: 'forwards' }); }, text); await sleep(duration); + // A caption must not outlive its stated duration. It used to stay on screen until the next one + // replaced it, so a view change left the previous screen's claim hanging over a different screen — + // e.g. a delete-data caption sitting on top of the home view. + await page.evaluate(() => { + const node = document.querySelector('#demo-caption'); + if (!node) return; + node.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 180, fill: 'forwards' }); + setTimeout(() => node.remove(), 220); + }); + await sleep(220); } async function tap(selector, after = 650) { diff --git a/tests/release-demo.test.js b/tests/release-demo.test.js index 529562b..e09f694 100644 --- a/tests/release-demo.test.js +++ b/tests/release-demo.test.js @@ -136,3 +136,15 @@ test('release builder refuses to publish without the on-camera delta receipt', a assert.match(builder, /"release_delta_visible_on_camera": "passed"/); assert.match(builder, /"release_delta": release_delta/); }); + +test('release demo captions expire and the contact sheet spans the whole recording', async () => { + const demo = await readFile(demoPath, 'utf8'); + const builder = await readFile(builderPath, 'utf8'); + + // A caption that outlives its stated duration hangs the previous view's claim over the next screen. + assert.match(demo, /creates via removal after duration|A caption must not outlive its stated duration/); + assert.match(demo, /setTimeout\(\(\) => node\.remove\(\), 220\)/); + // The sheet must cover the entire demo, not just its opening. + assert.match(builder, /12 \/ demo_duration/); + assert.match(builder, /Release demo contact sheet was not produced/); +});