Developer blog

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" }
  ]
}

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')

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")

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
}

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.