帮你快速理解、总结文档立即下载
文档中心>媒体处理>其他说明文档>WebSocket 音频端到端接入

WebSocket 音频端到端接入

最近更新时间:2026-09-04 11:04:31
我的收藏
本文档面向客户端接入方,描述如何通过 WebSocket 协议接入本服务的端到端实时语音对话能力(Speech-to-Speech, S2S),包含 URL 规范、鉴权流程、全量事件协议、交互时序、错误码与常见问题等。
说明:
本服务对外协议100%兼容 Realtime WebSocket GA 事件规范,按标准 Realtime GA SDK 编写的客户端可以直接接入。
服务端内部提供 audio-og-realtime-2.1 / audio-og-realtime-2.1mini / audio-s-realtime-3.0 三个能力系列,运行时根据客户请求自动选路。

1. 服务概述

通信协议:WebSocket (RFC 6455) over TLS,wss:// 生产强制、ws:// 仅内网调试。
消息格式Text 帧承载 JSON 事件,Binary不接受(会被服务端忽略并打 warn)。
音频承载:所有音频均以 base64 字符串放在 JSON 事件的 audio / delta 字段。
支持的能力系列
model 参数
定位
audio-og-realtime-2.1
端到端语音对话 · 基础能力。
audio-og-realtime-2.1mini
端到端语音对话 · 轻量型。
audio-s-realtime-3.0
端到端语音对话 · 增强能力。
说明:
客户端可以任意大小写传入(Audio-OG-realtime-2.1audio-og-realtime-2.1 等价),服务端在解析时统一归一为小写。规范值为上表中的小写形式,本文档下同。
统一协议:不同能力系列使用完全相同的 WebSocket 事件协议(与标准 openai Realtime 一致)。客户端无需为不同 model 编写分支代码;能力差异由服务端内部屏蔽,不支持的字段会静默忽略或在控制事件上以error(code=not_supported) 回复,不会中断会话。
单会话最大空闲超时:默认30秒,可通过 timeoutSec 查询参数覆盖,上限120秒。
未指定 model 时的兜底:服务端根据账号维度的默认路由做兜底,仍未命中时使用 audio-og-realtime-2.1

2. 接入 URL 与查询参数

2.1 URL 模板

wss://{host}/ete/v1/{appid}
?model=<audio-og-realtime-2.1|audio-og-realtime-2.1mini|audio-s-realtime-3.0>
&secretId=<TC3 SecretId>
&signature=<TC3-HMAC-SHA256 hex>
&timeStamp=<unix 秒>
&expired=<unix 秒>
&nonce=<随机整数>
&timeoutSec=<会话空闲超时秒>

2.2 路径参数

参数
必填
说明
appid
腾讯云 AppID,十进制无符号整数,随路径 /ete/v1/{appid} 传入。
其中 appid 是腾讯云用户账号的唯一标识(UInt64),可以从控制台账号中心 > 账号信息 页面获得:


2.3 其他参数(Query)

参数
必填
类型
说明
secretId
string
CAM SecretId。
signature
string
TC3-HMAC-SHA256 签名(hex,小写),见 签名
timeStamp
int64
签名时的当前 UNIX 时间戳(秒)。
expired
int64
签名过期时间的 UNIX 时间戳(秒),必须 > timeStamp> now
nonce
int64
一次性随机整数(防重放)。
model
string
能力系列别名,见 服务概述。未指定时按账号默认路由兜底。
timeoutSec
int
会话空闲超时(秒),默认30,最大120(超过上限会被截断到120)。
resId
string
业务侧资源 ID,透传用于对账。

2.4 URL 示例

wss://mps.cloud.tencent.com/ete/v1/1301234567
?model=audio-s-realtime-3.0
&secretId=AKIDzxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
&signature=8f2c...4b3a
&timeStamp=1735012345
&expired=1735015945
&nonce=8391023472
&timeoutSec=60
&resId=order_20260826_0001

3. 签名

签名沿用腾讯云 CAM 签名标准
流程参考:签名生成

3.1 参与签名的查询参数

signature 本身外的所有 query 参数都参与签名(按字母升序排列)。这意味着:
必填参数 secretId / timeStamp / expired / nonce 一定参与签名。
可选参数(如 model / timeoutSec / resId只要您在 URL 里带上了就必须参与签名;未带则不参与。
signature 参数本身不参与签名(作为签名结果放在 URL 上)。
其他业务参数(如音频格式、voice、instructions、tools 等)通过握手成功后的 session.update 事件传递,与 URL 无关。

3.2 签名算法

Step 1: 排序并 URL-encode,构造规范化查询串
keys = sort(参与签名的 params keys) // 除 signature 之外的所有 query
canonicalQueryString = keys.map(k => k + '=' + urlEncode(params[k])).join('&')

Step 2: 构造规范化请求
canonicalRequest =
"post" + "\\n"
+ "/ete/v1/{appid}" + "\\n"
+ canonicalQueryString + "\\n"
+ "content-type:application/json; charset=utf-8" + "\\n"
+ "host:" + host + "\\n"
+ "" + "\\n"
+ "content-type;host" + "\\n"

Step 3: 构造待签字符串
date = UTC(timeStamp) 的 YYYY-MM-DD
credentialScope = date + "/mps/tc3_request"
hashedRequest = hex( SHA-256( canonicalRequest ) )
stringToSign = "TC3-HMAC-SHA256" + "\\n"
+ timeStamp + "\\n"
+ credentialScope + "\\n"
+ hashedRequest

Step 4: 计算派生签名密钥
kDate = HMAC-SHA256( date, "TC3" + secretKey )
kService = HMAC-SHA256( "mps", kDate )
kSigning = HMAC-SHA256( "tc3_request", kService )

Step 5: 计算最终签名
signature = hex( HMAC-SHA256( stringToSign, kSigning ) ) // 小写 hex

3.3 JavaScript 参考实现

浏览器环境下的完整实现见 测试 Demo
generateSignature() 方法,使用浏览器原生 crypto.subtle API 完成。以下节选核心片段:
async function sign(cfg) {
const now = Math.floor(Date.now() / 1000);
const expired = now + 3600;
const params = {
// 除 signature 外的所有 query 参数, 均需参与签名
model: cfg.model, // 可选; 未传则不放入 params
timeStamp: String(now),
expired: String(expired),
nonce: String(Math.floor(Math.random() * 1e10)),
secretId: cfg.secretId,
timeoutSec: String(cfg.timeoutSec || 30),
// 若使用 resId, 也一并放入并参与签名:
// resId: cfg.resId,
};

const path = `/ete/v1/${cfg.appid}`;
const keys = Object.keys(params).sort();
const canonicalQueryString = keys
.map((k) => `${k}=${encodeURIComponent(params[k])}`).join('&');

const canonicalRequest = [
'post', path, canonicalQueryString,
'content-type:application/json; charset=utf-8',
`host:${cfg.host}`, '', 'content-type;host', '',
].join('\\n');

const hashed = await sha256Hex(canonicalRequest);
const date = new Date(now * 1000).toISOString().split('T')[0];
const scope = `${date}/mps/tc3_request`;
const stringToSign = ['TC3-HMAC-SHA256', String(now), scope, hashed].join('\\n');

const kDate = await hmacSha256(date, 'TC3' + cfg.secretKey);
const kService = await hmacSha256('mps', kDate);
const kSigning = await hmacSha256('tc3_request', kService);
const signature = bufToHex(await hmacSha256(stringToSign, kSigning));

const q = new URLSearchParams(); keys.forEach((k) => q.append(k, params[k]));
q.append('signature', signature);
return `wss://${cfg.host}${path}?${q.toString()}`;
}

3.4 Go 参考实现

func sign(host, appID, secretID, secretKey, model string, timeoutSec int) string {
now := time.Now().Unix()
expired := now + 3600
params := map[string]string{
"timeStamp": strconv.FormatInt(now, 10),
"expired": strconv.FormatInt(expired, 10),
"nonce": strconv.Itoa(rand.Intn(1e9)),
"secretId": secretID,
"timeoutSec": strconv.Itoa(timeoutSec),
}
if model != "" {
params["model"] = model
}
keys := make([]string, 0, len(params))
for k := range params { keys = append(keys, k) }
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, k+"="+url.QueryEscape(params[k]))
}
cqs := strings.Join(parts, "&")

path := "/ete/v1/" + appID
canonical := strings.Join([]string{
"post", path, cqs,
"content-type:application/json; charset=utf-8",
"host:" + host, "", "content-type;host", "",
}, "\\n")

hashed := sha256Hex([]byte(canonical))
date := time.Unix(now, 0).UTC().Format("2006-01-02")
scope := date + "/mps/tc3_request"
stringToSign := strings.Join([]string{
"TC3-HMAC-SHA256", strconv.FormatInt(now, 10), scope, hashed,
}, "\\n")

kDate := hmac256([]byte(date), []byte("TC3"+secretKey))
kService := hmac256([]byte("mps"), kDate)
kSigning := hmac256([]byte("tc3_request"), kService)
signature := hex.EncodeToString(hmac256([]byte(stringToSign), kSigning))

q := url.Values{}
for k, v := range params { q.Set(k, v) }
q.Set("signature", signature)
return "wss://" + host + path + "?" + q.Encode()
}

func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
func hmac256(msg, key []byte) []byte { m := hmac.New(sha256.New, key); m.Write(msg); return m.Sum(nil) }

3.5 鉴权失败的握手响应

服务端在 WebSocket 握手升级前就完成鉴权与并发检查,任一步失败会以 HTTP 响应拒绝,不会进入 WebSocket 通道,因此客户端不会收到 error 事件,而是拿到一个标准的 HTTP 状态码。见 第 9.1 节

4. 交互时序

4.1 关键约定

握手成功后服务端不主动下发任何事件。首条消息必须由客户端发起:
客户端发 session.update 完成会话初始化,服务端才回一条 session.updated
收到 session.updated 才代表“可以开始送音频”。

4.2 典型时序(服务端 VAD 模式)



4.3 打断当前 response

客户端可以随时发送 {"type": "response.cancel"} 打断当前正在生成的 response。
服务端会立即停止 TTS 下发,并回一条 response.donestatus 可能为 cancelled)。

5. 上行事件(Client → Server)

5.1 事件类型速览

事件 type
用途
session.update
更新会话参数(首次调用等价于“完成握手初始化”)。
input_audio_buffer.append
追加一段用户音频(base64 编码)。
input_audio_buffer.commit
手动提交音频缓冲(关闭 server_vad 时使用)。
input_audio_buffer.clear
清空未 commit 的上行音频缓冲。
response.create
主动请求服务端生成一次 response(可携带 instructions 用作打招呼 / 干预回复)。
response.cancel
打断当前正在生成的 response。
conversation.item.create
向会话上下文新增一条 item(含 function_call_output 回传)。
conversation.item.retrieve
请求获取指定 item 详情。
conversation.item.delete
删除指定 item。
conversation.item.truncate
截断已生成的 assistant 音频(部分场景不支持,会回 error(not_supported))。
output_audio_buffer.clear
清空当前 assistant 音频播放缓冲(服务端会同步映射为 response.cancel)。
transcription_session.update
独立 transcription 会话更新(当前不支持,会回 error(not_supported))。

5.2 session.update

{
"type": "session.update",
"session": {
"instructions": "你是一个专业的智能助手, 说话简洁友好。",
"audio": {
"input": {
"format": { "type": "audio/pcm", "rate": 16000 },
"transcription": { "language": "zh" }
},
"output": {
"format": { "type": "audio/pcm", "rate": 24000 },
"voice": "xxxxxx",
"speed": 100,
"loudness": 100
}
},
"tools": [
{
"type": "function",
"name": "get_current_time",
"description": "获取当前系统时间",
"parameters": { "type": "object", "properties": {}, "required": [] }
}
]
}
}
字段说明
字段
类型
说明
session.instructions
string
System instructions,覆盖服务端默认。
session.audio.input.format.type
string
上行音频格式,见 第 7 节
session.audio.input.format.rate
int
上行采样率(Hz)。
session.audio.input.transcription.language
string
ASR 转写语言(ISO 639-1,如 zh / en / ja)。留空则由服务端自动检测;部分能力基于内建语言自适应,客户端指定值可能会被忽略。
session.audio.output.format.type
string
下行音频格式。
session.audio.output.format.rate
int
下行采样率(Hz)。
session.audio.output.voice
string
发音人 ID。支持明文音色 ID加密音色 ID(由音色加密服务生成),服务端自动识别。若音色不适用于当前会话能力,服务端会静默 fallback 到默认音色(详见 Q9)。
session.audio.output.speed
int
语速(部分能力支持,不支持时静默忽略)。
session.audio.output.loudness
int
响度(部分能力支持,不支持时静默忽略)。
session.tools
array
Function tool 定义,遵循标准 GA Function Tool schema。
session.extension
object
预留的高级拓展字段(部分能力支持,不支持时静默忽略)。
VAD / turn_detection:服务端默认启用 server_vad。客户端在 session.audio.input.turn_detection 里的自定义参数,服务端会尽力适配;具体生效情况以 session.updated.session.audio.input.turn_detection 回执为准。

5.3 input_audio_buffer.append

{
"type": "input_audio_buffer.append",
"audio": "<base64-encoded audio chunk>"
}
audio一段完整的音频负载,服务端按 session.audio.input.format 声明的格式解码。
推荐分片大小:20ms-40ms(PCM 16k 单声道下约 640~1280 字节 base64 前)。
首次 session.update 之前送入的 append 会被静默丢弃。

5.4 input_audio_buffer.commit / clear

{ "type": "input_audio_buffer.commit" }
{ "type": "input_audio_buffer.clear" }
commit 仅在关闭 server_vad 时需要显式调用。
clear 用于取消当前未提交的用户音频,无论能力后端是否原生支持,服务端都会回一条 input_audio_buffer.cleared 事件给客户端。

5.5 response.create / cancel

{
"type": "response.create",
"response": {
"instructions": "你好, 我是你的智能助手。" // 可选
}
}
{ "type": "response.cancel" }
统一语义(各 model 行为一致):
response.instructions → 让服务端主动说这段话(打招呼 / 干预回复)。
不带 instructions → 尝试触发一次新的 response 生成。部分能力在等待生成上下文时会回复 error(code=not_supported),客户端可忽略此提示或改发带 instructions 的版本。
response.create 支持的字段:
字段
类型
说明
response.instructions
string
让服务端主动说出的文本。
response.voice
string
本轮临时覆盖的音色(可选)。
response.modalities
[]string
本轮输出模态(可选,标准)。
response.metadata
object
透传给服务端记录用的元数据(可选)。

5.6 conversation.item.create

用途 1:追加 user / assistant message
{
"type": "conversation.item.create",
"item": {
"type": "message",
"role": "user",
"content": [{ "type": "input_text", "text": "帮我查一下深圳今天的天气" }]
}
}
用途 2:回传 function call 结果(详见 第 8 节
{
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": "call_abc123",
"output": "2026-08-26 09:30:00"
}
}

5.7 conversation.item.retrieve / delete / truncate

{ "type": "conversation.item.retrieve", "item_id": "item_xxx" }
{ "type": "conversation.item.delete", "item_id": "item_xxx" }
{ "type": "conversation.item.truncate", "item_id": "item_xxx",
"content_index": 0, "audio_end_ms": 1500 }
注意:
conversation.item.truncate 在部分能力下不支持,服务端会以 error(code=not_supported) 回复;客户端可以改用 response.cancel +
output_audio_buffer.clear 实现等价效果(见 Q6)。

5.8 output_audio_buffer.clear

{ "type": "output_audio_buffer.clear" }
服务端会:
1. 内部触发一次 response.cancel 打断当前 TTS。
2. 回一条 output_audio_buffer.cleared,客户端据此清空本地音频播放缓冲。

6. 下行事件(Server → Client)

6.1 完整事件清单

事件 type
触发时机
session.updated
服务端应用 session.update 后的首条响应。
error
参数错误 / 服务后端异常 / 不支持的能力。
conversation.item.created
一条 item 加入会话上下文。
conversation.item.retrieved
响应 conversation.item.retrieve
conversation.item.deleted
响应 conversation.item.delete
conversation.item.input_audio_transcription.delta
用户 ASR 增量转写(非稳态)。
conversation.item.input_audio_transcription.completed
用户 ASR 稳态转写(可能携带 usage)。
conversation.item.input_audio_transcription.failed
用户 ASR 转写失败。
input_audio_buffer.committed
用户一句话音频入缓冲(VAD 触发或客户端 commit)。
input_audio_buffer.cleared
响应 input_audio_buffer.clear
input_audio_buffer.speech_started
服务端 VAD 检测到用户开始说话。
input_audio_buffer.speech_stopped
服务端 VAD 检测到用户停止说话。
response.created
一次 response 开始生成。
response.output_item.added
response 内新增一个 output_item(message / function_call)。
response.content_part.added
output_item 内新增一个 content part(audio / text)。
response.output_audio.delta
助手音频增量(base64)。
response.output_audio.done
助手音频结束。
response.output_audio_transcript.delta
助手音频伴随文本增量。
response.output_audio_transcript.done
助手音频伴随文本稳态。
response.output_text.delta / response.output_text.done
纯文本模态输出(预留,默认音频模态不下发)。
response.function_call_arguments.delta
Function call 参数增量(若能力后端按流式下发)。
response.function_call_arguments.done
Function call 参数完整下发。
response.content_part.done
content part 结束。
response.output_item.done
output_item 结束。
response.done
一次 response 全部结束(可能携带 usage)。
output_audio_buffer.started
服务端开始下发助手音频。
output_audio_buffer.stopped
服务端一段助手音频结束。
output_audio_buffer.cleared
响应客户端 output_audio_buffer.clear
rate_limits.updated
能力后端速率限额更新(若能力后端下发则透传)。

6.2 session.updated(服务端首条事件)

{
"type": "session.updated",
"event_id": "evt_xxxxxxxx",
"session": {
"id": "sess_xxxxxxxx",
"type": "realtime",
"model": "audio-s-realtime-3.0",
"audio": { "input": {...}, "output": {...} },
"voice": "xxxxxx",
"instructions": "..."
}
}
客户端应在收到此事件之后才开始送 input_audio_buffer.append。事件中的 session.model 是本次会话实际生效模型名(可能来自客户端 URL 参数,也可能来自服务端默认路由兜底)。

6.3 用户 ASR 事件

{
"type": "conversation.item.input_audio_transcription.delta",
"event_id": "evt_xxx", "item_id": "item_xxx",
"content_index": 0, "delta": "深圳"
}
{
"type": "conversation.item.input_audio_transcription.completed",
"event_id": "evt_xxx", "item_id": "item_xxx",
"content_index": 0, "transcript": "深圳今天天气怎么样",
"usage": { "type": "duration", "seconds": 2 }
}
usage 字段承载本次 ASR 转写的按秒计费用量(type="duration" + seconds)。
部分能力会携带此字段,其余情况缺省(omitempty);客户端应做字段可选兼容。
transcription.completed.usageresponse.done.response.usage(token 用量)是独立并列的两个计费维度:ASR 按音频转写秒数计费、response 按 token 用量计费。

6.4 助手音频 / 文本事件

{
"type": "response.output_audio.delta",
"event_id": "evt_xxx",
"response_id": "resp_xxx",
"item_id": "item_xxx",
"output_index": 0, "content_index": 0,
"delta": "<base64 pcm16 / opus chunk>"
}
{
"type": "response.output_audio_transcript.delta",
"event_id": "evt_xxx",
"response_id": "resp_xxx",
"item_id": "item_xxx",
"delta": "深圳"
}

6.5 response.done

{
"type": "response.done",
"event_id": "evt_xxx",
"response": {
"id": "resp_xxx",
"status": "completed",
"usage": {
"total_tokens": 123,
"input_tokens": 45,
"output_tokens": 78,
"input_token_details": {
"text_tokens": 20,
"audio_tokens": 15,
"cached_tokens": 10,
"cached_tokens_details": {
"text_tokens": 6,
"audio_tokens": 4
}
},
"output_token_details": {
"text_tokens": 30,
"audio_tokens": 48
}
}
}
}
status 常见取值:completed / cancelled / failed / incomplete
response.usage 承载本轮 response 的 token 用量明细,字段结构与标准 Realtime GA 100% 对齐;能力后端未返回 usage 时该字段缺省(omitempty)。客户端可用于成本观测或计费自查。

6.6 error

{
"type": "error",
"event_id": "evt_xxx",
"error": {
"type": "invalid_request_error",
"code": "unsupported_output_format",
"message": "unsupported opus output rate 44100, allowed: 8000/12000/16000/24000/48000",
"event_id": "客户端触发本 error 的原事件 event_id"
}
}
error 之后如果错误致命,服务端可能会主动关闭连接(例如 pipeline_init_failed / upstream_send_error / upstream_closed / idle_timeout),客户端应做相应处理。

7. 音频格式与采样率

7.1 上行音频(Input)

格式(format.type
说明
允许的采样率
pcm / audio/pcm / pcm16 / pcm_s16le / s16le
16-bit signed LE mono PCM
任意(推荐 16000)
opus / audio/opus
裸 Opus packet(每个 append 携带一个完整 packet)
8k / 12k / 16k / 24k / 48k

7.2 下行音频(Output)

格式
允许的采样率
备注
pcm
任意
服务端会按需做重采样至客户端声明的采样率。
opus
8k / 12k / 16k / 24k / 48k
服务端负责编码为裸 Opus packet;每个 response.output_audio.delta.delta 通常对应一个 20 ms Opus 帧。
兼容 MIME 别名:客户端可以用 audio/pcm / pcm / pcm16 / pcm_s16le 任一形式声明 PCM;用 audio/opus / opus 声明 Opus。服务端会自动归一化。

7.3 服务端下行链路策略

服务端在会话建立时会根据客户端声明的 session.audio.output.format 自动选择最优下行链路(直接透传或服务端重采样 / 格式转换),客户端无感知。
客户端只需保证 format / rate 落在 7.1 / 7.2 表格声明的合法范围内;实际生效的格式会在 session.updated.session.audio 里给出。

8. Function Call 完整流程

8.1 时序



8.2 关键字段

下行 response.function_call_arguments.done
{
"type": "response.function_call_arguments.done",
"event_id": "evt_xxx",
"response_id": "resp_xxx",
"item_id": "item_xxx",
"call_id": "call_abc123",
"name": "get_current_time",
"arguments": "{}"
}
上行 conversation.item.create 回传
{
"type": "conversation.item.create",
"item": {
"type": "function_call_output",
"call_id": "call_abc123",
"output": "2026-08-26 09:30:00"
}
}

8.3 完整可运行示例

参见 测试 Demo 中:
勾选左侧 “启用 Demo Tool” 复选框。
提问 “现在几点了?”。
观察运行日志中完整的 function call 时序。

9. 错误码与握手拒绝

9.1 握手阶段的 HTTP 拒绝码

在 WebSocket 升级完成前的鉴权与并发检查阶段,任一失败会以 HTTP 状态码拒绝:
HTTP 状态
Reason Phrase
触发原因
400
invalid_request: ...
URL 参数缺失 / appid 非法 / model 值非法。
401
invalid_timestamp
timeStampexpired 已过期或格式错误。
401
authentication_failed: code=<n>
CAM 签名校验失败。
403
account_invalid
账户欠费 / 冻结。
429
connection_limit_exceeded: uin=... num=... max=...
并发连接数超上限。
500
authentication_failed: <err>
CAM 后端调用异常。
500
check_user_status_error: <err>
账户状态查询异常。
500
limit_check_error: <err>
并发限流查询异常。
客户端应根据这些状态码提示用户重试 / 联系管理员。

9.2 会话阶段的 error 事件

握手成功后,业务错误通过 JSON error 事件下发:
error.type
error.code
说明
invalid_request_error
invalid_json
消息不是合法 JSON。
invalid_request_error
invalid_audio
上行音频 base64 解码或解码器解析失败。
invalid_request_error
unsupported_output_format
下行 formatpcm / opus,或 opus 采样率不在白名单。
invalid_request_error
not_supported
使用了当前会话能力下不支持的控制事件(如 conversation.item.truncate / transcription_session.update / 不带 instructions 的空 response.create)。
invalid_request_error
session_not_ready
未先发 session.update 就送控制事件(response.create / conversation.item.* 等)。
invalid_request_error
missing_item
conversation.item.createitem 字段。
invalid_request_error
missing_item_id
conversation.item.retrieve / deleteitem_id 字段。
server_error
pipeline_init_failed
服务端会话初始化失败。
server_error
upstream_send_error
服务端向能力后端发送数据失败。
server_error
upstream_error
能力后端返回异常。
server_error
upstream_closed
能力后端主动关闭。
session_expired
idle_timeout
会话空闲超时(默认 30 s,可通过 timeoutSec 覆盖)。

10. 常见问题(FAQ)

Q1: 为什么建连成功后什么事件都没有收到?

本服务采用“客户端主导”模式,握手成功后服务端不主动下发任何事件。
客户端必须先发一条 session.update 完成会话初始化,服务端才回 session.updated(作为“首次可见响应”),此后才能开始送音频。这与标准 GA 规范中 session.created 先行的行为有意不同,为的是更符合“客户端主动”的语义。

Q2: 客户端签名总是 401,如何排查?

优先检查:
1. 本机时钟偏差:TC3 允许 timeStamp 与服务器差异不超过 15 分钟,先用 date -u 校准机器时间。
2. 参与签名的参数集signature 外的所有 query 参数都要参与签名。
timeoutSec / model / resId 只要出现在 URL 上就必须一并进入 canonicalQueryString。
3. 参数排序:keys 必须按字母升序,然后逐个 k=urlEncode(v)& 拼接。
4. canonicalRequest 空行content-type;host 之前必须有一个空行(\\n\\n)。
5. date 使用 UTC:不要用本地时区。

Q3: 上行只能发 JSON 吗?为什么发 Binary 帧被忽略?

是的,本服务与标准 Realtime GA 规范保持一致,只接受 WebSocket Text 帧承载的 JSON 事件。音频通过 input_audio_buffer.append 事件里的 audio 字段以 base64 承载。

Q4: 如何让服务端主动打招呼?

session.updated 之后立即发:
{ "type": "response.create",
"response": { "instructions": "你好, 我是智能助手, 请问需要什么帮助?" } }
所有 model 都支持这种“客户端指定文本,让服务端主动说出”的用法。

Q5: 客户端下行想要 opus 而不是 pcm,怎么做?

session.update.audio.output.format 中声明:
{ "format": { "type": "opus", "rate": 48000 } }
采样率必须是 Opus 白名单:8000 / 12000 / 16000 / 24000 / 48000。返回的 response.output_audio.delta.delta 字段是裸 Opus packet 的 base64(一次 delta 通常对应一个20ms Opus 帧),客户端需用 Opus 解码器还原为 PCM。

Q6: conversation.item.truncate 报 not_supported,如何截断已生成的助手音频?

当前会话若回复了 not_supported,说明实际生效的 model 未提供原生的 truncate 能力。推荐替代方案
打断当前 TTS:发 response.cancel
清空已下发但未播完的音频:发 output_audio_buffer.clear,服务端会一并下发 output_audio_buffer.cleared,客户端本地清空播放调度即可。

Q7: 单次会话最长多久?

本服务不限制会话总时长,只做空闲超时:默认30秒无收发消息即断开。
可通过 URL 参数 timeoutSec 覆盖,上限120秒。心跳发法建议:客户端在语音间歇期偶发送20ms的静音 PCM input_audio_buffer.append 即可保活。

Q8: 有 Python / Java / iOS / Android 的 SDK 吗?

本服务对外协议 100% 兼容 标准 Realtime GA WebSocket API,因此任何按标准 GA 规范编写的客户端 SDK 都可以直接接入,只需改动:
WebSocket URL 换成本服务的 URL。
增加 TC3-HMAC-SHA256 签名逻辑。
session.audio.output.format 使用本服务支持的格式。

Q9: 如何使用加密音色 ID?和明文音色可以混用吗?

session.audio.output.voice 字段同时支持明文音色 ID 与加密音色 ID,服务端会自动识别并处理:
明文音色 ID:直接使用。
加密音色 ID(由音色加密服务生成):服务端通过内部解密接口换回真实音色 ID 后再使用。
Fallback 策略:以下情况都会静默降级到服务端配置的默认音色,不会中断会话建立:
1. 服务端解析异常 / 超时(内部会重试一定次数,仍失败则 fallback)。
2. 明文音色 ID(服务端无需解密,直接使用)。
3. 音色 ID 不适用于当前 model(例如把一个 model 下的专用音色传给了另一个 model)。
4. 音色 ID 不在当前 model 可用音色集内。
因此客户端可以放心把任何合法音色 ID 直接填进去,服务端会尽力解析并给出最好的结果;只有当所有路径都失败时才用服务端默认音色。

Q10: response.done.response.usagetranscription.completed.usage 分别怎么计费?

两个字段承载两个独立并列的计费维度:
conversation.item.input_audio_transcription.completed.usage
= 本次 ASR 转写的按秒用量(type="duration" + seconds);部分能力会携带此字段,其余情况缺省。
response.done.response.usage
= 本轮 response 的按 token 用量明细(total_tokens / input_tokens / output_tokens / *_token_details);能力后端未返回 usage 时该字段缺省。
客户端可用于本地成本观测;实际计费以账单为准。

附录:完整可运行 Demo

浏览器 Demo测试 Demo (单文件,零依赖,单击打开即可)。