Chinda ASR Realtime
Chinda ASR Realtime writes Thai speech down while people are still speaking, for live captions, call centres and voice input. Stream audio over a WebSocket: the sentence so far comes back about every 0.7 seconds, and each finished sentence a median 0.66 seconds after the speaker pauses, 90% of them within 1 second. Free during the launch.
A recording? Use Chinda ASR Pro v2: it is more accurate, labels each speaker, takes a list of names and up to 8 hours a file. Need the text while people are talking? Use Realtime. On the TVSpeech test set, Realtime gets 12.95% of characters wrong; Pro v2 gets 7.28% wrong.
/v3/store/speech/speech-to-text/realtime-v2Try it- PriceFreeduring the launch
- Served130.9Kcalls
Connect
Open wss://api.iapp.co.th/v3/store/speech/speech-to-text/realtime-v2 with your API key in the apikey header. A browser cannot set headers on a WebSocket, so a web page may put the key in the URL as ?apikey=YOUR_API_KEY; connect from your server where you can, since a key in a web page can be copied.
- Send one JSON text message with the settings below. Every field is optional.
- The server answers
{"type": "ready", "session_id": "..."}. - Send the audio as binary messages: 16-bit little-endian PCM, mono, 16 kHz, up to 1 second each (100 to 200 ms is a good size), at the speed it is recorded.
- Read
partialandfinalmessages while you send. - Send the text message
end. The server sends the last finals, thendone, and closes the connection.
language_codestringth, the default and the only language.sample_rateinteger16000, the default. Other rates are refused: resample on your side.encodingstringpcm_s16le, the default: 16-bit little-endian PCM, mono.latency_msinteger80, 320, 560 (the default) or 1120. Less is sooner and a little less accurate.end_silence_msinteger{"language_code": "th", "sample_rate": 16000, "encoding": "pcm_s16le", "latency_ms": 560, "end_silence_ms": 700}
Messages
partialobjectThe sentence so far. It changes as more audio arrives: show it, then replace it
partial.utteranceintegerpartial.textstringpartial.startnumberfinalobjectThe finished sentence. It does not change again
final.utterancefinal.textfinal.startpartialfinal.endnumberfinal.modelstringchinda-asr-realtime-v1doneobjectAfter end, the last message before the close
done.audio_secondsnumberdone.utterancesintegerA sentence ends when the speaker pauses for end_silence_ms, or after 30 seconds of speech without a pause. The messages below are the first sentence of the page's example recording, streamed on 9 November 2026.
{"type": "partial", "utterance": 0, "text": "สวัสดีค่ะชั้น", "start": 0.0}
{"type": "final", "utterance": 0, "text": "สวัสดีค่ะชั้นเสียงผู้ใหญ่ค่ะ", "start": 0.0, "end": 2.68, "model": "chinda-asr-realtime-v1"}
{"type": "done", "audio_seconds": 7.629, "utterances": 2}
Errors
An error arrives as {"type": "error", "code": "...", "message": "..."}. The connection then closes, except after BAD_AUDIO and BAD_MESSAGE. Opening the WebSocket without a valid key gets HTTP 401 before the upgrade.
| Code | When |
|---|---|
BAD_CONFIG | The first message is not valid JSON, or a setting has a value not listed above |
BAD_AUDIO | A binary message with an odd number of bytes, or more than 1 second of audio; the message is skipped |
BAD_MESSAGE | A text message other than end |
TIMEOUT | No settings within 30 seconds of opening, or no audio for 30 seconds; finals for the audio already sent come first |
TOO_LONG | The session reached 4 hours of audio |
TOO_FAST | More than 2 minutes of audio is waiting: it is being sent much faster than real time. For a recording, use Pro v2 |
BUSY | Every live session is in use. Retry after a few seconds |
ASR_FAILED | An internal error |
Example
In Python, streaming a 16 kHz mono file at the speed it was recorded:
import asyncio, json
import numpy as np, soundfile as sf, websockets
async def main(path, key):
audio, sr = sf.read(path, dtype="float32") # a 16 kHz mono file, for the example
pcm = (np.clip(audio, -1, 1) * 32767).astype("<i2").tobytes()
url = "wss://api.iapp.co.th/v3/store/speech/speech-to-text/realtime-v2"
async with websockets.connect(url, additional_headers={"apikey": key}) as ws:
await ws.send(json.dumps({"language_code": "th"}))
print(await ws.recv()) # ready
async def send():
step = 3200 # 100 ms of 16-bit audio
for i in range(0, len(pcm), step):
await ws.send(pcm[i:i + step])
await asyncio.sleep(0.1) # real time
await ws.send("end")
async def receive():
async for msg in ws:
ev = json.loads(msg)
if ev["type"] == "final":
print(f'{ev["start"]:7.2f}-{ev["end"]:7.2f} {ev["text"]}')
elif ev["type"] == "done":
break
await asyncio.gather(send(), receive())
asyncio.run(main("meeting.wav", "YOUR_API_KEY"))
Details
Measured
On the production service, 16 live sessions streaming at real time, 9 November 2026. Character error rate, lower is better.
| Measure | Result |
|---|---|
| The sentence so far | About every 0.7 s while someone speaks; the first about 1.1 s after speech starts |
| The finished sentence | A median 0.66 s after the speaker stops; 90% within 1 s |
| TVSpeech, 570 clips, streamed | 12.95% (the same model on each whole clip: 12.41%; Pro v2: 7.28%) |
| 10-minute recordings, streamed | 7.99% (the same model on each whole recording: 8.72%) |
| Live sessions at once | Up to 20 across all customers at the launch; more get BUSY |
Streamed text scores close to the same model reading each whole recording, so writing early costs little. Pro v2 is the more accurate model: send recordings to Pro v2.
Limits
- Thai speech, as 16 kHz 16-bit mono PCM.
- Up to 4 hours of audio a session.
- Audio at the speed it is recorded: more than 2 minutes waiting gets
TOO_FAST. - Not for live streams yet: a list of names to favour. It works for recordings on Pro v2.
- English words come back in Thai script.
Older endpoints
The earlier real-time endpoints, wss://api.iapp.co.th/v3/store/speech/speech-to-text/realtime, /asrx and /v1/audio/stt/rt, run on Chinda ASR Realtime since 9 November 2026 and keep the protocol they had: SegmentResult messages with is_final, then {"action": "stop"} and one SessionResult. They are free during the launch too. Two things changed:
- Thai only:
transcribe_langenorzhgets an error message, and the connection closes. - The speaker fields are empty.
New integrations should use realtime-v2, as described on this page.
Data handling
Consent and notice under PDPA and GDPR remain with you as the data controller.