YouTube Shorts rewards volume. Channels that post 5–10 times a day grow faster than those that post once a week.
Manual production at that volume is impossible. With an LLM writing scripts and a TTS API doing voiceover, you can generate 50 Shorts a week with a Python script that runs every morning.
The architecture
- Script generation — Claude writes a 30–60 second script
- Voiceover generation — LeanVox converts it to audio
- Video assembly — ffmpeg combines voiceover with stock footage
- Upload — YouTube Data API v3 schedules the video
Stage 1: Script generation
import anthropic
claude = anthropic.Anthropic()
def write_short_script(topic: str) -> str:
response = claude.messages.create(
model="claude-opus-4-5",
max_tokens=300,
messages=[{"role": "user", "content": f"""Write a YouTube Shorts script about: {topic}
Length: 45-60 seconds (~120-150 words). Start with a hook. End with a CTA. Return only the script."""}]
)
return response.content[0].text
Stage 2: Voiceover via CLI
For a single Short, use the CLI directly:
# Generate voiceover from script file
lvox generate \
--model pro \
--voice podcast_conversational_female \
--speed 1.1 \
--file script.txt \
--output voiceover.mp3
Or via Python SDK with async jobs for batch processing:
from leanvox import Leanvox
import requests, os
client = Leanvox(api_key="lv_live_...")
CHANNEL_VOICE = "podcast_conversational_female"
def generate_voiceover(script: str, output_path: str) -> str:
job = client.generate_async(
text=script,
model="pro",
voice=CHANNEL_VOICE,
speed=1.1,
)
result = job.wait()
audio = requests.get(result.audio_url).content
with open(output_path, "wb") as f:
f.write(audio)
return output_path
Stage 3: Video assembly
import subprocess, json
def get_audio_duration(audio_path: str) -> float:
result = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", audio_path],
capture_output=True, text=True
)
return float(json.loads(result.stdout)["streams"][0]["duration"])
def assemble_short(audio_path: str, background_video: str, output_path: str) -> str:
duration = get_audio_duration(audio_path)
subprocess.run([
"ffmpeg", "-y", "-stream_loop", "-1",
"-i", background_video, "-i", audio_path,
"-t", str(duration), "-c:v", "libx264", "-c:a", "aac",
"-vf", "scale=1080:1920", # vertical 9:16 for Shorts
output_path
], check=True, capture_output=True)
return output_path
Clone your voice for brand consistency
with open("my_voice.wav", "rb") as f:
voice = client.voices.clone(name="My Channel Voice", audio=f)
client.voices.unlock(voice.voice_id)
job = client.generate_async(text=script, model="pro", voice=voice.voice_id, speed=1.1)
Or describe your channel persona with Max tier:
job = client.generate_async(
text=script,
model="max",
instructions="Energetic tech educator, male, early 30s. Enthusiastic but not annoying. Clear and direct."
)
Weekly automated batch
import schedule, time
def weekly_batch():
topics = get_this_weeks_topics()
# Submit all jobs in parallel
pending = [(topic, client.generate_async(text=write_short_script(topic), model="pro", voice=CHANNEL_VOICE)) for topic in topics]
# Collect and assemble
for topic, job in pending:
result = job.wait()
video = assemble_short(result.audio_url, "bg.mp4", f"queue/{hash(topic)}.mp4")
schedule_youtube_upload(video, topic)
schedule.every().monday.at("06:00").do(weekly_batch)
while True:
schedule.run_pending()
time.sleep(60)
What it costs
A typical 45-second Short = ~750 characters.
| Volume | Cost/video | Monthly |
|---|---|---|
| 1 Short/day | $0.0075 | $0.23 |
| 5 Shorts/day | $0.0075 | $1.13 |
| 50 Shorts/day | $0.0075 | $11.25 |
Your $1.00 signup credit covers 130+ Shorts.
Try it
Browse voices · Get API key · Docs
Originally published at leanvox.com/blog
Top comments (0)