Streaming
Every message you send and receive on /v1/stream, in the order a session uses them, and how a stream ends.
Open a session #
Connect to /v1/stream with the subprotocol dicon.v1 and send session.create as the first message:
{"type":"session.create","protocol_version":"1.0","mode":"mt_asr","expected_num_speakers":"auto"}protocol_version—"1.0".mode—mt_asr(multi-talker) orts_asr(target speaker).expected_num_speakers—"auto"or an integer, within the engine's limit below.
What a session may ask for depends on the worker's engine. HEAT4, the one served, accepts mt_asr only and 1 to 8 speakers: a session keeps up to eight speakers apart and transcribes at most four at once, and "auto" means up to eight. The dicon engine accepts mt_asr and ts_asr with 1 to 4 speakers. GET /v1/metadata reports the engine as limits.asr_engine and the counts it accepts as limits.expected_num_speakers.
The server replies with session.accepted, carrying the session and request IDs and the initial state. An older two-request form also exists; use /v1/stream for new clients.
Target-speaker enrollment #
In ts_asr (the dicon engine only), send the voice to follow before stream.start. audio_b64 is base64 little-endian signed 16-bit mono PCM; Limits gives its length.
{"type":"enrollment.submit","audio_b64":"... little-endian signed 16-bit mono PCM ..."}Sending audio #
Send {"type":"stream.start"}, then the audio as binary messages of little-endian signed 16-bit mono PCM at 16 kHz. A chunk may end anywhere on a whole sample; empty or odd-byte payloads are rejected. A JSON audio.chunk message with a base64 payload also works, but binary is more compact.
Finish with {"type":"stream.end"}, or cancel with {"type":"session.cancel"}.
Lifecycle #
MT: created -> ready -> streaming -> flushing -> completed
TS: created -> enrolling -> ready -> streaming -> flushing -> completed
any nonterminal -> cancelled | failedServer events #
Every server event carries the session and request IDs and a seq that increases through the session. A speaker's speaker.update comes before that speaker's transcript.
session.statespeaker.update—candidate,admitted,suppressed,target_locked,target_not_foundtranscript.partialtranscript.finalsession.completed— withreason:end_of_stream,cancelled,timeoutorfailederrordrain.notice— the server you are on is being replacedsession.resumed— after a successful resume
After drain.notice the server delivers a terminal event and closes with 1001, reason server draining. It is planned maintenance, not a network failure: finish and reconnect on a new session.
Partial transcripts #
Partials are append-only deltas, not the transcript so far, so they stay small however long the session runs:
{"type":"transcript.partial","speaker":"speaker_0","revision":7,
"stable_prefix_words":42,"appended":["quick","brown"],"open_word":"fo",
"audio_end_s":12.4}stable_prefix_words— how many committed words you should already hold for this speaker. Committed words never change.appended— new committed words; add them to the end.open_word— the one word still being recognised. It replaces the previousopen_word;""means there is none.revision— increases with each partial for this speaker.
If your committed word count does not equal stable_prefix_words, you missed a partial because your client was reading too slowly. Show a gap and wait for transcript.final, which is never dropped and carries the speaker's complete text: replace what you show for that speaker with it. Final text never changes.
Close codes #
1000— normal completion, aftersession.completed.1001, reasonserver draining— the server is being replaced.4400, or1013if retrying may help — a protocol or validation failure.1006— never sent by the server: your client sees it when the connection drops with no close frame, for example on a network failure or a lost server.
There is exactly one terminal event per session. A dropped session cannot be resumed unless the service enables it; start a new session. Resume lists the codes used when reattaching, where 4400 means resume is off.
Two-request form (legacy) #
Older clients create a session over HTTP, then connect to the WebSocket URL it returns. POST /v1/sessions takes:
{
"protocol_version": "1.0",
"mode": "mt_asr",
"audio_format": {"encoding": "pcm_s16le", "sample_rate": 16000, "channels": 1},
"expected_num_speakers": "auto",
"client_request_id": "optional-id"
}client_request_id is optional; Limits gives its format. The response carries session_id, request_id, the initial state and a websocket_url, or a retryable 503 when the service is at capacity.
The WebSocket must reach the same server that created the session, which needs worker affinity that a load-balanced service does not provide. Use /v1/stream instead.
A session_id is not a bearer token: only the key that created a session may attach to it. A refused attach closes 4409 with reason unknown or already connected session, whether the session does not exist, belongs to someone else or is already attached.