Dubbing API

View as Markdown

The Dubbing API takes a source video or audio file and returns a fully localized version in one or more Indian languages, transcribed, translated, re-voiced, and re-mixed back onto the original timeline. Optionally, it clones each original speaker’s voice so the dub sounds like the same person speaking a different language.

It is part of Creative Agents, and is the programmatic equivalent of the Dubbing workspace in Creator Studio.

Base URL and authentication. See the API Reference for the current base URL and auth header. Generate a key from Key Management in the dashboard.

What you get from one job

A single job dubs into every target language, and produces every format in export_options for each of them. You never need one job per language. In the REST body those fields are src_lang / target_langs; in the Python SDK they are source_language_code / target_language_codes.

How it works

Dubbing is asynchronous by design, so you create a job, poll it, and collect the outputs, rather than getting results back from a single request/response call. Every integration follows the same five steps:

1

Create the job

POST /jobs with your languages and options. Returns a job_id and a short-lived signed upload_url.

2

Upload the media

PUT the raw file bytes to upload_url. This step is required, because the create call does not accept the file itself.

3

Start the pipeline

POST /jobs/{job_id}/start. The job moves from queued to in_progress.

4

Poll progress

GET /jobs/{job_id}/live-status for a progress percentage and the current pipeline step.

5

Fetch downloads

GET /jobs/{job_id}/export-status for one signed download_url per (language, format).

Runnable code for each individual call, in cURL and seven other languages with example requests and responses, is on that call’s page in the API Reference.

export-status returns a flat array, one entry per (target_language, export_type) pair, so a two-language, three-format job returns six entries in a single poll. Group by target_language on your side rather than polling per language.

Read status from the right place. live-status primarily tracks the video export’s completion, so treat it as a progress signal only and always read export-status before downloading. A multi-language job can also finish as partial_failure, meaning some target languages succeeded and others did not; it is a terminal status with real outputs to collect, so treat it as an exit condition alongside completed. Either way, pull only the entries whose own status is completed.

End-to-end with the Python SDK

Install the SDK (pip install sarvamai), set SARVAM_API_KEY, and place your source file next to the script as sample.wav (or change media).

1import os, time, mimetypes
2from pathlib import Path
3import httpx
4from sarvamai import SarvamAI
5from sarvamai.environment import SarvamAIEnvironment
6
7
8def call(fn, attempts=5):
9 for i in range(attempts):
10 try:
11 return fn()
12 except httpx.TransportError:
13 if i == attempts - 1:
14 raise
15 time.sleep(2**i)
16
17
18client = SarvamAI(
19 api_subscription_key=os.environ["SARVAM_API_KEY"],
20 environment=SarvamAIEnvironment(
21 base="https://api.sarvam.ai",
22 creative="https://api.sarvam.ai/dubbing",
23 production="wss://api.sarvam.ai",
24 ),
25)
26
27media = Path("sample.wav")
28source_language_code = "en-IN"
29target_language_codes = ["hi-IN"]
30export_options = ["audio"]
31
32created = call(
33 lambda: client.dubbing.create(
34 source_language_code=source_language_code,
35 target_language_codes=target_language_codes,
36 export_options=export_options,
37 voice_cloning=True,
38 num_speakers=1,
39 editor_flow=False,
40 job_name=media.name,
41 )
42)
43job_id = created.data.job_id
44
45
46def upload():
47 with media.open("rb") as fh:
48 response = httpx.put(
49 created.data.upload_url,
50 content=fh,
51 headers={
52 "Content-Type": mimetypes.guess_type(media.name)[0] or "application/octet-stream",
53 "x-ms-blob-type": "BlockBlob",
54 },
55 timeout=1800.0,
56 )
57 response.raise_for_status()
58
59
60call(upload)
61call(lambda: client.dubbing.start(job_id=job_id))
62
63deadline = time.monotonic() + 3600
64while time.monotonic() < deadline:
65 job = call(lambda: client.dubbing.get_live_status(job_id=job_id)).data
66 if job.status in {"completed", "failed", "partial_failure", "deleted"}:
67 break
68 time.sleep(10)
69else:
70 raise TimeoutError(job_id)
71
72if job.status != "completed":
73 raise RuntimeError(job.error_message or job.status)
74
75wanted = {(t, f) for t in target_language_codes for f in export_options}
76deadline = time.monotonic() + 3600
77while time.monotonic() < deadline:
78 exports = call(lambda: client.dubbing.get_export_status(job_id=job_id, limit=100)).data.exports or []
79 settled = {(e.target_language, e.export_type) for e in exports if e.status in {"completed", "failed"}}
80 if wanted <= settled:
81 break
82 time.sleep(10)
83else:
84 raise TimeoutError(job_id)
85
86downloads = {
87 (e.target_language, e.export_type): e.download_url
88 for e in exports
89 if e.status == "completed"
90}

The script retries transient transport errors, polls until live-status reaches a terminal state, then waits until every requested (language, export_option) pair has settled in export-status before collecting completed download URLs.

Supported languages

Dubbing uses BCP-47 codes for both source and target languages. The same 12 codes are valid for either.

LanguageCode
English (India)en-IN
Hindihi-IN
Bengalibn-IN
Gujaratigu-IN
Kannadakn-IN
Malayalamml-IN
Marathimr-IN
Odiaor-IN
Punjabipa-IN
Tamilta-IN
Telugute-IN
Assameseas-IN

Odia is or-IN for dubbing. od-IN is not accepted. If you have code that sends od-IN for Odia elsewhere, it will be rejected here with a 422 unprocessable_entity_error.

Removing the watermark

By default, exported dubbing assets carry a watermark. Pass disable_watermark: true when creating the job for watermark-free output. The flag is read at job creation, so you cannot remove a watermark from a job that has already been created; you’d need a new job with disable_watermark: true.

In a future release, watermark-free export will become the default behaviour and this parameter will no longer need to be specified. Passing true explicitly today is forward-compatible, and it will simply match the new default.

Next steps

Need help scoping a dubbing integration? Reach out on Discord.