← ForeA Technologies Blog index
Field Notes · OpenAudio Suite

Reaching Tamil, Telugu, Bengali, and Marathi with Indic-Parler-TTS

Kokoro’s 54 voices span nine languages — and exactly one of them is Indian. Closing that gap meant a natural-language-prompted model instead of a voice id, a gated repo hiding behind an Apache 2.0 badge, and two transitive dependencies that have never once published a wheel.

Shipped & verified · Indic-Parler-TTS (AI4Bharat), Apache 2.0 · RunPod Flash

The question that started this was a simple one: “we use cloud voice to design new voices, are we not using kokoro?” Following that thread turned up a real gap — Voice Design only ever offered Cloud voices as candidates, not Kokoro. Fixing that led to a bigger question a turn later: “kokoro voices can we have more different voices here,” and then, immediately after, “now can we also do it in indian languages and which languages.”

That last question had an answer worth checking rather than assuming. Kokoro’s ALIASES list nine language codes across its 54 voices — English (US and UK), Spanish, French, Italian, Portuguese, Japanese, Mandarin, and Hindi. One Indian language. No Tamil, Telugu, Kannada, Malayalam, Bengali, Marathi, Gujarati, or Punjabi anywhere in the catalog, confirmed directly against the ALIASES dict rather than assumed from the model’s marketing. There was no “just enable it” shortcut here the way there had been for Hindi — reaching those languages meant a genuinely different model, with its own licensing and deployment work.

Indic-Parler-TTS (AI4Bharat, IIT Madras) was the model that actually got there — 21 Indian languages including Tamil, Telugu, Kannada, Malayalam, Bengali, and Marathi, and a rare Apache 2.0 license stated explicitly on the model card as covering the fine-tuned weights, not just the training code, after a licensing scan that had ruled out most other candidates over the preceding weeks — the same discipline that made IndicWhisper’s MIT license worth double-checking earlier.

A different control surface, and a tokenizer setup that isn’t guessable

Every model in this stack so far takes a voice id. Indic-Parler-TTS doesn’t — it’s controlled by a natural-language description prompt (“Rohit’s voice, a clear, natural voice at a moderate pace”), a genuinely different architecture from everything deployed before it. Rather than expose that raw description control, the plan settled on curating the model card’s own recommended named speakers per language — Rohit and Divya for Hindi, Jaya for Tamil, Prakash and Lalitha for Telugu, Arjun and Aditi for Bengali, Sanjay and Sunita for Marathi — matching how every other engine in this stack offers a picked list rather than open-ended prompting.

The loading code for that description prompt isn’t obvious, either. The first pass at main.py guessed a subfolder="prompt_tokenizer" pattern, which was wrong. The model actually needs two separate tokenizers loaded two different ways: the prompt tokenizer comes straight from the model repo (AutoTokenizer.from_pretrained("ai4bharat/indic-parler-tts")), while the description tokenizer has to come from wherever model.config.text_encoder._name_or_path points — a different underlying repo entirely, not assumed to be the same one. generate() then needs both tokenizers’ attention_masks passed alongside both input_idss. Caught by fetching the model card’s actual example before writing a line of worker code, not after a failed deploy — the value of checking a verified source instead of a remembered pattern from a similar-looking model.

Gated doesn’t mean done, even at Apache 2.0

Apache 2.0 on the weights turned out not to mean “no further steps.” ai4bharat/indic-parler-tts is marked gated: "auto" on HuggingFace — auto-approved, no manual review queue, but still requiring a token whose account has clicked through the license once. The existing HF_TOKEN returned a clean 200 on metadata reads and a 403 on the actual file download.

403 Client Error: Forbidden for url: .../resolve/main/model.safetensors (metadata reads: 200 · file download: 403, confirmed separately)
Fix

The user visited the model page and accepted the license once, the one step in this whole stack that can’t be automated. File downloads worked cleanly right after.

flash dev’s hot-reload quietly drops env vars

After threading HF_TOKEN through the worker’s env={} and restarting flash dev, the deployed function still hit its own "HF_TOKEN is required" guard — even though the token was set at decorator-eval time in the parent process.

WatchFiles detected changes in '.flash/server.py'. Reloading... ✗ {"error": "HF_TOKEN is required to download the gated Indic-Parler-TTS weights"}

A likely cause: the hot-reload path re-evaluates the module in a way that doesn’t reliably inherit the original process’s environment.

Fix

Skip straight to flash deploy, which needed to happen anyway for a persistent endpoint — deploy reads the environment fresh at build time and bakes it into RunPod’s own endpoint config, not per-process inheritance.

Two dependencies that have never published a wheel

The first real flash deploy failed on a resolution error in parler-tts’s own dependency tree. It unconditionally imports dac (descript-audio-codec) at module load, which unconditionally imports audiotools (descript-audiotools) — both genuinely load-bearing, confirmed by reading the actual import chain rather than assumed dead code the way encodec was for Stable Audio Open. audiotools needs argbind and randomname, and neither has ever published a wheel, on any release, for any Python version — confirmed via the PyPI JSON API. Flash’s installer is wheels-only, so this blocked the same way encodec had.

ERROR: Cannot install parler-tts because these package versions have conflicting dependencies. argbind has no wheel for any Python version.
Fix

Different shape than either prior fix. encodec got routed around because it was dead code for this model’s architecture; stable-audio-tools needed a fork because it had a real bug. argbind/randomname are neither — pure-Python, zero native extensions, no bug, just a missing build artifact. Built the wheels directly (pip wheel --no-deps argbind==0.3.9, seconds, no code changes), hosted them as GitHub release assets on argbind-wheel, verified the full resolution locally with both substituted in before wiring into dependencies=[]. Second deploy succeeded.

Verification, and a teardown gotcha found for the second time

All five languages and nine named speakers were checked directly against the RunPod API before any backend wiring happened — Hindi, Tamil, Telugu, Bengali, Marathi, each producing real, decodable audio. Backend integration followed the shape Chatterbox and Stable Audio Open had already established: tts_indic_parler.py (async /run plus poll, the IndicWhisper-taught lesson about RunPod’s 60-second /runsync ceiling applied here without needing to be relearned), a third MODEL_REGISTRY["tts"] entry, and a new GET /api/tts/indic-parler-voices.

→ ఇది తెలుగు భాషలో ఒక పరీక్ష (Telugu, "Prakash" voice, generated live through the Studio UI) ← real, decodable audio · zero console errors

Tearing the endpoint down afterward surfaced a gotcha for the second time this stack has hit it: flash dev provisions its own separate, persistently-billing endpoint (live-indicparler-tts, distinct from the one flash deploy creates), and both had attached themselves to the same network volume. Deleting only the flash deploy endpoint left the volume refusing to delete — “must remove this network volume from all pods before deleting it” — because the flash dev endpoint was still holding it. The IndicWhisper post already flagged this once; this is the second confirmation that it’s a general Flash pattern, not a one-off.

Where it landed

On the frontend, the new engine got its own voice-picker state rather than reusing the existing Kokoro/Cloud voice state — a small but deliberate call, since Compare mode’s Cloud picker already owned that variable and reusing it would have silently corrupted it. Verified end to end: pytest covering the voice registry plus real Tamil and Bengali generation, and a full Playwright pass through the actual Studio page — selected “Prakash · Telugu” from the new picker, generated real audio, zero console errors. Both endpoints and the network volume were torn down immediately after, same cost discipline as always.

The checklist holds up. What changes each time is which assumption in it turns out to be wrong for this particular model.

Five self-hosted models in, the recurring shape keeps holding even as the specific blocker keeps moving. Kokoro broke on a CPU/GPU packaging assumption. Chatterbox broke on hardware generations and RunPod’s own infrastructure. Stable Audio Open broke on a third-party package that wasn’t installable at all. IndicWhisper broke on a stale generation config and an undocumented timeout. This one broke on a gated license behind an Apache 2.0 badge, a hot-reload path that drops environment state, and two packages that have simply never shipped a wheel. None of these show up in a demo notebook — they show up when a model has to survive a cold start, a redeploy, and a teardown, unattended, on someone else’s infrastructure.