Overview
Text streams in and audio streams out, over one persistent session. This is built for real-time, agent-driven applications where the text does not exist all at once — for example, arriving token-by-token from an LLM. You configure the voice once, then push text as it becomes available and receive audio chunks back as they are generated.The server buffers and batches incoming text intelligently before dispatching it to TTS workers, so you can send anything from a full sentence to a single token per message without worrying about segmentation.
This is a distinct endpoint (
/ws) from the Streaming Out API (/open). If your text is already complete when you call, that endpoint is simpler — use this one only when text has to stream in.Connection
Connect to the/ws endpoint with your API key:
Authentication is handled during the WebSocket handshake via the
x-api-key header or an x-api-key query parameter.
On a successful connection, the server sends a welcome message before you send anything:
Protocol flow
A typical session follows this sequence:1
Connect
Open the WebSocket with your
x-api-key and wait for the status / connected welcome message.2
Configure
Send a
stream-config message with at least a model. This must be sent before any text.3
Stream text
Send one or more
stream-text messages as text becomes available. Audio chunks stream back as JSON messages.4
End or cancel
Send
end-stream to flush and finish the current turn, or cancel to abort in-flight generation and clear buffers.Message format
Every message is a JSON object with anaction field. Request payloads can be placed in either a data or a config object — both are accepted interchangeably.
Client actions
stream-config
Sets the session-level TTS configuration. Must be sent before any stream-text message, otherwise the server responds with an error:
stream-config produces no reply — only failures are reported.
Example
Parameters
string
required
Model ID to use for generation (e.g.,
dd-etts-3.0). Only required field.string
UUID of the voice/emotion prompt that controls which voice the engine uses. Can be changed mid-stream via inline config tags.
string
Base64-encoded WAV to clone a voice inline, as an alternative to
voicePromptId. Expects mono 48 kHz audio, up to 1 MB.string
Language/locale code for the generated speech (e.g.,
en-US, he-IL, es-MX). Determines the language model and pronunciation rules. Can be changed mid-stream via inline config tags.string
default:"wav"
Output audio format:
wav (default), mp3, opus, mulaw, or s16le. Unlike the Streaming Out API, this endpoint does not reject unknown values — an unrecognized format silently produces wav, so check your spelling.For low-latency playback, s16le and mulaw avoid the container and decoder overhead of wav and mp3.integer
Output sample rate in Hz —
8000, 16000, 22050, 24000, 32000, 36000, 44100, or 48000. Internal generation is 48 kHz and is resampled to the requested rate. mulaw defaults to 8000 Hz; other formats default to 48 kHz.boolean
default:"true"
Prioritize low latency.
true gives the session real-time processing priority; false uses standard priority.boolean
default:"true"
Apply audio cleanup processing.
string
Target speaker gender,
male or female. Used for language-specific handling such as Hebrew diacritics.boolean
default:"false"
Return the diacritized (menukad) form of the synthesized Hebrew text alongside the audio. When enabled, the first audio chunk of each generation carries a
diacritized field. Only an explicit true opts in — sending false behaves the same as omitting the field.boolean
default:"true"
Whether Deepdub may record this session’s text in its server-side logs. Set to
false for confidential scripts to keep the text out of the logs. Applies to the whole connection.integer
default:"50"
Maximum time in milliseconds to wait for more text before flushing the first buffered segment to a worker. Lower values reduce time-to-first-audio.
boolean
default:"false"
Interpret a set of emoji in the streamed text as inline locale and emotion changes instead of speaking them. See Emoji tags.
outputDiacritized is available on request rather than enabled for every account. Contact support@deepdub.ai to have it turned on before integrating against it.stream-text
Sends text to be synthesized. The server buffers and batches text before dispatching it to workers, so messages can be as small as a single token.
text field is also accepted:
string
required
The text (or text fragment) to synthesize.
string
Optional opaque blob forwarded to the TTS worker alongside the next flush. It is not echoed back on any response frame — use
generationId to correlate audio with a generation.Inline config changes
locale and voicePromptId can be changed mid-stream by embedding a <config /> XML tag in the text:
- Text before the tag is synthesized with the previous config.
- The new config applies to all text after the tag.
- Tags may be split across multiple
stream-textmessages — incomplete tags are buffered until the closing>arrives. - Closing tags (
</config>) are stripped and ignored.
Emotion tags
If your voice has emotion variants configured, you can switch between them mid-stream by writing the emotion name in square brackets:[default]and[normal]revert to the base voice — the one set bystream-config, or by the most recent<config voicePromptId="..." />tag.- Any other name is looked up in the emotion set configured for your model and base voice. On a match, all text after the tag uses that emotion’s voice. Switching emotions does not change the base voice, so
[default]always returns to it. - Names are matched case-insensitively, and surrounding whitespace is ignored (
[Excited]and[ excited ]both work). - An unrecognized tag is left in place and spoken as literal text, so bracketed text that isn’t an emotion passes through unchanged.
- Tags may be split across
stream-textmessages; an incomplete fragment is buffered until the]arrives.
Emoji tags
When the session is configured withacceptEmojis: true, a small set of emoji are also interpreted as inline configuration rather than spoken:
Any other emoji is left in the text. With
acceptEmojis: false (the default), all emoji are treated as ordinary text.
end-stream
end-stream marks the end of a turn — a logical unit of text that should be spoken as one continuous utterance (for example, a single assistant reply).
Because the server buffers and batches incoming text to optimize prosody and latency, it does not know when you have finished sending text unless you tell it. end-stream does two things:
- Flushes the tail as the final segment. Any remaining buffered text is synthesized and marked as the last segment of the turn. This gives the tail clean sentence-final intonation instead of the “more is coming” prosody used for mid-stream segments.
- Closes the turn, so the final audio chunk is tagged with
"isFinal": true. This is the signal your client should wait for to know the whole turn is done.
Even without
end-stream, buffered text is eventually flushed and synthesized on its own — the tail is not lost. What you lose by omitting it is the explicit turn-end signal: no chunk is tagged "isFinal": true, so your client can’t tell a turn boundary from an ordinary segment boundary, and the tail is spoken as a continuation rather than a clean ending. Always send end-stream to close a turn cleanly.Multi-turn conversations
The connection is persistent and can be reused across many turns. After you receive theisFinal chunk for one turn, simply start sending stream-text again for the next turn — the session configuration from your initial stream-config is retained. Send end-stream again to close each subsequent turn.
This makes a single connection ideal for a back-and-forth conversation: keep it open for the whole session, configure once, and bracket each assistant reply with stream-text messages followed by an end-stream.
cancel and ping
action
Aborts in-flight generation and clears all buffers. Useful for barge-in — when the user interrupts, cancel the current turn so you stop generating audio the user will never hear. The server emits a synthetic finish message with
"isCancelled": true, after which you can start a new turn.action
Keepalive. The server replies with
{"action": "pong"}. Use it to hold a conversation connection open between turns.Server responses
Audio chunks
Audio arrives as JSON messages. Decode the base64data field and append the bytes in order.
string
Identifier of the generation this chunk belongs to. Use it to correlate chunks across concurrent generations.
integer
Sequential chunk index within a generation, starting from 0.
string
Base64-encoded audio data for this chunk.
boolean
true when this is the final chunk of a generation.boolean
true on the final chunk of the whole turn. Only produced after you send end-stream.boolean
true when the generation was aborted via cancel.string
Diacritized (menukad) Hebrew text for this generation’s segment. Sent only when the session opted in via
outputDiacritized, and only on the first chunk of each generation — later chunks omit the field entirely. In a multi-segment turn, each segment’s first chunk carries its own diacritized text. If a generation produces no audio at all, the field rides its isFinished chunk instead.outputDiacritized is enabled:
end-stream):
Errors
action: "error", a human-readable message, and a Unix-seconds time. Common messages:
The session stays open after a session-level error, so you can correct the configuration and continue.
Worker-level errors are reported per generation and are shaped differently — no
action, but a generationId identifying the generation that failed:
Code example
The following raw Python client connects, configures a session, streams text token-by-token, and collects audio chunks until the turn is final.Streaming from an LLM
The streaming API is built for exactly this pattern: forward tokens from a streaming LLM response intostream-text as they arrive, then send end-stream once the LLM finishes, and play the audio chunks as they come back.
Because sending text and receiving audio happen concurrently, run a producer (LLM → stream-text) and a consumer (audio chunks → playback) at the same time. The key rule is: call end-stream as soon as the LLM has produced its last token, and treat the isFinal chunk as “the assistant has finished speaking.”
