DEV Community

My Little Indie Studio
My Little Indie Studio

Posted on

Auto-Localize Your YouTube Uploads Into 30+ Languages

In the first tutorial we dubbed one video into one language in about ten lines. This time we do the thing creators actually ask for: take a single upload and fan it out into a whole batch of languages at once, then download every dub. It's a script you can hang off your upload pipeline so a new video ships localized on day one.

The problem

You publish a video. Your audience is global, but the video is in one language. The manual fixes don't scale. Re-recording, hiring voice actors, running each language through a separate tool: all of it falls apart past a couple of languages, never mind thirty. You want one call that says "dub this into Spanish, Hindi, Portuguese, Arabic, …" and a folder of finished MP4s at the end.

What you'll build

A script that takes a video URL and a list of target languages, submits one dub request for the whole batch, polls each job to completion, and downloads each dubbed video named by its language. Point it at a YouTube URL (or any direct or HLS link) right after upload and the whole video comes back localized in one run.

The one call that does it: target_langs

POST /api/v1/dub takes either a single target_lang or an array target_langs. Pass the array and you get one job per language back, all created in a single request. The minute hold is all-or-nothing across the batch: either every language is reserved or none is, so you never end up with a half-charged partial run.

// POST /api/v1/dub
{
  "source_type": "youtube",
  "input_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "target_langs": ["es", "hi", "pt", "ar", "de"]
}
// → 201
{
  "jobs": [
    { "id": 5001, "state": "queued", "targetLang": "es" },
    { "id": 5002, "state": "queued", "targetLang": "hi" },
    { "id": 5003, "state": "queued", "targetLang": "pt" },
    { "id": 5004, "state": "queued", "targetLang": "ar" },
    { "id": 5005, "state": "queued", "targetLang": "de" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each entry carries its own id and its targetLang, so you can map jobs back to languages. From here every job is polled and downloaded exactly like the single-language flow: GET /api/v1/jobs/{id} until state: "done", then grab archiveUrl. One reminder from tutorial #1: a failed job comes back as a 200 with state: "failed" and an errorMessage, not an HTTP error, so branch on state.

Two rules the API enforces, worth knowing before you wire this in:

  • Send both target_lang and target_langs in the same call and you get 422 { "error", "message": "Pass either target_lang or target_langs, not both." }.
  • An unsupported language code gets named back to you instead of being silently dropped: 422 … "Unsupported target language: xx. See the supported list at /api.". Validate your list against the supported languages first.

Full code

Set DV_API_KEY in your environment. Both scripts submit the batch, poll all jobs concurrently, and write dubbed-<lang>.mp4 for each.

Node (18+, global fetch)

// Node 18+ (global fetch). Set DV_API_KEY in your environment.
import { writeFile } from 'node:fs/promises'

const KEY = process.env.DV_API_KEY
const base = 'https://dubanyvideo.com/api/v1'
const headers = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))

const INPUT_URL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const LANGS = ['es', 'hi', 'pt', 'ar', 'de']

// 1. One request -> one job per language: 201 { jobs: [{ id, state, targetLang }] }
const { jobs } = await fetch(`${base}/dub`, {
  method: 'POST',
  headers,
  body: JSON.stringify({ source_type: 'youtube', input_url: INPUT_URL, target_langs: LANGS }),
}).then((r) => r.json())

// 2. Poll each job to completion (in parallel) and 3. download its dubbed MP4.
async function finish({ id, targetLang }) {
  let job
  do {
    await sleep(5000)
    job = await fetch(`${base}/jobs/${id}`, { headers }).then((r) => r.json())
    console.log(`[${targetLang}] ${job.state}${job.stage ? ` — ${job.stage}` : ''}`)
  } while (job.state !== 'done' && job.state !== 'failed')

  if (job.state === 'failed') {
    console.error(`[${targetLang}] failed: ${job.errorMessage}`)
    return
  }
  const mp4 = await fetch(job.archiveUrl).then((r) => r.arrayBuffer())
  await writeFile(`dubbed-${targetLang}.mp4`, Buffer.from(mp4))
  console.log(`[${targetLang}] saved dubbed-${targetLang}.mp4`)
}

await Promise.all(jobs.map(finish))
console.log('all languages done')
Enter fullscreen mode Exit fullscreen mode

Python (pip install requests)

# pip install requests. Set DV_API_KEY in your environment.
import os, time, requests
from concurrent.futures import ThreadPoolExecutor

base = "https://dubanyvideo.com/api/v1"
headers = {"Authorization": f"Bearer {os.environ['DV_API_KEY']}"}

INPUT_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
LANGS = ["es", "hi", "pt", "ar", "de"]

# 1. One request -> one job per language: 201 { "jobs": [{ "id", "state", "targetLang" }] }
jobs = requests.post(f"{base}/dub", headers=headers, json={
    "source_type": "youtube",
    "input_url": INPUT_URL,
    "target_langs": LANGS,
}).json()["jobs"]

# 2. Poll one job to completion and 3. download its dubbed MP4.
def finish(entry):
    lang = entry["targetLang"]
    while True:
        job = requests.get(f"{base}/jobs/{entry['id']}", headers=headers).json()
        print(f"[{lang}] {job['state']}")
        if job["state"] == "done":
            break
        if job["state"] == "failed":
            print(f"[{lang}] failed: {job['errorMessage']}")
            return
        time.sleep(5)
    with open(f"dubbed-{lang}.mp4", "wb") as f:
        f.write(requests.get(job["archiveUrl"]).content)
    print(f"[{lang}] saved dubbed-{lang}.mp4")

# Poll all languages in parallel.
with ThreadPoolExecutor(max_workers=len(jobs)) as pool:
    list(pool.map(finish, jobs))
print("all languages done")
Enter fullscreen mode Exit fullscreen mode

One voice across every language (optional)

By default each job auto-detects speakers and gives them their own voices. If you'd rather use one fixed voice for the whole batch, say a single narrator across every language, add diarization: "skip" with a voice. It applies to every language in the batch:

{
  "source_type": "youtube",
  "input_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "target_langs": ["es", "hi", "pt"],
  "diarization": "skip",
  "voice": "…"          // pick from the voices listed on /api
}
Enter fullscreen mode Exit fullscreen mode

Omit both to keep automatic per-speaker voices.

Wiring it into your upload flow

  • Trigger it on publish. Call this right after a new video is uploaded (a webhook from your CMS or YouTube ingestion, a queue worker, a CI step) with your standard language list.
  • Estimate first if you want a preflight number. POST /api/v1/estimate is free and returns the duration; multiply by your language count to preview the minute cost before you commit the batch.
  • Store the id → targetLang map from the create response so a later poll worker can pick jobs up asynchronously instead of blocking on one long request.

Cost & limits

  • You're billed video-duration minutes per language. A 4-minute video into 5 languages costs 20 minutes. The batch hold is all-or-nothing.
  • One balance covers both the dashboard and the API. Max source length is 30 minutes per job, and input is by link only (YouTube, HLS, or direct URL).
  • Rate limits: 60 requests/min per key. New submissions are capped per minute by plan (free 2 · starter 5 · medium 10 · super 20), and a target_langs batch counts as one submission, so fanning out into many languages doesn't burn your submit budget.
  • Prefer pay-per-dub? The same engine is on the RapidAPI marketplace, billed per job.

Get your key

Grab a key and localize your next upload into every language your audience speaks: dubanyvideo.com/api. Next in this series: adding a "Dub this video" button to your own platform.

Top comments (0)