How I turned my podcast into YouTube video without recording anything

How I turned my podcast into YouTube video without recording anything
ContenidoContents

My podcast is 100% synthetic: an AI writes the script, a synthetic voice reads it and a script stitches it together with music. I never turn on a mic. So when I thought about bringing it to YouTube, the question wasn’t “how do I shoot video?” but “how do I turn the audio I already have into something watchable?”. Spoiler: you can, it’s free and no camera required.

This is the story of how I built it, with its obstacles and tricks. I wrote it so you can replicate it step by step if you fancy it, but without getting in the way: the thread is the story, and the technical detail lives in boxes you can skip.

First decision: where does the video live?

My site reaches the internet through a tunnel from a Mac mini I keep at home. Serving heavy MP4s from there would saturate my home upload: an audio episode is ~6 MB; in video it jumps to tens or hundreds. Quick conclusion: YouTube hosts the video (discovery, indexed subtitles and zero bandwidth cost for me) and my site, at most, embeds it.

The format

I wanted something clean and recognizable: the episode cover big, with a waveform reacting to the voice; and when the intro music ends, the cover shrinks into a corner and subtitles appear in the center. All of that can be done with ffmpeg and a couple of tricks.

Obstacle 1: I need the timing of each word

To subtitle, you need to know when each thing is said. Since the audio already exists, I transcribe it locally with whisper.cpp (fast, using the Mac’s GPU):

1brew install whisper-cpp
2# model (once):
3curl -L -o ggml-large-v3-turbo.bin \
4  https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin
5# audio -> wav 16 kHz mono
6ffmpeg -i ep.mp3 -ar 16000 -ac 1 ep.wav
7# transcription with per-word timing
8whisper-cli -m ggml-large-v3-turbo.bin -f ep.wav -l es \
9  -ml 1 --split-on-word -osrt -of ep

Obstacle 2: Whisper makes up the intro

Whisper placed the first sentence starting at second 0… but there’s only music there. It stretched “Welcome to a new episode” across 10 seconds of theme tune. The elegant fix: detect where the voice comes in by measuring the audio’s energy, and transcribe from there, then add that offset back. Without the music in front, Whisper nails the timing.

In short: I scan the audio in 50 ms windows computing its volume (RMS); the first point that stays above a threshold is the voice onset. I trim from there, transcribe, and add the offset to every timestamp.

Obstacle 3: faithful subtitles (not “what the AI heard”)

Whisper is ~95% accurate, but the exact script already exists. It would be absurd to publish transcription typos when I have the good text. So I align the real script with Whisper’s timings, matching word by word, and keep the best of each: the script’s text, Whisper’s timing. Zero typos.

In short: a difflib.SequenceMatcher between Whisper’s (normalized) words and the script’s transfers each timestamp to the right script word. Where they differ, it interpolates.

Obstacle 4: my ffmpeg couldn’t “burn” subtitles

It turns out the ffmpeg build I had is compiled without libass, so the subtitle filter didn’t even exist (Unknown filter 'subtitles'). Instead of fighting it by reinstalling half the system, I solved it the other way around: I render each subtitle as a PNG image —with my typeface and colors— using Python and Pillow, and overlay it. Bonus: total typographic control.

Obstacle 5: the cover animation

Making the cover shrink and travel to the corner at just the right moment is done with time expressions in ffmpeg: size and position are recomputed on every frame.

# p = transition progress (0 to 1); T = moment the voice comes in
[1:v]scale=w='820+(300-820)*clip((t-T)/D,0,1)':h='...':eval=frame[cov];
[bg][cov]overlay=x='(W-w)/2*(1-p)+(W-w-45)*p':y='...':eval=frame

Obstacle 6: scaling to long episodes

Feeding 80 subtitle images as inputs to ffmpeg is fragile. The clean solution: build a single subtitle track with transparency —concatenating the PNGs with their timings— and overlay it in one go. Doesn’t matter if the episode has 80 subtitles or 200.

Upload to YouTube, and publish with the episode

The upload is done by the YouTube API (OAuth). It uploads as private first —so nothing goes out half-baked— but in my flow the episode’s green light covers everything: when I approve the audio, the episode and the video go public at the same time, and the video lands in its playlist. One single “ok” for both formats.

Here’s the result, episode 10 already in video:

And all in one command

In the end I wrapped it into a single command, so every new episode comes out as video with no manual work:

1make-podcast-video ep011 --upload

It detects the voice, transcribes, aligns the script, generates the subtitles, builds the animation and uploads to YouTube. What used to be an afternoon of trial and error is now one line.

What’s next: a presenter with a face

The next step, already on my to-do list, is to try an AI avatar that puts a face on it and lip-syncs to the audio. Does it improve the connection with the viewer, or cross into the “uncanny valley”? We’ll see. Coming soon. 🎭

The takeaway

I didn’t need to change how I produce the podcast: just reuse what I already had —audio, script, cover— and chain together tools that were already there. Sometimes “making video” isn’t shooting video, it’s looking at what you have from another angle.

CompartirShare