适用版本:tcpv2.5+(duplex 分支)
##START / ##END 包裹的定长头 + 消息 类型 + payload)与 tcpV2.5.md 完全一致,只是通过认证参数里的 mode:duplex 切换到这条链路。核心差异:LISTEN start 作为音频门控,之后不再有 LISTEN stop,也没有"一轮对话"的概念。task_id 或发任何控制消息。task_id 降级为会话常量:全程固定为 duplex00,seq 单调递增。TEXT(下行)时序反转:其他模式下 TEXT 先于音频到达(预告),duplex 下 TEXT 是追认,晚于音频到达(实测约 0.8~1 秒)。| manual | vad(auto) | duplex | |
|---|---|---|---|
| 语音结束判定 | 客户端发 END_FRAME | 服务端 VAD 检测静音 | 服务端按输出能量静默判定 |
task_id | 客户端生成,按轮次变化 | 客户端生成,按轮次变化 | 固定为 duplex00 |
LISTEN start(下行) | 无 | 每轮开始 | 仅一次,作为音频门控 |
LISTEN stop(下行) | 无 | 语音结束时下发 | 没有这个消息 |
| 打断方式 | 客户端发新 LISTEN start | 客户端发新 LISTEN start | 无需客户端动作 |
TEXT 到达时序 | 先于音频 | 先于音频 | 晚于音频 |
SPEAK 消息 | 支持 | 支持 | 不支持,返回 ##ERROR |
| 方式 | 地址 | 适用场景 |
|---|---|---|
| 直连 TCP | 203.208.134.27:8007 | 已有语音网关/嵌入式设备,自行编解码二进制帧 |
| WebSocket 网关 | wss://api.adventists.ai/duplex/ws | Web / App,字节流原样透传,语义与直连 TCP 一致 |
https://adventists.ai/demo/duplex/mode:duplex:{JWT_TOKEN}##mode:duplex##format:pcm##input_audio_format:pcm{ "token": "eyJ..." }POST https://api.adventists.ai/duplex/init(body {})创建一个测试角色,响应 { "code": 201, "data": "<npcid>" }。STATUS,消息类型 5):成功:##INFO:认证成功,NPCID: <npcid>, 模式: duplex
失败:##ERROR:<原因>LISTEN start 门控{"taskid": "duplex00", "type": "listen", "state": "start", "mode": "duplex"}LISTEN。task_id(duplex00)发送 AUDIO_FRAME(消息类型 2),必须持续发送、包括静音——断流会被服务端解读为没有声音输入。AUDIO_FRAME(下行持续音频流,空闲是静音帧)、TEXT(追认文字,晚于音频)、END_FRAME(一段话说完)。用户随时开口即视为打断,服务端自动停止旧内容、开始新内容,客户端无需任何额外动作。| 参数 | 说明 | 默认 |
|---|---|---|
mode | 必须是 duplex | — |
format | 下行音频编码,pcm(推荐)或 opus | pcm |
input_audio_format | 上行音频编码 | pcm |
voiceid | 强制指定音色(不填则用角色配置的默认音色) | — |
resolution | 摄像头画面分辨率提示(纯文本标注) | — |
[##START][MessageType:8][task_id:8字节][seq:4字节][JSON消息体][##END]{"taskid": "duplex00", "type": "listen", "state": "start", "mode": "duplex"}LISTEN start/stop 不会产生任何效果,服务端仅回一条 ack:##INFO:LISTEN start ignored in duplex mode| 方向 | 采样率 | 编码 | 通道 | 帧长 |
|---|---|---|---|---|
| 上行 | 16000 Hz | PCM16(小端)或 Opus | 单声道 | 建议 60ms 一帧 |
| 下行 | 16000 Hz | 跟 AUTH 里的 format 一致 | 单声道 | 约 100ms 一帧,持续不断 |
TEXT,消息类型 4)TEXT 是"AI 已经说了什么"的追认,晚于对应音频到达(实测约 0.8~1 秒,按整句聚合后才发)。做口型同步或字幕同步的客户端不能沿用"先显示文字再等音频"的写法,字幕会明显滞后于声音;TEXT 只适合当作事后补充的文字记录。##INFO:prompt:<识别出的用户原话>STATUS 通道(消息类型 5),语音识别结果的文本形式。{ "status": "ok", "path": "http://.../xxx.jpg" }status 字段。建议每 5 秒上传一帧,跟对话音频完全解耦。客户端 -> 服务端: AUTH (##mode:duplex...)
服务端 -> 客户端: STATUS ##INFO:认证成功,NPCID: <npcid>, 模式: duplex
客户端 -> 服务端: MCP register ← 可选,端侧工具注册,见 mcp-protocol.md
服务端 -> 客户端: LISTEN {"state":"start"} ← 收到这条才可以开始推音频
客户端 -> 服务端: AUDIO_FRAME ...(持续推流,包括静音)
服务端 -> 客户端: AUDIO_FRAME / TEXT / END_FRAME ...##INFO:delegation:start:<内部任务id>
##INFO:delegation:end:<内部任务id>AUDIO_FRAME/TEXT 通道到达,这两条纯粹用于显示"正在处理"的等待动画,不处理也不影响功能。参考实现:../website/demo/duplex/duplex-demo.js 的 showCallingBubble/hideCallingBubble。[AI 正在说话,持续下发 AUDIO_FRAME]
客户端 -> 服务端: AUDIO_FRAME ...(用户开始说话,正常持续推流即可)
服务端检测到用户说话,自动打断
服务端 -> 客户端: AUDIO_FRAME ...(新内容,旧内容的下发就此停止)服务端 -> 客户端: AUDIO_FRAME ...(说话中)
服务端 -> 客户端: TEXT ...(追认这句话说了什么,晚于对应音频)
服务端 -> 客户端: END_FRAMEEND_FRAME 只表示"这一段话说完了",不代表整场对话结束——duplex 下 task_id 是会话常量,分段信号统一看 END_FRAME。| 类型 | 值 | duplex 下的语义 |
|---|---|---|
| AUTH | 1 | 认证消息,需带 ##mode:duplex |
| AUDIO_FRAME | 2 | 音频帧,双向持续流(含静音) |
| END_FRAME | 3 | 仅下行有意义:一段话说完;上行发送被忽略 |
| TEXT | 4 | 仅下行:追认文字,晚于对应音频到达 |
| STATUS | 5 | 认证回执 / 错误 / 用户原话回显 / 委派开始结束提示 |
| MCP | 6 | 端侧工具注册(认证成功后约 1 秒窗口内) |
| SPEAK | 7 | 不支持,返回 ##ERROR |
| LISTEN | 8 | 仅下行会发一次 start 作为音频门控;上行发送被忽略 |
| EMOJI | 9 | 与其他模式一致,见 Emoji 模式对接指南 |
LISTEN start 没反应?LISTEN 会被忽略(只回一条 ack),轮次和打断完全由服务端判定,删除这部分客户端逻辑即可。LISTEN start?AUTH 后收到 ##ERROR: 并断连?/duplex/login 换新 token。TEXT 本来就滞后于音频,不要用它驱动实时口型/字幕同步。SPEAK 让角色说指定内容吗?##ERROR:duplex 模式暂不支持 SPEAK。status:"fail" 但 HTTP 是 200?status 字段。