Download

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.

# 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:

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:

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:

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:

# 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:

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):

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

# 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

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

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

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

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:

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

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

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)

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)

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

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)

# 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.

{"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:

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.