{
"event": "task_start",
"model": "speech-2.8-turbo",
"language_boost": "Chinese",
"voice_setting": {
"voice_id": "English_expressive_narrator",
"speed": 1,
"vol": 1,
"pitch": 0
},
"pronunciation_dict": {
"tone": [
"Omg/Oh my god"
]
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
},
"session_id": "my-session-001"
}{
"event": "task_continue",
"text": "Omg(sighs), the real danger is not that computers start thinking like people, but that people start thinking like computers. Computers can only help us with simple tasks."
}{
"event": "task_cancel"
}{
"event": "task_flush"
}{
"event": "task_finish"
}{
"session_id": "xxxx",
"event": "connected_success",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "xxxx",
"event": "task_started",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "sentence_start",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"data": {
"audio": "xxx"
},
"extra_info": {
"audio_channel": 1,
"audio_format": "mp3",
"audio_length": 9914,
"audio_sample_rate": 32000,
"audio_size": 157869,
"bitrate": 128000,
"invisible_character_ratio": 0,
"usage_characters": 158,
"word_count": 158
},
"is_final": true,
"session_id": "301871346491491",
"trace_id": "04ee3794e2c9e4a6d5f99e77742f06fd",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "sentence_end",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "task_canceled",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "task_flushed",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "xxxx",
"event": "task_finished",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "xxxx",
"event": "task_failed",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 1004,
"status_msg": "XXXXXXX"
},
"connect_id": "301871346491491"
}Text to Speech (T2A) WebSocket (Bidirectional)
WebSocket text-to-speech with streaming text input: send text character by character and let the server buffer it into sentences.
Differences from /ws/v1/t2a_v2
This API targets streaming text input — piping an LLM’s streaming output straight into speech, token by token.
/ws/v1/t2a_v2 | /ws/v1/t2a_v2_bidi (this API) | |
|---|---|---|
| Sentence buffering | Client must detect sentence boundaries | Server buffers automatically |
| Sending text char by char | One synthesis per character, choppy audio | Buffered into full sentences first |
| Interruption | Not supported, must close the connection | task_cancel, and you can continue afterwards |
| Sentence boundary events | None | sentence_start / sentence_end |
task_finish | Closes the connection immediately | Flushes the buffer first, then closes |
| Flush without ending the session | None | task_flush |
task_start are identical to /ws/v1/t2a_v2, so an existing integration can reuse them as-is.
Event flow
- Establish the connection and receive
connected_success - Send
task_startand receivetask_started - Send
task_continue(at any granularity, including a single character); the server buffers the text and starts synthesizingsentence_startis returned when a sentence starts synthesizing- audio arrives in chunks via
task_continued sentence_endis returned when that sentence is done
- Send
task_cancelto interrupt; aftertask_canceledyou may keep sendingtask_continue - When a turn is over and you want its tail audio right away, send
task_flush; the session continues aftertask_flushed - Send
task_finish; the server synthesizes any leftover buffered text, then returnstask_finishedand closes the connection
is_final marks the end of audio for one request, sentence_end marks the end of the current sentence, and task_finished marks the end of the whole session.task_start again after the task has started returns 2206 (illegal event order) and closes the connection.Sentence buffering and latency
The server detects sentence boundaries from punctuation, so the punctuation in the text you send directly affects how natural the audio sounds:| Text situation | Server behaviour |
|---|---|
Ends with sentence-final punctuation (。!?…!?. or a newline) | Synthesized immediately, no added latency |
Contains secondary punctuation (,、;:,;:) | Splits only once enough text has accumulated, so it never emits tiny fragments |
| Long text with no punctuation at all | Force-split once it hits the length cap; the boundary is then unrelated to meaning and the audio will sound clipped |
| Ends without punctuation, but enough text has accumulated | Flushed after a short silence window |
| Ends without punctuation and is short | Keeps waiting — the server will not synthesize half a word just to lower latency |
task_flush to push it out explicitly.Note that task_finish is not a substitute — it closes the connection, so it cannot be used when running multiple turns over one connection.Keeping the connection alive
A connection that stays idle for roughly 120 seconds is closed by the server with2201. “Idle” means the server is neither receiving upstream events nor sending audio.
2201.Send rate and retries
2205 means too much text is queued for synthesis on the server, usually because you are sending faster than synthesis can keep up.
2205 is not a quota rate limit, and it is a soft failure: neither the connection nor the session is closed. Just resend that task_continue a moment later — no reconnect and no second task_start are needed.task_continue longer than 10,000 characters returns 2204; that piece is skipped and the connection and session likewise stay open.{
"event": "task_start",
"model": "speech-2.8-turbo",
"language_boost": "Chinese",
"voice_setting": {
"voice_id": "English_expressive_narrator",
"speed": 1,
"vol": 1,
"pitch": 0
},
"pronunciation_dict": {
"tone": [
"Omg/Oh my god"
]
},
"audio_setting": {
"sample_rate": 32000,
"bitrate": 128000,
"format": "mp3",
"channel": 1
},
"session_id": "my-session-001"
}{
"event": "task_continue",
"text": "Omg(sighs), the real danger is not that computers start thinking like people, but that people start thinking like computers. Computers can only help us with simple tasks."
}{
"event": "task_cancel"
}{
"event": "task_flush"
}{
"event": "task_finish"
}{
"session_id": "xxxx",
"event": "connected_success",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "xxxx",
"event": "task_started",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "sentence_start",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"data": {
"audio": "xxx"
},
"extra_info": {
"audio_channel": 1,
"audio_format": "mp3",
"audio_length": 9914,
"audio_sample_rate": 32000,
"audio_size": 157869,
"bitrate": 128000,
"invisible_character_ratio": 0,
"usage_characters": 158,
"word_count": 158
},
"is_final": true,
"session_id": "301871346491491",
"trace_id": "04ee3794e2c9e4a6d5f99e77742f06fd",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "sentence_end",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "task_canceled",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "my-session-001",
"connect_id": "301871346491491",
"event": "task_flushed",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
}
}{
"session_id": "xxxx",
"event": "task_finished",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 0,
"status_msg": "success"
},
"connect_id": "301871346491491"
}{
"session_id": "xxxx",
"event": "task_failed",
"trace_id": "0303a2882bf18235ae7a809ae0f3cca7",
"base_resp": {
"status_code": 1004,
"status_msg": "XXXXXXX"
},
"connect_id": "301871346491491"
}Sending the "task_start" event officially begins the speech synthesis task. The task is considered successfully started when the server returns a "task_started" event. Only after receiving this event can you send "task_continue" or "task_finish" events to the server.
After receiving the task_started event from the server, the task officially begins and you can send task_continue events carrying the text to synthesize.
Key difference from /ws/v1/t2a_v2: text is not synthesized one message at a time. It enters a server-side sentence buffer and is only sent for synthesis once it forms a sentence, so it is safe to send text character by character (or token by token):
- flushed immediately on sentence-ending punctuation (
。!?…;!?.and newline) - flushed on secondary punctuation (
,、:,;:) once the buffered segment is long enough - force-split when the buffer reaches its length limit
- after you stop sending for a short while, the leftover buffer is flushed automatically
Whitespace-only text is discarded silently and returns no error. If no new event is sent within 120s after the last server response, the WebSocket connection closes automatically.
Send task_cancel to interrupt synthesis immediately: the server discards any text still in the sentence buffer, aborts the in-flight synthesis, and replies with task_canceled.
Audio already delivered to the client is not revoked.
After cancelling, the session returns to the task_started state and you may keep sending task_continue without reconnecting. This is intended for barge-in during real-time conversation.
Send task_flush to make the server synthesize whatever text is still buffered, without ending the session or closing the connection. task_flushed is returned once that audio has been sent.
Use it when a turn is over but its last piece of text is short and unpunctuated. Such a remainder is not flushed by the short silence window (that would risk cutting mid-word), so it otherwise waits for a longer backstop window. task_flush gets that audio out immediately.
Difference from task_finish: task_finish closes the connection, so it cannot be used to flush one turn's tail when you run multiple turns over a single connection. After task_flush the session stays in task_started and you may keep sending task_continue.
If the buffer happens to be empty, task_flushed is still returned normally; that is not an error.
On receiving task_finish, the server first flushes any text still sitting in the sentence buffer, waits until all of that audio has been returned, and only then replies with task_finished and closes the connection.
As a result a trailing incomplete sentence is never dropped, and the client does not need to append punctuation before finishing.
Notification that T2A task has started
Notification that T2A task has started
Returned when the server has buffered a complete sentence and starts synthesizing it. The audio for that sentence then arrives in chunks via task_continued until sentence_end.
Use it to count how many sentences the server actually grouped the text into
Notification that T2A task is continuing
Returned when all audio for the current sentence has been delivered.
Note the three levels of completion: is_final marks the end of audio for one request, sentence_end marks the end of the current sentence, and task_finished marks the end of the whole session
The server returns task_canceled to confirm the interruption took effect. You may continue sending task_continue afterwards
The server returns task_flushed to indicate that all audio for this task_flush has been sent. It is ordered after the audio frames produced by the flush, so receiving it means this turn's audio is complete. You may keep sending task_continue afterwards.
Notification that T2A task has completed successfully
If the task_failed event is received, it indicates that the task has failed. In this case, the WebSocket connection must be closed, and the error should be handled.