← ForeA Technologies Blog index
Field Notes · OpenAudio Suite

Self-Hosting Transcription with IndicWhisper

Every other engine had gone self-hosted. Transcription was still quietly calling OpenAI. Closing that gap meant a model that isn’t on HuggingFace, a generation config from before timestamp support existed, and a RunPod timeout that only bites when the cold start runs long.

Shipped & verified · IndicWhisper (AI4Bharat), MIT · RunPod Flash

By the time Kokoro, Chatterbox, and Stable Audio Open had shipped, three of OpenAudio Suite’s four AI capabilities were self-hosted. The fourth wasn’t. /api/transcribe and /api/voices/analyze were both still handing audio straight to openai/whisper-1 through OpenRouter — a paid, hosted call, sitting right next to three engines that had spent real effort moving off exactly that pattern. Whisper’s architecture is open. What we were calling wasn’t.

The fix that closed this gap also happened to close a second one. IndicWhisper (AI4Bharat, IIT Madras) is a Whisper checkpoint fine-tuned specifically on Indian languages, and its license — MIT, explicitly stated to cover the fine-tuned weights, not just the training code — put it in rare company after a licensing scan that had ruled out most of the well-known open audio models over the preceding weeks. One model, two gaps closed. It just didn’t deploy the way the plan assumed it would.

The checkpoint that isn’t on HuggingFace

The original plan, written before any of this was checked, assumed faster-whisper or whisper.cpp loading a HuggingFace checkpoint — the same shape as every other self-hosted model so far. Reading AI4Bharat’s actual repository turned up a maintainer comment that changed the plan before a line of worker code was written:

We have not uploaded the model checkpoints to huggingface so you would need to download the model and provide the model path instead.

The real checkpoints are direct zip downloads from AI4Bharat’s own object store, one file per language. Hindi’s is 4.26GB. And the format inside is a standard transformers-pipeline checkpoint, not CTranslate2 — so faster-whisper would have needed an unverified conversion step for no clear benefit. The actual plan became: load it with transformers.pipeline(), exactly the way AI4Bharat’s own inference snippet does it, and cache the unzipped checkpoint on a RunPod network volume so the 4.3GB download only happens once.

A generation config from before timestamps existed

The checkpoint downloaded, extracted, and loaded cleanly — a good sign that turned out to be premature. The first real transcription request failed immediately:

ValueError: You are trying to return timestamps, but the generation config is not properly set. Make sure to initialize the generation config with the correct attributes that are needed such as `no_timestamps_token_id`.

This IndicWhisper checkpoint predates the generation-config fields transformers now requires for return_timestamps=True — a known gap for older or custom Whisper fine-tunes, not specific to this model.

Fix

Replace the pipeline’s generation_config with a fresh one borrowed from the base model IndicWhisper was fine-tuned from (GenerationConfig.from_pretrained("openai/whisper-medium")), then reapply Hindi’s forced_decoder_ids on top of that borrowed config — not on model.config, which is where the original code had set it and where newer transformers no longer looks for it at generation time.

RunPod’s undocumented-outside-a-skill 60 second ceiling

With the pipeline fixed, deploying for real (flash deploy, not just flash dev) surfaced a second failure — on both the fallback test and the live test, every time:

{"detail":"Transcription failed: RunPod job did not complete: {'id': 'sync-d2db19b5...', 'status': 'IN_QUEUE'}"}

The client code called RunPod’s /runsync endpoint with a generous 1800-second timeout on the HTTP request itself. That timeout was irrelevant. /runsync has its own hard 60-second ceiling on RunPod’s side, enforced regardless of what the caller passes — and IndicWhisper’s cold start, mostly spent pulling the container image, had already measured at roughly 104 seconds on its own during earlier testing. No client-side timeout setting could fix a server-side one.

Fix

Switch to async /run plus a poll loop against /status/{job_id}, instead of /runsync. Every prior model’s cold start happened to land under 60 seconds, which is exactly why this hadn’t come up yet — it isn’t a bug in the other three, it’s a limit that only bites once a cold start runs long enough to hit it.

Closing the loop on itself

Testing self-hosted Hindi transcription needs Hindi audio, and getting some turned out to be free: Kokoro had picked up four unused Hindi voices in the same multilingual push that motivated this model, shipped a session earlier as a one-line catalog fix. Generating a clip with Kokoro’s hf_alpha voice and feeding it straight into IndicWhisper meant the whole test was self-contained — no external sample, no manual recording, just one shipped feature verifying the next one.

→ नमस्ते, आज मौसम बहुत अच्छा है। (spoken by Kokoro) ← नमस्ते आज मौसम बहुत अच्छा है (transcribed by IndicWhisper, 4.8s once warm)

Where it landed

stt_indicwhisper.py follows the RunPod-only pattern from Chatterbox — no CPU fallback, since a ~769M-parameter whisper-medium fine-tune isn’t CPU-viable the way Kokoro’s 82M parameters are. /api/transcribe gained a language parameter: hi routes to IndicWhisper when the endpoint is configured, and falls back to the existing OpenRouter path otherwise — the same graceful-degradation shape as every other engine in this stack, never a hard failure just because a self-hosted option isn’t live right now.

Verified end to end: the pytest suite green in both configured and unconfigured states, the Kokoro-to-IndicWhisper round trip above, and a full browser pass through the actual Transcribe page — upload, Hindi toggle, a correct transcript with the right language badge and timestamps, zero console errors. And, predictably by now, flash dev had again left a second real billing endpoint running that needed deleting separately — the same gotcha first found deploying Kokoro, still worth checking for every single time. Both endpoints and the network volume were torn down immediately after verification, same as every model before it.

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

Four models into this stack, the shape of the work has stopped changing even as the specific bugs keep being different ones. 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 simply wasn’t installable. This one broke on a stale generation config and an undocumented timeout — neither of which a demo script calling the model directly would ever have hit, because a demo script doesn’t need to survive a cold start on someone else’s infrastructure.