app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
← Commit history
Publish 1.0.2
11 files changed
+265−67
.gitignoreadded+4@@ -0,0 +1,4 @@+target/+Cargo.lock+*.swp+.DS_Store
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-video-post"-version = "1.0.1"+version = "1.0.2" edition = "2021" description = "Post-process screen recordings: speed up boring sections with overlay captions, voiceover after the fact, publish to the Adom Wiki" license = "MIT"
README.md+23−13@@ -1,6 +1,16 @@ # adom-video-post -The **AI-driven studio for finishing demo videos.** Capture a demo as a series of clips, then review every snippet, flag and re-record the weak ones ("clip 3 is awful — redo it with…"), speed up the dead air, narrate, mux + auto-level the audio, validate the final cut, and publish to the wiki — driven end-to-end by your AI, no DaVinci Resolve.+The **AI-driven studio for finishing demo videos.** Capture a demo as a series of clips, then review every snippet, flag and re-record the weak ones ("clip 3 is awful, redo it with…"), speed up the dead air, narrate, mux + auto-level the audio, validate the final cut, and publish to the wiki, driven end-to-end by your AI, no DaVinci Resolve.++++The Review / Narrate / Publish phases live behind one tab. Narrate over the sped-up cut with a 3-2-1 countdown, then auto-level the audio:++++Slow sections become labelled timelapses with a clear on-screen banner so the viewer always knows what was happening:++ ## Why this exists @@ -10,11 +20,11 @@ We are building a lot of demo recordings of Adom apps, skills, molecules, librar 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.+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.+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. @@ -45,18 +55,18 @@ The user originally asked for this as a "post-process script for the Fusion demo - 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+- 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.+> **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 `adom-video-post mark init` after Hydrogen recording starts and `adom-video-post process` after it stops. But adom-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 adom-video-post the same way.+`adom-desktop`'s `scripts/demo-record.sh` is the consumer, it calls `adom-video-post mark init` after Hydrogen recording starts and `adom-video-post process` after it stops. But adom-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 adom-video-post the same way. ## What it does (3 phases) -### Phase 1 — Speedup post-processor+### 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. @@ -82,7 +92,7 @@ adom-video-post process \ 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+### Phase 2, Voiceover overlay ✅ shipped ```bash adom-video-post voiceover --input demo-fast.webm@@ -90,16 +100,16 @@ adom-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+### Phase 3, Wiki publish ✅ shipped ```bash adom-video-post publish \- --input demo-fast-narrated.webm \- --page apps/adom-desktop \+ --input demo-fast-narrated.mp4 \+ --page adom/adom-desktop-fusion-bridge \ --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.+Pushes the final narrated video into the wiki page's repo via `adom-wiki repo push`, so it lands in the page's Files tab and serves at `https://wiki.adom.inc/api/v1/pages/<slug>/files/<file>`. The command returns that served URL so you can embed it. Now the polished demo video lives alongside the artifact it documents, so anyone visiting the page sees the demo, not just the docs. (The old `adom-wiki asset upload` pillar this used to wrap was removed; `repo push` is the supported path.) ## The full pipeline @@ -130,7 +140,7 @@ Four commands. Total wall-clock time under 10 minutes for a demo that captured 3 ## How the AI uses it -Adom users mostly don't run adom-video-post directly — their Claude Code session does. The skill file (`SKILL.md`) is auto-installed to `~/.claude/skills/adom-video-post/` and Claude Code finds it via trigger words like "speed up demo recording", "timelapse video", "voiceover after recording", "publish demo to wiki".+Adom users mostly don't run adom-video-post directly, their Claude Code session does. The skill file (`SKILL.md`) is auto-installed to `~/.claude/skills/adom-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.
bin/adom-video-postadded⋯ 1 unchanged line ⋯
install.shadded+17@@ -0,0 +1,17 @@+#!/bin/bash+# adompkg install hook for adom-video-post. Places the prebuilt binary and+# deploys the embedded SKILL.md via the binary's own `install` subcommand.+set -e+HERE="$(cd "$(dirname "$0")" && pwd)"++mkdir -p "$HOME/.local/bin"+cp "$HERE/bin/adom-video-post" "$HOME/.local/bin/adom-video-post"+chmod +x "$HOME/.local/bin/adom-video-post"+# Back-compat `video-post` alias (the short name used in skills + demos).+ln -sf "$HOME/.local/bin/adom-video-post" "$HOME/.local/bin/video-post"+echo "OK: adom-video-post binary at ~/.local/bin/adom-video-post (alias: video-post)"++# Deploy the embedded SKILL.md (idempotent).+"$HOME/.local/bin/adom-video-post" install || true++echo "OK: adom-video-post installed. Run 'adom-video-post health' to verify."
page.json+17−3@@ -1,7 +1,7 @@ { "slug": "adom-video-post", "title": "Adom Video Post-Production",- "summary": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones (\"clip 3 is awful \u2014 redo it\u2026\"), speed up dead air, narrate, mux + auto-level, validate the final cut, and publish to the wiki \u2014 driven end-to-end by your AI.",+ "summary": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.", "category": "Developer Tools", "tags": [ "video",@@ -18,11 +18,25 @@ "status": "live", "hero": { "type": "image",- "path": "screenshots/hero.png"+ "path": "screenshots/hero-v2.png" }, "visibility": "public",- "brief": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones (\"clip 3 is awful \u2014 redo it\u2026\"), speed up dead air, narrate, mux + auto-level, validate the final cut, and publish to the wiki \u2014 driven end-to-end by your AI.",+ "brief": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.", "install": { "binary_name": "adom-video-post"+ },+ "type": "app",+ "version": "1.0.2",+ "description": "AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.",+ "bundled": true,+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "org": "adom",+ "author": {+ "name": "John Lauer",+ "email": "[email protected]" } }
screenshots/hero-v2.pngadded⋯ 1 unchanged line ⋯
screenshots/review-studio.pngadded⋯ 1 unchanged line ⋯
src/publish.rs+85−50@@ -1,12 +1,20 @@ //! Phase 3: publish a final video to the Adom Wiki. //!-//! Wraps `adom-wiki asset upload <page-ref> --asset-type video --file <path>`-//! so the polished demo video gets attached to the wiki page for whatever-//! you're demoing (an app, a skill, a molecule, a library, a datasheet).+//! Pushes the polished demo video into the wiki page's repo via+//! `adom-wiki repo push --files <video> <org/slug> -m "<msg>"`. Binaries push+//! fine: the mp4 lands in the page's Files tab and serves at+//! `https://wiki.adom.inc/api/v1/pages/<slug>/files/<file>`, which is the URL we+//! return so callers can embed it.+//!+//! (The old `adom-wiki asset upload` pillar this used to wrap was removed; the+//! CLI no longer has an `asset` command. For versioned media,+//! `adom-wiki release upload <ref> <ver> <file>` exists, but for a page demo+//! video `repo push` is the right call.) -use std::path::Path; use std::process::Command; +const WIKI_BASE: &str = "https://wiki.adom.inc";+ pub struct PublishConfig { pub input: std::path::PathBuf, pub page: String,@@ -22,18 +30,10 @@ pub fn run(cfg: PublishConfig) -> Result<String, String> { )); } - // Validate page-ref shape: <type>/<slug>- let mut parts = cfg.page.splitn(2, '/');- let page_type = parts.next().unwrap_or("");- let page_slug = parts.next().unwrap_or("");- if page_type.is_empty() || page_slug.is_empty() {- return Err(format!(- "invalid --page '{}': expected format <type>/<slug> like apps/adom-desktop, skills/symbol-creator, molecules/drv8411a-breakout. Hint: see https://wiki-ufypy5dpx93o.adom.cloud for valid pages.",- cfg.page- ));- }+ // Validate page-ref shape: <owner>/<slug> (e.g. adom/adom-desktop-fusion-bridge).+ let (_owner, slug) = parse_page_ref(&cfg.page)?; - // Validate file extension+ // Validate file extension against the asset type. let ext = cfg .input .extension()@@ -60,7 +60,7 @@ pub fn run(cfg: PublishConfig) -> Result<String, String> { )); } - // Check adom-wiki is installed+ // Check adom-wiki is installed. if which("adom-wiki").is_none() { return Err( "adom-wiki CLI not found in PATH. Hint: it ships with every Adom container. Run `which adom-wiki` to verify, or contact your container admin."@@ -68,52 +68,59 @@ pub fn run(cfg: PublishConfig) -> Result<String, String> { ); } - // Build the upload command+ let filename = cfg+ .input+ .file_name()+ .and_then(|f| f.to_str())+ .ok_or_else(|| format!("could not read filename from {}", cfg.input.display()))?;++ let message = match &cfg.caption {+ Some(c) => format!("Add {} demo {}: {}", cfg.asset_type, filename, c),+ None => format!("Add {} {} via video-post publish", cfg.asset_type, filename),+ };++ // Build the push command: adom-wiki repo push --files <input> <page> -m "<msg>" let mut cmd = Command::new("adom-wiki");- cmd.args(["asset", "upload", &cfg.page]);- cmd.args(["--asset-type", &cfg.asset_type]);- cmd.args(["--file"]).arg(&cfg.input);- if let Some(caption) = &cfg.caption {- cmd.args(["--caption", caption]);- }+ cmd.arg("repo").arg("push");+ cmd.arg("--files").arg(&cfg.input);+ cmd.args(["-m", &message]);+ cmd.arg(&cfg.page); println!("uploading {} to wiki page {}...", cfg.input.display(), cfg.page); let result = cmd.output().map_err(|e| format!("adom-wiki spawn failed: {}", e))?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr);- return Err(format!("adom-wiki upload failed:\n{}", stderr));+ let stdout = String::from_utf8_lossy(&result.stdout);+ return Err(format!(+ "adom-wiki repo push failed:\n{}{}",+ stdout, stderr+ )); } - let stdout = String::from_utf8_lossy(&result.stdout);- // Try to extract the asset URL or id from the JSON response- let url = extract_asset_url(&stdout, &cfg.page);- Ok(url)+ // The served URL is deterministic: <base>/api/v1/pages/<slug>/files/<file>.+ Ok(served_file_url(slug, filename)) } -fn extract_asset_url(stdout: &str, page: &str) -> String {- // adom-wiki returns JSON like {"asset":{"id":42,"asset_type":"video","filename":"...","caption":"...","url":"..."}}- // Try to parse and extract the URL or fall back to a wiki page link.- if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(stdout) {- if let Some(url) = parsed- .get("asset")- .and_then(|a| a.get("url"))- .and_then(|u| u.as_str())- {- return url.to_string();- }- if let Some(file) = parsed- .get("asset")- .and_then(|a| a.get("filename"))- .and_then(|f| f.as_str())- {- return format!(- "https://wiki-ufypy5dpx93o.adom.cloud/wiki/{} (asset: {})",- page, file- );- }+/// Parse a page ref of the form `<owner>/<slug>` and return (owner, slug).+/// The served file URL keys off the slug only (the last path segment).+fn parse_page_ref(page: &str) -> Result<(&str, &str), String> {+ let mut parts = page.splitn(2, '/');+ let owner = parts.next().unwrap_or("");+ let slug = parts.next().unwrap_or("");+ if owner.is_empty() || slug.is_empty() || slug.contains('/') {+ return Err(format!(+ "invalid --page '{}': expected format <owner>/<slug> like adom/adom-desktop-fusion-bridge, adom/symbol-creator. Hint: browse pages at {}.",+ page, WIKI_BASE+ )); }- format!("https://wiki-ufypy5dpx93o.adom.cloud/wiki/{}", page)+ Ok((owner, slug))+}++/// Build the URL the pushed file serves at, e.g.+/// https://wiki.adom.inc/api/v1/pages/adom-desktop-fusion-bridge/files/library-demo.mp4+fn served_file_url(slug: &str, filename: &str) -> String {+ format!("{}/api/v1/pages/{}/files/{}", WIKI_BASE, slug, filename) } fn which(cmd: &str) -> Option<std::path::PathBuf> {@@ -126,3 +133,31 @@ fn which(cmd: &str) -> Option<std::path::PathBuf> { } None }++#[cfg(test)]+mod tests {+ use super::*;++ #[test]+ fn parses_owner_and_slug() {+ let (owner, slug) = parse_page_ref("adom/adom-desktop-fusion-bridge").unwrap();+ assert_eq!(owner, "adom");+ assert_eq!(slug, "adom-desktop-fusion-bridge");+ }++ #[test]+ fn rejects_missing_slug() {+ assert!(parse_page_ref("adom").is_err());+ assert!(parse_page_ref("").is_err());+ assert!(parse_page_ref("/slug").is_err());+ assert!(parse_page_ref("a/b/c").is_err());+ }++ #[test]+ fn builds_served_url_from_slug_only() {+ assert_eq!(+ served_file_url("adom-desktop-fusion-bridge", "library-demo.mp4"),+ "https://wiki.adom.inc/api/v1/pages/adom-desktop-fusion-bridge/files/library-demo.mp4"+ );+ }+}
tests/publish_serves.rsadded+113@@ -0,0 +1,113 @@+//! End-to-end test for `adom-video-post publish`.+//!+//! It generates a tiny real mp4, runs the actual `publish` subcommand against a+//! live wiki page, then curls the URL the command returns to confirm the file+//! serves as a valid `ftyp` mp4 over HTTP.+//!+//! It needs network + a writable wiki page, so it is gated behind an env var and+//! is a no-op (passes) under a plain `cargo test`:+//!+//! VIDEO_POST_TEST_PAGE=adom/adom-video-post cargo test --test publish_serves -- --nocapture+//!+//! Requires `ffmpeg`, `adom-wiki`, and `curl` on PATH.++use std::process::Command;++const BIN: &str = env!("CARGO_BIN_EXE_adom-video-post");+const TEST_FILE: &str = "_video-post-selftest.mp4";++#[test]+fn publish_pushes_and_serves_a_valid_mp4() {+ let page = match std::env::var("VIDEO_POST_TEST_PAGE") {+ Ok(p) if !p.trim().is_empty() => p,+ _ => {+ eprintln!(+ "skipping: set VIDEO_POST_TEST_PAGE=<owner>/<slug> to run the live publish test"+ );+ return;+ }+ };++ // 1. Generate a tiny but real mp4 named so the served URL is predictable.+ let mp4 = std::env::temp_dir().join(TEST_FILE);+ let _ = std::fs::remove_file(&mp4);+ let ff = Command::new("ffmpeg")+ .args(["-y", "-f", "lavfi", "-i", "color=c=teal:s=128x128:d=1", "-r", "8"])+ .arg(&mp4)+ .output()+ .expect("spawn ffmpeg");+ assert!(+ ff.status.success(),+ "ffmpeg failed: {}",+ String::from_utf8_lossy(&ff.stderr)+ );+ assert!(mp4.exists(), "ffmpeg produced no file at {}", mp4.display());++ // 2. Run the real publish subcommand.+ let out = Command::new(BIN)+ .args(["publish", "--input"])+ .arg(&mp4)+ .args(["--page", &page, "--caption", "video-post publish self-test"])+ .output()+ .expect("spawn adom-video-post");+ let stdout = String::from_utf8_lossy(&out.stdout);+ let stderr = String::from_utf8_lossy(&out.stderr);+ assert!(+ out.status.success(),+ "publish failed:\nstdout: {stdout}\nstderr: {stderr}"+ );++ // 3. Extract the served URL the command printed (strip ANSI color codes).+ let plain = strip_ansi(&stdout);+ let url = plain+ .split_whitespace()+ .find(|t| t.starts_with("https://") && t.ends_with(".mp4"))+ .unwrap_or_else(|| panic!("no served mp4 URL in output:\n{plain}"))+ .to_string();+ eprintln!("served URL: {url}");++ // 4. Curl the URL and confirm it serves a valid ftyp mp4.+ let head = Command::new("curl")+ .args(["-s", "--max-time", "60", &url])+ .output()+ .expect("spawn curl");+ assert!(head.status.success(), "curl failed for {url}");+ let body = head.stdout;+ assert!(body.len() > 64, "served body too small ({} bytes)", body.len());+ // ISO base-media files carry the `ftyp` box marker at bytes 4..8.+ let marker = &body[4..8.min(body.len())];+ assert_eq!(+ marker, b"ftyp",+ "served bytes are not a valid mp4 (expected ftyp at offset 4, got {:?})",+ String::from_utf8_lossy(marker)+ );++ // 5. Best-effort cleanup: remove the test file from the page repo.+ let _ = Command::new("adom-wiki")+ .args(["repo", "rm", &page, TEST_FILE])+ .output();+ let _ = std::fs::remove_file(&mp4);+}++fn strip_ansi(s: &str) -> String {+ let mut out = String::with_capacity(s.len());+ let bytes = s.as_bytes();+ let mut i = 0;+ while i < bytes.len() {+ if bytes[i] == 0x1b {+ // Skip CSI sequence: ESC '[' ... final byte in 0x40..=0x7e+ i += 1;+ if i < bytes.len() && bytes[i] == b'[' {+ i += 1;+ while i < bytes.len() && !(0x40..=0x7e).contains(&bytes[i]) {+ i += 1;+ }+ i += 1; // consume final byte+ }+ } else {+ out.push(bytes[i] as char);+ i += 1;+ }+ }+ out+}
uninstall.shadded+5@@ -0,0 +1,5 @@+#!/bin/bash+set -e+rm -f "$HOME/.local/bin/adom-video-post" "$HOME/.local/bin/video-post"+rm -rf "$HOME/.claude/skills/adom-video-post"+echo "OK: adom-video-post removed."