Shivaani ASR
Realtime and offline speech recognition for 65 Indian languages, plus live translation across 22 of them. This reference covers every integration path: batch file transcription, two flavors of streaming, and translation — with request/response shapes and working code for each.
Developers integrating Shivaani into an application — a call-center transcript pipeline, a captioning widget, a translation feature. If you're looking for the interactive demo instead, it's on the same host at / and /live.
Quickstart
Transcribe a file in one request:
curl -X POST $SHIVAANI_URL/v1/audio/transcriptions \ -H "Authorization: Bearer $SHIVAANI_API_KEY" \ -F file=@sample.wav
{
"text": "and so my fellow americans ask not what your country can do for you",
"engine": "torchscript",
"duration": 11.0,
"decode_seconds": 0.11,
"rtf": 0.0099,
"words": [{ "word": "and", "start": 0.24, "end": 0.56 }, ...]
}
Need a key? See Authentication. Jump to streaming or translation for the other two use cases.
Base URL & access
https://amd-halo.taila88819.ts.net
Reachable from the open internet — no VPN or network membership needed. The API key below is what actually controls access.
Authentication
Every endpoint under /v1/audio and /v1/translate, plus both streaming WebSocket endpoints, require an API key. /healthz and /v1/translate/languages don't.
| Client type | How to send the key |
|---|---|
| REST (curl, server-side code) | Authorization: Bearer <key> header |
| WebSocket | ?api_key=<key> query parameter — browsers can't set custom headers on a WebSocket handshake, so this is required even from server-side WS clients for consistency. |
There's no self-serve signup yet — keys are issued directly by the Devnagri team. Reach out with what you're building and expected volume.
A missing or invalid key returns 401 on REST endpoints, or a {"type":"error"} event followed by WebSocket close code 4401 on streaming ones.
Rate limits
Limits are enforced per API key, in-process. Build in backoff using Retry-After — a 429 is a normal, expected response under load, not an error condition to alert on.
Response headers
| Header | Sent on | Meaning |
|---|---|---|
X-RateLimit-Limit | Every REST response | The ceiling for this endpoint |
X-RateLimit-Remaining | Every REST response | Requests left in the current window |
Retry-After | 429 responses only | Seconds until the window has room again |
Streaming connections over the cap are refused at the WebSocket handshake with close code 1013 ("try again later") and a JSON error event first.
Errors
REST errors are a JSON body with an error string field and a non-2xx status. WebSocket errors are a {"type": "error", "text": "..."} event sent before the socket closes (or, for a recoverable problem like a bad mid-stream command, without closing).
200Success.400Malformed request — e.g. audio ffmpeg can't decode, or a translate call missingsrc_lang/tgt_lang.401Missing or invalid API key. See Authentication.429Rate limit exceeded. See Rate limits.500Transcription/translation engine raised — check the response body for detail.502The translation microservice didn't respond correctly.503The translation microservice is unreachable (e.g. still warming up after a restart).
POST/v1/audio/transcriptions
Offline batch transcription. Accepts any format ffmpeg can decode — wav, mp3, m4a, webm/opus, whatever your recorder produces. Upload a whole file (or a whole recorded utterance), get back text, word-level timestamps, and optionally speaker labels.
Request
multipart/form-data
| Field | Type | Description | |
|---|---|---|---|
file | required | file | The audio file. |
diarize | optional | bool | Attach speaker labels. Default false. See Speaker diarization. |
engine | optional | "torchscript" | "onnx" | Default "torchscript". See Engine choice. |
Response
| Field | Type | Description |
|---|---|---|
text | string | Full transcript. |
engine | string | Which engine actually served this request. |
duration | number | Audio length, seconds. |
decode_seconds | number | Wall-clock time to transcribe. |
rtf | number | Real-time factor (decode_seconds / duration) — well under 1 is faster than real time. |
words | array | {word, start, end} per word, seconds. Empty when engine="onnx" (see below). |
segments | array | Present only if diarize=true succeeded. {speaker, start, end, text}. |
speakers | array | Present alongside segments. Distinct speaker labels found. |
diarization_error | string | Present instead of segments if diarize=true was requested but the diarization backend couldn't run. |
curl -X POST $SHIVAANI_URL/v1/audio/transcriptions \ -H "Authorization: Bearer $SHIVAANI_API_KEY" \ -F file=@call_recording.wav \ -F diarize=true
import requests with open("call_recording.wav", "rb") as f: r = requests.post( f"{SHIVAANI_URL}/v1/audio/transcriptions", headers={"Authorization": f"Bearer {SHIVAANI_API_KEY}"}, files={"file": f}, data={"diarize": "true"}, ) r.raise_for_status() result = r.json() print(result["text"]) for seg in result.get("segments", []): print(f"{seg['speaker']}: {seg['text']}")
const form = new FormData(); form.append('file', fileInput.files[0]); form.append('diarize', 'true'); const res = await fetch(`${SHIVAANI_URL}/v1/audio/transcriptions`, { method: 'POST', headers: { 'Authorization': `Bearer ${SHIVAANI_API_KEY}` }, body: form, }); const result = await res.json(); console.log(result.text);
Speaker diarization
When diarize=true, speaker turns are computed independently and merged against word timestamps to produce speaker-labeled segments — the transcript itself doesn't change, only how it's split up. If the diarization backend is unavailable, you still get your full text/words back normally; check for diarization_error to detect the degraded case rather than assuming segments is always present when requested.
Engine choice
Two inference backends serve the same offline model, picked with the engine field:
| Engine | Hardware | Word timestamps | Typical use |
|---|---|---|---|
torchscript (default) | GPU | Yes | General use — this is what you want unless you have a specific reason not to. |
onnx | CPU | No (words returns empty) | Running alongside heavy GPU load elsewhere, or comparing engine output. |
Choosing a stream
Both streaming endpoints take the same input — 16 kHz mono, 16-bit PCM, little-endian, sent as binary WebSocket frames of any size — and emit the same basic event shape. They differ in how they get from audio to text, which shows up as an accuracy/latency tradeoff.
/ws/transcribe
VAD-gated offline engine- Higher accuracy — same model as the offline endpoint
- Detects a pause, then re-transcribes the whole utterance
- No lookahead control
/ws/live
Cache-aware streaming engine- True incremental decode — every audio block is processed once
- Selectable lookahead: 0 / 80 / 480 / 1040 ms (latency vs. accuracy)
- Supports live translation — see below
If you're not sure: start with /ws/live at the default 1040ms lookahead. Switch to /ws/transcribe if accuracy matters more than latency, or drop the lookahead if latency matters more than accuracy.
WS/ws/transcribe
Connect to /ws/transcribe?api_key=<key>, then stream raw PCM16 binary frames.
text holds the full, re-transcribed utterance.text holds a message.Send the text frame "close" to end the session cleanly (flushes any in-progress utterance as a final final event first).
WS/ws/live
Connect with query parameters, then stream PCM16 binary frames exactly as above.
| Query param | Description | |
|---|---|---|
api_key | required | See Authentication. |
lookahead | optional | 0 | 80 | 480 | 1040 (ms). Default 1040. Higher = more accurate, more latency. |
source_lang | optional | FLORES code (e.g. hin_Deva). Enables live translation — see Live translation. |
target_lang | optional | FLORES code. Required together with source_lang. |
text is cumulative for the current utterance, not a delta.text is final; translation is present (and non-null) only when both language params were set on connect.set_lookahead command took effect. ms holds the new value.text holds a message.Mid-connection commands
Send a JSON text frame to change lookahead without reconnecting. It applies immediately if no utterance is in progress, or queues until the current one finalizes:
{ "type": "set_lookahead", "ms": 80 }
Language pair is fixed for the life of a connection — reconnect with new source_lang/target_lang values to change it.
Example
// mic capture omitted -- see the demo page's source (/live) for a full // AudioWorklet example that resamples to 16kHz mono PCM16 const ws = new WebSocket( `wss://${host}/ws/live?api_key=${SHIVAANI_API_KEY}&lookahead=480&source_lang=hin_Deva&target_lang=eng_Latn` ); ws.binaryType = 'arraybuffer'; ws.onopen = () => { /* start streaming PCM16 frames via ws.send() */ }; ws.onmessage = (e) => { const ev = JSON.parse(e.data); if (ev.type === 'final') { console.log(ev.text, '->', ev.translation); } };
import asyncio, json, websockets async def stream(pcm_chunks): url = f"{WS_URL}/ws/live?api_key={SHIVAANI_API_KEY}&lookahead=480&source_lang=hin_Deva&target_lang=eng_Latn" async with websockets.connect(url) as ws: async def sender(): for chunk in pcm_chunks: # int16 PCM bytes, 16kHz mono await ws.send(chunk) await ws.send("close") async def receiver(): async for raw in ws: ev = json.loads(raw) if ev["type"] == "final": print(ev["text"], "->", ev.get("translation")) await asyncio.gather(sender(), receiver())
GET/v1/translate/languages
Returns the full set of translation-supported languages as {code: display_name}. Fetch this at runtime rather than hardcoding it — it's the authoritative list.
curl $SHIVAANI_URL/v1/translate/languages
{
"eng_Latn": "English",
"hin_Deva": "Hindi",
"tam_Taml": "Tamil",
"ben_Beng": "Bengali",
// ...30 more (22 scheduled Indic languages + regional variants + English)
}
The ASR models transcribe 65 languages; translation covers 22 Indic languages plus English. Not everything Shivaani can transcribe can be used as a translation source_lang — check against this list before offering a language pair in your UI.
POST/v1/translate
Stateless single-text translation. Model routing (English→Indic, Indic→English, or Indic→Indic) is automatic based on the language codes you pass — you don't choose a model directly.
Request body
| Field | Type | Description | |
|---|---|---|---|
text | required | string | Text to translate. |
src_lang | required | string | FLORES code, e.g. hin_Deva. |
tgt_lang | required | string | FLORES code. |
curl -X POST $SHIVAANI_URL/v1/translate \
-H "Authorization: Bearer $SHIVAANI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Ask not what your country can do for you.",
"src_lang": "eng_Latn",
"tgt_lang": "tam_Taml"
}'
import requests r = requests.post( f"{SHIVAANI_URL}/v1/translate", headers={"Authorization": f"Bearer {SHIVAANI_API_KEY}"}, json={ "text": "Ask not what your country can do for you.", "src_lang": "eng_Latn", "tgt_lang": "tam_Taml", }, ) print(r.json()["text"])
{ "text": "உங்கள் நாடு உங்களுக்கு என்ன செய்ய முடியும் என்று கேட்காதீர்கள்.", "src_lang": "eng_Latn", "tgt_lang": "tam_Taml" }
Live translation (streaming)
For speech-to-translated-text in real time, don't call /v1/translate yourself — pass source_lang and target_lang directly to /ws/live. Each final event then arrives with translation already attached, computed server-side against the committed utterance (never against interim partial text, which would waste compute translating text that's still changing).
GET/healthz
Liveness and model status. Useful for monitoring; not rate-limited.
{
"status": "ok",
"product": "Shivaani ASR",
"device": "cuda",
"sample_rate": 16000,
"models": {
"1.0": { "format": "torchscript", "device": "cuda", "streaming": false },
"1.0-onnx": { "format": "onnx", "device": "cpu" },
"0.5-live": { "format": "torchscript", "device": "cuda", "streaming": true, "lookaheads_ms": [1040,480,80,0] }
}
}
Status & support
For integration questions or to report an issue with this API, reach out to the Devnagri engineering team directly. This document describes the current deployment; endpoints and limits may change as this moves toward general availability.