---
name: adom-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.
---

# adom-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 `adom-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
adom-video-post storyboard /tmp/my-demo-manifest.json \
  --tab-name 'adom-video-post: <name> review' --port 8797
# Activate + screenshot to confirm it rendered
adom-cli hydrogen workspace active-tab --name 'adom-video-post: <name> review'
adom-cli hydrogen screenshot panel --name 'adom-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`

`adom-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: `adom-video-post concat` drops audio

Today `adom-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 `adom-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 webview 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 `adom-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 adom-video-post and inherits libvpx-vp9's default keyframe-only-at-frame-0 behavior.

`adom-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

`adom-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. `adom-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
adom-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
  adom-video-post manifest add --manifest demo.json \
    --id <id> --title "<title>" \
    --raw <id>-narrated.webm <id>-narrated.webm
  adom-cli hydrogen caption hide
done
adom-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 `adom-video-post mark start` / `mark end` around
slow operations, post-processed by `adom-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. adom-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)
adom-video-post health

# Initialize the markers file at the START of a recording session
adom-video-post mark init

# Wrap any slow command with a speedup marker
adom-video-post wrap --speed 20 --label "Exporting gerbers" -- adom-desktop fusion_export_gerbers
adom-video-post wrap --speed 15 --label "Pulling source files" -- pull_and_analyze "C:/tmp/foo.fbrd"

# OR drop manual start/end markers
adom-video-post mark start --speed 50 --label "Cloud search"
# ... run slow operation ...
adom-video-post mark end

# Inspect the markers you've collected
adom-video-post inspect

# After the recording stops, post-process it
adom-video-post process \
  --input  /home/adom/project/recordings/demo.webm \
  --markers /tmp/adom-video-post-markers.jsonl
# → produces /home/adom/project/recordings/demo-fast.webm
```

## Commands

### `health`

```bash
adom-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
adom-video-post mark init [--file /tmp/adom-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
adom-video-post mark start --speed 20 --label "Exporting gerbers"
# ... run slow operation ...
adom-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
adom-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
adom-video-post mark start --speed 20 --label "Exporting gerbers"
adom-desktop fusion_export_gerbers
RC=$?
adom-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
adom-video-post inspect [--file /tmp/adom-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
adom-video-post process \
  --input /home/adom/project/recordings/demo.webm \
  --markers /tmp/adom-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` (Adom Video Post-Processing webview app)

```bash
adom-video-post voiceover \
  --input /home/adom/project/recordings/demo-fast.webm \
  [--output /home/adom/project/recordings/demo-fast-narrated.webm] \
  [--markers /tmp/adom-video-post-markers.jsonl] \
  [--panel-id <leaf-id>] \
  [--tab-name "Adom Video Post-Processing"]
```

This is the full Adom 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
  `adom-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
adom-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
adom-video-post install
```

Deploys the embedded `SKILL.md` to `~/.claude/skills/adom-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
adom-video-post mark init

# (run your demo, wrapping slow sections with `adom-video-post wrap`)

adom-cli hydrogen recording stop  # → /home/adom/project/recordings/demo.webm

# 2. Speedup pass
adom-video-post process --input demo.webm --markers /tmp/adom-video-post-markers.jsonl
# → demo-fast.webm

# 3. Voiceover pass (Phase 2)
adom-video-post voiceover --input demo-fast.webm
# → demo-fast-narrated.webm

# 4. Publish to wiki (Phase 3)
adom-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/adom-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 `adom-video-post mark init` after the Hydrogen recording starts
- `demo-record.sh stop` calls `adom-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"
adom-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.
