Streaming guide¶
Both transports start sending audio after the first 80 ms frame is decoded. The difference is control: REST is one request in, one byte stream out; the WebSocket keeps a connection open for many utterances and tells you what is happening as it happens.
POST /v1/tts (chunked REST) |
WS /v1/tts/stream |
|
|---|---|---|
| Connection | one per request | one per conversation, utterances in order |
| Audio | body bytes in the chosen container | binary messages (pcm16 or Ogg/Opus pages) |
| Timing info | x-request-id header; TTFA in your portal usage |
start, chunk, first_audio_ms, chunk_end, end events with ttfa_ms, rtf, audio_s |
| Normalised text | not returned (use /v1/normalize) |
in the start event |
| Capacity refusal | 503 + Retry-After before any audio |
error event with status: 503, retry_after; socket stays open |
| Cancel | close the connection | close the socket (or stop sending) |
| Best for | files, server pipelines, simple integrations, serverless | voice agents, IVR, anything with a human waiting |
Time to first audio, honestly¶
Three numbers matter, and the portal playground shows all of them:
- Engine TTFA (
ttfa_msinend, and in Usage): from a worker picking the job up to the first decoded frame. ~18 ms warm with a cached text prefill, ~55–80 ms when the chunk's prefill is cold, on a Sapphire Rapids vCPU in Mumbai. - Server TTFA (
first_audio_mson the WebSocket): from the request message to the first audio message, including queueing. - Your TTFA: what your user hears, adding the network. Mumbai to Mumbai is a few ms; cross-country India ~30–60 ms; add TLS and connection setup for REST unless you keep connections alive.
Repeated text is fast: the server caches the text prefill per (voice, chunk). An IVR line that plays 10 000 times pays the cold cost once.
WebSocket event timeline¶
→ {"text": "...", "voice": "anjura", "format": "pcm16", "request_id": "turn-7"}
← {"type":"start","request_id":"turn-7","voice":"anjura","format":"pcm16","chunks":2,"normalized":"..."}
← {"type":"chunk","request_id":"turn-7","chunk":0,"text":"...","tokens":31}
← <binary: 3840 bytes = 80 ms of pcm16> ← first audio
← {"type":"first_audio_ms","request_id":"turn-7","chunk":0,"ms":44.2}
← <binary …> <binary …> ← ~320 ms of audio per message after the first
← {"type":"chunk_end","request_id":"turn-7","chunk":0,"frames":39,"audio_s":3.12,"prefill_ms":0.1,"prefill_cached":true,"eos":true}
← {"type":"chunk","request_id":"turn-7","chunk":1,"text":"...","tokens":18}
← <binary …>
← {"type":"chunk_end","request_id":"turn-7","chunk":1, …}
← {"type":"end","request_id":"turn-7","audio_s":4.9,"frames":61,"chunks":2,"ttfa_ms":17.8,"wall_ms":690.3,"rtf":0.141}
Send several messages back to back; they are served in order on that connection, and every event carries the request_id you gave
(or a generated one), so a client can correlate. Keep one connection per conversation; open a second connection if you need two things
spoken in parallel (each connection uses one worker at a time).
Playback: buffer a little, not a lot¶
- Audio messages after the first carry about 320 ms of speech each and arrive faster than real time (RTF ≈ 0.14–0.19), so the buffer only grows. Start playback when you have 100–200 ms buffered; that covers network jitter without adding noticeable delay.
pcm16is 16-bit signed little-endian, 24 kHz, mono: 48 000 bytes per second. Schedule consecutive buffers back to back (Web Audio:source.start(nextTime); the browser snippet in the quickstart does this).- For telephony, resample 24 kHz → 8 kHz and encode to μ-law/A-law on your side, or ask for
opuswhere the carrier supports it.
Sentence by sentence from an LLM¶
Send each sentence as soon as the model finishes it, as its own message; do not wait for the whole reply. The normaliser puts a
terminal mark on every utterance and the chunker splits anything over ~50 tokens, so sentence-sized messages give the lowest latency and
the most natural prosody. Give each message a request_id so you can drop audio for a turn that was interrupted.
Barge-in and cancellation¶
When the user starts talking, stop playing and close the WebSocket (or the REST connection): synthesis stops within one frame and you are billed only for audio already produced. Open a fresh connection for the next turn; a TLS handshake is far cheaper than finishing a sentence nobody hears. On the WebSocket you cannot cancel one utterance and keep the next on the same connection: closing cancels the one in flight and discards anything queued behind it, which is what barge-in wants.
Reconnects¶
| Close code / condition | Meaning | Do |
|---|---|---|
1000 |
you closed | nothing |
1001, 1006 |
server restart or network drop | reconnect with backoff (100 ms, 200, 400 …) |
error event status: 503 |
capacity exhausted for that utterance | retry that utterance after retry_after s; keep the socket |
error event with status: 400/413 |
bad text for that utterance | fix and resend; keep the socket |
error event timeout |
120 s request limit hit | split the text |
A server shutting down for a deploy finishes in-flight streams first (SIGTERM is graceful), so a 1001 normally arrives between
utterances.
REST tips¶
- Keep connections alive (HTTP/1.1 keep-alive or HTTP/2) to avoid a TLS handshake per line.
- Read the body incrementally; don't wait for the end if the audio is for a human.
"stream": falsewhen you need a WAV with a correct length (e.g. uploading to a telephony provider that validates headers).- The
x-request-idheader is set from yourrequest_idor generated; it appears in your usage log and in support conversations.