If you’ve ever tried to produce a YouTube video, you’ll know that the most time‑consuming part isn’t shooting footage or writing the script—it’s the audio. Recording a human voice, cleaning it up, and syncing it to the video can eat up hours of a creator’s day. That’s where text‑to‑speech (TTS) and voice‑cloning come in: they let you generate high‑quality narration with a few lines of code.
In this post I’ll walk you through building a lightweight voice‑over tool that takes a script, turns it into a natural‑sounding voice, and even produces a subtitle file that you can drop straight into your YouTube upload. We’ll use ElevenLabs as our TTS engine because it offers state‑of‑the‑art neural voices and an easy‑to‑use API.
Pro tip: If you’re new to ElevenLabs or want a free trial, use this affiliate link: https://try.elevenlabs.io/kr07zfuqn1bp. It unlocks a generous quota of free synthesis hours.
ElevenLabs gives you access to a catalog of voices. You can:
When you clone a voice, you’ll get a unique voice ID that you’ll reference later. Make sure the voice you choose matches the tone of your channel (e.g., friendly, professional, energetic).
Below is a minimal Python script that sends a script to ElevenLabs, receives back an MP3 file, and writes a subtitle file in SRT format. Feel free to adapt it to your preferred language.
import requests
import json
import os
ELEVEN_API_URL = "https://api.elevenlabs.io/v1/text-to-speech"
API_KEY = "YOUR_ELEVEN_API_KEY"
VOICE_ID = "YOUR_CLONED_VOICE_ID"
with open("script.txt", "r", encoding="utf-8") as f:
script = f.read()
payload = {
"text": script,
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.8
}
}
headers = {
"xi-api-key": API_KEY,
"Content-Type": "application/json"
}
response = requests.post(
f"{ELEVEN_API_URL}/{VOICE_ID}",
headers=headers,
data=json.dumps(payload)
)
if response.status_code == 200:
with open("narration.mp3", "wb") as out:
out.write(response.content)
print("✅ Audio generated!")
else:
print(f"❌ Error {response.status_code}: {response.text}")
stability) and how closely the voice mimics the source ( similarity_boost).
YouTube accepts SRT files, and syncing subtitles is a breeze once you have timestamps. ElevenLabs can return timestamps if you set output_format to json. Let’s tweak our script to capture those timestamps:
payload["output_format"] = "json"
response = requests.post(
f"{ELEVEN_API_URL}/{VOICE_ID}",
headers=headers,
data=json.dumps(payload)
)
if response.status_code == 200:
data = response.json()
audio_bytes = requests.get(data["audio_url"]).content
with open("narration.mp3", "wb") as out:
out.write(audio_bytes)
srt_lines = []
for i, segment in enumerate(data["segments"]):
start = segment["start"]
end = segment["end"]
text = segment["text"]
srt_lines.extend([f"{i+1}", f"{format_time(start)} --> {format_time(end)}", text, ""])
with open("subtitles.srt", "w", encoding="utf-8") as srt:
srt.write("\n".join(srt_lines))
print("✅ Audio + subtitles ready!")
else:
print(f"❌ Error {response.status_code}: {response.text}")
def format_time(seconds: float) -> str:
"""Convert seconds to SRT timestamp format."""
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
ms = int((s - int(s)) * 1000)
return f"{int(h):02}:{int(m):02}:{int(s):02},{ms:03}"
Result:
narration.mp3 – the spoken audio.
subtitles.srt – a perfectly synced subtitle file.
While you can still use the YouTube web UI, the YouTube Data API lets you push the video, audio, and subtitles in one go. Below is a quick Node.js snippet that uploads a video and attaches the SRT file:
const fs = require('fs');
const { google } = require('googleapis');
const youtube = google.youtube('v3');
// 1️⃣ Authenticate (OAuth2 flow required)
const auth = new google.auth.OAuth2(
'YOUR_CLIENT_ID',
'YOUR_CLIENT_SECRET',
'YOUR_REDIRECT_URI'
);
// Assume you have a valid refresh token
auth.setCredentials({ refresh_token: 'YOUR_REFRESH_TOKEN' });
async function uploadVideo() {
const res = await youtube.videos.insert({
auth,
part: 'snippet,status',
requestBody: {
snippet: {
title: 'My AI‑Generated Voice‑Over',
description: 'Generated with ElevenLabs TTS',
tags: ['AI', 'TTS', 'ElevenLabs'],
categoryId: '22' // People & Blogs
},
status: { privacyStatus: 'public' }
},
media: {
body: fs.createReadStream('narration.mp3')
}
});
const videoId = res.data.id;
// Upload subtitles
await youtube.captions.insert({
auth,
part: 'snippet',
requestBody: {
snippet: {
videoId,
language: 'en',
name: 'English subtitles',
isDraft: false
}
},
media: {
body: fs.createReadStream('subtitles.srt')
}
});
console.log(`✅ Video uploaded: https://youtu.be/${videoId}`);
}
uploadVideo().catch(console.error);
Heads‑up: Setting up OAuth2 is a bit more involved. Check the Google API docs for a full walkthrough, but once you have the refresh token, the rest is just a few lines of code.
| Task | Tool | Why it matters |
|---|---|---|
| Voice selection | ElevenLabs | Natural‑sounding, customizable |
| Text formatting | Markdown or HTML | Keeps scripts readable |
| Subtitle sync | ElevenLabs JSON output | Accurate timings |
| Video upload | YouTube Data API | Fully automated pipeline |
If you find yourself generating a lot of videos, consider wrapping this entire flow in a Docker container or a serverless function. That way you can trigger a new voice‑over job from a webhook whenever a new script lands in a GitHub repo.
| Issue | Fix |
|---|---|
| Audio too short / missing words | Use output_format=json to get timestamps; double‑check the script length. |
| Voice sounding robotic | Increase stability andsimilarity_boost in the payload. |
| API rate limits | Cache the generated MP3s; batch requests if you have many scripts. |
| Subtitle mis‑alignment | Verify timestamps with a tool like ffmpeg -i narration.mp3 -f ffmetadata . |
You now have a fully automated pipeline:
You can extend this by adding background music, dynamic pacing, or even generating multiple language tracks.
If you haven’t tried ElevenLabs yet, jump on the affiliate link above: https://try.elevenlabs.io/kr07zfuqn1bp. It gives you free credits and a smooth onboarding process, so you can focus on creating content, not on the tech stack. Happy coding—and happy talking!