SpeechRelay Docs

Streaming

The production create-and-stream path and the stream protocol.

Create a session on the stream

Connect to /v1/stream with subprotocol dicon.v1, then make the first message:

{"type":"session.create","protocol_version":"1.0","mode":"mt_asr","expected_num_speakers":"auto"}

The server replies with session.accepted, including stable session and request IDs and the initial lifecycle state. Creation and all mutable streaming state share one connection, so a multi-worker load balancer needs no affinity between an HTTP request and a later WebSocket handshake. This is the production and example-client path; the legacy per-session route /v1/sessions/{session_id}/stream also exists.

Message fields

Target-speaker enrollment

For TS-ASR, first send the enrollment audio:

{"type":"enrollment.submit","audio_b64":"... little-endian signed 16-bit mono PCM ..."}

The audio_b64 payload is little-endian signed 16-bit mono PCM.

Sending audio

Send {"type":"stream.start"}, then send audio. The efficient form is binary messages of little-endian signed 16-bit mono PCM at 16 kHz; a JSON audio.chunk message with a base64 payload is available for compatibility. Arbitrary complete-sample chunk boundaries are accepted; empty or odd-byte payloads are rejected.

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 | failed

Server events

Every server event carries the session and request IDs and a monotonically increasing session seq. Speaker discovery or update precedes that row's transcript. The events are:

Partial transcripts

Partials are append-only deltas, not the transcript so far, so their size does not grow with session length:

{"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 is how many committed words you should already hold; append appended to them. RNNT greedy output is monotonic, so a committed word never revises. open_word is the single in-flight word and replaces whatever open_word you were last given ("" means there is none). revision increases per speaker.

Missed partials, finals, disconnects

If your committed word count does not equal stable_prefix_words, you missed a partial under backpressure: render a gap and wait for transcript.final, which always carries that speaker's complete text and is never dropped. The server merges rather than discards partials wherever it can, so a gap means the connection was already flagged as a slow consumer.

Final text never revises, and there is exactly one terminal event. By default a disconnected session cannot be resumed; create a new session.