Thai ASR API
Streaming Thai speech-to-text over HTTP + WebSocket.
The Thai ASR product is a three-step flow: inspect defaults, create a session, then stream PCM audio to the returned WebSocket URL. The service is Thai-only and currently reports the `typhoon` engine in responses.
/api/thai-asr/configInspect defaults and limits
Returns the current backend name, decode defaults, tenant information, and request/session/audio quota limits for the calling key.
/api/thai-asr/sessionsCreate a streaming session
Returns a fresh `session_id`, the resolved `ws_url`, the accepted config, and current SLA target hints for the low-latency benchmark surface.
/api/thai-asr/streamStream audio and receive transcripts
After the server emits `ready`, send a `start` message, then raw PCM16LE mono audio chunks. Watch `partial`, `stabilized_partial`, and `final` events.
GET /api/thai-asr/config
Samplecurl -sS \
-H "x-api-key: YOUR_API_KEY" \
"https://kaleidovid.com/api/thai-asr/config"Config response example
Sample{
"language": "th",
"mode": "benchmark_spike",
"backend": "persistent_decoder",
"engine": "typhoon",
"defaults": {
"sample_rate": 16000,
"frame_ms": 20,
"stream_chunk_ms": 80,
"partial_interval_ms": 40,
"min_decode_audio_ms": 240,
"decode_window_ms": 1600,
"vad": true
},
"notes": [
"This spike measures websocket, VAD, and partial transcript latency for Thai ASR on the 8x4090 host."
],
"tenant": {
"team_id": "team_cuid",
"team_name": "Acme Team",
"key_prefix": "abcd1234"
},
"limits": {
"requests_per_minute": 120,
"concurrent_sessions": 3,
"daily_audio_seconds": 3600,
"today_audio_seconds_used": 420
}
}Create a session
The session creation payload is JSON. You can omit fields to use the server defaults, but most clients should send the same values they expect to use on the WebSocket start message so there is no mismatch between planning and runtime.
| Field | Type | Required | Notes |
|---|---|---|---|
sample_rate | integer | No | 8,000 to 48,000. Use 16,000 for the current Thai ASR defaults. |
frame_ms | integer | No | 10 to 200. The built-in smoke test uses 20 ms. |
partial_interval_ms | integer | No | 20 to 1,000. Controls how often the service tries to emit new partials. |
min_decode_audio_ms | integer | No | 100 to 5,000. Minimum buffered audio before partial decoding starts. |
decode_window_ms | integer | No | 200 to 15,000. Rolling audio window used for decode requests. |
vad | boolean | No | Defaults to true. Emits `speech_start` when voice activity is detected. |
benchmark_label | string | No | Optional label up to 200 characters. |
POST /api/thai-asr/sessions
Samplecurl -sS \
-X POST \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"sample_rate": 16000,
"frame_ms": 20,
"partial_interval_ms": 40,
"min_decode_audio_ms": 240,
"decode_window_ms": 1600,
"vad": true
}' \
"https://kaleidovid.com/api/thai-asr/sessions"Session response example
Sample{
"session_id": "e6a4c4f6c69f4ca1aa8f9b3ad8d515f0",
"mode": "benchmark_spike",
"language": "th",
"engine": "typhoon",
"backend": "persistent_decoder",
"ws_url": "wss://kaleidovid.com/api/thai-asr/stream?session_id=e6a4c4f6c69f4ca1aa8f9b3ad8d515f0",
"config": {
"sample_rate": 16000,
"frame_ms": 20,
"partial_interval_ms": 40,
"min_decode_audio_ms": 240,
"decode_window_ms": 1600,
"vad": true,
"benchmark_label": null
},
"sla_targets": {
"speech_detected_p95_ms": 60,
"first_partial_p95_ms": 250,
"final_after_eou_p95_ms": 800
},
"tenant": {
"team_id": "team_cuid",
"team_name": "Acme Team",
"key_prefix": "abcd1234"
}
}WebSocket flow
Use the exact ws_url returned by session creation. Browser clients typically append api_key to that URL because custom WebSocket headers are harder to set in the browser. Binary audio frames are the preferred transport.
stabilized_partial when available, otherwise fall back to partial. Only persist the transcript after receiving final.Client messages
| Field | Type | Required | Notes |
|---|---|---|---|
start | JSON message | Yes | Send once after the server emits `ready`. Carries the same config fields used in session creation. |
binary audio frame | raw bytes | Yes | Recommended path. Send PCM16LE mono audio bytes only, without WAV headers. |
audio | JSON message | No | Fallback JSON shape: `{ "type": "audio", "audio_b64": "..." }` where the payload is base64-encoded PCM16LE audio. |
flush | JSON message | No | Forces the service to emit a best-effort partial from current buffered audio. |
end / end_utterance | JSON message | Yes | Finalizes the utterance and triggers the `final` event. |
reset | JSON message | No | Clears buffered state so another utterance can run on the same socket. |
Server events
| Field | Type | Required | Notes |
|---|---|---|---|
ready | server event | Always | First event on a successful connection. Includes team info, backend, limits, and sample rate. |
started | server event | Always | Confirms the stream is configured and ready for audio frames. |
speech_start | server event | Optional | Emitted when VAD first detects speech. Includes `offset_ms` and `latency_ms`. |
partial | server event | Optional | Best-effort rolling transcript. Includes `text`, `segments`, `audio_ms`, `latency_ms`, `queue_ms`, and `decode_ms`. |
stabilized_partial | server event | Optional | A partial that repeated enough times to look stable. Use this for live captions when available. |
final | server event | Always on success | Final transcript with segments and latency metrics. Persist only this event in production workflows. |
error | server event | On failure | Carries `code` and `error` for issues such as timeout, decode failures, or stream failures. |
reset_ok | server event | Optional | Acknowledges a successful `reset` command. |
JavaScript integration example
Sampleconst API_KEY = "YOUR_API_KEY";
const HTTP_BASE = "https://kaleidovid.com";
const config = await fetch(`${HTTP_BASE}/api/thai-asr/config`, {
headers: { "x-api-key": API_KEY },
}).then(async (response) => {
if (!response.ok) throw new Error(await response.text());
return response.json();
});
const session = await fetch(`${HTTP_BASE}/api/thai-asr/sessions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY,
},
body: JSON.stringify({
sample_rate: 16000,
frame_ms: 20,
partial_interval_ms: 40,
min_decode_audio_ms: 240,
decode_window_ms: 1600,
vad: true,
}),
}).then(async (response) => {
if (!response.ok) throw new Error(await response.text());
return response.json();
});
const wsUrl = `${session.ws_url}&api_key=${encodeURIComponent(API_KEY)}`;
const socket = new WebSocket(wsUrl);
socket.onmessage = (event) => {
const payload = JSON.parse(event.data);
switch (payload.type) {
case "ready":
socket.send(JSON.stringify({
type: "start",
sample_rate: 16000,
frame_ms: 20,
partial_interval_ms: 40,
min_decode_audio_ms: 240,
decode_window_ms: 1600,
vad: true,
}));
break;
case "partial":
case "stabilized_partial":
console.log("live", payload.text);
break;
case "final":
console.log("final", payload.text, payload.segments);
break;
case "error":
console.error(payload.code, payload.error);
break;
}
};
// `pcmChunks` must contain raw PCM16LE mono audio frames.
// Do not send WAV headers after the socket is started.
for (const chunk of pcmChunks) {
socket.send(chunk);
}
socket.send(JSON.stringify({ type: "flush" }));
socket.send(JSON.stringify({ type: "end" }));`final` event example
Sample{
"type": "final",
"session_id": "e6a4c4f6c69f4ca1aa8f9b3ad8d515f0",
"engine": "typhoon",
"backend": "persistent_decoder",
"text": "สวัสดีครับ นี่คือการทดสอบระบบถอดเสียงภาษาไทยแบบหน่วงต่ำ",
"segments": [
{
"start": 0.0,
"end": 2.84,
"text": "สวัสดีครับ นี่คือการทดสอบระบบถอดเสียงภาษาไทยแบบหน่วงต่ำ"
}
],
"audio_ms": 2840,
"latency_ms": 134,
"queue_ms": 0,
"decode_ms": 134,
"processing_time_ms": 133,
"audio_duration_ms": 2840
}