A faceless YouTube pipeline on a free local stack
Turn a script JSON into a captioned long-form video plus three derivative Shorts with no subscriptions, using edge-tts, headless Edge, and ffmpeg.
The symptom
I needed a repeatable way to turn a written script into a polished, captioned YouTube video and its three derivative Shorts, all without recording a face or paying a monthly SaaS fee. Every tool I found either cost money or handed control to a third-party renderer I could not inspect. I wanted something I could run locally, understand completely, and extend when something was wrong.
The first attempt failed silently: headless Chrome wrote no PNG, ffmpeg shook on zoomed clips, and the captions were synced to sentences instead of words so the karaoke effect was useless.
What was actually happening
Three independent bugs, all subtle:
1. The headless browser attached to the running instance instead of rendering.
Edge (and Chrome) require --user-data-dir to point at an isolated, non-shared directory when running headlessly from a subprocess. If you pass the path to your normal profile directory, or omit the flag, the process attaches to whatever is already running, fires no render, and exits cleanly with code 0. The PNG never appears and there is no error.
2. ffmpeg’s zoompan shook on every scene.
zoompan in ffmpeg expects input frames at the filter’s output resolution. If the source frame is smaller or larger, the crop window shakes because ffmpeg is rounding pixel coordinates against the wrong grid. The fix is to scale the input to a supersample width (4x or 6x output width for short versus long scenes) before piping to zoompan. Without that scale step, Ken Burns motion stutters at every whole-pixel boundary.
3. edge-tts emitted sentence timings instead of word timings.
edge-tts 7.x changed the default boundary type to SentenceBoundary. The word-synced karaoke caption system needs WordBoundary events, which you must request explicitly:
communicate = edge_tts.Communicate(text, voice, rate=rate)
async for event in communicate.stream():
if event["type"] == "WordBoundary":
timings.append((event["offset"], event["duration"], event["text"]))
elif event["type"] == "audio":
audio_chunks.append(event["data"])
Without the WordBoundary filter, each timing event covers a full sentence and the caption highlight snaps once per sentence instead of once per word.
The fix
The production pipeline, now packaged as the Faceless YouTube Automation Kit, resolves all three:
scene JSON
-> fill HTML template per scene
-> headless Edge (isolated --user-data-dir, device-scale-factor 2)
=> 2160x3840 PNG (4x headroom for zoompan)
-> edge-tts with WordBoundary -> per-word timing list
=> karaoke ASS subtitle file
-> ffmpeg:
scale to supersample -> zoompan Ken Burns (ZOOM_RATE = 0.009/s)
-> burn captions -> whoosh/pop SFX in silent tail -> fade
-> concat all scenes -> final encode
-> long-form mp4 (prints chapter timestamps for YouTube description)
-> 3 Short mp4s from the same scene JSON
The b-roll is fetched per-buyer from Pexels or Pixabay using the buyer’s own API key, which keeps the license clean (you hold the download right) and keeps the kit distributable. A remotion-broll/ module ships for animated data overlays (FocusCurve, StatCard, CompareBars) if you need motion graphics rather than footage; it is optional and Node-only. The core pipeline is Python with no Node requirement.
The music bed is CC-BY (Kevin MacLeod tracks fetched at setup time); the attribution is auto-written to ATTRIBUTION.txt and the long-form description template includes the credit line.
The lesson
A “headless render” that exits 0 and writes nothing means the process attached to the wrong target, not that rendering succeeded. Verify the output file exists before treating the subprocess as done.
For zoompan, scale first, zoom second. The filter math breaks if the input is not already at the resolution the filter expects.
For word-synced captions with edge-tts, request WordBoundary explicitly; the library changed its default and will not warn you when it silently switches to sentence boundaries.
The kit is available now with launch code LAUNCH for EUR 19 (regular price EUR 29). The code is use-capped and will be removed once the intro period closes.
Discussion
Powered by GitHub. Sign in to leave a comment.