From 58b7feddc8177f3f8ed8c962730e80b6c7cea404 Mon Sep 17 00:00:00 2001 From: Timmy Release Bot Date: Sat, 10 Oct 2026 12:11:11 +0000 Subject: [PATCH] ci: expire demo captions and span the whole recording in the contact sheet Two defects found inspecting the first demonstration of this release: - A caption stayed on screen until the next one replaced it, so the delete-data caption was still displayed over the home view after the navigation. Captions now fade and remove at the end of their stated duration, so no caption outlives the screen it describes. - The contact sheet used a fixed 1/2 fps, so a 88s demo only showed its first 24s in the review sheet. The sheet rate is now derived from the probed duration and a missing sheet fails the build. --- scripts/build_release.py | 8 +++++++- scripts/record_release_demo.mjs | 10 ++++++++++ tests/release-demo.test.js | 12 ++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) 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/); +});