实时语音合成 WebSocket API 参考

更新时间:
复制 MD 格式

实时语音合成接口通过一条 WebSocket 连接完成「边送文本、边出音频」的流式合成,适用于大模型流式输出转语音、实时对话、流式播报、长文本播报等场景。本文介绍通过WebSocket连接访问Voice Studio实时语音合成服务的交互流程、接口地址和请求头。

接口地址

WebSocket 地址固定如下:

wss://convai-platform.aliyuncs.com/api-ws/v1/inference

鉴权

客户端类型

鉴权方式

示例

服务端(Java / Python / Node / Go 等)

握手请求头 Authorization: Bearer <api_key>

Authorization: Bearer sk-9f2ac...d41b

浏览器

URL 查询参数 ?token=<api_key>

wss://.../api-ws/v1/inference?token=sk-9f2ac...d41b

  • API Key 由 Voice Studio 签发(形如 sk- + 32 位十六进制),获取方式见《API Key》。百炼原生 Key 无法通过鉴权(返回 HTTP 401)。

  • 浏览器的 WebSocket 对象无法自定义握手请求头,因此凭证与所有开关只能走 URL 查询参数。浏览器场景请由你的后端签发临时令牌(有效期 ≤ 2 小时)下发,不要下发长效 API Key。

  • 鉴权失败时:握手阶段返回 HTTP 401(浏览器只能观察到关闭码 1006);连接升级后的凭证问题以关闭码 4401 表达。

请求头

握手阶段的参数都是会话级的:连接建立后不可更改,需要换值请另开连接。

请求头

必填

取值

说明

Authorization

服务端必填

Bearer <api_key>

Voice Studio 签发的 API Key

X-TTS-TN

否

enable

开启文本归一化(TN)。取值必须精确等于 enable(区分大小写),Enable / true / 1 均视为关闭

URL 查询参数

参数

必填

默认值

说明

token

浏览器必填

-

浏览器场景的鉴权凭证(服务端请用 Authorization 请求头)

tn

否

关闭

enable 开启文本归一化,等价于 X-TTS-TN: enable,两者任一命中即开启

voice

否

-

会话级默认音色,取值为平台音色 ID。run-task 未传 parameters.voice 时回退到此值

requestId

否

-

你自带的追踪 ID,≤ 64 字符,仅用于日志排障。强烈建议始终携带:握手被拒时浏览器只能拿到 1006,该值是事后定位的唯一线索

握手示例(服务端,开启 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

交互流程

  1. 建立连接:按上文携带鉴权信息发起 WebSocket 握手,收到 101 Switching Protocols。默认的严格模式下没有任何额外下行帧。

  2. 发送 run-task:指定音色与音频参数,开启一个任务。task_id 由你生成(UUID),同一任务的所有帧必须一致。

  3. 等待 task-started:收到该事件后才可以发送文本。提前发文本会报 InvalidParameter。

  4. 发送 continue-task:把文本增量送入,可以发多次。两次之间的间隔必须 小于 20 秒。

  5. 接收 result-generated:服务端按句下发元数据,type 依次为 sentence-begin → sentence-synthesis(可多次)→ sentence-end。

  6. 接收 BINARY 音频帧:每个 type=sentence-synthesis 的 result-generated 之后紧跟一个 BINARY 帧。按接收顺序追加播放或写文件即可,无需重排。

  7. (可选)发送 flush:不足一句的残句会一直被缓存不出声。需要它立即出声时发 flush 强制合成。

  8. 发送 finish-task:不可省略。服务端会刷净缓冲并强制合成所有剩余内容(含不足一句的尾巴)。

  9. 继续接收直到 task-finished:finish-task 之后仍会有音频帧到达,提前关闭连接会丢失尾部音频。task-finished 中带回本次任务的计费字符数与链路 ID。

  10. 复用或关闭:任务结束后可以在同一条连接上用新的 task_id 发起下一个 run-task,也可以直接关闭连接。

状态约束

约束

违反后果

必须先 run-task,收到 task-started 后再发文本

InvalidParameter

同一任务所有帧的 task_id 必须完全一致

InvalidParameter: task_id mismatch

一条连接同时只允许 1 个任务

InvalidParameter: another task is running

finish-task 不可省略

尾部缓存文本不会被合成,音频不完整

finish-task 后必须继续收流直到 task-finished

提前关闭会丢失尾部音频

上行帧必须是 TEXT 帧

发 BINARY 帧返回 task-failed(InvalidParameter),连接保留

客户端事件(上行)

所有上行事件必须是 TEXT 帧,统一使用如下信封:

{
  "header": {
    "action": "<事件名>",
    "task_id": "<你生成的 UUID>",
    "streaming": "duplex"
  },
  "payload": { }
}

header 字段

类型

必填

取值

说明

action

String

是

run-task / continue-task / finish-task

其他值报 InvalidParameter: unknown action: xxx

task_id

String

是

UUID 字符串

由你生成;同一任务的全部帧必须相同

streaming

String

建议必填

固定 duplex

传则必须为 duplex;省略不报错,为兼容后续版本建议固定传

未知字段一律忽略,不报错(前向兼容)。

1. run-task —— 开启任务

发送时机:连接建立后的第一帧。

响应事件:成功返回 task-started,失败返回 task-failed。

数量限制:一条连接同时只允许 1 个任务;上一个任务结束后可以用新 task_id 再次发送。

payload 顶层

参数

类型

必填

说明

model

String

否

已废弃,保留仅为兼容百炼 SDK 的请求体结构。合成模型由 parameters.voice 对应的平台音色决定,传任何值都不生效也不报错

parameters

Object

是

合成参数,见下两表

input

Object

是

run-task 阶段固定传 {},文本通过 continue-task 发送

parameters —— 音频参数

参数

类型

必填

默认值

取值范围

说明

voice

String

是(可由握手 voice 兜底)

握手 voice

-

平台音色 ID,UUID 形式。不接受 {model}:{voice} 或裸音色名

format

String

否

mp3

mp3 / pcm / wav / opus

音频编码格式

sample_rate

Integer

否

22050(format=opus 时为 24000)

8000 / 16000 / 22050 / 24000 / 44100 / 48000

采样率(Hz)。format=opus 只支持 8000 / 16000 / 24000 / 48000

volume

Integer

否

50

[0, 100]

音量

rate

Float

否

1.0

[0.5, 2.0]

语速倍率

pitch

Float

否

1.0

[0.5, 2.0]

音调倍率

bit_rate

Integer

否

引擎默认(约 32)

[6, 510]

码率(kbps)。仅 format=mp3 / opus 允许传,其他格式传了报 InvalidParameter

parameters —— 功能参

参数

类型

必填

默认值

取值 / 约束

说明

enable_ssml

Boolean

否

false

与 vocabulary_id 互斥

开启 SSML 标记解析。开启后整个任务只允许一次 continue-task;

word_timestamp_enabled

Boolean

否

false

与 flush 互斥

字级时间戳。开启后 sentence-end 返回 words[]

language_hints

Array[String]

否

-

如 ["zh"]

目标语言提示。当前引擎仅生效第一个元素

seed

Integer

否

-

[0, 65535]

随机种子。相同 seed + 相同文本可复现合成结果

instruction

String

否

-

≤ 2000 字符

指令控制(方言 / 情感 / 角色)。部分模型不支持

vocabulary_id

String

否

-

UUID

引用当前鉴权身份可访问且已启用的客户托管词库;

hot_fix

Object

否

-

如 {"replace":[{"源词":"替换文本"}],"pronunciation":[...]}

热修复规则。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 字段

类型

必填

约束

input.text

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 字段

类型

必填

取值

说明

input

Object

是

{}

正常结束固定传空对象

input.directive

String

否

仅 cancel

取消当前任务。其他非空值报 InvalidParameter

正常结束

{
  "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": {}
}

字段

类型

说明

header.event

String

固定 task-started

header.task_id

String

回显你的 task_id

header.attributes

Object

恒为空对象

payload

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

含义

后续是否紧跟音频帧

words[] 是否非空

sentence-begin

句子开始

无

否

sentence-synthesis

音频块(同一句可多次)

有,紧跟一个 BINARY 帧

否(恒为空数组)

sentence-end

句子结束

无

仅 word_timestamp_enabled=true 时非空

字段说明

字段

类型

出现条件

说明

output.type

String

恒有

子事件类型

output.original_text

String

sentence-begin / sentence-end

分句后的句子文本。开启 TN 时为归一化后文本

output.sentence.index

Integer

恒有

句序号,从 0 单调递增(任务内唯一)

output.sentence.words[ ].text

String

见上表

字文本

output.sentence.words[ ].begin_index

Integer

见上表

字在句中起始位置(从 0)

output.sentence.words[ ].end_index

Integer

见上表

字在句中结束位置

output.sentence.words[ ].begin_time

Integer

见上表

字对应音频起始时间戳(毫秒,相对任务起点)

output.sentence.words[ ].end_time

Integer

见上表

字对应音频结束时间戳(毫秒)

payload.usage.characters

Integer

仅 sentence-end

截止当前累计的计费字符数。最终以 task-finished 为准

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

字段

类型

说明

header.attributes.request_uuid

String

服务端链路 ID,报障必填。百炼 SDK 的 getLastRequestId() 读取此字段

header.attributes.upstream_request_uuid

String / 缺省

上游引擎请求 ID

header.attributes.session_id

String / 缺省

会话 ID,报障建议一并提供

payload.usage.characters

Integer

本任务累计计费字符数(含所有分段)。开启 TN 时按归一化后文本计;cancel 场景为 0。口径见《计费规则》

为空的字段不输出(而非输出 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"
    }
  }
}

字段

类型

说明

header.error_code

String

错误码(与 payload.error.code 一致)。百炼 SDK 的 on_error / onError 读这里

header.error_message

String

错误描述(与 payload.error.message 一致)

payload.error.code

String

错误码

payload.error.message

String

可读错误描述

payload.error.type

String

client_error(改参数 / 改调用方式,重试无意义)/ server_error(可退避重试)

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 仅供人看,请勿用于程序判断。

关键概念与限制

约束速查

约束

值

单次 continue-task 文本

≤ 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

送到引擎的文本

你的原文

归一化后文本

result-generated.output.original_text

原文分句

归一化后文本的分句

words[].begin_index / end_index

相对原文句子

相对归一化后的句子

usage.characters(计费字符)

按原文

按归一化后文本(口语读法通常更长)

单次 continue-task ≤ 20,000 校验

按原文

仍按原文

单任务累计 ≤ 200,000

按原文

按归一化后文本

若你需要把音频时间戳映射回原始输入文本下标,请勿开启 TN(服务端不下发原文与归一化文本的映射关系)。
与 enable_ssml 不建议同开(TN 会处理标签内的数字);与 word_timestamp_enabled 可同开。

超时矩阵

超时项

阈值

触发条件

结果

任务内文本间隔

20 秒

任务进行中连续 20s 未收到 continue-task / finish-task

task-failed(IdleTimeout),连接保留可复用

业务空闲超时

60 秒

连续 60s 无任何业务消息

服务端主动关闭,关闭码 4408

单连接最大时长

3600 秒

连接建立满 1 小时

补发 task-finished 后正常关闭(1000);任务进行中不截断,等任务结束再关

上游任务确认

30 秒

上游引擎 30s 未确认任务

task-failed(EngineUnavailable),连接保留

finish-task 后合成

无超时

—

长文本不会被截断,等引擎吐完为止

心跳

建议 15~30s 一次 WS ping

你主动发送

服务端自动回 pong

重要:心跳(ping / pong)不重置业务空闲计时。纯心跳的空闲连接仍会在 60s 被回收 —— 心跳只维持链路不被中间代理断开,与业务资源回收是两个独立机制。

并发限制

  • 网关按账号维度限制同时在线的 WebSocket 连接数:连接建立算一路,连接关闭即释放一路。

  • 超限时握手返回 HTTP 429(浏览器表现为 1006);连接升级后被拒以关闭码 4429 表达。

  • 默认额度为 5 路:同一工作空间同时在线的 WebSocket 连接数上限为 5。连接建立即占用一路,连接关闭即释放一路。

  • 空闲连接同样占用额度。挂着不合成的连接不会自动让出槽位,用完请主动关闭。

  • 需要提升并发额度请联系平台。

错误码与关闭码

业务错误码(task-failed)

error.code

HTTP

关闭码

类型

说明

InvalidParameter

400

—

client_error

参数校验失败:format / sample_rate / volume / rate / pitch / bit_rate / seed 越界、opus 采样率非法、task_id 不一致、任务已在进行、SSML 下二次发文本、非法 action、上行 BINARY 帧

Unauthorized

401

4401

client_error

凭证无效 / 缺失 / 过期

Forbidden

403

4403

client_error

音色越权、音色审核未通过

NotFound

404

—

client_error

音色不存在或非本账号可见(不区分,防探测)

TextTooLong

413

—

client_error

单次 > 20,000 字符或单任务累计 > 200,000 字符

RateLimitExceeded

429

4429

client_error

并发连接超限或调用频次被拒

IdleTimeout

—

4408

client_error

任务内文本间隔超 20s(task-failed)或连接业务空闲超 60s(断连 4408)

InternalError

500

1011

server_error

服务内部错误

EngineUnavailable

503

1011

server_error

上游引擎不可用 / 超时

type 只有两种:client_error 需要改参数或调用方式,重试无意义;server_error 可退避重试。

WebSocket 关闭码

关闭码

含义

处理建议

1000

正常关闭(含 1 小时到期)

有后续需求时新建连接

1001

服务端实例下线(发布 / 缩容)

立即重连(会路由到新实例),无需退避

1006

握手阶段被拒(浏览器读不到真实状态码)

用 fetch 探测同一路径区分 401 / 400 / 429;提供握手 URL 里的 requestId 报障

1011

服务端内部错误 / 上游引擎失败

指数退避重连

4401

鉴权失败或超时

刷新凭证后重连

4403

音色越权

检查音色归属与权限,重试无效

4408

业务空闲超时(60s)

正常回收,按需重连

4429

并发超限

提示用户,延迟重连

握手阶段失败没有应用层 Close 帧:凭证缺失 / 无效返回 401,protocol 取值非法返回 400,并发限流返回 429。浏览器一律只能观察到 1006,因此握手 URL 请始终携带 requestId 便于事后定位。