# OpenThai2.0 ASR Streaming service: reference Install, start and example calls: see the main [README](../README.md). ## Endpoints | endpoint | what it does | |---|---| | `POST /v1/transcribe` | multipart `file` + the fields below → JSON | | `WS /v1/live` | a live stream of any length: JSON config → PCM16 LE 16 kHz mono frames → text `end`. Replies `partial` (the sentence so far) and one `final` each time the speaker pauses, then `done` | | `WS /v1/stream` | one utterance: JSON config → frames → `end`; replies `partial`… then one `final` | | `GET /v1/health` | loaded model and settings | | field | values | default | |---|---|---| | `mode` | `standard` · `conversation` · `meeting` | `standard` | | `lang` | `th-TH` · `th-mix` | `th-TH` | | `latency_ms` | 80 · 320 · 560 · 1120 | 560 | | `diarize` | `true` · `false` | on for `meeting` | | `split` (files) | `auto` · `on` · `off` | `auto`: cut files longer than 30 s at pauses | | `end_silence_ms` (`/v1/live`) | 300–3000 | 700 | **Response fields:** - `text` (raw, as scored) and `display_text` (for display). - `duration_s`, `lang`, `latency_ms`, `mode`, `policy`, `delta`, `model`, `processing_s`. - A file cut at pauses also has `segments` `[{start, end, text}]`, one per piece. - With speaker labels, also `segments` `[{speaker, start, end, text}]` and `speakers`. **Errors:** 400 empty file · 415 unreadable audio · 422 bad option. ## What each mode does The blank bias δ controls how readily the model writes something: the more negative it is, the fewer words get dropped. | mode | decode | |---|---| | `standard` | δ −1.0; clips under 2 s re-decoded at −2.5; empty output re-decoded at −4.0 | | `conversation` | δ −2.0; empty output re-decoded at −4.0 | | `meeting` | δ −1.0 + speaker labels from `nvidia/Nemotron-3-Diarization` (downloaded on first use) | ## Environment variables | var | default | meaning | |---|---|---| | `CHINDA_DEVICE` | `cpu` | `cuda` for GPU | | `CHINDA_MODEL` | `../openthai2.0-asr-streaming_raw.nemo` | must be the raw file; `*_bias-*` files are refused (the bias would apply twice) | | `CHINDA_PRELOAD` | unset | `1` = load at start-up; otherwise the first request waits about 20 s | | `CHINDA_REPLICAS` | `1` | model copies, about 2.6 GB each; each copy runs one decode step at a time | | `CHINDA_NUM_THREADS` | min(16, CPUs); 1 on macOS | CPU threads. On Apple silicon each extra thread makes decoding slower, so macOS uses 1 | | `CHINDA_DIAR_MODEL` | download | path to a local `Nemotron-3-Diarization.nemo` | | `CHINDA_CUDA_GRAPHS` | `0` | leave at 0: `1` crashes on the request after speaker labels | | `CHINDA_SPLIT_FROM_S` | `30` | `split=auto` cuts files longer than this many seconds | ## Long audio Do not decode a long recording as one stream. After a pause of near-silence this model drops some of the speech that follows: TVSpeech's videos, rebuilt by joining their clips with one second of silence, score 23.5% character errors as one stream and 12.7% cut at pauses (12.4% clip by clip). So: - `POST /v1/transcribe` cuts any file longer than 30 s where nobody speaks for 0.7 s, and decodes the pieces on their own (`split=auto`). Speaker labels (`mode=meeting`) still decode one stream. - `WS /v1/live` ends an utterance at each pause and starts the next one fresh, like iApp's hosted OpenThai ASR Realtime. - `WS /v1/stream` decodes everything you send as one utterance; keep it for short commands and single sentences. - In Python: `chinda_realtime.transcribe_long(asr, audio)` for recordings, `chinda_realtime.RealtimeSession` for live audio. The pause detector is an energy rule (`chinda_realtime.Endpointer`): a 20 ms frame is speech when it is louder than both the loudest recent level − 30 dB and the quietest recent level + 8 dB. Stretches with no speech are skipped. ## Good to know - No authentication or rate limiting. - With `diarize` on `/v1/stream`, speaker labels arrive with the `final` message only; `/v1/live` has no speaker labels. Speakers are anonymous (`speaker_0`, …). - Live text equals decoding the finished utterance on its own. Batch decoding (the benchmark) can differ by the last word on clips under 2 s. - With `ffmpeg` on the PATH, the API also accepts mp3, m4a and ogg. - The decode loop is adapted from NVIDIA NeMo's `speech_to_text_cache_aware_streaming_infer.py` (Apache-2.0).