# How to Build a Voice-Over Tool for YouTube Videos

> Source: <https://dev.to/voice_developer/how-to-build-a-voice-over-tool-for-youtube-videos-1fjg>
> Published: 2026-10-27 22:56:55+00:00

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](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.

``` python
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"

# 1️⃣ Read the script
with open("script.txt", "r", encoding="utf-8") as f:
    script = f.read()

# 2️⃣ Build the request payload
payload = {
    "text": script,
    "voice_settings": {
        "stability": 0.75,
        "similarity_boost": 0.8
    }
}

headers = {
    "xi-api-key": API_KEY,
    "Content-Type": "application/json"
}

# 3️⃣ Make the request
response = requests.post(
    f"{ELEVEN_API_URL}/{VOICE_ID}",
    headers=headers,
    data=json.dumps(payload)
)

# 4️⃣ Save the MP3
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)
)

# Parse timestamps
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)

    # Build SRT
    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:

``` js
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` and`similarity_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](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!
