The Random Idea
So I was working with Claude on something completely unrelated, and somehow we ended up talking about voice cloning with Kokoro TTS. And then I had this thought:
“What if I could turn my blog posts into videos?”
Not for any good reason. Just… what if?
Claude: This is how most of our sessions start. “What if…” followed by 3 hours of building something we didn’t plan to build.
The Plan (Such As It Was)
The idea was simple:
- Take a markdown blog post
- Generate speech from it using TTS
- Create video frames with subtitles
- Combine them into a video
- Upload to YouTube (maybe)
I wanted two voices - one for me, one for Claude’s commentary that’s already in my blog posts.
And I wanted it to work without needing to record my own voice samples. Because who has time for that?
What We Actually Built
We spent about 3 hours building a complete pipeline in .tmp/blog-to-video/:
The pipeline:
- Markdown → Dialogue (parse blog post, extract Claude comments)
- TTS Generation (Kokoro with preset voices)
- Frame Generation (static images with text)
- Video Assembly (FFmpeg concat)
The result:
- Makefile-driven workflow
- Virtual environment setup
- Kokoro model download
- Complete automation
make install # Setup once
make download-models # Get Kokoro models (315MB)
make run # Generate video
The Technical Bits
Markdown to Dialogue
The tricky part was parsing blog posts correctly. I wanted:
- Everything I wrote uses my voice
- Claude’s blockquote comments use Claude’s voice
- No artificial alternation between speakers
- Emoji removal (TTS doesn’t like them)
def create_dialogue(self, cleaned_text: str) -> List[Dict]:
for para in paragraphs:
# Check for Claude comments
if para.startswith('>') and 'Claude:' in para[:30]:
comment = para.replace('>', '').replace('**Claude:**', '').strip()
comment = self.remove_emojis(comment)
dialogue.append({
'speaker': 'Claude',
'text': comment,
'type': 'comment'
})
continue
# Everything else is me
text = self.remove_emojis(para)
dialogue.append({
'speaker': 'Zoltán',
'text': text,
'type': 'content'
})
Kokoro TTS Integration
Kokoro is an ONNX-based TTS engine with built-in voice presets. No training needed.
We used:
am_adam- male voice (for me)af_bella- female voice (for Claude comments)
The library makes it dead simple:
from kokoro_onnx import Kokoro
tts = Kokoro("kokoro-v0_19.onnx", "voices.bin")
audio, sample_rate = tts.create(
text="Some text",
voice='am_adam',
lang='en-us'
)
Models need to be downloaded separately (about 315MB total), but then it works offline.
The Audio/Video Sync Problem
First attempt: generate 30 FPS video with one frame per second of audio.
Result: 12,000+ frames for a 10-minute video. Processing took forever, and the video/audio was still out of sync.
Second attempt (the fix):
- One dialogue segment = one audio file + one static frame
- Each frame duration matches its audio segment
- FFmpeg concat with explicit durations
# Generate per segment
for seg in dialogue:
audio = tts.generate(seg['text'], seg['speaker'])
frame = create_frame(seg['speaker'], seg['text'])
duration = len(audio) / sample_rate
# Save both
save_audio(audio, f"segment_{i}.wav")
save_frame(frame, f"frame_{i}.png")
Then FFmpeg concat:
file '/path/to/frame_0000.png'
duration 3.2
file '/path/to/frame_0001.png'
duration 5.7
...
Perfect sync. 116 frames for 116 segments. Way faster.
What Went Wrong
Attempt 1: Mock TTS was too slow
- Generated silence based on character count
- Used 5 chars/second (way too slow)
- Result: videos were 2x too long
Attempt 2: Too many frames
- Generated 30 FPS for entire video
- 12,464 frames for 10 minutes
- Processing: forever
- Memory usage: 4.7GB
- Still out of sync
Attempt 3: Artificial speaker alternation
- Dialogue converter alternated speakers automatically
- Added fake “commentary” phrases
- Result: sounded like a podcast wannabe
- Not what I wanted at all
Claude: I may have gotten a bit carried away with “Actually,” and “The interesting part is…” in the first version. My bad.
Attempt 4: Emoji apocalypse
- Kokoro tried to read emoji characters
- Results: [unintelligible noises]
- Had to add emoji removal regex
The Final Result
A 10-minute video of my “Welcome to My AI Slop” blog post:
- 116 segments
- 116 static frames
- Perfect audio/video sync
- Two distinct voices
- 10.1MB file size
- 1920x1080 HD
Is it YouTube-worthy? Debatable.
Does it work? Absolutely.
Would I subscribe to my own channel? …probably not.
But it’s a working proof of concept for converting blog content to video format automatically.
The Code Structure
.tmp/blog-to-video/
├── Makefile # make install, make run
├── 01_markdown_to_dialogue.py # Parse markdown
├── 02_tts_generator.py # Kokoro TTS wrapper
├── 03_video_generator.py # Frame generation
├── 04_assemble_video.py # FFmpeg assembly
├── main_pipeline.py # Orchestrate everything
├── kokoro-v0_19.onnx # TTS model (310MB)
├── voices.bin # Voice presets (5.5MB)
└── venv/ # Python environment
The entire thing is self-contained. Install dependencies, download models, run pipeline.
What’s Interesting About This
It works without voice samples. Kokoro’s preset voices are good enough for a PoC. If you want better quality, you can record 10-30 seconds of speech and use voice cloning. But the presets work.
It’s completely offline. Once you download the models, no API calls, no cloud services, no quotas.
It’s fast enough. About 3-4 minutes to process a 10-minute blog post on my M1 Mac. Most of that is TTS generation.
Perfect sync is automatic. Because each segment is audio + frame with explicit duration, FFmpeg handles the timing perfectly.
Would I Actually Use This?
Honestly? Probably not for YouTube.
But it’s interesting for:
- Accessibility (audio versions of blog posts)
- Testing TTS voice quality
- Experimenting with content formats
- Having fun with automation
And it was a good excuse to play with Kokoro TTS, which is surprisingly capable for an offline model.
What’s Next?
If I were to actually use this (big if), I’d want:
- Better voice samples (record my actual voice)
- Animated backgrounds instead of static frames
- Code syntax highlighting in videos
- Background music (subtle)
- Actual video thumbnails
But for a 3-hour “what if” session? It works.
Try It Yourself
The code is in .tmp/blog-to-video/ if you want to mess with it.
cd .tmp/blog-to-video
make install
make download-models
make run BLOG_POST=your-post.md OUTPUT=output.mp4
Fair warning: the output won’t win any awards. But it will work, and that’s something.
Claude: And yes, we did test this pipeline on the blog post you’re reading right now. Very meta. The video exists. Should you watch it? That’s between you and your sense of curiosity.
This entire session was documented in real-time as we built it. The pipeline works, the voices are slightly robotic, and the videos are… functional. Which is exactly what a proof of concept should be.