DubAnyVideo

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. Now let's do the thing creators actually want:
take a single upload and fan it out into a whole batch of languages at once, then
download every dub — 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 fix — re-record, hire voice actors, or run each language through a separate
tool — doesn't scale past a couple of languages, let alone 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 resulting job to completion, and downloads
each dubbed video named by its language. Point it at a YouTube URL (or any direct /
HLS link) right after upload and you've localized the whole video 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 back one job per language, created in a
single request — the minute hold is all-or-nothing across the batch (either every
language is reserved, or none is; you never get 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. (Reminder from tutorial #1: a failed job is a 200 with
state: "failed" + errorMessage, not an HTTP error — branch on state.)

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

  • Send both target_lang and target_langs and you get 422 { "error", "message": "Pass either target_lang or target_langs, not both." }.
  • An unsupported language code is named back to you, not silently dropped:
    422 … "Unsupported target language: xx. See the supported list at /api." — so
    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 — e.g. 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 on publish. Call this right after a new video is uploaded (a webhook
    from your CMS/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 committing 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 the dashboard and the API; max source length is 30 minutes
    per job; input is by link only (YouTube / HLS / 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) — 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.


Pre-publish checklist: set cover; run the target_langs batch once against
the live API
on a short clip with 2 languages to confirm the { jobs: [...] }
response and per-language download before publishing (the single-language flow is
already live-verified; the batch shape here is grounded in the API code but should
be exercised once); re-confirm the 30-min limit, plan submit caps, and language
list against /api.