Jeremy Voiceover Video
Make a broadcast-quality narrated video end to end: write a scene-segmented script, narrate it in
Jeremy's cloned voice, generate cinematic B-roll of Jeremy from his face LoRA for the
"talking" moments, cut in real product screenshots, burn colorful animated subtitles, and
render it all with ffmpeg. When asked for options, produce several distinct variant cuts and let
a jury pick the winner. Always orchestrate the production with Fable (see Orchestration).
Segment by scene because the per-scene audio durations drive BOTH the subtitle timings AND how long
each visual stays up — known text + measured duration = perfect sync and cuts that land on the beat.
1. The voice-clone TTS (LAN)
- Host:
192.168.1.5. ssh shoemoney@192.168.1.5. Toolkit: ~/Services/VoiceStudioLaravel.
- Engine: mlx-audio (Qwen3-TTS), OpenAI-style
/v1/audio/speech on localhost:8200. Confirm up:
curl -s -o /dev/null -w '%{http_code}' localhost:8200/v1/models → 200. If down, restart the
mlx-audio server in the toolkit dir (see server.log) before synth.
- Synthesize:
./say <voice> "<text>" --no-play --mp3 --out <path.wav> — voices: jeremy, amber,
brandi, trump. --no-play REQUIRED over SSH; --temperature default 0.8 (livelier).
- SRT/transcribe:
./transcribe <audio> --srt (local mlx-whisper) — quality check + fallback SRT.
2. Jeremy's face LoRA (Replicate)
Generate cinematic stills of Jeremy for the "when I'm talking" scenes with his Flux face LoRA:
- Model:
shoemoney/jshoe-flux-v3 (private) · trigger word: jshoe · version pinned in
scripts/gen_lora.py. This is the correct one — NOT any Hunyuan3D / "h3" avatar.
- Generate:
REPLICATE_API_TOKEN=… scripts/gen_lora.py <out_dir> <prompts.txt> (one prompt/line,
fires all in parallel, downloads jeremy-NN.png). Token lives in ~/.claude/aigate/mcp-keys.env.
- Prompt style: start with
jshoe, then describe — bald, short beard, black t-shirt, dark studio,
subtle green rim light (brand), cinematic, photorealistic, 16:9. Vary pose per scene (hero
close-up, at desk with monitors, mid-gesture, arms-crossed CTA).
- Flux gives stills, not a lip-synced talking head. Animate them with Ken-Burns (
zoompan) and
fade/dissolve into the screenshot that follows — that's the "he's talking, then we cut to the
product" rhythm the brand wants. (For a true talking head you'd add a lip-sync model — out of scope
unless asked.)
3. Pipeline
3a. Script — one scene per line
First person for Jeremy ("I built this because…"). 150 wpm (2.5 w/s): a 12-word line ≈ 5s; a ~2-min
video is ~280–320 words / ~8–12 scenes. AIR copy rule: say "AI", never "the machine"; never
"corpus". Save one line per scene to script.txt.
3b. Voiceover + timing + SRT
scripts/build_vo.sh <voice> script.txt <out_dir> → scene-NN.wav, voice.mp3, voice.srt,
manifest.json ([{scene,text,start,end,dur}] — the timing map everything else reads). Transcribe a
scene back to catch garbled acronyms/numbers; respell in the script and regenerate just that scene.
3c. Jeremy B-roll stills
Generate the LoRA stills (§2) for the talking scenes into <out>/jeremy/.
3d. Scenes — hybrid, hyper-focused on the product
For each scene, build a clip exactly dur seconds long (read manifest.json):
- Jeremy scenes: his LoRA still, slow Ken-Burns, then fade/dissolve into the feature.
- Feature scenes: full-screen (1920×1080) animated HTML in brand identity built around the real
screenshot — the product is the star. Callouts/highlights point at the exact feature named in the
VO. Reuse brand tokens/fonts. No webfont links.
- Author scene HTML with subagents (Sonnet), render each with
render_scene.js (Playwright recordVideo,
full Chromium) → normalize to 1920×1080/30fps/h264 via ffmpeg at its dur.
3e. Colorful animated subtitles
Captions must be styled + animated, not plain white. Preferred (no ffmpeg deps): inject an HTML
caption overlay into each scene before rendering — `scripts/inject_caption.py scene.html ""
1---2name: jeremy-voiceover-video3description: Produce polished narrated tutorial/promo/demo/explainer VIDEOS (MP4 + colorful subtitles) voiced in Jeremy Schoemaker's cloned voice, intercut with real UI screenshots and cinematic B-roll of Jeremy generated from his Replicate face LoRA. Use this WHENEVER the user wants a video with a voiceover/narration — "make a tutorial video", "voiceover video", "narrated demo", "promo video", "explainer video", "walkthrough", "generate the voiceover", "add subtitles/SRT" — or wants several variant cuts juried down to a winner. Also use when only the voiceover + SRT are wanted (that's the first half). Covers script → Jeremy VO → face-LoRA stills → hybrid scenes (brand motion-graphics + real screenshots + Jeremy B-roll with fade transitions) → colorful animated subtitles → ffmpeg render → N variants → multi-model jury picks the winner → YouTube/Chrome-Web-Store MP4s. Orchestrate the whole run with Fable.4---56# Jeremy Voiceover Video78Make a broadcast-quality narrated video end to end: write a scene-segmented script, narrate it in9Jeremy's **cloned voice**, generate cinematic **B-roll of Jeremy from his face LoRA** for the10"talking" moments, cut in **real product screenshots**, burn **colorful animated subtitles**, and11render it all with ffmpeg. When asked for options, produce **several distinct variant cuts** and let12a **jury pick the winner**. **Always orchestrate the production with Fable** (see Orchestration).1314Segment by scene because the per-scene audio durations drive BOTH the subtitle timings AND how long15each visual stays up — known text + measured duration = perfect sync and cuts that land on the beat.1617## 1. The voice-clone TTS (LAN)1819- **Host:** `192.168.1.5`. `ssh shoemoney@192.168.1.5`. Toolkit: `~/Services/VoiceStudioLaravel`.20- **Engine:** mlx-audio (Qwen3-TTS), OpenAI-style `/v1/audio/speech` on `localhost:8200`. Confirm up:21 `curl -s -o /dev/null -w '%{http_code}' localhost:8200/v1/models` → `200`. If down, restart the22 mlx-audio server in the toolkit dir (see `server.log`) before synth.23- **Synthesize:** `./say <voice> "<text>" --no-play --mp3 --out <path.wav>` — voices: **jeremy, amber,24 brandi, trump**. `--no-play` REQUIRED over SSH; `--temperature` default 0.8 (livelier).25- **SRT/transcribe:** `./transcribe <audio> --srt` (local mlx-whisper) — quality check + fallback SRT.2627## 2. Jeremy's face LoRA (Replicate)2829Generate cinematic stills of Jeremy for the "when I'm talking" scenes with his **Flux face LoRA**:3031- **Model:** `shoemoney/jshoe-flux-v3` (private) · **trigger word: `jshoe`** · version pinned in32 `scripts/gen_lora.py`. This is the correct one — **NOT** any Hunyuan3D / "h3" avatar.33- **Generate:** `REPLICATE_API_TOKEN=… scripts/gen_lora.py <out_dir> <prompts.txt>` (one prompt/line,34 fires all in parallel, downloads `jeremy-NN.png`). Token lives in `~/.claude/aigate/mcp-keys.env`.35- **Prompt style:** start with `jshoe,` then describe — bald, short beard, black t-shirt, dark studio,36 **subtle green rim light** (brand), cinematic, photorealistic, 16:9. Vary pose per scene (hero37 close-up, at desk with monitors, mid-gesture, arms-crossed CTA).38- Flux gives **stills**, not a lip-synced talking head. Animate them with Ken-Burns (`zoompan`) and39 **fade/dissolve into the screenshot** that follows — that's the "he's talking, then we cut to the40 product" rhythm the brand wants. (For a true talking head you'd add a lip-sync model — out of scope41 unless asked.)4243## 3. Pipeline4445### 3a. Script — one scene per line46First person for Jeremy ("I built this because…"). ~150 wpm (~2.5 w/s): a 12-word line ≈ 5s; a ~2-min47video is ~280–320 words / ~8–12 scenes. **AIR copy rule: say "AI", never "the machine"; never48"corpus".** Save one line per scene to `script.txt`.4950### 3b. Voiceover + timing + SRT51`scripts/build_vo.sh <voice> script.txt <out_dir>` → `scene-NN.wav`, `voice.mp3`, `voice.srt`,52`manifest.json` (`[{scene,text,start,end,dur}]` — the timing map everything else reads). Transcribe a53scene back to catch garbled acronyms/numbers; respell in the script and regenerate just that scene.5455### 3c. Jeremy B-roll stills56Generate the LoRA stills (§2) for the talking scenes into `<out>/jeremy/`.5758### 3d. Scenes — hybrid, hyper-focused on the product59For each scene, build a clip **exactly `dur` seconds** long (read `manifest.json`):60- **Jeremy scenes:** his LoRA still, slow Ken-Burns, then **fade/dissolve** into the feature.61- **Feature scenes:** full-screen (1920×1080) animated HTML in brand identity **built around the real62 screenshot** — the product is the star. Callouts/highlights point at the exact feature named in the63 VO. Reuse brand tokens/fonts. No webfont links.64- Author scene HTML with subagents (Sonnet), render each with `render_scene.js` (Playwright recordVideo,65 full Chromium) → normalize to 1920×1080/30fps/h264 via ffmpeg at its `dur`.6667### 3e. Colorful animated subtitles68Captions must be styled + animated, not plain white. **Preferred (no ffmpeg deps): inject an HTML69caption overlay into each scene before rendering** — `scripts/inject_caption.py scene.html "<text>"70<style> out.html` adds a brand-styled, animated caption (styles: `green` / `pop` / `lower`). Because71each scene is exactly one VO line, the caption timing is automatic and the pop/fade is real CSS. This72is what `render_variant.sh` uses. (Homebrew ffmpeg here has **no libass**, so `-vf ass=` fails — the73HTML route sidesteps that entirely.) `scripts/make_subs.py manifest.json out.ass <style>` still builds74a colorful ASS file for use as a **toggleable sidecar** on YouTube/store, and always ship `voice.srt`.7576### 3f. Render + assemble77`scripts/build_video.sh` renders each scene, concats, muxes `voice.mp3`, and (with subs) burns the ASS.78Output: 1920×1080 H.264+AAC `+faststart` MP4. Reference mux:79```bash80ffmpeg -f concat -safe 0 -i clips.txt -i vo/voice.mp3 -map 0:v -map 1:a \81 -c:v libx264 -pix_fmt yuv420p -r 30 -c:a aac -b:a 192k -shortest -movflags +faststart out.mp482```8384## 4. Variants + final jury8586When the user wants options, produce **N distinct variant cuts** — genuinely different treatments, not87recolors (e.g. *founder-cut*: heavy Jeremy B-roll + moody fades; *kinetic-cut*: high-energy motion88typography; *product-cut*: clean demo, screenshots front-and-center). Same VO/SRT across all.8990**Final jury** picks the winner: extract a representative **contact sheet of frames per variant**91(`ffmpeg … -vf fps=1/2` or key timestamps), and have a vision-capable jury (this harness + optionally92OpenRouter vision models, or `/design-jury-loop`; use `/matrix-council` for a harder adversarial call)93score each on: clarity of the toolbar value-prop, scroll-stopping motion, brand fit, subtitle94legibility, and how well Jeremy's B-roll lands. **Rank them and name the outputs by rank:**95`1-<name>.mp4`, `2-<name>.mp4`, `3-<name>.mp4` (rank-prefixed, descriptive slug). Report why #1 won.9697## 5. Orchestration — always with Fable9899Run the whole thing as a **Fable-orchestrated** job. In the Workflow, use **`model: 'fable'`** for the100planning/design/jury stages (variant concepting, scene-brief design, final jury judgment) and101**`model: 'sonnet'`** for scene-HTML build; Haiku for any research. Fable plans the cut list and the102three variant treatments, Sonnet builds the scenes, the deterministic shell steps (VO, Replicate,103render, ffmpeg, subs) run inline, and Fable chairs the final jury. Drive the phases in order:104`script → VO → LoRA stills → (Fable) variant briefs → (Sonnet) scenes ×N → subs → render ×N → (Fable) jury → name`.105106## Gotchas107108- **TTS server must be up on 192.168.1.5:8200** before synth; check first, don't retry blindly.109- **Always `--no-play` over SSH.** TTS on .5, ffmpeg/Playwright/Replicate local — synth on .5, pull wavs.110- **Durations drive everything** — read `manifest.json`, never hand-guess scene lengths.111- **Replicate JSON carries control chars** in `logs` — parse with `json.loads(…, strict=False)` or the112 poll loop dies mid-run (the image is usually fine; re-fetch the prediction to recover the output URL).113- **LoRA = `jshoe-flux-v3`, trigger `jshoe`, NOT the h3/Hunyuan avatar.** Verify a test still looks like114 him before generating the batch.115- **Full Chromium** (`chromium-*`, not `headless_shell`) is needed for recordVideo; the global116 `playwright` module is at `/opt/homebrew/lib/node_modules` (set `NODE_PATH`).117- **Regenerate one scene, not the whole track**, when a line is wrong.118- Keep a scene ≲ ~18s; split longer lines so subtitles stay readable.