全双工语音通信
ctx.duplex 让同一通电话在播放语音时持续接收客户音频。客户说话可以打断当前播报,识别结果随后驱动下一句回复。它适合需要多轮对话和插话的业务;已注册的 duplex-ai-chat-demo-business 展示了完整流程。
链路如何工作
ctx.duplex.start() 在当前 FreeSWITCH 通道上启动独立的 media bug,并让 mod_audio_fork 连接全双工网关。网关把客户音频转发给 ASR,再把识别事件送回 FreeSWITCH;callflow-esl 从 ESL 事件收到 onTranscription 和 onFinal。
业务调用 ctx.duplex.speak() 时,控制器先通过 uuid_broadcast 打开 audio_fork:// 播放目标,再用 uuid_audio_fork send_text 把话术交给网关。网关向 TTS 请求 PCM 流,经原有 WebSocket 回送下行音频;FreeSWITCH 播放它时仍继续推送客户上行音频。下行结束时网关发送零长音频帧,控制器以匹配当前播放目标的 PLAYBACK_STOP 确认自然播完。
这里的 audio_fork:// 是全双工会话的播放目标。普通单呼 TTS 的流式 profile 也可以使用同名播放前缀,但它通过 callflow-esl 直接创建 TTS 流链接;两条调用链见 TTS 对接。
插话与识别回调
全双工网关只在当前句允许打断、且正在播放时判定插话。bargeInTrigger 有三种选择:
| 值 | 打断时机 |
|---|---|
speech-start | 收到 VAD 开嗓事件或识别事件中的语音活动信号 |
partial | 收到达到网关 bargeIn.minTextLength 的识别文本;当前配置选用此值 |
final | 收到达到最小文本长度的句末结果 |
命中后,网关取消当前 TTS 流并发送 kill_audio;控制器停止当前播放句柄、触发 onBargeIn,使正在等待的 ctx.duplex.speak() 返回 interrupted: true。interruptible: false 可用于不希望客户插话打断的单句,例如告别语。业务也能用 ctx.duplex.interrupt(reason) 主动停止当前播报。
onTranscription 接收 partial 和 final,onFinal 只接收 final。业务通常在 onFinal 中决定回复,但回调本身不等待异步播报;连续 final 应放进串行任务队列,避免多句回复并发抢占。ctx.duplex.speak() 返回 completed、interrupted 和 durationMs;超时或下发失败时也可能返回 completed: false, interrupted: false,业务应按结果决定是否继续。精确签名和事件字段见 组件 API。
业务接入
- 按 服务目录 启动并检查
audio_fork_server,确认 FreeSWITCH 能访问网关的 WebSocket 地址。网关自身需要能访问 ASR 与 TTS;它们的地址由speech-gateway/audio-fork-server/config.toml配置。 - 在
apps/callflow-esl/config.toml启用[duplex],把wsUrl指向网关的/audio。字段和当前示例值见 配置参考。[audioFork]是普通hear/callHear链路,不作为全双工网关地址。 - 在业务接通后调用
ctx.duplex.start(options, handlers),用ctx.duplex.speak()播报,用onFinal处理识别结果,并在业务退出时调用ctx.duplex.stop()。start()返回的DuplexSession包含通道 UUID、media bug 名称和启动时的状态快照;播放与停止仍通过ctx.duplex调用。
全双工激活期间,普通 ctx.speak()、ctx.hear() 和 ctx.callHear() 不可混用。启动全双工前也不能有正在运行的 hear 或 callHear;如需切回普通组件,先停止全双工会话。通话结束时 SessionRunner 还会兜底停止全双工链路。
可直接从 src/business/demo/duplex-ai-chat-demo-business.ts 查看接通、录音、final 串行处理、告别挂机和 finally 清理的完整实现。启动网关和 callflow-esl 进行联调后,请在任务结束前停止本次启动的服务。
排查入口
| 现象 | 优先核对 |
|---|---|
start() 报未启用或缺少地址 | [duplex].enabled 和 [duplex].wsUrl;网关 GET /health |
日志出现 duplex_ws_url_unidirectional_asr_warning | wsUrl 是否误指向单向 ASR 的 10096 或 10099;该检查只告警,不阻止启动 |
| 有识别结果但播报未结束 | 网关到 TTS 的连接、PCM 流和 FreeSWITCH 的 PLAYBACK_STOP;关联 duplex_speak_dispatched 与 duplex_speak_completed 日志 |
| 播报未被插话打断 | interruptible、bargeInTrigger、网关 bargeIn.enabled 与 minTextLength,以及 ASR 是否输出相应的 VAD 或文本事件 |
| 普通播报或识别组件报互斥错误 | 先 await ctx.duplex.stop(),再启动普通组件 |
网关健康检查地址在 [duplex].healthUrl 中提供给部署和联调使用;ctx.duplex.start() 直接发起 FreeSWITCH 命令,不会先调用该 HTTP 健康检查。