实时语音合成接口通过一条 WebSocket 连接完成「边送文本、边出音频」的流式合成,适用于大模型流式输出转语音、实时对话、流式播报、长文本播报等场景。本文介绍通过WebSocket连接访问Voice Studio实时语音合成服务的交互流程、接口地址和请求头。
接口地址
WebSocket 地址固定如下:
wss://convai-platform.aliyuncs.com/api-ws/v1/inference
鉴权
客户端类型 | 鉴权方式 | 示例 |
服务端(Java / Python / Node / Go 等) | 握手请求头 |
|
浏览器 | URL 查询参数 |
|
API Key 由 Voice Studio 签发(形如
sk-+ 32 位十六进制),获取方式见《API Key》。百炼原生 Key 无法通过鉴权(返回 HTTP 401)。浏览器的
WebSocket对象无法自定义握手请求头,因此凭证与所有开关只能走 URL 查询参数。浏览器场景请由你的后端签发临时令牌(有效期 ≤ 2 小时)下发,不要下发长效 API Key。鉴权失败时:握手阶段返回 HTTP 401(浏览器只能观察到关闭码
1006);连接升级后的凭证问题以关闭码 4401 表达。
请求头
握手阶段的参数都是会话级的:连接建立后不可更改,需要换值请另开连接。
请求头 | 必填 | 取值 | 说明 |
| 服务端必填 |
| Voice Studio 签发的 API Key |
| 否 |
| 开启文本归一化(TN)。取值必须精确等于 |
URL 查询参数
参数 | 必填 | 默认值 | 说明 |
| 浏览器必填 | - | 浏览器场景的鉴权凭证(服务端请用 |
| 否 | 关闭 |
|
| 否 | - | 会话级默认音色,取值为平台音色 ID。 |
| 否 | - | 你自带的追踪 ID,≤ 64 字符,仅用于日志排障。强烈建议始终携带:握手被拒时浏览器只能拿到 |
握手示例(服务端,开启 TN)
GET /api-ws/v1/inference?requestId=svc-20260903-0001 HTTP/1.1
Host: convai-platform.aliyuncs.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhl************==
Authorization: Bearer sk-9f2ac...d41b
X-TTS-TN: enable握手示例(浏览器)
wss://convai-platform.aliyuncs.com/api-ws/v1/inference
?token=sk-9f2ac...d41b
&tn=enable
&voice=9f2a1c4e-7b30-4d51-8a6c-2e5f1b8d0a56
&requestId=web-20260903-7f3a91交互流程
建立连接:按上文携带鉴权信息发起 WebSocket 握手,收到
101 Switching Protocols。默认的严格模式下没有任何额外下行帧。发送
run-task:指定音色与音频参数,开启一个任务。task_id由你生成(UUID),同一任务的所有帧必须一致。等待
task-started:收到该事件后才可以发送文本。提前发文本会报InvalidParameter。发送
continue-task:把文本增量送入,可以发多次。两次之间的间隔必须 小于 20 秒。接收
result-generated:服务端按句下发元数据,type依次为sentence-begin→sentence-synthesis(可多次)→sentence-end。接收 BINARY 音频帧:每个
type=sentence-synthesis的result-generated之后紧跟一个 BINARY 帧。按接收顺序追加播放或写文件即可,无需重排。(可选)发送
flush:不足一句的残句会一直被缓存不出声。需要它立即出声时发 flush 强制合成。发送
finish-task:不可省略。服务端会刷净缓冲并强制合成所有剩余内容(含不足一句的尾巴)。继续接收直到
task-finished:finish-task之后仍会有音频帧到达,提前关闭连接会丢失尾部音频。task-finished中带回本次任务的计费字符数与链路 ID。复用或关闭:任务结束后可以在同一条连接上用新的
task_id发起下一个run-task,也可以直接关闭连接。
状态约束
约束 | 违反后果 |
必须先 |
|
同一任务所有帧的 |
|
一条连接同时只允许 1 个任务 |
|
| 尾部缓存文本不会被合成,音频不完整 |
| 提前关闭会丢失尾部音频 |
上行帧必须是 TEXT 帧 | 发 BINARY 帧返回 |
客户端事件(上行)
所有上行事件必须是 TEXT 帧,统一使用如下信封:
{
"header": {
"action": "<事件名>",
"task_id": "<你生成的 UUID>",
"streaming": "duplex"
},
"payload": { }
}
| 类型 | 必填 | 取值 | 说明 |
| String | 是 |
| 其他值报 |
| String | 是 | UUID 字符串 | 由你生成;同一任务的全部帧必须相同 |
| String | 建议必填 | 固定 | 传则必须为 |
未知字段一律忽略,不报错(前向兼容)。
1. run-task —— 开启任务
发送时机:连接建立后的第一帧。
响应事件:成功返回 task-started,失败返回 task-failed。
数量限制:一条连接同时只允许 1 个任务;上一个任务结束后可以用新 task_id 再次发送。
payload 顶层
参数 | 类型 | 必填 | 说明 |
| String | 否 | 已废弃,保留仅为兼容百炼 SDK 的请求体结构。合成模型由 |
| Object | 是 | 合成参数,见下两表 |
| Object | 是 |
|
parameters —— 音频参数
参数 | 类型 | 必填 | 默认值 | 取值范围 | 说明 |
| String | 是(可由握手 | 握手 | - | 平台音色 ID,UUID 形式。不接受 |
| String | 否 |
|
| 音频编码格式 |
| Integer | 否 |
|
| 采样率(Hz)。 |
| Integer | 否 |
|
| 音量 |
| Float | 否 |
|
| 语速倍率 |
| Float | 否 |
|
| 音调倍率 |
| Integer | 否 | 引擎默认(约 32) |
| 码率(kbps)。仅 |
parameters —— 功能参
参数 | 类型 | 必填 | 默认值 | 取值 / 约束 | 说明 |
enable_ssml | Boolean | 否 | false | 与 vocabulary_id 互斥 | 开启 SSML 标记解析。开启后整个任务只允许一次 continue-task; |
word_timestamp_enabled | Boolean | 否 | false | 与 flush 互斥 | 字级时间戳。开启后 sentence-end 返回 words[] |
language_hints | Array[String] | 否 | - | 如 | 目标语言提示。当前引擎仅生效第一个元素 |
seed | Integer | 否 | - | [0, 65535] | 随机种子。相同 seed + 相同文本可复现合成结果 |
instruction | String | 否 | - | ≤ 2000 字符 | 指令控制(方言 / 情感 / 角色)。部分模型不支持 |
vocabulary_id | String | 否 | - | UUID | 引用当前鉴权身份可访问且已启用的客户托管词库; |
hot_fix | Object | 否 | - | 如 | 热修复规则。replace 由平台执行,作为任务临时规则,可覆盖词库中的同源规则 |
enable_markdown_filter | Boolean | 否 | false | - | 合成前过滤 Markdown 标记。部分模型不支持 |
enable_aigc_tag | Boolean | 否 | false | - | 在音频中嵌入 AIGC 隐性标识,配合 aigc_propagator / aigc_propagate_id 使用 |
aigc_propagator | String | 否 | - | - | AIGC 标识传播方 |
aigc_propagate_id | String | 否 | - | - | AIGC 标识传播 ID |
pronunciation 等发音控制是否生效取决于音色对应的模型。
"replace" 临时规则 ≤ 128 条;规范紧凑 JSON 的 UTF-8 体积 ≤ 16 KiB 。
enable_ssml=true 时不能携带 vocabulary_id
示例
{
"header": {
"action": "run-task",
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"streaming": "duplex"
},
"payload": {
"parameters": {
"voice": "9f2a1c4e-7b30-4d51-8a6c-2e5f1b8d0a37",
"format": "mp3",
"vocabulary_id": "123e4567-e89b-42d3-a456-426614174000",
"hot_fix": {
"replace": [
{"ConvAI": "康维智能"},
{"VIP": "贵宾"}
]
}
"sample_rate": 22050,
"volume": 50,
"rate": 1.0,
"pitch": 1.0,
"word_timestamp_enabled": true,
"language_hints": ["zh"],
"seed": 1024
},
"input": {}
}
}2. continue-task —— 增量发送文本
发送时机:收到 task-started 之后,可多次发送。
响应事件:result-generated + BINARY 音频帧(凑够一句时)。文本不足一句时无下行事件。
数量限制:单次 ≤ 20,000 字符(按原文计),单任务累计 ≤ 200,000 字符。enable_ssml=true 时全任务只允许一次。
payload 字段 | 类型 | 必填 | 约束 |
| String | 是 | 单次 ≤ 20,000 字符;单任务累计 ≤ 200,000 字符 |
{
"header": {
"action": "continue-task",
"task_id": "550e8400-e29b-41d4-a716-446655440000",
"streaming": "duplex"
},
"payload": { "input": { "text": "你好,欢迎使用语音合成服务。" } }
}约束
两次
continue-task间隔必须 < 20 秒,否则task-failed(IdleTimeout),连接保留可发新任务。服务端按序透传并自动分句;不足一句的文本会被引擎缓存,直到凑够句子、收到 flush 或收到
finish-task。缺
input.text报InvalidParameter: payload.input.text is required;空串""被接受但不产生音频。中文长文本注意:上游按 UTF-16 计数,中文每字占 2 字节,因此单次实际上限约 10,000 个中文字,超出时返回
InvalidParameter。
3. finish-task —— 结束任务(不可省略)
发送时机:文本全部发完之后。
响应事件:剩余 BINARY 音频帧,最后是 task-finished。
payload 字段 | 类型 | 必填 | 取值 | 说明 |
| Object | 是 |
| 正常结束固定传空对象 |
| String | 否 | 仅 | 取消当前任务。其他非空值报 |
正常结束
{
"header": { "action": "finish-task", "task_id": "550e8400-...", "streaming": "duplex" },
"payload": { "input": {} }
}服务端先刷净 TN 缓冲(若开启),再由引擎强制合成所有缓存内容(含不足一句的尾巴)。
发送后必须继续接收音频,直到
task-finished。服务端对
finish-task之后的合成等待无超时,长文本不会被截断。
取消任务(directive: cancel)
{
"header": { "action": "finish-task", "task_id": "550e8400-...", "streaming": "duplex" },
"payload": { "input": { "directive": "cancel" } }
}与正常结束语义相反:立即返回
task-finished,不再输出后续音频,丢弃未合成的缓存内容。取消后可以在同一条连接上重新发起新任务,无需重连。
下发的
usage.characters为0。对应百炼 SDK 的
streamingCancel()/streaming_cancel()。
取消不代表不计费。取消前已经送入引擎的文本仍会计入用量,详见《计费规则》。
4. flush —— 强制合成缓存文本
用途:大模型流式输出场景中,不足一句的文本会一直被缓存不出声;flush 强制合成已缓存内容,让残句立即出声。
有两种等价写法,语义完全相同,区别只在协议模式是否接受:
写法 A:continue-task + input.flush=true(推荐,两种模式都支持)
百炼 SDK 的 streaming_flush() 发出的就是这种帧。注意 input 里不带 text。
{
"header": { "action": "continue-task", "task_id": "550e8400-...", "streaming": "duplex" },
"payload": { "input": { "flush": true } }
}行为
先刷出 TN 缓冲(若开启),再让引擎强制合成;随后立即收到该残句的
result-generated+ BINARY 音频帧。任务仍在进行,可继续
continue-task。无待合成内容时为安全空操作(无任何下行事件)。
每次 flush 形成一次韵律断句,频繁 flush 会让听感更碎,建议仅在确需压低出声延迟时使用。
与
word_timestamp_enabled互斥:开启字级时间戳后发 flush 报InvalidParameter(分段会让时间戳无法连续化)。尾部残句本身不会丢:
finish-task一定会刷净所有缓存文本,不需要先 flush 再结束。
服务端事件(下行)
下行共 4 类 TEXT 事件 + BINARY 音频帧。所有 TEXT 事件统一 header / payload 信封。
请忽略未知字段:服务端遵循前向兼容,后续新增字段不会破坏存量接入。
1. task-started —— 任务确认
{
"header": { "event": "task-started", "task_id": "550e8400-...", "attributes": {} },
"payload": {}
}字段 | 类型 | 说明 |
| String | 固定 |
| String | 回显你的 |
| Object | 恒为空对象 |
| Object | 恒为空对象 |
收到此事件后方可发送 continue-task。
2. result-generated —— 句级元数据
{
"header": { "event": "result-generated", "task_id": "550e8400-..." },
"payload": {
"output": {
"type": "sentence-end",
"original_text": "你好,欢迎使用语音合成服务。",
"sentence": {
"index": 0,
"words": [
{ "text": "你", "begin_index": 0, "end_index": 1, "begin_time": 0, "end_time": 263 },
{ "text": "好", "begin_index": 1, "end_index": 2, "begin_time": 263, "end_time": 520 }
]
}
},
"usage": { "characters": 14 }
}
}output.type 取值
type | 含义 | 后续是否紧跟音频帧 |
|
| 句子开始 | 无 | 否 |
| 音频块(同一句可多次) | 有,紧跟一个 BINARY 帧 | 否(恒为空数组) |
| 句子结束 | 无 | 仅 |
字段说明
字段 | 类型 | 出现条件 | 说明 |
| String | 恒有 | 子事件类型 |
| String |
| 分句后的句子文本。开启 TN 时为归一化后文本 |
| Integer | 恒有 | 句序号,从 0 单调递增(任务内唯一) |
| String | 见上表 | 字文本 |
| Integer | 见上表 | 字在句中起始位置(从 0) |
| Integer | 见上表 | 字在句中结束位置 |
| Integer | 见上表 | 字对应音频起始时间戳(毫秒,相对任务起点) |
| Integer | 见上表 | 字对应音频结束时间戳(毫秒) |
| Integer | 仅 | 截止当前累计的计费字符数。最终以 |
3. BINARY 音频帧
按
run-task指定的format/sample_rate编码,紧跟在type=sentence-synthesis的result-generated之后到达。sentence-begin/sentence-end仅为元数据,其后没有音频帧。按接收顺序追加播放或写文件即可,无需重排。
format=pcm为裸 PCM(int16 小端、单声道),保存为文件需自行补 WAV 头。
4. task-finished —— 任务结束
{
"header": {
"event": "task-finished",
"task_id": "550e8400-...",
"attributes": {
"request_uuid": "a1f5c7e93b8d4e2f9c0a6b1d7e4f8c25",
"upstream_request_uuid": "9c0a6b1d-7e4f-8c25-a1f5-c7e93b8d4e2f",
"session_id": "3f8a1c92-0d4e-4b77-9c31-6a5f2e8b4d10"
}
},
"payload": { "usage": { "characters": 42 } }
}字段 | 类型 | 说明 |
| String | 服务端链路 ID,报障必填。百炼 SDK 的 |
| String / 缺省 | 上游引擎请求 ID |
| String / 缺省 | 会话 ID,报障建议一并提供 |
| Integer | 本任务累计计费字符数(含所有分段)。开启 TN 时按归一化后文本计; |
为空的字段不输出(而非输出 null),请容忍键缺失。收到此事件表示任务完整结束、音频已全部传输,可关闭连接或复用发起新任务。
5. task-failed —— 任务失败
{
"header": {
"event": "task-failed",
"task_id": "550e8400-...",
"error_code": "TextTooLong",
"error_message": "Text too long: text length 30000 exceeds 20000 per continue-task",
"attributes": {
"request_uuid": "a1f5c7e93b8d4e2f9c0a6b1d7e4f8c25",
"session_id": "3f8a1c92-0d4e-4b77-9c31-6a5f2e8b4d10"
}
},
"payload": {
"error": {
"code": "TextTooLong",
"message": "Text too long: text length 30000 exceeds 20000 per continue-task",
"type": "client_error"
}
}
}字段 | 类型 | 说明 |
| String | 错误码(与 |
| String | 错误描述(与 |
| String | 错误码 |
| String | 可读错误描述 |
| String |
|
6. connected —— 平台扩展帧(仅扩展模式)
严格模式(默认)不下发此帧。 仅当握手声明?protocol=extended时下发,且不走header/payload信封。
{
"event": "connected",
"userId": "1405573632598746",
"workspaceId": "llm-xxxxxxxx",
"sessionId": "3f8a1c92-0d4e-4b77-9c31-6a5f2e8b4d10",
"requestId": "a1f5c7e93b8d4e2f9c0a6b1d7e4f8c25",
"message": "WebSocket connected, gateway auth passed"
}主要用途是在建连阶段就拿到 sessionId / requestId 便于排障。message 仅供人看,请勿用于程序判断。
关键概念与限制
约束速查
约束 | 值 |
单次 | ≤ 20,000 字符(中文实际约 10,000 字) |
单任务累计文本 | ≤ 200,000 字符 |
任务内两帧文本间隔 | < 20 秒 |
连接空闲(无业务消息) | < 60 秒 |
单连接最大时长 | 3600 秒(1 小时) |
单连接同时进行的任务 | 1 个(串行复用) |
上行帧类型 | 必须 TEXT 帧 |
音色
voice 必须是平台音色 ID,即音色管理界面获取到的音色ID(形如 9f2a1c4e-7b30-4d51-8a6c-2e5f1b8d0a37)。
文本归一化(TN)
把文本中的数字、日期、时间、金额、电话号码、单位符号等书面写法转成口语读法后再合成,避免「12345」被逐字念错。
默认关闭,通过
X-TTS-TN: enable或?tn=enable开启,会话级生效,连接内所有任务共享。流式行为:TN 逐字符喂入归一化管道,含数字 / 字母的片段会缓冲到标点边界才输出。因此句子请带句末标点(
。!?;);残句要立即出声需发 flush;finish-task一定会刷净缓冲,尾部文本不会丢。fail-open:归一化组件不可用时自动退回原文透传,不报错、不断连,你无需为 TN 写错误分支。
开启 TN 后的口径变化
项目 | 关闭 TN | 开启 TN |
送到引擎的文本 | 你的原文 | 归一化后文本 |
| 原文分句 | 归一化后文本的分句 |
| 相对原文句子 | 相对归一化后的句子 |
| 按原文 | 按归一化后文本(口语读法通常更长) |
单次 | 按原文 | 仍按原文 |
单任务累计 ≤ 200,000 | 按原文 | 按归一化后文本 |
若你需要把音频时间戳映射回原始输入文本下标,请勿开启 TN(服务端不下发原文与归一化文本的映射关系)。
与enable_ssml不建议同开(TN 会处理标签内的数字);与word_timestamp_enabled可同开。
超时矩阵
超时项 | 阈值 | 触发条件 | 结果 |
任务内文本间隔 | 20 秒 | 任务进行中连续 20s 未收到 |
|
业务空闲超时 | 60 秒 | 连续 60s 无任何业务消息 | 服务端主动关闭,关闭码 4408 |
单连接最大时长 | 3600 秒 | 连接建立满 1 小时 | 补发 |
上游任务确认 | 30 秒 | 上游引擎 30s 未确认任务 |
|
| 无超时 | — | 长文本不会被截断,等引擎吐完为止 |
心跳 | 建议 15~30s 一次 WS ping | 你主动发送 | 服务端自动回 pong |
重要:心跳(ping / pong)不重置业务空闲计时。纯心跳的空闲连接仍会在 60s 被回收 —— 心跳只维持链路不被中间代理断开,与业务资源回收是两个独立机制。
并发限制
网关按账号维度限制同时在线的 WebSocket 连接数:连接建立算一路,连接关闭即释放一路。
超限时握手返回 HTTP 429(浏览器表现为
1006);连接升级后被拒以关闭码 4429 表达。默认额度为 5 路:同一工作空间同时在线的 WebSocket 连接数上限为 5。连接建立即占用一路,连接关闭即释放一路。
空闲连接同样占用额度。挂着不合成的连接不会自动让出槽位,用完请主动关闭。
需要提升并发额度请联系平台。
错误码与关闭码
业务错误码(task-failed)
| HTTP | 关闭码 | 类型 | 说明 |
| 400 | — | client_error | 参数校验失败: |
| 401 | 4401 | client_error | 凭证无效 / 缺失 / 过期 |
| 403 | 4403 | client_error | 音色越权、音色审核未通过 |
| 404 | — | client_error | 音色不存在或非本账号可见(不区分,防探测) |
| 413 | — | client_error | 单次 > 20,000 字符或单任务累计 > 200,000 字符 |
| 429 | 4429 | client_error | 并发连接超限或调用频次被拒 |
| — | 4408 | client_error | 任务内文本间隔超 20s( |
| 500 | 1011 | server_error | 服务内部错误 |
| 503 | 1011 | server_error | 上游引擎不可用 / 超时 |
type只有两种:client_error需要改参数或调用方式,重试无意义;server_error可退避重试。
WebSocket 关闭码
关闭码 | 含义 | 处理建议 |
| 正常关闭(含 1 小时到期) | 有后续需求时新建连接 |
| 服务端实例下线(发布 / 缩容) | 立即重连(会路由到新实例),无需退避 |
| 握手阶段被拒(浏览器读不到真实状态码) | 用 fetch 探测同一路径区分 401 / 400 / 429;提供握手 URL 里的 |
| 服务端内部错误 / 上游引擎失败 | 指数退避重连 |
| 鉴权失败或超时 | 刷新凭证后重连 |
| 音色越权 | 检查音色归属与权限,重试无效 |
| 业务空闲超时(60s) | 正常回收,按需重连 |
| 并发超限 | 提示用户,延迟重连 |
握手阶段失败没有应用层 Close 帧:凭证缺失 / 无效返回 401,protocol 取值非法返回 400,并发限流返回 429。浏览器一律只能观察到 1006,因此握手 URL 请始终携带 requestId 便于事后定位。