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_langandtarget_langsin the same call and you get422 { "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/estimateis free and returns the duration; multiply by your language count to preview the minute cost before you commit the batch. - Store the
id → targetLangmap 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_langsbatch 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)