← Commit history
README.md+168−2
@@ -1,3 +1,169 @@-# Adom Video Post-Production+# video-post -Speed up the boring parts, record a voiceover, and publish straight to the wiki — the post-production studio for Adom demos.+A demo recording post-production CLI for the Adom platform.++## Why this exists++We are building a lot of demo recordings of Adom apps, skills, molecules, libraries, and datasheets. Every recording follows the same pattern:++1. Start a Hydrogen screen recording+2. Walk through a sequence of steps where Claude Code drives the workflow on the user's behalf (open a file, run a search, export gerbers, pull files back to Docker, etc.)+3. Stop the recording and upload it to the Adom Wiki for the artifact you were demoing++This works, but the videos that come out the other end are unwatchable. **80% of the runtime is dead air** — the viewer staring at unchanged screens while Claude Code waits for a Fusion 360 export to complete, or while the Fusion cloud BFS walker enumerates folders, or while a 30-second AI tool call processes in the background. A 12-step demo turns into a 5-minute video where most of the time nothing visible is happening, even though the AI is doing real work the whole time.++We tried recording with manual edits afterwards. Manual editing is slow, requires a separate video tool, breaks the "Claude Code drives the demo end to end" workflow, and is the kind of work no Adom user signed up for. The team should be able to record a demo and end up with a polished video without ever opening DaVinci Resolve.++We also tried recording at a faster monitor refresh rate to make the playback feel snappier. That doesn't help — the slow operations are still slow, and the user still has to wait through them.++The actual fix is **explicit "this part is boring, speed it up later" markers dropped during the demo flow, then a single post-process pass that turns the marked sections into 10x-100x timelapses with a clear visual indicator** (a "20x SPEEDUP: Exporting gerbers" red banner). The viewer sees the slow part happen, understands what was happening, and isn't asked to sit through 30 seconds of an unchanged screen.++That's video-post phase 1. Phase 2 is the Video Post-Processing app: open the sped-up video in a Hydrogen webview, hit record, narrate over it, auto-level the audio (EBU R128 broadcast standard), keep every take and every auto-leveled version as separate tabs, save whichever you like as the final. Phase 3 adds wiki publishing so the final narrated video gets attached to the wiki page for the artifact you demoed.++## What's in the Video Post-Processing app (phase 2)++The voiceover subcommand is a full webview app, not just a "record button". Running `video-post voiceover --input sped-up.webm --markers markers.jsonl` opens a Hydrogen tab with:++- **Speedup Timeline card** showing the source video as a horizontal bar, speedup regions highlighted, summary stats (source vs output duration, seconds saved, % shorter, number of speedup segments). Populated from the markers file the `process` step wrote.+- **Video tab strip** above the player: "Original" (silent source), "Take 1" (your raw voiceover mux), "Take 1 auto-leveled" (same take with loudness normalized), "Take 2", "Take 2 auto-leveled", and so on. Every finalize produces two new tabs at once. Click any tab to play that version. Every tab is a separate file on disk; nothing is ever overwritten.+- **Record voiceover button** with a caption-based 3-2-1 countdown, automatic video playback from 00:00, live timer, wrap-up countdown in the final 5 seconds so you can cleanly close your narration, and auto-stop when the video ends.+- **Finalize & mux** runs ffmpeg to combine the captured audio with the video, then immediately auto-runs ffmpeg loudnorm (EBU R128, -16 LUFS target, -1.5 dBTP peak ceiling) to produce the auto-leveled sibling, both tabs appear at once.+- **Save as final** copies whichever tab you picked over the canonical output path. Sidecars stay on disk.+- **Reveal in VS Code** button opens the currently playing file in VS Code's file explorer.+- **Audio waveform visualization** of the current take, raw vs normalized, for visual before/after comparison.+- **Takes history card** below the player lists every take on disk with size, modified time, and click-to-switch.+- **2-way HTTP comms**: every user-visible action has a curl-addressable endpoint so an AI can drive the full flow end to end without clicking anything, and UI state always reflects server state (400ms poller, server as single source of truth).+- **JS console forwarding**: every `console.log/warn/error` in the frontend is mirrored to the server and readable via `GET /console`, so an AI debugging UI behavior has a log surface to read without DevTools.++The full feature list, endpoint catalog, and recovery flows live in `SKILL.md`. The patterns used here (relative URLs, state poller, eval channel, tooltips, brand-compliant styling, now-playing indicator, save-before-publish gating) are documented in `gallia/skills/app-creator` as the canonical reference for building any Adom app.++The whole pipeline takes any Adom user from "raw silent recording" to "polished narrated demo video on the wiki page" in four commands and under 10 minutes of wall-clock work, regardless of how long the source recording was.++## Why this lives in its own repo (not in adom-desktop or gallia)++The user originally asked for this as a "post-process script for the Fusion demo I'm recording right now". I started writing it inside `adom-desktop/scripts/demo-record.sh` as a one-off helper, but the use case generalizes immediately:++- Anyone recording any kind of demo (KiCad workflow, pup automation, molecule import, datasheet annotation, library publishing) hits the same dead-air problem+- The marker + speedup + overlay technique works on any `.webm` from any Hydrogen recording, not just adom-desktop ones+- The voiceover overlay is also a generic workflow — it doesn't know or care what was being recorded+- Wiki publishing should work for any wiki page type (apps, skills, molecules, libraries, datasheets, footprints, symbols, 3dcomps), not just adom-desktop++So it lives in its own repo, follows the standard Rust + clap + SKILL.md + wiki-discovery pattern (the `tool-publisher` skill), and gets installed by any container that needs it via the Adom Wiki auto-discovery flow. Same model as `adom-molecule`, `shotlog`, and other on-demand tools.++> **Recording skills live here now.** The full demo-recording skill (script → record → mux → storyboard → publish, with the Hydrogen / pup / native-browser capture mechanics) is bundled under [`skills/demo-recording/`](skills/demo-recording/SKILL.md) — homed in this repo since gallia was retired. Recording the user's **real, logged-in** browser at full framerate in the background is the adom-browser-extension's `native-browser-recording` skill.++`adom-desktop`'s `scripts/demo-record.sh` is the consumer — it calls `video-post mark init` after Hydrogen recording starts and `video-post process` after it stops. But video-post itself doesn't know about adom-desktop. Any other recording orchestrator (even a hand-rolled bash script for a one-off demo) can use video-post the same way.++## What it does (3 phases)++### Phase 1 — Speedup post-processor++Drop start/end markers around slow sections during the demo, then run a single command after recording to produce a sped-up version with overlay captions.++```bash+# At the start of recording+video-post mark init++# Wrap any slow command (this is the most common pattern)+video-post wrap --speed 20 --label "Exporting gerbers" -- adom-desktop fusion_export_gerbers+video-post wrap --speed 15 --label "Pulling source files" -- pull_and_analyze "C:/tmp/foo.fbrd"++# Or drop manual start/end markers if you need finer control+video-post mark start --speed 50 --label "Cloud search"+# ... slow operation ...+video-post mark end++# After Hydrogen recording stops, post-process+video-post process \+  --input  /home/adom/project/recordings/demo.webm \+  --markers /tmp/video-post-markers.jsonl+# → produces /home/adom/project/recordings/demo-fast.webm+```++The output video has the marked sections accelerated (default cap 100x) with a red "20x SPEEDUP: Exporting gerbers" banner. Normal sections play at 1x. Audio is dropped (phase 1 expects silent recordings).++### Phase 2 — Voiceover overlay ✅ shipped++```bash+video-post voiceover --input demo-fast.webm+```++Starts a tiny HTTP server and opens a Hydrogen webview tab with a video player, a record button, and a real-time audio waveform indicator. You watch the sped-up video and narrate over it. When you stop recording, ffmpeg muxes the audio with the video to produce the final narrated output.++### Phase 3 — Wiki publish ✅ shipped++```bash+video-post publish \+  --input demo-fast-narrated.webm \+  --page apps/adom-desktop \+  --caption "Fusion 360 deep dive: BQ25792 manufacturing pipeline"+```++Wraps `adom-wiki asset upload` to attach the final narrated video to whichever wiki page you're demoing. Now the polished demo video lives alongside the artifact it documents — anyone visiting the apps/adom-desktop page sees the demo, not just the docs.++## The full pipeline++```bash+# 1. Record (silent, no voiceover)+adom-cli hydrogen sharing request --share screen --reason "Demo recording"+adom-cli hydrogen recording start --countdown 3+video-post mark init++# (run your demo, wrapping slow sections with `video-post wrap`)++adom-cli hydrogen recording stop  # → demo.webm++# 2. Speedup pass+video-post process --input demo.webm++# 3. Voiceover (Phase 2)+video-post voiceover --input demo-fast.webm++# 4. Publish (Phase 3)+video-post publish \+  --input demo-fast-narrated.webm \+  --page apps/adom-desktop \+  --caption "Fusion 360 deep dive"+```++Four commands. Total wall-clock time under 10 minutes for a demo that captured 30+ minutes of raw recording.++## How the AI uses it++Adom users mostly don't run video-post directly — their Claude Code session does. The skill file (`SKILL.md`) is auto-installed to `~/.claude/skills/video-post/` and Claude Code finds it via trigger words like "speed up demo recording", "timelapse video", "voiceover after recording", "publish demo to wiki".++Once Claude finds the skill, it knows the marker workflow and the four-command pipeline above. The AI drops markers as it runs commands during the demo, then runs `process` automatically after the recording stops, and offers to run `voiceover` and `publish` next.++This is the same pattern as `adom-molecule` (Claude drives the molecule import workflow) and `shotlog` (Claude annotates screenshots during a debug session). Build the CLI well, write the SKILL.md well, and the AI does the rest.++## Install++```bash+# From source+git clone https://github.com/adom-inc/video-post+cd video-post+./build.sh++# After v0.1.0 release: from the wiki auto-discovery flow+gh release download v0.1.0 --repo adom-inc/video-post --pattern video-post -D /usr/local/bin+chmod +x /usr/local/bin/video-post+video-post install+```++`video-post install` deploys the SKILL.md to `~/.claude/skills/video-post/` so Claude Code in this container picks it up immediately.++## Prerequisites++- `ffmpeg` and `ffprobe` (`sudo apt-get install ffmpeg`)+- Rust toolchain to build from source (`rustup default stable`)+- For the integration with `adom-desktop/scripts/demo-record.sh`: a working Hydrogen recording setup++## Status++- **Phase 1 (speedup):** ✅ shipped in v0.1.0+- **Phase 2 (voiceover web server):** 🚧 planned for v0.2.0+- **Phase 3 (wiki publish):** 🚧 planned for v0.3.0++## License++MIT
SKILL.mdadded+521
@@ -0,0 +1,521 @@+---+name: video-post+description: >+  Post-production for demo recordings: speed up boring parts with overlay+  captions, add voiceover narration after the fact, or publish the final video+  to a wiki page. For pre-production and recording (writing demo scripts,+  setting up panels, driving the recording), use the demo-recording skill+  instead. Trigger words: speed up demo recording, speed up video, timelapse+  video, fast forward boring parts, compress slow parts of video, video+  post-production, voiceover after recording, narrate a recording, demo+  speedup, demo post-process, post-process demo, publish demo video to wiki.+---++# video-post++Post-production CLI for demo recordings. For pre-production and recording+(writing scripts, setting up panels, driving the recording), use the+**demo-recording** skill.++## 🛑 Storyboard review is MANDATORY for Flow A demos — do not skip++The whole point of building a demo as per-section clips is so the+human reviewer can **look at each clip individually** and tell you+which ones are broken. AI-made demos fail in specific, localized+ways — "clip 5 measured the wrong pads," "clip 8 never clicked+anything," "clip 11 walkthrough didn't advance." The reviewer spots+those instantly when clips are numbered, described, and separately+playable. Asked to watch a 3–5 minute uninterrupted video, they can't —+they'd have to scrub to a timestamp and guess which segment was+broken. Skipping storyboard makes their job genuinely hard and turns+the feedback loop from precise ("clip 5 is wrong") to useless+("something's off somewhere"). Don't do that to them.++**The rule, in one line:** after `manifest add` for all clips, the next+command MUST be `video-post storyboard <manifest>`, and you MUST wait+for the user's per-clip feedback before concat / upload.++```bash+# Open storyboard → user reviews → you fix bad clips → repeat → only then concat+video-post storyboard /tmp/my-demo-manifest.json \+  --tab-name 'video-post: <name> review' --port 8797+# Activate + screenshot to confirm it rendered+adom-cli hydrogen workspace active-tab --name 'video-post: <name> review'+adom-cli hydrogen screenshot panel --name 'video-post: <name> review' \+  --reason 'show user the clip storyboard for review'+# Then point the user at the tab and wait for "clip N is broken" / "looks good"+```++### Every clip needs a `--description`++`video-post manifest add --description "<text>"` is how the storyboard+UI explains what each clip is supposed to show. Without descriptions,+the user is still blind — they see a player but no context. Write+descriptions that are **concrete about the intent**, not just the+feature name. Compare:++- ❌ "inspect tool" — no hint what's expected to happen+- ❌ "measure demo" — can't tell if a bad clip is your intent being+  wrong or your execution being wrong+- ✅ "BME680 cinematic slow orbit for 15s. Camera starts iso, completes+  one rotation, no HUDs opened."+- ✅ "iCE40-USB Measure: commit MP1→MP3 (diagonal 181 mm), then U1→J1,+  then MC_USB_DP→DM. Each shows ΔX/ΔY/distance HUD."++Rule of thumb: if a teammate reading just the description couldn't+write the clip's interaction script, the description is too vague.++### Known bug: `video-post concat` drops audio++Today `video-post concat --kind raw` only writes the video stream.+Until fixed, use ffmpeg's concat demuxer as a drop-in replacement AFTER+storyboard approval:++```bash+LIST=$(mktemp)+for c in $(jq -r '.clips[].raw' /tmp/my-demo-manifest.json); do+  echo "file '$c'" >> $LIST+done+ffmpeg -y -f concat -safe 0 -i $LIST -c copy ~/project/recordings/final.webm+ffprobe -v error -show_streams ~/project/recordings/final.webm | grep codec_type+# Both `video` AND `audio` must appear. If audio is missing, the concat dropped TTS.+```++## 🗣️ TTS pronunciation — consult the `tts-pronunciation` skill, not a table here++`edge-tts` and other neural engines mispronounce many Adom tool names and acronyms (`adom-tsci` → "adom-sci", `JLCPCB` → garbled, `UART` → "yoo-art", etc.). The canonical pronunciation table lives in the **`tts-pronunciation` skill** at `gallia/skills/tts-pronunciation/SKILL.md` (data file: `pronunciations.json`). **Always consult that skill when drafting or validating narration** — do not maintain a parallel table in this skill.++**Rule:** before muxing a clip, every Adom-ish proper noun in its narration text must match the `narration` column of an entry in `pronunciations.json`. If a term isn't in the table, stop and add it (contribution flow documented in that skill).++**Verify before rendering** with a one-shot:++```bash+edge-tts --voice en-US-AndrewMultilingualNeural \+  --text "adom t s c i version one point three point seven" \+  --write-media /tmp/_test.mp3+# Listen with ffplay — it should spell out T S C I, not say "sci" as a word.+```++Fixing one clip pre-mux is cheap; re-rendering 13 clips after mux because you skipped the pronunciation check is not.++## 📏 Clip duration must match narration duration — don't ship loose clips++A clip where the video is 2:17 and the narration is 0:30 is broken regardless of which one is "correct." The viewer stares at the last frame for a minute and a half, or the narration cuts off while the video keeps going.++**Rule:** **video length ≈ narration length, ±15%.** If they diverge, fix whichever is wrong:++- Narration too long: tighten the script, drop filler ("So…", "What we're looking at here…").+- Video too long: either trim on recording, split the clip into N smaller clips (each with its own matched narration), or apply `video-post process` speedups in the boring bits with caption overlays.++**Split before you speedup when:**+- The sub-sections are genuinely different things you'd narrate separately ("open it / click around / close it" → three clips, three narrations).+- One sub-section loads slowly from the app side (e.g. cycling through 8 example molecules where each takes 3 s to render). Speed-up masks the lag but smaller-clip recording lets you control the pacing.+- You want surgical re-shoots — a 2:17 "examples" clip is too coarse to re-record if one of the 8 fails; eight 15-second clips let you re-shoot just the broken one.++**Check duration fit before muxing:**+```bash+for f in /tmp/clips-raw/*.webm; do+  vd=$(ffprobe -v error -show_entries format=duration -of default=nk=1:nw=1 "$f")+  ad=$(ffprobe -v error -show_entries format=duration -of default=nk=1:nw=1 "${f%.webm}.mp3")+  ratio=$(python3 -c "print(max($vd,$ad)/min($vd,$ad))")+  echo "$(basename $f): video=${vd}s  tts=${ad}s  ratio=$ratio"+done+# Anything with ratio > 1.15 needs a tighten or a split.+```++## 🖥️ Fullscreen review in a Hydrogen webview is blocked — route users to pup++Hydrogen's Web View panel wraps the storyboard URL in an iframe without `allow="fullscreen"` (filed as `adom-inc/hydrogen#329`). The ⛶ Expand button on each clip card calls `element.requestFullscreen()` which throws silently. This is OUT OF scope for `video-post` to fix.++**When the user asks why fullscreen doesn't work** (and they will), tell them: same URL, standalone Chrome window via `pup`:++```bash+# Open the storyboard in a standalone Chrome window (fullscreen works there)+pup open-tab --url "https://<container>.adom.cloud/proxy/8797/" --name "storyboard fullscreen review"+```++Do not wait for hydrogen#329 to ship before offering this — the workaround is fine and it's the better place to review clips anyway (larger screen real-estate, native fullscreen).++## 🚨 Wiki webm: keyframes every 1 s — non-negotiable on the final encode++Every webm/mp4 you upload to the Adom Wiki must have a keyframe interval of ~1 s (`-g 30 -keyint_min 30` for 30 fps content). Without that, the `<video>` element on the wiki page can play start-to-end but **cannot scrub** — `<video>` seeks only land on keyframes, and a clip with one keyframe at frame 0 is one giant seekable unit. Users want to jump around the timeline; without dense keyframes, they can't.++This bites especially on the **single-shot path** (`adom-cli hydrogen recording stop` straight to `adom-wiki asset upload`) which skips all of video-post and inherits libvpx-vp9's default keyframe-only-at-frame-0 behavior.++`video-post concat`'s final mux **must** include `-g 30 -keyint_min 30` on every code path that produces a webm bound for the wiki (concat output, snippet narration mux, hero image extract is fine without it). The same rule lives in the `demo-recording` skill — these two skills enforce identical encode contracts on every wiki-bound clip. If you find a code path that skips it, fix it before letting the upload proceed.++**Quick verify after encode:**++```bash+ffprobe -v error -select_streams v:0 -show_entries packet=pts_time,flags \+  -of csv=p=0 final.webm | awk -F, '$2 ~ /K/' | head -10+# Should print keyframes (",K_") at ~0.0s, ~1.0s, ~2.0s, ...+# If you only see one ",K_" line at 0.0s, the encode dropped the rule —+# re-run ffmpeg with -g 30 -keyint_min 30 added.+```++## 🎬 Two flows, pick the right one++`video-post` supports **two** demo-assembly flows. Pick based on what+you're making:++### Flow A — Per-section concat (preferred for feature tours)++Each feature / section is recorded as its own short clip with its own+TTS narration. `video-post concat` stitches them. This is the+canonical path for feature-tour demos ("here are 14 things+adom-tsci can do") because:++- Re-shoots are surgical — if clip #7 is bad, re-record *just* that+  one, re-concat. The other 13 are untouched.+- TTS is clean per-clip — no timing puzzle from a long mixed recording.+- Manifest has clear section boundaries — the reviewer app shows+  them as distinct entries.+- Hero image selection is easier — each clip's first good frame is a+  candidate.++The orchestration loop (full details in the demo-recording skill):++```bash+video-post manifest init --output demo.json+for each section:+  adom-cli hydrogen caption show "<title>" -d 0+  adom-cli hydrogen recording start --reason "<title>"+  # ...drive UI silently for 8-25 seconds...+  adom-cli hydrogen recording stop+  edge-tts --voice en-US-AndrewNeural --text "<narration>" \+    --write-media "<id>.mp3"+  ffmpeg -y -i <clip>.webm -i <id>.mp3 -map 0:v -map 1:a \+    -c:v copy -c:a libopus -shortest <id>-narrated.webm+  video-post manifest add --manifest demo.json \+    --id <id> --title "<title>" \+    --raw <id>-narrated.webm <id>-narrated.webm+  adom-cli hydrogen caption hide+done+video-post concat --manifest demo.json --output final.webm --kind raw+```++**TTS voice:** default `en-US-AndrewNeural` (Microsoft Edge neural,+via `edge-tts` on PATH). Alternatives: `en-US-AriaNeural`,+`en-US-GuyNeural`. One voice per demo — don't switch mid-demo.++**TTS future path:** when the `aci voice` subcommand ships+(`$ACI_VOICE_API` env var + `aci voice say "<text>" > out.mp3`), the+demo-recording helper will auto-detect and use it instead of `edge-tts`.+Same Edge-neural backend, but the call goes through Adom's+infrastructure. Until then, `edge-tts` on PATH is the correct default.++### Flow B — Single long take + speedup markers++A single recording, with `video-post mark start` / `mark end` around+slow operations, post-processed by `video-post process` to compress+boring sections. This is the right flow for **live-narration** demos+where the speech timing is inseparable from the action, or where the+demo has unpredictable pauses (cloud search results, AI tool latency,+fabrication exports) that you want to compress.++Flow B is the original pipeline. Still supported, still correct when+it's the right fit.++## CLI Reference++A CLI for taking raw screen recordings and turning them into watchable narrated+demo videos. Phases:++1. **Speedup** (Flow B) — drop start/end markers around slow sections during recording, then run a single command to produce a sped-up version with overlay captions like "20x SPEEDUP: Exporting gerbers".+2. **Concat** (Flow A) — register each per-section clip in a manifest, then `concat` them into one polished video.+3. **Voiceover** — either per-clip TTS (Flow A, via `edge-tts`) or a live-narration webview over a merged video (Flow B).+4. **Publish** — upload the final narrated video to the wiki page for whatever you're demoing (an app, a skill, a molecule, a library, a datasheet).++## Why this exists++Demo recordings have long boring stretches: file exports (10-30s), cloud searches (5-30s), AI tool call latency (2-5s per call). A 12-step demo turns into a 5-minute video where 80% of the runtime is the viewer watching unchanged screens. Nobody watches that. video-post compresses the boring parts to ~5% of their wall-clock time with a clear visual indicator that the speedup is happening.++For **feature-tour demos** (Flow A), the problem is different: you want each section to be a tidy, narrated clip, and you want the final video to be a clean concatenation rather than one continuous take with timing gymnastics. `manifest` + `concat` does that job.++## Quick start++```bash+# Health check (verify ffmpeg + ffprobe are installed)+video-post health++# Initialize the markers file at the START of a recording session+video-post mark init++# Wrap any slow command with a speedup marker+video-post wrap --speed 20 --label "Exporting gerbers" -- adom-desktop fusion_export_gerbers+video-post wrap --speed 15 --label "Pulling source files" -- pull_and_analyze "C:/tmp/foo.fbrd"++# OR drop manual start/end markers+video-post mark start --speed 50 --label "Cloud search"+# ... run slow operation ...+video-post mark end++# Inspect the markers you've collected+video-post inspect++# After the recording stops, post-process it+video-post process \+  --input  /home/adom/project/recordings/demo.webm \+  --markers /tmp/video-post-markers.jsonl+# → produces /home/adom/project/recordings/demo-fast.webm+```++## Commands++### `health`++```bash+video-post health+```++Verifies `ffmpeg` and `ffprobe` are installed and returns their versions. Exit 0 if ready, 1 with an install hint if missing.++```+OK: ffmpeg 6.1.1 + ffprobe 6.1.1 ready.+```++### `mark init`++```bash+video-post mark init [--file /tmp/video-post-markers.jsonl]+```++Clears the markers file and writes a `recording_start` event with the current epoch. Call this immediately after `adom-cli hydrogen recording start --countdown N` returns. The post-processor uses this timestamp as t=0 for the video.++### `mark start` / `mark end`++```bash+video-post mark start --speed 20 --label "Exporting gerbers"+# ... run slow operation ...+video-post mark end+```++Drops a `speedup_start` event (with speed multiplier and caption label) and a matching `speedup_end` event. The post-processor will accelerate everything between these two timestamps and overlay the caption.++`--speed` must be in `[2, 100]`. Above 100, ffmpeg's `setpts` produces visual glitches. Below 2, it's not really a speedup.++`--label` is the text shown in the caption overlay during the speedup section. Keep it short (the overlay box wraps awkwardly with long strings).++### `wrap`++```bash+video-post wrap --speed 20 --label "Exporting gerbers" -- adom-desktop fusion_export_gerbers+```++Convenience wrapper that drops a start marker, runs the command, drops the end marker, and returns the command's exit code. Equivalent to:++```bash+video-post mark start --speed 20 --label "Exporting gerbers"+adom-desktop fusion_export_gerbers+RC=$?+video-post mark end+exit $RC+```++Use this in your demo script for any command that takes more than ~3 seconds and isn't visually interesting.++### `inspect`++```bash+video-post inspect [--file /tmp/video-post-markers.jsonl]+```++Prints a human-readable summary of the markers file: how long the recording has been running, how many speedup segments are defined, total time saved at the configured speeds.++```+OK: Recording started 142.3s ago, 4 speedup segments, ~98.7s of source video → ~4.9s after speedup (94.7% reduction)+  - 12.5s @ 20x: "Exporting gerbers"  →  0.6s+  - 8.3s  @ 15x: "Pulling source files"  →  0.6s+  - 45.2s @ 30x: "Cloud search"  →  1.5s+  - 32.7s @ 15x: "Exporting EAGLE"  →  2.2s+```++### `process`++```bash+video-post process \+  --input /home/adom/project/recordings/demo.webm \+  --markers /tmp/video-post-markers.jsonl \+  [--output /home/adom/project/recordings/demo-fast.webm] \+  [--max-speed 100]+```++The main post-processor. Reads the input video and the markers file, builds a single ffmpeg `filter_complex` graph that:++1. Splits the video at every marker boundary into segments+2. Normal segments play at 1x with no modification+3. Marked segments accelerate via `setpts=(1/speed)*(PTS-STARTPTS)` and overlay caption text via `drawtext`+4. Concatenates everything back into a single output++Audio is dropped (`-an`). Phase 1 expects silent recordings. Phase 2 (the `voiceover` subcommand, coming soon) adds audio back as a separate narration layer.++Default output is `<input-stem>-fast.webm`. Override with `--output`.++### `voiceover` (Video Post-Processing webview app)++```bash+video-post voiceover \+  --input /home/adom/project/recordings/demo-fast.webm \+  [--output /home/adom/project/recordings/demo-fast-narrated.webm] \+  [--markers /tmp/video-post-markers.jsonl] \+  [--panel-id <leaf-id>] \+  [--tab-name "Video Post-Processing"]+```++This is the full Video Post-Processing app. It ships as a Rust-backed+HTTP server with a Hydrogen webview frontend, Adom brand-compliant+Familjen Grotesk + Satoshi typography, frosted-glass card layout, and+2-way HTTP comms so both the human user and an AI driver can operate+the same endpoints.++Features:++- **Speedup Timeline card** at the top of the app, rendering the source+  video as a horizontal bar with the speedup regions highlighted, plus+  a second bar showing the after-speedup compressed result. Summary+  stats: source duration, after-speedup duration, seconds saved, %+  shorter, number of speedup segments. Shown only when `--markers` is+  passed so the card reflects the real speedup plan from an earlier+  `video-post process` run. Each segment has a deep tooltip+  explaining EBU R128 loudness parameters in plain English.+- **Video tab strip** above the player: "Original" (the silent source),+  "Take N" (raw mux for each voiceover take), "Take N auto-leveled"+  (each take normalized via ffmpeg loudnorm automatically on finalize),+  plus legacy + committed entries. Click any tab to swap the player to+  that version. Every tab is a file on disk, nothing is ever overwritten.+- **Record voiceover button** triggers a caption-based 3-2-1 countdown,+  then plays the silent source from the top while Hydrogen captures+  the mic via `adom-cli hydrogen audio start` (no iframe getUserMedia+  permission dance). A wrap-up countdown appears in the last 5 seconds+  so the narrator can cleanly say "thanks for watching", then auto-stop+  when the video ends.+- **Finalize & mux** produces `<output>-take-N.webm` as the raw mux,+  then automatically runs ffmpeg loudnorm (EBU R128, -16 LUFS target,+  -1.5 dBTP peak ceiling) to produce `<output>-take-N-norm.webm` as a+  paired auto-leveled version. Both tabs appear on the strip+  simultaneously, with the auto-leveled version selected by default.+- **Save as final** copies the currently selected tab over the canonical+  `--output` path. Sidecar files stay on disk alongside, nothing is+  destroyed.+- **Reveal in VS Code** button opens the currently playing file in VS+  Code via `code-server --reuse-window` so you can see exactly where on+  disk it lives.+- **Waveform card** shows the audio envelope via ffmpeg `showwavespic`,+  rendered in Adom teal for raw takes and purple for normalized takes.+- **Takes history card** below the player lists every version with+  kind pill (committed / auto-leveled / raw / legacy), size, modified+  time, and click-to-switch.+- **JS console forwarding** mirrors every `console.log/warn/error` from+  the UI to `POST /console`, and the server exposes `GET /console` so+  an AI driving the app can read UI-side logs without DevTools access.+- **State poller** at 400ms interval reads `GET /state` and drives all+  UI transitions, so server state is the single source of truth: the+  AI can curl `POST /start-recording` and the UI responds identically+  to a human click, same code path.+- **All endpoints are curl-driveable** so an AI can operate the full+  flow: `POST /start-recording`, `POST /stop-recording`,+  `POST /finalize`, `POST /normalize`, `POST /save-final`,+  `POST /revert-to-raw`, `POST /select-take`, `POST /reveal`,+  `POST /replay`, `POST /adopt-latest-audio`. Plus `GET /state`,+  `GET /takes`, `GET /timeline`, `GET /console`, `GET /waveform-raw`,+  `GET /waveform-preview`, `GET /video`, `GET /final-video`.+- **Recovery**: `POST /adopt-latest-audio` scans `~/project/audio/` for+  the most recent Hydrogen audio file and attaches it to state as the+  current take, useful after a server restart that cleared in-memory+  state but left the recording on disk.++### `publish` (Phase 3, coming soon)++```bash+video-post publish \+  --input /home/adom/project/recordings/demo-fast-narrated.webm \+  --page apps/adom-desktop \+  --caption "Fusion 360 deep dive: BQ25792 manufacturing pipeline"+```++Uploads the final video to the wiki page for the artifact you're demoing. Wraps `adom-wiki asset upload` so the video lives alongside the app/skill/molecule/library/datasheet it documents.++### `install`++```bash+video-post install+```++Deploys the embedded `SKILL.md` to `~/.claude/skills/video-post/SKILL.md` so Claude Code finds it automatically via trigger words. Run once after building.++## The full pipeline (when all 3 phases ship)++```bash+# 1. Record (silent — no voiceover)+adom-cli hydrogen sharing request --share screen --reason "Demo recording"+adom-cli hydrogen recording start --countdown 3+video-post mark init++# (run your demo, wrapping slow sections with `video-post wrap`)++adom-cli hydrogen recording stop  # → /home/adom/project/recordings/demo.webm++# 2. Speedup pass+video-post process --input demo.webm --markers /tmp/video-post-markers.jsonl+# → demo-fast.webm++# 3. Voiceover pass (Phase 2)+video-post voiceover --input demo-fast.webm+# → demo-fast-narrated.webm++# 4. Publish to wiki (Phase 3)+video-post publish --input demo-fast-narrated.webm --page apps/adom-desktop \+  --caption "Fusion 360 deep dive"+```++Four commands, four short steps, total wall-clock under 10 minutes for a demo that captured 30+ minutes of raw recording.++## Marker file format++JSONL, one event per line. Self-contained — the file embeds the recording start time so the post-processor doesn't need a separate `--recording-start` arg.++```jsonl+{"action":"recording_start","epoch":1776038500.000}+{"action":"speedup_start","epoch":1776038510.500,"speed":20,"label":"Exporting gerbers"}+{"action":"speedup_end","epoch":1776038545.700}+{"action":"speedup_start","epoch":1776038550.123,"speed":15,"label":"Pulling source files"}+{"action":"speedup_end","epoch":1776038562.456}+```++Default file path: `$VIDEO_SPEEDUP_MARKERS` env var, or `/tmp/video-post-markers.jsonl` if unset.++## Integration with adom-desktop demo flow++`adom-desktop`'s `scripts/demo-record.sh` integrates with this tool:++- `demo-record.sh start` calls `video-post mark init` after the Hydrogen recording starts+- `demo-record.sh stop` calls `video-post process` automatically if any speedup markers exist, producing both `demo.webm` and `demo-fast.webm`++Per-step demo commands wrap their slow operations:++```bash+step_caption "Step 7/12: Exporting gerbers"+video-post wrap --speed 20 --label "Exporting gerbers" -- bash -c '+  GERBER_RESP=$(adom-desktop fusion_export_gerbers 2>&1)+  pull_and_analyze "$ZIP_PATH"+'+fusion_grid "Step 7" /tmp/step-7.png+push_step 7 "Export gerbers" "$GERBER_ANALYSIS" /tmp/step-7.png+```++The `fusion_grid` and `push_step` calls happen at 1x so the visual payoff is clearly visible. Only the slow command + file pull gets sped up.++## Prerequisites++- `ffmpeg` and `ffprobe` (`sudo apt-get install ffmpeg`)+- A Hydrogen recording (any `.webm` produced by `adom-cli hydrogen recording stop`)+- A markers JSONL file with at least a `recording_start` event++## Notes++- **Audio is dropped in Phase 1.** The pipeline expects silent recordings — voiceover comes in Phase 2 as a separate step.+- **Filter graphs scale.** ffmpeg handles 50+ trim segments fine. The Rust code builds the graph as a `Vec<String>` and joins with `;`.+- **Speed is capped at 100x.** Anything above is visually unusable.+- **Out-of-order markers get sorted.** A warning is printed but processing continues.+- **Unclosed `speedup_start` markers** (no matching `speedup_end`) are treated as ending at the end of the video, with a warning.
screenshots/hero.pngadded
⋯ 1 unchanged line ⋯
skills/demo-recording/SKILL.mdadded+341
@@ -0,0 +1,341 @@+---+name: demo-recording+description: >+  Use when the user wants to record a demo, make a video, create a walkthrough,+  record their screen, or capture a demo of a feature. Handles the full+  pre-production and recording workflow: writing the demo script, generating+  captions, verifying the setup, choreographing panels, and driving the+  recording. Hands off to video-post for post-production (speedup, voiceover,+  publish). Trigger words: record a video, record a demo, make a movie, create+  a walkthrough, record my screen, demo video, screen recording, narrate a+  walkthrough, capture a demo, make a demo.+---++# Demo Recording — orchestration++Record polished demo videos of Adom features. This file is the+**orchestration entry point**. Detailed mechanics live in three+sub-docs you should follow when their phase becomes relevant:++| Sub-doc | When to load it |+|---|---|+| [recording-mechanics.md](recording-mechanics.md) | Before you touch `recording start` / `browser_record_start`. Covers Hydrogen vs pup, foreground-rebump, page-side repaint, and the **NEVER stitch screenshots** rule. |+| [script-and-tts.md](script-and-tts.md) | Before you record the first clip. Script template, narration phonetics, captions, panel choreography, voiceover draft. |+| [per-snippet-flow.md](per-snippet-flow.md) | While you record + after. Setup verification, the record loop, manifest add, **storyboard review (mandatory)**, concat, hero, wiki upload, workspace restore. |++For post-production after recording (speedup, voiceover, publish-to-wiki),+hand off to the `video-post` skill.++## What "a real demo" means at Adom — non-negotiable shape++When the user says "make a real demo," "make me a demo," "publish a demo,"+"polished tour," or anything similar **that is NOT explicitly the quick path+in [per-snippet-flow.md](per-snippet-flow.md)** ("Quick recording" section),+every one of these steps has to ship — none of them is optional:++1. **Sectioned script in the repo** at `<repo>/demo/<name>-demo-script.md`+   with a row-per-scene table containing **caption + narration + target+   length + driver actions**. The narration column is phoneticized for TTS+   per [`tts-pronunciation`](../tts-pronunciation/SKILL.md). Detail in+   [script-and-tts.md](script-and-tts.md).+2. **One short clip per section, recorded separately** — never a single long+   take covering every feature. Per-clip lengths track narration ±15%.+3. **Per-clip TTS via `adom-tts say`** (uses Adom's pronunciation cache + house+   voice `en-US-AndrewMultilingualNeural`). Falls back to `edge-tts` only+   when adom-tts isn't installed.+4. **Per-clip mux** — TTS audio combined with the silent recording into one+   `<id>-narrated.webm` (`ffmpeg -i clip.webm -i tts.mp3 ... libopus`).+5. **`video-post manifest add`** for every clip with a `--description` that+   states **intent** (not just feature name) — see video-post skill rules.+6. **`video-post storyboard <manifest>` review with the user** — MANDATORY+   gate before concat. Per-clip review catches "clip 5 measured the wrong+   thing" in seconds; reviewing a 3-min concat blind takes 20× as long.+   Skip this step ONLY if the user has explicitly told you they're+   unavailable (e.g. "I'm at the gym, just ship it") — and call it out+   prominently in the final report so they can re-review later.+7. **`ffmpeg -f concat -safe 0 -c copy` to the final** — `video-post concat`+   currently drops audio (known bug, documented in video-post skill).+8. **Hero image** — `ffmpeg -ss <t> -i final.webm -frames:v 1 hero.png`+   from a representative frame.+9. **Show the final concat in `adom-video` BEFORE wiki upload.** Drop the+   webm into `/tmp/videolib/`, run `adom-video play /tmp/videolib/<final>.webm`,+   then open the proxy URL `https://<container>.adom.cloud/proxy/<port>/?file=<final>.webm`+   as a pup tab. The status line of `adom-video play` prints the port (or+   `adom-video status` if the server is already up). adom-video is the+   canonical playback surface — frame-accurate scrub, A-B loop, pixel zoom+   to 8×, frame-extract to PNG, trim & export. **Never** `python3 -m+   http.server` + raw file URL; that loses scrub fidelity and you lose every+   adom-video CLI hook (`seek`, `extract-frame`, `trim`). Memory rule:+   `feedback_video_playback_uses_adom_video.md`.+10. **Upload BOTH to the wiki** — `adom-wiki asset upload <page>+    --asset-type video --file final.webm` AND `--asset-type hero-image+    --file hero.png`. Re-publish the page so `pub_version` bumps.+11. **Demo script published to the wiki too** so other Adom users can+    reproduce — `adom-wiki page edit` adds the script content under the+    "Demo" section of the page body.++If any of those steps gets dropped, the output is not a "real demo" — it's a+screen capture. Tell the user explicitly which steps you skipped and why.++🛑 **Step 9 is the most-frequently-missed step.** I produce a final webm,+report `final.webm at /tmp/...`, and stop — leaving the user to find it,+copy it locally, and open it in some random player. That's wrong. The user+should never have to leave their pup window to review a video. Default+behavior: when a final webm is produced, it gets shown in adom-video as+part of the same response.++## 🚨 Ten failure modes that wreck demos every session — read this BEFORE recording++Every demo session that goes wrong goes wrong in the same ten ways. If+you catch yourself in any of these patterns, **stop and re-read this+section.** The user has paid the cost of teaching this lesson many+times; they should not have to teach it again.++### 1. Skipping the snippet flow because you read the skill once and "got the gist"++Symptom: you record clips back-to-back, mux them, upload to wiki, ask+the user to review the final video.++Why it happens: scanning the skill once feels like understanding it.+The "🛑 MANDATORY storyboard review" gate looks like ceremony you can+skip if you're confident.++Why it is wrong: per-clip review catches "clip 5 measured the wrong+thing" in 30 seconds. Watching a 60–90 s mux to find the same problem+takes 20× longer AND the user has to re-watch every revision.++Fix: the storyboard gate is non-negotiable. If the user says "watch+later" once, that does NOT extend to follow-up shoots. Open+`video-post storyboard <manifest>` after every recording pass. Detail+in [per-snippet-flow.md](per-snippet-flow.md) ("Step 5" section).++### 2. One-shot batch instead of iterative++Symptom: record-all → mux → upload → "is it good?" → "no, redo X" →+record-all → mux → upload → repeat.++Why it happens: feels like progress because each cycle ends with a+shippable artifact.++Why it is wrong: round-trip on one snippet ≈ 30 s. Round-trip on a+full mux + upload + watch ≈ 5 min. Three rounds of one-shot = 15+minutes of the user watching slop they didn't approve.++Fix: per-clip flow. Record → manifest add (with intent description) →+storyboard review → re-shoot only the bad clips → mux is the LAST+step, and it runs in the background while the user reviews snippets.++### 3. Lame shots because you ran out of stamina++Symptom: clips 1–3 are well-staged, clips 4–6 are programmatic+`/eval` shortcuts that "kind of show the feature."++Why it happens: by clip 5 you've burned an hour wrestling the capture+pipeline. The remaining clips become "good enough, ship."++Why it is wrong: this is the user-facing artifact. "Good enough"+reads as "this team didn't care."++Fix: every clip gets the same staging treatment. If you don't have+the bandwidth for six high-quality clips, drop to four. Don't ship+six mixed-quality clips and hope the average lands well.++### 4. Faking interactions you can't actually drive++Symptom: user asks for "click an edge in the 3D viewer." You can't+synthesize a real face-pick from `/eval` because Babylon's pick uses+live OS pointer state. Instead of saying so, you fake it with+`addMeasurePick({point: bbox.center})` and ship a clip that doesn't+show what was asked.++Why it happens: faking is faster than admitting a limitation.++Why it is wrong: the user can tell. Faked interactions look faked.+The original feature request goes un-tested.++Fix: when you hit a UI capability you can't reach via current APIs,+you have **two acceptable paths** — and faking is never an acceptable+third option:+- **(a) Tell the user explicitly:** "I can't synthesize a face-click+  from `/eval`. Want to do that part yourself in 5 seconds, or want+  me to add a CLI command for it?"+- **(b) Add the CLI command.** Pattern: `<tool> <verb> --face <mesh>+  --uv 0.5,0.5` synthesizes a real `PickingInfo` and routes through+  the same handlers the mouse pick fires. Now the demo CLI doubles+  as proof the feature works headlessly.++### 5. Punting on snippets that fail because the underlying feature is broken++Symptom: the Inspector clip won't record because something in the+inspector breaks `scene.activeCamera` access mid-clip. You drop the+clip from the demo and ship N-1 instead of N.++Why it happens: dropping the clip is one line. Investigating the bug+is unbounded.++Why it is wrong: **the demo IS a unit test of the user's product.**+Failing to record told you there's a real bug in feature X. Skipping+the clip means the demo is incomplete AND the bug stays. Both bad.++Fix: investigate the failure. Fix the underlying bug. Get the+snippet. The demo and the product co-evolve. If the bug is genuinely+beyond the demo session's budget, raise it explicitly: "Inspector is+hitting bug X. Options: 30 min to investigate, drop clip with a+follow-up tracking item, or you decide." Don't decide unilaterally.++### 6. Treating mux/concat as the finish line instead of as a slow background task++Symptom: snippets done → run `ffmpeg concat` (60–90 s of waiting) →+upload → ask user to review final.++Why it happens: ffmpeg's "the file is done" output feels like+completion.++Why it is wrong: ffmpeg concat doesn't need user attention. The user+reviewing snippets in storyboard doesn't need ffmpeg done. These+should run in parallel.++Fix: the moment the last snippet is recorded, kick off+`(ffmpeg concat ... &)` (background bash) AND+`video-post storyboard <manifest>` (foreground for the user). By the+time the user clicks through snippets, the mux is either done OR+you've already killed it because they flagged a re-shoot. Detail in+[per-snippet-flow.md](per-snippet-flow.md) ("Background-mux while the user reviews" section).++### 7. Surprise-omitting parts of what was asked++Symptom: midway through recording you realize Inspector won't work or+Strobe is unstable. You silently drop those clips. The user finds out+from the final video.++Why it happens: surprise omissions feel less awkward than confessing+a gap mid-flow.++Why it is wrong: the user expects what they asked for. Silent omits+look like lying. They erode trust on every wrap-up message you write+afterward.++Fix: as soon as a snippet fails twice and you don't see the fix path,+message the user immediately: "Inspector clip is failing because of+X. Three options: I investigate (~30 min), I drop with a follow-up+tracking item, or you confirm we ship without it." Wait for the+answer. Don't decide unilaterally.++### 8. Failing to capture lessons in the skill so the next session repeats them++Symptom: same demo session, same ten failure modes, three different+conversations. The user is the only continuity.++Why it happens: it's "obvious" you'll remember next time. You won't —+your memory ends with the conversation.++Why it is wrong: skill files in gallia load on every Claude Code+session. Memory is local. Lessons that apply to "demos in general"+belong in the skill file, not in chat.++Fix: if the user gives you feedback that applies to FUTURE demo+sessions, the conversation isn't done until you've written the+lesson into the skill file and committed it to gallia.++### 9. Tolerating a slow capture mechanism instead of fixing the page++Symptom: you spend 60–90 minutes wrestling with paint-throttling,+screenshot polling, and frame deduplication. You eventually settle+for a 1–2 fps slideshow instead of fixing the page-side repaint bug.++Why it happens: the capture API is the visible surface; "the page+isn't repainting" is invisible.++Why it is wrong: documented in the "NEVER stitch screenshots" rule+in [recording-mechanics.md](recording-mechanics.md). The capture+mechanism (`browser_record_start`) is fine. The page-side fix+(continuous setInterval pump that mutates camera + `scene.render()`;+strip ArcRotateCamera input handlers) is the answer.++Fix: read the "NEVER stitch screenshots" rule before starting any+pup recording. Treat it as a precondition, not a fallback to+remember after the slideshow ships.++### 10. Inflating the wrap-up message past what was actually delivered++Symptom: "All 6 clips at 20-30 fps with real motion!" — when actually+clip 5 is barely above the SSIM threshold, clip 6 was deferred,+Inspector got skipped, and the measure picks are bbox-centroid fakes.++Why it happens: wrap-ups feel like victory laps. You skim past the+compromises.++Why it is wrong: the user can scrub through 70 seconds of video.+They WILL notice. "I claimed it was great" → they distrust every+wrap-up message you write afterward.++Fix: end-of-demo report is brutal-honest. Per-clip table with+quality grade per clip. Mark skipped clips, faked interactions,+lower-quality compromises, and SSIM headroom. End with "OK to ship?"+not "shipped, here's the URL."++## Step 1: Ask how the user wants to record++Present the user with a choice using AskUserQuestion:++> **How would you like to record?**+>+> 1. **Quick recording** — I'll start recording your screen with your mic on.+>    You narrate live, stop when done. Ready to upload immediately.+> 2. **Polished recording** — I'll write a sectioned script first, then record+>    each section as its own clip, generate TTS per section, and `video-post`+>    concats them into one final video. This is the canonical path for+>    feature-tour demos.+> 3. **Just post-process** — I already have a recording. (→ hand off to+>    `video-post` skill)++### Quick recording++If the user picks option 1, see+[per-snippet-flow.md](per-snippet-flow.md) ("Quick recording" section).++### Polished recording (the canonical path)++If the user picks option 2, follow this sequence:++1. **Read [script-and-tts.md](script-and-tts.md)** and write the demo+   script at `<repo>/demo/<name>-demo-script.md`. Show the script to+   the user for approval BEFORE recording.+2. **Read [recording-mechanics.md](recording-mechanics.md)** to pick+   your capture surface (Hydrogen by default; pup if Hydrogen is+   busy) and apply the page-side repaint guarantees.+3. **Read [per-snippet-flow.md](per-snippet-flow.md)** and execute+   Steps 3–8 (verify, record, manifest, storyboard review, concat,+   hero, publish, restore workspace).++Each sub-doc cross-links back here and to siblings — load the one+that matches the phase you're in, not all three at once.++## See also++- [recording-mechanics.md](recording-mechanics.md) — capture path, surfaces, frame-rate sanity, NEVER stitch screenshots+- [script-and-tts.md](script-and-tts.md) — script template, narration phonetics, captions+- [per-snippet-flow.md](per-snippet-flow.md) — verify, record, manifest, storyboard review, concat, hero, publish, workspace restore+- [tts-pronunciation](../tts-pronunciation/SKILL.md) — Adom-specific TTS phonetics+- `video-post` skill (system-installed) — speedup, voiceover, publish-to-wiki post-recording++## Recorded-tab verification BEFORE recording starts++Always `pup browser_screenshot` the target tab and visually confirm it before calling `recording start`. The `active-tab` selector can silently miss (off-screen tab, multi-window setup, recently-closed tab repurposed). A bad active-tab selection can cost an entire 30+ scene demo to re-record.++## Board fills the recording frame++For a PCB-centric demo, never zoom so far that the board looks tiny in the frame. For RP2040-class boards, set radius 1.2–1.5 after `view isometric`. Screenshot the framing before `recording start` and check visually — text-only "view is set" is insufficient proof.++## Demo recording fixes (hard-won)++These are the specific framings and setup tweaks that the user has confirmed across multiple demos:++- **Maximize the Fusion window** before any Fusion-side recording — partial windows look amateurish+- **Use `browser_focus_window` for pup**, NOT `desktop_bring_to_front`. The latter is jarring and disrupts the user; the former just brings the pup window forward without animating+- **Revoke approvals in ALL demo setups** — old "always allow" approvals get bypassed during the recording and the demo looks like the user already trusted everything (which the demo is supposed to be showing as a fresh setup)+- **Never run demos as background bash scripts** — they need foreground control so the recorder can pause/resume cleanly++## Use adom-video for video playback++For "show / play / view / watch / scrub a video" requests, drop the file in `/tmp/videolib/` and open the adom-video viewer at the appropriate port-mapped URL. Gives frame-accurate scrub + A-B loop + pixel zoom. Never serve raw `.webm` over a one-off `python -m http.server` + raw file URL — the viewer features are worth the setup.
skills/demo-recording/per-snippet-flow.mdadded+525
@@ -0,0 +1,525 @@+# Per-snippet flow — record, manifest, storyboard review, concat, publish, cleanup++This is the **execution reference** for a polished demo. It assumes+you already have the script approved+([script-and-tts.md](script-and-tts.md)) and you know the capture+mechanics ([recording-mechanics.md](recording-mechanics.md)).++The flow is:++1. Verify your setup (Step 3 below — ralph-loop yourself before recording)+2. Record one section at a time, each as its own clip+3. `manifest add` each clip with an INTENT description+4. **🛑 MANDATORY storyboard review with the user**+5. Surgical re-shoots based on feedback+6. Concat → hero → wiki upload+7. **🧹 MANDATORY workspace restore**++> Parent skill: [SKILL.md](SKILL.md). Read the "Ten failure modes"+> first — modes 1, 2, 5, 6, 7, 10 are all about exactly this flow.++## Quick recording (live narration, no per-snippet flow)++If the user picked "quick recording" in Step 1 of the parent skill,+this whole file does not apply. Use:++```bash+adom-cli hydrogen recording start --reason "Descriptive reason here"+# User narrates and demonstrates live+adom-cli hydrogen recording stop   # saves to ~/project/recordings/+```++Done. Upload to wiki via `adom-wiki asset upload`. Skip the rest of+this file.++## 🎬 Polished recording — record sections INDIVIDUALLY (non-negotiable)++**The rule:** a polished demo is a sequence of **separately-recorded+clips**, one per section / feature, each with its own TTS narration.+`video-post concat` stitches them together at the end. Do NOT record+one long take covering every feature. Reasons:++- A single long recording turns every mistake into a re-shoot of+  everything. One-take demos always have at least one mumbled line+  or a mis-click and then you re-record 4 minutes to fix a 5-second+  flub.+- Per-clip TTS means Andrew-neural (or whatever voice) narrates each+  section cleanly. A single long TTS over a mixed recording loses+  the intentional pauses between features.+- Hero image selection is trivial when each section is its own file —+  just pick the best frame from clip #1 or #2.+- Re-shoots are surgical: if the Measure tool scene is bad, re-record+  `measure.webm`, update the manifest entry, `video-post concat` again.+  The other 12 clips are untouched.++### Why the storyboard gate exists (read this before skipping it)++AI-made demos fail in specific, localized ways: "clip 5 measured the+wrong pads," "clip 8 never clicked anything," "clip 11 walkthrough+didn't advance past step 1." The human reviewer catches these+instantly **when clips are numbered + described + individually+playable**. On a 3–5 minute uninterrupted video they can't — they'd+have to scrub to the minute:second timestamp and guess which segment+you meant. Skipping storyboard forces the user to do something that's+genuinely hard, and turns the feedback loop from "clip 5 is broken"+(precise) into "something's wrong somewhere" (useless).++Every `manifest add` carries a `--description` so the user can read+your INTENT per clip before hitting play. If your intent is wrong+(e.g. you thought "measure tool = some clicks" instead of "measure+tool = commit three pad-to-pad distances showing HUD readouts"), they+can catch the intent bug without even watching the clip.++## Step 3: Verify your setup (ralph loop yourself)++**Before hitting record, screenshot the workspace and LOOK at it.** Check:++- Are the right panels visible and loaded? (not empty states, loading spinners,+  dead webviews, or cloud icons)+- Is the content you're about to demo actually rendering?+- Is the workspace layout clean — no overlapping panels, no stale tabs?+- Do all the tools/CLIs you need actually work? (test a command first)++If anything looks wrong, fix it before recording. Do NOT start recording and+hope for the best. This is the #1 cause of wasted takes.++**Common setup pitfalls:**+- Webview tabs showing blank/cloud icon → navigate with full DNS URL, not+  relative `/proxy/` path+- Shotlog server has stale VSCODE_IPC_HOOK_CLI → restart shotlog so it picks+  up the current socket+- adom-vscode not installed → `gh release download` from adom-inc/adom-vscode+- Panel not loading → check the server is running (`health` command)++### 🎞 Keep motion on screen — no static panels++Any demo scene that lingers on a table, list, or panel needs visible+**action** happening inside it, not a held still. Examples:++- **BOM / Parts list** — click through several rows, not just one. Each+  click swaps the detail view on the right and makes the scene feel+  alive. 3-4 part clicks over 15 seconds beats one click and dead air.+- **Components HUD** — toggle a couple of groups or individual chips+  on/off rather than staring at the list.+- **Schematic / PCB tabs** — pan or zoom so the artwork isn't frozen.+- **Walkthrough / tour** — let it auto-advance; don't pause on a step.++A frozen panel reads as "this tool is boring." Action — even small+clicks that add up to a few state changes — reads as "this tool is+responsive and worth learning." Every scene longer than 8 seconds+needs at least one visible interaction.++### 📐 Frame the split at 30% AI / 70% feature — don't maximize the feature pane++When recording `adom-*` demos the VS Code pane (showing Claude Code live in+the chat) MUST stay meaningfully visible. The canonical ratio is **0.30**+— 30% of the width is VS Code / Claude chugging, 70% is the feature+(adom-tsci, KiCad, Fusion, etc.). That's the Adom pitch: **AI-driven EDA**.+The viewer needs to see Claude actually *doing* the thing they're+watching in the feature pane. A full-width feature pane with no chat looks+like any other tool. A split where Claude is chugging on the left and the+feature is responding live on the right is what makes Adom Adom.++```bash+# Canonical: 30% AI / 70% feature+adom-cli hydrogen workspace resize --split-id "$SPLIT_ID" --ratio 0.30++# Wrong: feature pane maximized, AI hidden+adom-cli hydrogen workspace resize --split-id "$SPLIT_ID" --ratio 0.01+# Wrong: too much AI, feature squeezed+adom-cli hydrogen workspace resize --split-id "$SPLIT_ID" --ratio 0.50+```++Exception: if the demo is genuinely AI-free (a static pitch deck, a hero+shot for thumbnails, a no-interaction explainer), maximizing the feature+pane is fine. Default is 0.30 until you have a reason otherwise.++## Step 4: Record section by section++For a polished / feature-tour demo, use the **per-section** flow. One+clip per section, TTS per section, aggregated at the end.++### 🛑 Screenshot-verify the target tab BEFORE `recording start` — every clip, no exceptions++**The rule:** between opening/activating a tab and pressing `recording start`, **take a screenshot of the tab and actually LOOK at it.** If what you see isn't what you expected to record, you're about to capture that bad state for the entire clip duration — and you won't notice until post-mux storyboard review (best case) or until the user plays the final and asks "why is every example black?" (worst case, cost = full re-shoot).++Why this breaks: `adom-cli hydrogen workspace active-tab` can silently miss. `webview open-or-refresh` returns 204 before the page has loaded its WASM / Babylon / whatever. A 3D viewer may need 5-15 seconds after `--url` lands before the board mesh is actually visible. Every second of `recording start` before the scene is visually ready = one second of black frames locked into the muxed clip.++**The check, one-liner before each `recording start`:**++```bash+# Activate, then SLEEP + screenshot + visual verify before recording+adom-cli hydrogen workspace active-tab --name "<tab>"+sleep 2+adom-cli hydrogen screenshot panel --name "<tab>" --reason "pre-record verify for <clip-id>"+# Read the saved file. If it's not showing what you intended — DO NOT RECORD YET.+# Poll-and-retry, longer sleep, re-navigate, etc.+```++In a driver script the pattern is:++```python+def wait_for_ready(tab_name, check_substring=None, timeout=15):+    for i in range(timeout):+        img = take_panel_screenshot(tab_name)+        if looks_ready(img, check_substring):+            return True+        time.sleep(1)+    raise RuntimeError(f'tab {tab_name} never rendered')+```++Where `looks_ready()` does one or more of:+- Size/histogram check — a black frame is low-variance; a board has mid-tones+- OCR substring — caption or chip label visible+- DOM probe via `adom-tsci eval` — `window.viewer && viewer.getScene().meshes.length > 10`++**Cost of skipping this rule:** past incident — shot 36 scenes of a demo, every one of them a blank webview because active-tab missed. Full re-shoot. Same has happened on 18 of 20 scenes in the 1.3.8 tour. Don't be the next case study.++### The record loop++```bash+MANIFEST=/tmp/<name>-manifest.json+CLIPS=/tmp/<name>-clips/+mkdir -p $CLIPS+video-post manifest init --output $MANIFEST++for each section in script:+  # 1. Show caption (viewer-side context)+  adom-cli hydrogen caption show "<section title>" -d 0     # leave up for whole clip+  # 2. Activate the target tab + any CLI pre-positioning+  adom-cli hydrogen workspace active-tab --name "<feature tab>"+  adom-tsci view isometric --port <port>                    # example+  # 3. 🛑 SCREENSHOT-VERIFY — skip this and you're shooting blanks (see above)+  adom-cli hydrogen screenshot panel --name "<feature tab>" \+    --reason "pre-record verify <clip-id>"+  # Read the file. Confirm the board is visible. Retry if not.+  # 4. Start recording+  adom-cli hydrogen recording start --share tab --mic false --countdown 0 \+    --reason "<section title> — <what you'll see>"+  # 5. Drive UI silently — 8-25 seconds of focused action+  #    adom-tsci view top / hover / click / etc+  # 6. Stop+  adom-cli hydrogen recording stop+  # last recording is saved to ~/project/recordings/<timestamp>.webm+  CLIP=$(ls -t ~/project/recordings/*.webm | head -1)+  mv "$CLIP" "$CLIPS/<id>.webm"+  # 5. Generate TTS narration+  edge-tts --voice en-US-AndrewNeural \+    --text "<narration text for this section>" \+    --write-media "$CLIPS/<id>.mp3"+  # 6. Mux audio onto the video (replace the silent track)+  ffmpeg -y -i "$CLIPS/<id>.webm" -i "$CLIPS/<id>.mp3" \+    -map 0:v -map 1:a -c:v copy -c:a libopus -shortest \+    "$CLIPS/<id>-narrated.webm"+  # 7. Register in manifest — --description is MANDATORY, see below+  video-post manifest add \+    --id "<id>" --title "<short label>" \+    --description "<full intent: what this clip shows, which project, \+                   which features, what the viewer should notice>" \+    --raw "$CLIPS/<id>-narrated.webm" \+    $MANIFEST+  # 8. Hide caption before the next section+  adom-cli hydrogen caption hide+```++### 📝 `--description` on every `manifest add` — not optional++Every clip registered in the manifest MUST carry a `--description` that+says **what you INTENDED the clip to show**. Not just the feature name:+the project, the interactions, the expected visible payoff. The+storyboard UI (Step 5 below) renders this under each clip so the user+can tell "did Claude even understand what this clip was supposed to+do?" before watching it.++Without per-clip descriptions, the user watches a 3-5 minute video+blind and has to guess which segment is broken. With descriptions, they+look at clip N's intent, watch its 15-second snippet, and tell you+exactly "clip 5 measured the wrong pads" or "clip 8 never clicked+anything." That feedback loop is the whole reason this skill exists.++Rule of thumb: if a teammate reading just the description couldn't+write the clip's ffmpeg filter / interaction script themselves, the+description is too vague. Examples of GOOD descriptions:++- ✅ "BME680 cinematic slow orbit for 15s. Camera starts iso, completes+  one full rotation. No HUDs, no tool activation — just the board."+- ✅ "iCE40-USB Measure: commit MP1→MP3 (diagonal 181 mm), clear, commit+  U1→J1 (chip to USB-C connector, ~50 mm), clear, commit MC_USB_DP→DM.+  Each measurement's ΔX/ΔY/distance HUD stays visible for 5s."+- ❌ "show measure tool"+- ❌ "inspect demo"++**Always use a descriptive `--reason`** on `recording start`. The user+sees it in the approval dialog. Bad: "Demo recording". Good: "Recording+adom-tsci Inspect tool — hover pads, show net + JLCPCB part card."++**TTS voice:** `en-US-AndrewNeural` is the default. Alternatives when+the demo needs variety: `en-US-AriaNeural` (polished female),+`en-US-GuyNeural` (warm neutral male). Pick ONE voice for a whole+demo — switching voices mid-demo is jarring.++## Step 5: 🛑 MANDATORY — open video-post storyboard and let the user review every clip++**This step is not optional.** The whole per-section recording flow+exists so the user can audit each clip individually, because AI-driven+demos routinely screw up details a full-video watch can't pinpoint.+Skipping this step — going straight from manifest → concat → wiki+upload — destroys the user's ability to tell you "clip 5 measured the+wrong thing" or "clip 8 never clicked anything." Don't do it.++```bash+# Opens a Hydrogen webview listing every clip in the manifest with its+# title, description, and a player. The user can play each one and+# tell you which clips are bad without watching the whole video.+video-post storyboard $MANIFEST \+  --tab-name 'video-post: <name> review' \+  --port 8797+```++Then:++1. **Actively point the user at the storyboard tab.** Activate it,+   screenshot to confirm it rendered, tell the user the tab name to look+   at. "The storyboard is open in tab `video-post: …`, 13 clips, take a+   look and tell me which ones are wrong."+2. **Wait for user feedback.** The user will tell you "clip 5 needs+   more parts clicked," "clip 11 walkthrough isn't advancing," etc. —+   or just "looks good."+3. **Re-shoot surgically.** `video-post manifest remove --id <id>`,+   re-record that one clip, `manifest add` with updated description,+   point the user back at storyboard. Repeat until they approve.+4. **Only then** proceed to Step 6 (concat + publish).++Never concatenate or upload a demo the user hasn't seen clip-by-clip.++### Background-mux while the user reviews++The concat step is slow (60–90 s for a 6-clip demo). The user+reviewing snippets in storyboard does NOT need it done. These should+run in parallel:++```bash+# Kick concat as a background job the moment manifest is final.+( ffmpeg -y -f concat -safe 0 -i $LIST -c copy \+    ~/project/recordings/<name>-final.webm \+  && echo "concat DONE" ) &+CONCAT_PID=$!++# In parallel, open the storyboard for review.+video-post storyboard $MANIFEST --tab-name 'video-post: <name> review' --port 8797++# When user approves: concat is already done. Upload.+# When user flags a re-shoot: kill the in-flight concat, re-shoot the clip,+# update manifest, restart concat.+kill $CONCAT_PID 2>/dev/null+```++Treating concat as a finish-line task instead of a parallel one wastes+60–90 s × number-of-revisions of user time.++## Step 6: Concat + publish — ONLY after storyboard approval++```bash+# Option A: `video-post concat` (drops audio today — KNOWN BUG, use B)+# Option B: ffmpeg concat demuxer (preserves all streams)+LIST=$(mktemp)+for c in $CLIPS/*-narrated.webm; do echo "file '$c'" >> $LIST; done+ffmpeg -y -f concat -safe 0 -i $LIST -c copy \+  ~/project/recordings/<name>-final.webm+rm $LIST+```++Verify audio is present (voiceover is the whole point) — an all-video,+no-audio output is a bug to catch here, not after upload:++```bash+ffprobe -v error -show_streams <name>-final.webm | grep codec_type+# Must show both `video` and `audio`.+```++Then publish (Step 7). If any section is bad, `video-post manifest+remove --id <id>`, re-record that ONE clip, re-register with updated+description, reopen storyboard for another review pass, then re-concat.+The surgical re-shoot is the whole point.++## Step 6a: 🛑 Show the final in `adom-video` BEFORE wiki upload++Before any `adom-wiki asset upload`, the final webm goes through+adom-video so the user can scrub it in pup. Do NOT report+`final.webm produced at /tmp/...` and stop — that leaves the user to+find the file, copy it locally, and open it in a random player. The+result is review friction and missed bugs.++The right shape is: produce the webm, drop it in `/tmp/videolib/`, run+`adom-video play`, navigate the user's pup tab to the proxy URL.+Two-line completion:++```bash+mkdir -p /tmp/videolib && cp ~/project/recordings/<name>-final.webm /tmp/videolib/+adom-video play /tmp/videolib/<name>-final.webm    # prints "OK: webview url: ..."+```++The CLI also exposes `seek`, `pause`, `set-rate`, `extract-frame`,+`trim` — meaning if the user spots a bad section, `adom-video trim+--in <t> --out <t>` cuts it without re-encoding, no need to re-shoot.+That's why adom-video is the canonical playback surface and why+`python3 -m http.server` + raw file URL is wrong: you lose every one+of those CLI hooks.++If `adom-video` isn't installed, install it from the wiki page first —+don't fall back to a generic player. Memory rule:+`feedback_video_playback_uses_adom_video.md`.++## Step 6b: Hero image + supplementary screenshots++Pick one frame from the final video as the wiki hero. Usually the+frame where the marquee feature is clearest (e.g. the Inspect card+hovering a chip). Then pull 2-3 more for the README / wiki body.++```bash+ffmpeg -y -ss <t> -i final.webm -frames:v 1 hero.png+shotlog resize hero.png     # keeps wiki pages lean+```++## Step 7: Publish to wiki — video, hero, screenshots, AND the demo script itself++```bash+# Final video+adom-wiki asset upload apps/<page> --asset-type video \+  --file <name>-final.webm \+  --caption "<one-line summary — what the viewer learns>"++# Hero image+adom-wiki asset upload apps/<page> --asset-type hero \+  --file hero.png \+  --caption "<the marquee feature in one phrase>"++# Supplementary section screenshots+for s in hero section1 section2 ...; do+  adom-wiki asset upload apps/<page> --asset-type screenshot \+    --file $s.png --caption "<what this shot shows>"+done++# ⭐ Demo script itself — upload as a "reproducible recipe" asset+# so another Adom user (or another Claude thread) can find + replay+# it without spelunking the source repo.+adom-wiki asset upload apps/<page> --asset-type demo-script \+  --file <repo>/demo/<name>-demo-script.md \+  --caption "Reproducible script for this demo — scenes, narration, TTS config, reproduction commands"+```++### Commit the script to the repo in the same PR as the app changes++The demo script is *code* for the purposes of review and diff. When you release a new version of the app, the demo script gets bumped too (new features → new scenes → new narration). Rule of thumb: **the version at the top of the demo script and the version of the app shipping in the PR must match.** A PR that bumps the app without bumping the demo script is incomplete.++```bash+git add <repo>/demo/<name>-demo-script.md <repo>/demo/record.py <repo>/demo/manifest.json+git commit -m "demo-script: update for <version> — <what changed>"+```++### Why the script is a first-class asset++- **Discoverability.** Another Adom user browsing the wiki app page sees the video and the script. They click the script and learn: "oh, here's exactly how this was demonstrated, and I can re-render it for my fork."+- **AI-reproducibility.** A fresh Claude thread pointed at the wiki page can pull the script and re-record the demo against a new version of the app without hand-holding. That's the payoff of putting the narration + TTS config + reproduction block in the file.+- **Review + tweak.** Someone spots a typo in scene 7's narration? They edit the script file, open a PR, CI re-renders the affected clip, hero regenerates. No mystery about what changed.++The hero image is what shows up on the wiki landing page, in+link-preview cards, and as the social-share image — pick it+deliberately. A board on a white background beats a dark canvas full+of toolbar buttons. A card / panel that's clearly *adom-tsci* beats a+generic 3D render.++## Step 8: 🧹 MANDATORY — restore the user's workspace before you stop++**Do not leave the user's Hydrogen workspace cluttered with demo+artifacts.** The recording flow adds tabs, swings split ratios to 20/80,+activates panels, and spawns background servers (storyboard, dev-harness,+animation-server, whatever). None of that should persist past "demo+published." Leaving the workspace in its mid-recording state is the+same bug as AI panels parking themselves over VS Code — the user has+to manually drag everything back every single time.++### What to restore++Before `recording start`, capture three things:++1. **Split ratios.** `adom-cli hydrogen workspace get` → walk the tree+   → note every split's `ratio`. Save them to a temp file (you'll need+   them for the restore).+2. **Pre-existing tabs.** `adom-cli hydrogen workspace tabs` → record+   `{ tabId, name }` for every tab that existed before you started. Any+   tab that DIDN'T exist before is yours to remove at the end.+3. **The focused / active tab per pane.** So you don't leave the user+   staring at your storyboard tab after the demo ends.++After `adom-wiki asset upload` finishes, tear down in reverse:++```bash+# 1. Kill background servers you spawned.+pkill -f "video-post storyboard"+# (add any app-specific dev servers you started, e.g. tsci dev on 8850)++# 2. Remove every tab you added during the demo. Compare the current+#    tab list against the pre-demo snapshot; the delta is yours.+for name in "<tab you added>" "<video-post: ... review>"; do+  adom-cli hydrogen workspace remove-tab --name "$name"+done++# 3. Restore every split's ratio from the pre-demo snapshot.+adom-cli hydrogen workspace resize --split-id "<id>" --ratio <original>++# 4. Re-activate the tab the user was looking at before you started.+adom-cli hydrogen workspace active-tab --name "<pre-demo active tab>"++# 5. Clean up any scratch files you created in the project tree+#    (temp `.kicad_pcb` copies, /tmp/demo-clips/ if you want — though+#    leaving clips in /tmp is generally fine since /tmp is scoped;+#    the hard rule is the user's visible project tree).+rm -f <any temp files you copied into project dirs>+```++### Verification++End the demo with a `workspace get` screenshot to confirm the tree+matches the pre-demo snapshot (same split ratios, same tab count, same+focused tab). If it doesn't match, keep cleaning until it does.++### Why this matters++A user asking for a "demo" is asking for a deliverable (video + wiki+page). They are NOT asking to have their workspace rearranged for half+an hour. The mid-recording 20/80 split + temporary tabs are part of the+*recording apparatus*, not part of the deliverable. Treat them like+scaffolding — build with them, demolish them when the video is up.++If a future demo skill (like the adom-tsci skill, or an adom-chipfit+demo) inherits this doctrine, it should do the same: snapshot before+record, restore after publish. Record a follow-up `feedback` memory+entry if you discover an artifact type this skill didn't cover+(e.g. "adom-tsci demo left tsci dev running on :8850" → add a `pkill`+line to the tool-specific demo flow).++## Legacy one-take + speedup path (not preferred)++The older flow recorded a single long take then sped up slow parts+with `video-post process --markers`. It still works and is the right+call for *live* narration demos where timing is inseparable from+speech — but for **scripted feature tours**, the per-section path+above beats it on every axis: easier re-shoots, cleaner TTS, better+hero selection, no "narrate fast here, slow there" timing puzzle.++If you do need the legacy path:++```bash+video-post inspect                      # show speedup summary+video-post process --input <file>.webm --markers /tmp/video-post-markers.jsonl+video-post voiceover --input <file>-fast.webm   # live-narration UI+adom-wiki asset upload apps/<page> --asset-type video --file <final>.webm \+  --caption "..."+```++## See also++- [SKILL.md](SKILL.md) — parent: orchestration, the ten failure modes, the "real demo" 10-step shape+- [recording-mechanics.md](recording-mechanics.md) — capture path, surfaces, frame-rate sanity, NEVER stitch screenshots+- [script-and-tts.md](script-and-tts.md) — script template, narration phonetics, captions
skills/demo-recording/recording-mechanics.mdadded+292
@@ -0,0 +1,292 @@+# Recording mechanics — capture path, surfaces, frame-rate sanity++This is the **capture-mechanism reference** for demo recordings.+Cross-cuts both Hydrogen and pup paths. Read this before touching+`recording start` / `browser_record_start` for the first time in a+session. Everything that's catastrophic-if-skipped is in the first+half of the file.++> Parent skill: [SKILL.md](SKILL.md). The "Ten failure modes" and+> "What a real demo means" sections in the parent are preconditions+> to this file. If you haven't read them, stop and go back.++## 🚨 NEVER stitch `browser_screenshot` calls into a video++**The capture mechanism for ALL pup-recorded video demos is+`adom-desktop browser_record_start` / `browser_record_stop` — full+stop.** Do not write a recorder that polls `browser_screenshot` in a+loop and ffmpeg-stitches PNGs into a webm. `browser_screenshot` is a+~770 ms-per-call one-shot devtools capture; even 4-way concurrent it+caps around 3 fps, and the resulting "video" plays as a slideshow.+Users cannot diagnose this — they just see "the demo looks horrific"+and the conversation gets uglier from there.++**Diagnostic:** if your `browser_record_start` recording came out+"frozen" (CDP screencast deduplicates identical pixel frames), the+problem is **the page didn't repaint**, NOT the capture mechanism.+Fixes go on the page side, not the recorder side:++1. Run a `setInterval(() => { engine.beginFrame(); scene.render(); engine.endFrame(); }, 33)` for the duration of the recording so the canvas always paints.+2. Animate the camera (alpha += small_increment) on every tick so each frame has unique pixels — CDP only deduplicates pixel-identical frames.+3. Strip ArcRotateCamera input handlers (`cam.inputs.removeByType('pointers'|'mousewheel'|'keyboard')`) before mutating alpha — they recompute camera state from inertia every frame and clobber direct writes.+4. UI events (hover, ctxmenu, isolate, inspector) stack ON TOP of the always-painting canvas — they never replace it.++`browser_record_start` reliably captures **20–30 fps real footage**+when the page is actually painting. That's 10–20× the rate of+screenshot-stitching, and the resulting webm scrubs / plays smooth.++**Post-process for size or framerate AFTER recording, not by+sacrificing capture rate:**++```bash+# Recorded at 30 fps; downsample to 24 fps + cut bitrate+ffmpeg -i raw.webm -c:v libvpx-vp9 -b:v 1.2M -r 24 -g 30 -keyint_min 30 \+       -c:a copy out-24fps.webm++# Or two-pass for tighter quality at a target bitrate+ffmpeg -y -i raw.webm -c:v libvpx-vp9 -b:v 1.5M -pass 1 -an -f null /dev/null+ffmpeg    -i raw.webm -c:v libvpx-vp9 -b:v 1.5M -pass 2 -c:a libopus out.webm+```++If you find yourself reaching for `browser_screenshot` polling+because `browser_record_start` "doesn't work," **stop and re-read this+section.** The capture is fine; the page isn't painting.++## 🚨 Wiki webm: keyframes every 1 s — NEVER upload raw recordings++**Every webm uploaded to the wiki must be re-encoded with a keyframe+interval of ~1 s (`-g 30 -keyint_min 30` for 30 fps).** Without that,+the wiki server's video element can't seek — the user can play the+clip start-to-end but can't scrub the timeline, jump to a section, or+share a deep-linked timestamp. `<video>` seeks land on the nearest+keyframe; without dense keyframes, the entire clip is one giant+seekable unit.++Raw `adom-cli hydrogen recording stop` output and raw CDP-frames mux+output **default to one keyframe at frame 0 only** — every subsequent+frame is a P-frame, no scrub possible. This bites every time someone+uploads a single-shot recording straight to `adom-wiki asset upload`+without going through the video-post pipeline. (Bit me on+`molecules/brianna-led-nameplate`'s `brianna-demo.mp4`; bit Kyle the+same day.)++**The rule:** the LAST ffmpeg pass on any clip destined for the wiki+must include `-g 30 -keyint_min 30`. For 60 fps recordings use+`-g 60 -keyint_min 60`. The exact incantation depends on codec:++| Codec | Keyframe flags |+|---|---|+| `libvpx-vp9` (webm) | `-g 30 -keyint_min 30` |+| `libx264` (mp4) | `-g 30 -keyint_min 30 -force_key_frames "expr:gte(t,n_forced*1)"` |+| `libvpx` (legacy webm) | `-g 30 -keyint_min 30` |++The `-force_key_frames` expression on x264 is belt-and-suspenders —+some x264 builds honor `-g` as a soft hint and let scene-detection+override it; the expression forces a keyframe at integer-second+boundaries regardless.++**Verify after encode:**++```bash+ffprobe -v error -select_streams v:0 -show_entries packet=pts_time,flags \+  -of csv=p=0 final.webm | grep ',K' | head -10+# Should show keyframes (",K_") at ~0.0s, ~1.0s, ~2.0s, ...+# If you only see one ",K_" line at 0.0s, the encode dropped the rule.+```++If you find yourself running `adom-wiki asset upload --asset-type+video --file <raw>.webm` without a re-encode pass first, **stop**.+Pipe through `ffmpeg -i raw.webm -c:v libvpx-vp9 -b:v 2M -g 30+-keyint_min 30 -c:a copy out.webm` first, even on a single-shot+recording you weren't going to edit. The re-encode adds ~10 s for a+30 s clip and saves the user from a non-scrubbable wiki video.++The video-post `concat` path SHOULD apply this on the final mux — if+your demo went snippet → manifest → concat, you're covered. The+single-shot path (just `recording stop` → upload) is where this gets+silently skipped, so the rule above applies *especially* there.++## Recording surface — Hydrogen by default, pup when Hydrogen is busy++The default recording surface is `adom-cli hydrogen recording start` — the+Hydrogen webview captures the full editor (panels + workspace) into a clean+webm, with `caption show` integrated.++**When Hydrogen is unavailable** — another Adom tool is occupying the webview+panels (e.g. `adom-tsci` showing a 3D preview, an existing demo storyboard+up for review) — switch to **pup** (a separate+Chrome window via Puppeteer). Pup recordings happen on the user's Windows+desktop, fully orthogonal to anything happening inside Hydrogen, so the two+surfaces never compete.++| Surface | When | Capture mechanism | Caption tooling | Notes |+|---|---|---|---|---|+| **Hydrogen** | Default. Demoing a Hydrogen-hosted feature. | `adom-cli hydrogen recording start --reason ...` → MediaRecorder on the workspace iframe. | `adom-cli hydrogen caption show "<text>"` overlays cleanly. | Best resolution + cleanest output. Don't use if another tab in the same workspace is occupying webviews you'd want to recapture later. |+| **pup** | "Aci is using webviews", "don't touch my Hydrogen layout", "the tool I'm demoing has its own UI in a Chrome tab anyway." | `adom-desktop browser_open_window` + `browser_focus_window` + `browser_record_start` (CDP screencast on the pup tab). | No `caption show` — render the caption inside the page itself with a temporary `<div>` injected via `browser_eval`, OR rely entirely on TTS narration. | Frame rate is paint-throttled by Chrome unless the window is OS-foregrounded — see "🔥 The pup foreground-rebump trick" below. Tar pulls of large recordings may exceed the 60-second WebSocket timeout — pull frames in batches of ~40 if `pull_file` returns null. |++### Third surface — the NATIVE browser (only for logged-in demos)++When a demo needs the user's **real session / cookies / logins** (a tool gated behind their account), neither+Hydrogen nor pup will do — those are throwaway/managed browsers with **no logins**. Only the user's real Chrome+has them. Record it through the **adom-browser-extension** (`nbrowser_record_start {method:"wgc"|"getdisplaymedia"}`).++**The canonical example is `chip-fetcher`.** It fetches each part's CAD by signing into the engineer's **stored+vendor accounts** — Mouser, DigiKey, SnapEDA/SnapMagic, ComponentSearchEngine, TI, etc. A pup or Hydrogen browser+isn't logged into any of those, so a chip-fetcher demo there would just hit login walls. To record chip-fetcher+(or anything that leans on the user's saved logins), you **must** drive + capture the user's real Chrome via the+extension — that's the whole reason the native-browser recording path exists.++The twist: you **must NOT foreground-rebump the user's real working browser** (the pup trick below would wreck+their flow). Instead, make their Chrome **paint while occluded** by launching it with the occlusion/throttle-disable+flags — a restart-into-background-recording mode you enter, record in, then **offer to revert** (battery). **Full+recipe: the [`native-browser-recording`](https://wiki.adom.inc/john/adom-browser-extension/files/skills/native-browser-recording/SKILL.md)+skill in the adom-browser-extension.** Use this surface ONLY when stored logins are required; otherwise prefer+Hydrogen or pup.++### 🔥 The pup foreground-rebump trick — required for high frame rate++Chrome **paint-throttles** any tab whose window is not the OS foreground. CDP `Page.startScreencast` only emits a frame when the page repaints, so a backgrounded pup tab silently drops to ~5 fps even when you asked for 15 fps. Symptoms: 275 captured frames in 46 s of wall-clock = ~6 fps real, video plays back ~3× faster than reality, narration desyncs from action.++**The trick:** `browser_focus_window` calls Win32 `SetForegroundWindow` to raise the pup window to the OS foreground. Calling it ONCE before `browser_record_start` is not enough — anything that grabs OS focus during the recording (Hydrogen tab switch, a notification, the user clicking elsewhere) re-throttles the pup tab. The fix is to **re-bump foreground before every action**, not just at the start.++```bash+SESS="demo-snippet"++# Open + initial focus+adom-desktop browser_open_window "$(jq -nc --arg s "$SESS" --arg u "$URL" \+  '{sessionId:$s, profile:$s, url:$u}')"+sleep 4+adom-desktop browser_focus_window "$(jq -nc --arg s "$SESS" '{sessionId:$s}')"+sleep 1++# Re-bump RIGHT BEFORE record_start — this is the critical one+adom-desktop browser_focus_window "$(jq -nc --arg s "$SESS" '{sessionId:$s}')"+RID=$(adom-desktop browser_record_start "$(jq -nc --arg s "$SESS" \+  '{sessionId:$s, fps:15, quality:75}')" | jq -r '.recordingId')++# Wrap every action so foreground gets re-bumped before each one+focused_eval() {+  adom-desktop browser_focus_window "$(jq -nc --arg s "$SESS" '{sessionId:$s}')" >/dev/null+  adom-desktop browser_eval "$(jq -nc --arg s "$SESS" --arg e "$1" \+    '{sessionId:$s, expr:$e}')" >/dev/null+}++focused_eval 'document.getElementById("btn-foo").click()'+sleep 3                        # short sleeps are fine — focus persists across them+focused_eval 'document.getElementById("btn-bar").click()'+sleep 3+adom-desktop browser_record_stop "$(jq -nc --arg s "$SESS" --arg r "$RID" \+  '{sessionId:$s, recordingId:$r}')"+```++**Verification — sniff-test the captured frame count vs wall-clock duration:**++```bash+# After every recording, check this BEFORE you commit to mux + TTS:+STOP=$(adom-desktop browser_record_stop ...)+FRAMES=$(echo "$STOP" | jq -r '.frameCount')+DUR_MS=$(echo "$STOP" | jq -r '.durationMs')+ACTUAL_FPS=$(python3 -c "print(${FRAMES}/(${DUR_MS}/1000))")+echo "captured ${ACTUAL_FPS} fps (asked for 15)"+# Below 12 fps → the window dropped foreground at some point. Re-record with the rebump pattern, don't try to mask with playback speedup.+```++**Don't substitute `browser_alert_window`** — it flashes the taskbar but explicitly does NOT steal foreground (so Chrome stays paint-throttled). Don't substitute `desktop_bring_to_front` either — it works but per the project memory ("use browser_focus_window for pup, not desktop_bring_to_front"), the browser-typed call is the canonical one.++**Side-effect to know about:** because `SetForegroundWindow` is OS-level, the user briefly sees the pup window raise to front during recording. Tell them in advance ("you'll see a Chrome window pop forward for ~30 s") so they don't think your code is fighting their workspace.++### Foreground alone isn't enough — also keep something painting++Foreground-rebump fixes Chrome's window-level paint throttle, but there's a **second** throttle: CDP `Page.startScreencast` is **event-driven**. It only emits a frame when the page actually repaints. A static, idle page (e.g. "user looking at the player view, video paused, no animation") produces zero frames per second even if the window is fully foregrounded.++Symptom: 7 seconds of "let the player view sit" captures 1 frame. The whole demo plays back too fast.++Two fixes, pick one:++1. **Keep a small animation always running on the page.** A 1-second CSS pulse on a non-distracting element does it — e.g. on the `.hud .dot`:++   ```css+   .hud .dot { animation: hud-pulse 1s infinite ease-in-out; }+   @keyframes hud-pulse { 0%,100%{opacity:.6} 50%{opacity:1} }+   ```++   This keeps the compositor running, which keeps frames flowing through CDP. Cost: a barely-visible animated dot. Benefit: full requested frame rate during static sections.++2. **Force micro-motion via `browser_eval`** between actions when you can't add CSS to the target page. Inject a 1-pixel scroll back-and-forth every 200 ms, or update a CSS variable that affects a transform:++   ```bash+   # Background loop nudging the page during the recording+   (+     while [ -f /tmp/avdemo/.recording ]; do+       focused_eval 'document.documentElement.style.setProperty("--nudge", Math.random())'+       sleep 0.2+     done+   ) &+   ```++**Sniff-test** with the same `actual_fps` formula above: if it's still <12 after the foreground rebump, the issue is paint-throttling on a static page, not OS focus.++### Before you re-record — try Hydrogen instead++If you've fought pup paint-throttling for two attempts and the demo still isn't smooth, **switch to Hydrogen recording** (`adom-cli hydrogen recording start`). Hydrogen captures the workspace via `getDisplayMedia` + `MediaRecorder`, which polls the framebuffer at the requested rate regardless of whether the page is repainting. Cost: you need the Hydrogen workspace free of competing tabs. Benefit: full 30 fps with no rebump tricks.++Pup is the right answer when Hydrogen is genuinely occupied. When Hydrogen is free, prefer it.++**Pup-recording per-section pattern** (substitute for the Hydrogen recording+block in [per-snippet-flow.md](per-snippet-flow.md) when the user's Hydrogen is busy):++```bash+URL="https://<container>.adom.cloud/proxy/$PORT/"   # the proxied URL of the tool being demoed++# Per section:+SESS="demo-$ID"+adom-desktop browser_open_window "$(jq -nc --arg s "$SESS" --arg u "$URL" \+  '{sessionId:$s, profile:$s, url:$u}')"+sleep 4+adom-desktop browser_focus_window "$(jq -nc --arg s "$SESS" '{sessionId:$s}')"+sleep 1++RID=$(adom-desktop browser_record_start "$(jq -nc --arg s "$SESS" \+  '{sessionId:$s, fps:15, quality:75}')" | jq -r '.recordingId')++# ... drive the section via browser_eval calls ...+adom-desktop browser_eval "$(jq -nc --arg s "$SESS" --arg e "<JS>" \+  '{sessionId:$s, expr:$e}')"++STOP=$(adom-desktop browser_record_stop "$(jq -nc --arg s "$SESS" --arg r "$RID" \+  '{sessionId:$s, recordingId:$r}')")+TAR=$(echo "$STOP" | jq -r '.tarPath')++adom-desktop pull_file "$(jq -nc --arg t "$TAR" '{filePaths:[$t], saveTo:"/tmp/demo/tar"}')"+adom-desktop browser_close_window "$(jq -nc --arg s "$SESS" '{sessionId:$s}')"++# Mux the CDP frames → webm+EXTRACT=/tmp/demo/tar/${ID}-extract && mkdir -p "$EXTRACT"+tar -xf "/tmp/demo/tar/$(basename "$TAR")" -C "$EXTRACT"+FRAME_DIR=$(find "$EXTRACT" -name ffmpeg-concat.txt -printf '%h\n' | head -1)+( cd "$FRAME_DIR" && ffmpeg -y -loglevel error -f concat -safe 0 \+    -i ffmpeg-concat.txt -c:v libvpx-vp9 -b:v 2M -deadline realtime \+    -cpu-used 8 -row-mt 1 -g 30 -keyint_min 30 \+    -an "/tmp/demo/raw/${ID}.webm" )++# Then standard TTS + mux + manifest add (same as Hydrogen path —+# see per-snippet-flow.md)+adom-tts say "<narration>" --out /tmp/demo/tts/${ID}.mp3 --voice en-US-AndrewMultilingualNeural+ffmpeg -y -loglevel error -i /tmp/demo/raw/${ID}.webm -i /tmp/demo/tts/${ID}.mp3 \+  -filter_complex "[0:v]tpad=stop_mode=clone:stop_duration=10[v]" \+  -map "[v]" -map 1:a -c:v libvpx-vp9 -b:v 2M -deadline realtime -cpu-used 8 \+  -g 30 -keyint_min 30 \+  -c:a libopus -b:a 96k -shortest /tmp/demo/narrated/${ID}.webm+video-post manifest add /tmp/demo/manifest.json --id "$ID" --title "..." \+  --description "..." --raw /tmp/demo/narrated/${ID}.webm /tmp/demo/narrated/${ID}.webm+```++The `tpad=stop_mode=clone:stop_duration=10` filter holds the last frame so+short recordings don't cut the narration off — `-shortest` then trims the+output to whichever stream ends first (almost always the audio).++## See also++- [SKILL.md](SKILL.md) — parent: orchestration, the ten failure modes, the "real demo" 10-step shape+- [per-snippet-flow.md](per-snippet-flow.md) — manifest, storyboard review, mux + concat, hero, upload+- [script-and-tts.md](script-and-tts.md) — script template, narration phonetics, captions
skills/demo-recording/script-and-tts.mdadded+268
@@ -0,0 +1,268 @@+# Demo script + TTS narration + captions++This is the **content reference** — what to write down before+recording. The demo script is a first-class artifact (checked into+the repo, published to the wiki); narration text is approved by the+user BEFORE TTS rendering; captions are rendered live in the+workspace during capture.++> Parent skill: [SKILL.md](SKILL.md). Read the "Ten failure modes"+> and "What a real demo means" sections first.++## Write the demo script — self-contained, reproducible, checked into the repo++The demo script is a **first-class artifact** of the repo, not scratch work. It must be checked into the project git repo at `<repo>/demo/<name>-demo-script.md` (not just `/tmp/`) and — at the end of [per-snippet-flow's wiki publish step](per-snippet-flow.md) — published to the wiki so other Adom users can run, review, or tweak the demo without digging through chat history. Treat it the way you'd treat a `Makefile`: if another person (or another Claude) can't reproduce the demo from this file alone, the file is incomplete.++> **Repo location.** `<repo>/demo/<name>-demo-script.md` is the canonical home. Put companion files (record.py, mux.py, generated manifest, any custom TTS overrides) in the same `demo/` directory. `/tmp/` paths are fine *during execution* but the source of truth lives in the repo so git history tracks every demo revision.++### The script MUST be self-sufficient — every piece, one file++The script is a runnable spec. A fresh Claude thread (or a teammate) reading it should be able to re-render the entire demo without asking any questions. That means every setting that affects the output goes in the file, not in the assistant's head:++- **Metadata header** — version being demoed, target duration, recording date, hero frame timestamp, final output path+- **Tooling deps** — `edge-tts` / `adom-cli` / `video-post` / `ffmpeg` versions / required env vars+- **Voice + rate** — `en-US-AndrewMultilingualNeural` / `+5%` / any per-scene overrides+- **Workspace setup** — canonical split ratio (default 0.30 AI / 0.70 feature), which panels, which apps pre-opened+- **Scene list** — numbered, with captions, interactions, commands, panel choreography, speedup markers, **and narration text** (see table below)+- **Manifest path** — canonical `/tmp/<name>-manifest.json` location + `<repo>/demo/manifest.json` checked-in copy+- **Reproduction block** — the exact commands another user runs to regenerate the demo end-to-end (see template below)+- **Wiki page target** — where this demo publishes (`apps/<page>`)++Omitting any of the above means the next person who tries to regenerate the demo has to reverse-engineer your choices from the muxed output. Don't do that to them.++### Template — start every demo script from this shape++```markdown+# <Product Name> <version> — Feature Tour++**Target duration:** 4:30–5:00+**Recorded:** 2026-04-23+**Final output:** `~/project/recordings/<name>-<version>-tour.webm`+**Wiki target:** `apps/<page>`+**Hero frame:** `00:12` of scene 1 (BME680 first iso shot)++## Dependencies++- `edge-tts` — `pip install edge-tts` (tested with 6.1.x)+- `adom-cli` — container-provided+- `video-post` — built from `/home/adom/project/video-post/` (`cargo build --release`)+- `ffmpeg 6+` — container-provided+- Env vars: none required beyond the container defaults++## Recording config++- **Voice:** `en-US-AndrewMultilingualNeural`+- **Rate:** `+5%`+- **Workspace split:** 0.30 (VS Code / AI left, feature pane right)+- **Captions:** on, via `adom-cli hydrogen caption show`+- **Resolution:** native (1280×…)++## Scenes++| # | id | Caption (on-screen) | Narration (TTS, phonetics applied) | Target length | Commands + choreography |+|---|---|---|---|---|---|+| 1 | intro | `adom-tsci 1.3.7` | `This is adom t s c i version one point three point seven — the interactive t s circuit preview.` | 8s | `adom-tsci view isometric --port 8853` — cinematic slow orbit |+| 2 | examples | `8 example molecules` | `Adom ships eight example molecules — let's open a BME680 breakout.` | 10s | switch to example folder, `adom-tsci start <dir>` |+| … | … | … | … | … | … |++(13 scenes for the adom-tsci 1.3.7 tour; ~5:00 total)++## Reproduction++Another user regenerates this demo end-to-end with:++\`\`\`bash+cd <repo>+python3 demo/gen_tts.py           # renders TTS from this script's narration column+python3 demo/record.py            # drives scene-by-scene recording+python3 demo/mux.py               # muxes TTS onto clips, concatenates final+video-post storyboard /tmp/<name>-manifest.json   # review+# after review:+video-post concat /tmp/<name>-manifest.json --kind raw --output ~/project/recordings/<name>-<version>-tour.webm+adom-wiki asset upload apps/<page> --asset-type video --file ~/project/recordings/<name>-<version>-tour.webm+\`\`\`++## Known issues from this rev++- Clip 02 originally recorded as one 2:17 mega-clip; split into 8 × 12s clips in revision 2 for surgical re-shoot support.+- TTS in rev 1 mispronounced `adom-tsci` as one word; fixed in rev 2 by spelling as `adom t s c i` in narration column.+```++### Why the repo + wiki double-home++- **In the repo:** git history tracks every demo revision. If someone files an issue "the demo is out of date after v1.4.0 ships" you can diff the script.+- **On the wiki:** other Adom users find it when browsing `apps/<page>`. They can click "reproduce" and a Claude thread says "I see a demo-script for this app — want me to re-record against the current version?"+- **Both:** git is the source of truth (atomic with code changes); wiki is the discoverable surface.++### Copy this script file during recording, don't write straight to `/tmp/`++```bash+mkdir -p <repo>/demo+cp /tmp/<name>-demo-script.md <repo>/demo/<name>-demo-script.md    # if you drafted in /tmp+# ...or write directly to the repo path in the first place (preferred).+```++Before touching the record button, show the script to the user for approval. Save to `<repo>/demo/<name>-demo-script.md`.++## 🗣️ TTS pronunciation — consult the `tts-pronunciation` skill++Neural TTS engines mispronounce many Adom product names, acronyms, and hyphenated terms. The canonical table lives in the [`tts-pronunciation` skill](../tts-pronunciation/SKILL.md) (data file: [`pronunciations.json`](../tts-pronunciation/pronunciations.json)). For every narration scene, look up each proper-noun / acronym / version-number in that table and substitute its `narration` spelling into the narration column. Captions and on-screen text keep the real spelling; only the narration-column text gets phoneticized.++Most common fixes (the full table is in the tts-pronunciation skill):++- `adom-tsci` → `adom t s c i`+- `JLCPCB` / `FPGA` / `PCB` / `SPI` → spell letters with spaces+- `1.3.7` → `one point three point seven`+- `KiCad` → `kai cad`+- `BOM` → `bill of materials` (TTS engines read "BOM" as the word "bomb")++**If a term you need isn't in the table, add it.** The contribution flow is in `tts-pronunciation/SKILL.md` — verify the mispronunciation with a one-shot `edge-tts`, find a spelling that fixes it, append to `pronunciations.json`, commit. Every user benefits.++**Verify before rendering the full tour:**++```bash+edge-tts --voice en-US-AndrewMultilingualNeural \+  --text "adom t s c i version one point three point seven" \+  --write-media /tmp/_test.mp3+# Listen with ffplay /tmp/_test.mp3 — it should spell out T S C I, not say "sci" as a word.+```++Fixing narrations at script time costs seconds. Fixing them after you've muxed 13 clips means re-running TTS, re-muxing, re-concat, re-storyboarding.++## 📏 Clip duration ≈ narration duration, ±15%++Don't let a clip's video length drift too far from its narration length. A 2:17 video with 0:30 of narration is broken either way:++- **Either** the narration is too thin — tighten the video ("cycle through 8 example molecules" is one scene if narrated in 30s, but only if you can render all 8 in 30s).+- **Or** the video is genuinely 2:17 — then split it into multiple clips, each with its own matched-length narration.++**Split-before-speedup heuristic.** When a single clip concept spans distinct sub-actions (open / click / close, or cycle-through-N-things), the correct path is N clips with N narrations, not one mega-clip with a speedup pass. Reasons:++- Surgical re-shoots — if one of 8 examples fails to render, a single split clip rerecord fixes it. A mega-clip has to be fully rerecorded.+- Pacing control — app-side lag (e.g. loading a new example molecule) bakes into a mega-clip. Smaller clips let you cut-on-ready.+- Narration clarity — "now we see the BME680... and the BHI360... and the SN65HVD230..." is unnatural. "This is the BME680." [clip break] "The BHI360." is tight.++## Scene list++Numbered scenes with:+- What's on screen+- Commands to run+- Expected duration (real time + after speedup)++## 🎙️ Narration text for every scene — the TTS that will be rendered++**The full spoken narration for every scene goes in the demo script, visible to the user for approval BEFORE any recording or TTS rendering happens.** This is the single most important line-item in the script. Everything else (captions, interactions, commands) is cheaper to change than narration once rendered — if you skip the script-time review and discover the wrong pronunciation / wrong wording / wrong length at mux time, you're re-rendering N clips to fix it.++The script table should have a dedicated **Narration** column next to each scene:++| Scene | Caption (seen) | Narration (heard) — TTS-safe phonetics applied | Target length |+|---|---|---|---|+| 1 | `adom-tsci 1.3.7` | `This is adom t s c i version one point three point seven — the interactive t s circuit preview.` | 8s |+| 2 | `BME680 breakout` | `Let's open a BME680 breakout molecule. Four in one sensor: temperature, humidity, pressure, gas.` | 10s |+| … | … | … | … |++**Rules for the narration column:**++1. Apply the **TTS pronunciation** table (above) — the user approves the phonetic spelling, so what they read in the script is exactly what the TTS will say. `adom-tsci` never appears literally in narration column; it's always `adom t s c i`.+2. **Target length per scene** must be consistent with the planned video length (±15%, per the section above). If a scene is planned for 15 s of video, narration is ~80–120 words. Count words as a sanity check; at 160 wpm spoken pace that's ~13–17 s.+3. User-approved narration text is the **only** source of truth for `edge-tts --text "…"` later. No paraphrasing at record time. If you need to change narration during recording, stop and update the script (and re-get approval).+4. **Narration ≠ caption.** Caption is short, on-screen, SEO-ish ("BME680 breakout"). Narration is conversational, spoken ("Let's look at the BME680 breakout molecule…"). Both are required. Don't reuse one for the other.++**Why narration lives in the script (approved) not at render time:** Fixing a typo or a pronunciation issue in the approved script costs seconds. Finding it after mux costs 13 × (TTS render + ffmpeg mux + concat) minutes + another storyboard-review round. The script-approval step is where all cheap-to-fix-now mistakes get caught.++## Captions for every scene++**Always generate a caption for every scene.** Captions are rendered live in+the workspace during recording via `adom-cli hydrogen caption show`. They+serve two purposes:++1. **Make the video self-explanatory** — viewers understand each scene on mute+2. **Narrator cues** — during the voiceover pass, the user sees the captions+   in the video and knows exactly when and what to say for each scene++Display captions at the START of each scene:++```bash+adom-cli hydrogen caption show "Isometric view — full board layout" -d 5+```++Hide when transitioning: `adom-cli hydrogen caption hide`++**Do NOT use video-post `--label` for captions.** Those are ugly ffmpeg text+overlays. Use `adom-cli hydrogen caption show` for clean, rendered captions+that appear natively in the workspace.++**Do NOT use Adom Viewer (AV) for captions or narration — ever.** No+`html_interactive` caption panels, no `av_display` narration overlays, no+custom AV widgets that exist just to show status text. The Hydrogen caption+overlay is the only supported captioning path for demos and for any live+narration of a multi-step task ("show me what's happening as you go"). AV is+for visual *content* (3D models, schematics, gerbers, scope screenshots) —+never for captions *about* that content. This applies to every demo skill,+including tours and any ad-hoc "narrate while you work" requests.++Show the full caption table to the user as part of the script:++| Scene | Caption |+|-------|---------|+| 1 | `shotlog health — server running` |+| 2a | `Isometric view — full board layout` |+| 2b | `Top-down — trace routing and pad layout` |+| 3 | `Timeline — 4 screenshots with descriptions` |++## Panel choreography++If the demo involves multiple panels (e.g. a 3D viewer AND a log viewer),+script **explicit panel switches** between each action so the viewer sees both+sides. Don't run 4 CLI commands with one panel visible — alternate:++```text+1. Show 3D viewer → rotate to isometric+2. Switch to shotlog → screenshot appeared in timeline+3. Switch to 3D viewer → rotate to top-down+4. Switch to shotlog → 2 entries now+```++This makes the demo visually interesting instead of staring at one panel while+CLI commands run off-screen.++## Speedup markers++Mark slow operations (exports, searches, AI latency, screenshot capture) with+their speedup factor. Do NOT use `--label` for captions — speedup labels are+only for internal tracking. Visual captions come from `adom-cli hydrogen caption`.++```text+Speedup: 10x — screenshot capture + inject+Speedup: 20x — exporting gerbers+```++## Voiceover draft++Write a short narration script the user can read during the voiceover pass.+The live captions displayed during recording serve as **narrator cues** — when+the user watches the sped-up video during voiceover, they see each caption+appear and know exactly what to say at that moment. Structure the voiceover+script as caption-keyed paragraphs:++```text+[Caption: "Isometric view — full board layout"]+"Here's our CAN transceiver molecule from the isometric angle, showing the+full board with standoffs and the SN65HVD230 IC in the center."++[Caption: "Top-down — trace routing"]+"From the top, you can see the trace routing and pad layout clearly."+```++Keep total narration under 60 seconds for most demos.++## Target final duration++Aim for 30-60s. Nobody watches a 5-minute demo.++## See also++- [SKILL.md](SKILL.md) — parent: orchestration, the ten failure modes, the "real demo" 10-step shape+- [recording-mechanics.md](recording-mechanics.md) — capture path, surfaces, frame-rate sanity, NEVER stitch screenshots+- [per-snippet-flow.md](per-snippet-flow.md) — manifest, storyboard review, mux + concat, hero, upload+- [tts-pronunciation](../tts-pronunciation/SKILL.md) — Adom-specific phoneme table