Automated YouTube Pipeline: Python, edge-tts, and MoviePy
Zero cost and one day of coding is all it took to build a faceless YouTube channel for my SaaS, WhaleTrack. Instead of messing with Premiere or hiring a designer, I just scripted the whole thing to turn live site data into videos.
The Tech Stack
- edge-tts: Microsoft's neural TTS. I used the 'Andrew' voice—it's free, doesn't need an API key, and sounds surprisingly human.
- moviepy 1.0.3: For the heavy lifting of video assembly.
- Pillow: Handles the image processing.
- Puppeteer: Used to grab high-res screenshots of the live site.
- ffmpeg: The engine under the hood.
The Workflow
1. Capturing Visuals
I didn't want static mockups, so I used Puppeteer to screenshot the actual site.
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
await page.goto('https://whaletrack.app');
await page.screenshot({ path: 'screenshots/home.png' });
Pro tip: If your package.json is set to "type": "module", require() will crash. Just name your file .cjs to avoid the headache.
2. Generating Audio
The edge-tts library is a hidden gem for this.
import edge_tts
comm = edge_tts.Communicate(script, voice='en-US-AndrewNeural', rate='+8%')
await comm.save('audio.mp3')
3. Adding Movement (Ken Burns Effect)
Static images kill retention. I wrote a helper function to add a slow zoom and pan, making the screenshots feel like dynamic b-roll.
def apply_ken_burns(img, progress, zoom_from, zoom_to, pan_from, pan_to):
zoom = zoom_from + (zoom_to - zoom_from) * progress
iw, ih = img.size
crop_w, crop_h = int(iw / zoom), int(ih / zoom)
ox = int(pan_from[0] + (pan_to[0] - pan_from[0]) * progress) * (iw - crop_w)
oy = int(pan_from[1] + (pan_to[1] - pan_from[1]) * progress) * (ih - crop_h)
return img.crop((ox, oy, ox + crop_w, oy + crop_h)).resize((1920, 1080), Image.LANCZOS)
4. Final Assembly
I used MoviePy to glue the generated audio and the animated frames together.
from moviepy.editor import VideoClip, AudioFileClip
def make_frame(t):
progress = t / duration
frame = apply_ken_burns(bg_image, progress, 1.0, 1.12, (0,0), (0.03,0.02))
return np.array(frame)
clip = VideoClip(make_frame, duration=duration)
clip = clip.set_audio(AudioFileClip('audio.mp3'))
The result is a full 1080p video produced entirely by a Python script. It's a solid AI workflow for anyone wanting to scale content without a production budget.
All Replies (3)
MoviePy memory leaks are a nightmare. Anyone found a more stable library for this?
MoviePy subtitles are a disaster to align. Which specific method are you using to keep them synced?
My renders always break after CSS updates. Do saved screenshot fixtures actually stop those random pipeline failures?