Skip to main content

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

StageResult
Raw recording556.8 s, single OBS take, one segment per chapter
Silence-cut167 keep-segments auto-extracted → 439.9 s body (+9 s outro hold = 448.9 s ≈ 7.5 min)
Chapter detectionAll 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
StingerAuto-inserted at the cold-open boundary ("this week… let's prep", item 12)
On-screen overlays8 chapter-tag PNGs + outline card (46 s) + outro card (14.5 s), all auto-timed to the post-cut timeline
A/V syncEvery cut snapped to the 60 fps frame grid (prevents sub-frame drift accumulating)
LoudnessTwo-pass: measured −18.61 LUFS raw → −14.46 LUFS (YouTube −14 target)
Output2026-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)

PieceStateAction
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.htmlKeep
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 transcriptBuild 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 savedManual 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.mjsfast-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

  1. Record hub (OBS Longform profile), drop raw in 01-raw\ named <date>_swpd_ep<NN>_<slug>.mp4.
  2. Make this episode's outline-card.png (until make-cards.mjs lands, copy the Ep1 card's design and swap the outline text).
  3. node render-episode.mjs --in 01-raw\<file>.mp4 --ep <NN> --slug <slug> --outline-card <png>
  4. A/B the audio by ear, eyeball the overlay timing, then upload from 02-exports\.
  5. Move the published file to 03-published\; update PRODUCTION-TRACKER.md.

Tunables (defaults that worked on the Sower)

  • silencedetect=n=-30dB:d=0.35 — raise d if it over-cuts breaths, lower n (toward −35 dB) if a quiet room leaves long gaps.
  • Keep-segment min length 0.3 s, pad 0.22 s pre / 0.25 s post, merge gap < 0.12 s — all in the planner.
  • Chapter-tag overlays show for 4.5 s; outro hold 9 s.

5. Open items / fast-follows

  • make-cards.mjs — needs the original card-design source (the Ep1 outline-card.png generator wasn't saved). Until then the outline card is a manual swap.
  • Shorts pipeline — BUILT as tools/render-shorts.mjs (see §6 below).
  • Validate silencedetect params 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>.mp4 to 02-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 1 on the overlay PNGs. -loop 1 -i badge.png -loop 1 -i lt.png deadlocked ffmpeg — 0-byte output, process hung indefinitely. Fix: single-frame image inputs (no -loop); overlay's default eof_action=repeat holds 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 --detect reliable and the split fully automatic.
  • Headless Chrome needs network for the brand fonts (Playfair Display / Manrope via Google Fonts; --virtual-time-budget=3500 lets 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.