Video Pipeline — Review & Standard (Sower → repeatable)
Date: 2026-06-13 · Status: canonical · Supersedes the Descript-primary assumption in production-process.md Stage 3.
This documents what actually shipped Episode 1 (the Sower hub, JgX830MoqmM) and turns it into a repeatable, parameterized pipeline. The Sower was edited with zero Descript — a fully code-driven Whisper + ffmpeg chain.
1. What shipped (Sower hub, recorded + edited 2026-06-12)
Verified from the working artifacts (plan.json, measured.json, the filter-graphs):
| Stage | Result |
|---|---|
| Raw recording | 556.8 s, single OBS take, one segment per chapter |
| Silence-cut | 167 keep-segments auto-extracted → 439.9 s body (+9 s outro hold = 448.9 s ≈ 7.5 min) |
| Chapter detection | All 11 markers found by transcript keyword — cold open, text-in-60, LensLines, Show-Don't-Tell, detail, clinic, do's/don'ts, path, outline, CTA, signoff |
| Stinger | Auto-inserted at the cold-open boundary ("this week… let's prep", item 12) |
| On-screen overlays | 8 chapter-tag PNGs + outline card (46 s) + outro card (14.5 s), all auto-timed to the post-cut timeline |
| A/V sync | Every cut snapped to the 60 fps frame grid (prevents sub-frame drift accumulating) |
| Loudness | Two-pass: measured −18.61 LUFS raw → −14.46 LUFS (YouTube −14 target) |
| Output | 2026-06-12_swpd_ep01_sower_FINAL.mp4 → published JgX830MoqmM |
Plus the one-time stinger-master.mp4 (illustrated desk scene, blank→worked-page match cut, founder VO) — built the same day, reusable on every hub forever.
2. The pipeline, stage by stage (the reconstructed commands)
Tooling on this machine (verified 2026-06-13): ffmpeg 8.1 + ffprobe (winget Gyan build), Node, and Whisper.cpp whisper-cli.exe (+ ggml-base.en.bin model).
0. ffprobe -v error -show_entries format=duration -of csv=p=0 IN.mp4 → DUR
1. ffmpeg -y -i IN.mp4 -ar 16000 -ac 1 -c:a pcm_s16le ep.wav (16 kHz mono for Whisper)
2. whisper-cli -m ggml-base.en.bin -f ep.wav -oj -of ep-transcript → ep-transcript.json
3. ffmpeg -i IN.mp4 -af silencedetect=n=-30dB:d=0.35 -f null - 2> silences.txt
4. node episode-post-planner.mjs (reads transcript + silences + DUR)
→ full-graph.txt, measure-graph.txt, plan.json
• keep-list (silence cuts, padded, merged, 60fps-snapped)
• chapter markers by keyword → overlay + stinger timing
5. ffmpeg -i IN.mp4 -i stinger-master.mp4 -filter_complex_script measure-graph.txt \
-map "[afinal]" -f null - → measured.json (loudnorm pass 1)
6. node episode-post-planner.mjs measured.json (regenerate full-graph with measured values, linear=true)
7. ffmpeg -y -i IN.mp4 -i stinger-master.mp4 \
-loop 1 -t <t> -i tag-text60.png ... (each inputsExtra in order) \
-loop 1 -t <t> -i outline-card.png -loop 1 -t <t> -i outro-card.png \
-filter_complex_script full-graph.txt \
-map "[vfinal]" -map "[afinal]" \
-c:v libx264 -crf 18 -pix_fmt yuv420p -c:a aac -b:a 192k -ar 48000 \
02-exports/<date>_swpd_ep<NN>_<slug>_FINAL.mp4
ffmpeg input order is load-bearing — the filter-graph references inputs by index: 0 = raw, 1 = stinger, then the inputsExtra PNGs in array order (8 tags, outline card, outro card). Feed them in that exact order.
3. Captured vs. NOT captured (before this standardization)
| Piece | State | Action |
|---|---|---|
episode-post-planner.mjs | ✅ saved to tools/ (identical to temp copy) | Generalize (was Ep1-hardcoded) |
stinger-master.mp4, outro-card.png | ✅ in SermonPrepDesk/assets/; stinger source in channel-assets/stinger-scene.html | Keep |
| 8 chapter-tag PNGs | ⚠️ in Temp/swpd-frames/ (ephemeral) — but identical every episode (fixed chapter titles) | Promote to permanent assets/chapter-tags/ — reusable forever |
| Orchestration (steps 0–7) | ❌ ran as inline commands; lived only in the session transcript | Build render-episode.mjs |
| Whisper binary + model | ⚠️ in Temp/whispercpp/ (ephemeral, 150 MB) | Move to permanent pipeline/whisper/ |
| Outline card | ⚠️ per-episode (text differs); generator not saved | Manual for now; make-cards.mjs is a fast-follow |
Key realization that shrinks the per-episode work: the 8 chapter tags carry the fixed segment titles ("The text in 60 seconds", "LensLines", "Show, Don't Tell"…) — identical on every hub. So they're build-once, reuse-forever assets, not per-episode. Only the outline card changes per episode (its text is the episode's outline, already structured in the hub script). Stinger and outro card are also fixed.
4. The Standard (going forward)
Permanent layout (NOT in git — binaries + video)
C:\Users\johnm\Videos\SermonPrepDesk\
pipeline\
whisper\ whisper-cli.exe + DLLs + ggml-base.en.bin
work\ep<NN>\ per-episode working files (wav, transcript, graphs, plan.json, measured.json)
assets\
stinger-master.mp4 (fixed, reusable)
outro-card.png (fixed, reusable)
chapter-tags\tag-*.png (fixed, reusable — 8 files)
01-raw\ 02-exports\ 03-published\
Code (in git, tools/)
render-episode.mjs— one command runs steps 0–7:node render-episode.mjs --in <raw.mp4> --ep 02 --slug wheat-and-weeds [--outline-card <png>]episode-post-planner.mjs— generalized (env-driven; no Ep1 hardcoding). Reproduces Ep1 byte-for-byte when run with Ep1 defaults (regression check).make-cards.mjs— fast-follow: regenerate the per-episode outline card (and re-render tags if the brand changes) from the hub script's outline block.
Per-episode checklist
- Record hub (OBS Longform profile), drop raw in
01-raw\named<date>_swpd_ep<NN>_<slug>.mp4. - Make this episode's
outline-card.png(untilmake-cards.mjslands, copy the Ep1 card's design and swap the outline text). node render-episode.mjs --in 01-raw\<file>.mp4 --ep <NN> --slug <slug> --outline-card <png>- A/B the audio by ear, eyeball the overlay timing, then upload from
02-exports\. - Move the published file to
03-published\; updatePRODUCTION-TRACKER.md.
Tunables (defaults that worked on the Sower)
silencedetect=n=-30dB:d=0.35— raisedif it over-cuts breaths, lowern(toward −35 dB) if a quiet room leaves long gaps.- Keep-segment min length
0.3 s, pad0.22 spre /0.25 spost, merge gap< 0.12 s— all in the planner. - Chapter-tag overlays show for
4.5 s; outro hold9 s.
5. Open items / fast-follows
make-cards.mjs— needs the original card-design source (the Ep1outline-card.pnggenerator wasn't saved). Until then the outline card is a manual swap.- Shorts pipeline — BUILT as
tools/render-shorts.mjs(see §6 below). - Validate
silencedetectparams on Episode 2's quieter/louder takes; record the winning values here.
6. Shorts pipeline (render-shorts.mjs) — BUILT 2026-06-13
The Shorts batch (all of a week's Shorts recorded in one vertical file) is finished by tools/render-shorts.mjs:
node render-shorts.mjs --in <batch.mp4> --detect— prints the longest silence gaps as split-point candidates.node render-shorts.mjs --in <batch.mp4> --manifest tools/shorts-manifest-epNN.json— per Short: renders the brand top-badge (format + scripture ref) + the static lower-third via headless Chrome, cuts the clip on the manifest timestamps, overlays both, normalizes to −14 LUFS, and writes<date>_swpd_short-epNN-<idx>_<slug>.mp4to02-exports\.
On-screen layout (every Short): top-left format badge + scripture ref · centered burned-in captions (your CapCut/Descript pass) · bottom-left Pastor John | SermonWise.ai lower-third. Templates: channel-assets/shorts-badge.html (__FORMAT__ / __REF__ placeholders) + channel-assets/shorts-lower-third.html (+ pre-rendered .png). Manifest example: tools/shorts-manifest-ep01.json.
Validated end-to-end on the Ep1 Sower Shorts — 4 clips (42.5 / 66.7 / 59.0 / 60.0 s). Still your manual pass first: captions, eye-contact, Studio Sound — done before the batch is handed to this tool. The format-badge title is the optional pillar label (production-process.md Stage 0); the lower-third + scripture ref were the required ones the recording checklist already called for.
7. Gotchas / lessons learned (the Ep1 Shorts pass, the hard way)
- NEVER
-loop 1on the overlay PNGs.-loop 1 -i badge.png -loop 1 -i lt.pngdeadlocked ffmpeg — 0-byte output, process hung indefinitely. Fix: single-frame image inputs (no-loop); overlay's defaulteof_action=repeatholds the overlay for the whole clip, bounded by the trimmed main video. Both render-shorts.mjs and the hub render now use single-frame inputs. (Encoding then took ~5 s/clip instead of hanging.) - Leave ~3 s of silence between Shorts when recording. Descript/CapCut tighten the inter-Short pauses, so silence-split (
--detect) becomes ambiguous — the Ep1 batch had only 2 clean gaps for 4 Shorts, so the split came from transcribing the hooks instead. A 3 s gap makes--detectreliable and the split fully automatic. - Headless Chrome needs network for the brand fonts (Playfair Display / Manrope via Google Fonts;
--virtual-time-budget=3500lets them load before the screenshot). Offline, the CSS falls back to Georgia/Arial — legible but off-brand. - Shorts can run long (Ep1: two at ~60–67 s). Still valid Shorts (YouTube allows up to 3 min), but tighten delivery toward the 20–45 s target where you can; the hook must still land in the first ~1.5 s.