Shivaani ASR — API Reference
v1 Operational

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.

Who this is for

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 typeHow 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.
Getting a key

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.

60requests / 60sPOST /v1/audio/transcriptions
120requests / 60sPOST /v1/translate
3concurrent connections/ws/transcribe + /ws/live combined

Response headers

HeaderSent onMeaning
X-RateLimit-LimitEvery REST responseThe ceiling for this endpoint
X-RateLimit-RemainingEvery REST responseRequests left in the current window
Retry-After429 responses onlySeconds 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 missing src_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

FieldTypeDescription
filerequiredfileThe audio file.
diarizeoptionalboolAttach speaker labels. Default false. See Speaker diarization.
engineoptional"torchscript" | "onnx"Default "torchscript". See Engine choice.

Response

FieldTypeDescription
textstringFull transcript.
enginestringWhich engine actually served this request.
durationnumberAudio length, seconds.
decode_secondsnumberWall-clock time to transcribe.
rtfnumberReal-time factor (decode_seconds / duration) — well under 1 is faster than real time.
wordsarray{word, start, end} per word, seconds. Empty when engine="onnx" (see below).
segmentsarrayPresent only if diarize=true succeeded. {speaker, start, end, text}.
speakersarrayPresent alongside segments. Distinct speaker labels found.
diarization_errorstringPresent 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:

EngineHardwareWord timestampsTypical use
torchscript (default)GPUYesGeneral use — this is what you want unless you have a specific reason not to.
onnxCPUNo (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.

speech_startVoice activity detected; a new utterance has begun.
finalA pause was detected. text holds the full, re-transcribed utterance.
errorSomething went wrong server-side; 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 paramDescription
api_keyrequiredSee Authentication.
lookaheadoptional0 | 80 | 480 | 1040 (ms). Default 1040. Higher = more accurate, more latency.
source_langoptionalFLORES code (e.g. hin_Deva). Enables live translation — see Live translation.
target_langoptionalFLORES code. Required together with source_lang.
speech_startVoice activity detected.
partialInterim transcript, updates continuously while speech is ongoing. text is cumulative for the current utterance, not a delta.
finalUtterance committed. text is final; translation is present (and non-null) only when both language params were set on connect.
lookahead_changedConfirms a set_lookahead command took effect. ms holds the new value.
errorSomething went wrong; 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)
}
Narrower than transcription

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

FieldTypeDescription
textrequiredstringText to translate.
src_langrequiredstringFLORES code, e.g. hin_Deva.
tgt_langrequiredstringFLORES 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.