数字人说完之后,什么时候重新开麦

工程与部署 · 2026-10-10 · 5 分钟

区分服务端收尾、播放进度和客户端尾部缓冲,用三态回声保护与轮次关联恢复语音监听。

实时数字人对话里,重新开麦的时刻会直接影响下一轮输入。开得太早,扬声器里剩余的尾音可能进入识别;开得太晚,用户接着说的话又可能错过。

Hanzora 的早期回声保护依赖静音等待和守护期。后续实现移除了音量检测依赖,改为跟随播放进度和客户端尾部缓冲。项目还修过另一条独立路径:服务端取消播报后,唤醒词监听没有恢复。

服务端收尾不代表浏览器已经播完

speak:end 表示服务端正在收尾,SDK 保留它作为可观察事件,不直接用它恢复语音识别。

回声保护跟随 subtitle:playback-end,再等待 tailRenderBufferMs。字幕播放进度到达末尾时,音频可能仍在客户端播放链路的缓冲中;这段等待用于覆盖尾音。

这个参数是时序补偿配置。项目代码和逻辑测试能够验证「等待期间不恢复」,实际应该等待多久,仍要结合浏览器、扬声器和传输条件测量。

把保护期写成三种状态

EchoGuardOrchestrator 使用三个明确的阶段:

阶段 进入条件 处理
idle 尚未播报,或本轮收尾完成 等待唤醒词,或保持正常监听
muted 收到 speak:start 暂停 ASR 推流,进入可打断的关键词模式
tail_buffer 播放进度结束且需要尾部等待 继续保持保护,等待缓冲窗口结束

尾部定时器记录创建时的 speechCycle。执行时发现已经进入新的播报轮次,就跳过旧回调,避免上一轮的收尾把新一轮监听状态改掉。

正常连续对话可以重新激活 ASR;一次性指令或用户主动打断则恢复唤醒词监听,等待下一次明确唤醒。两种恢复动作承担不同的交互语义。

取消必须有自己的出口

服务端取消不一定产生正常的播放结束事件。如果只等 subtitle:playback-end,保护状态就可能一直留在 muted,关键词表也仍处于打断模式。

修复后单独订阅 speak:cancelled:取消尾部定时器、消费本轮待处理模式、回到 idle,并恢复唤醒词。已经处于 idle 时再次收到取消,直接忽略,避免重复恢复。

客户端轮次与服务端任务不能靠名称相等

客户端生成的唤醒轮次标识与服务端 task_id 可能不同。仅用字符串关联,就可能丢掉本轮「一次性指令」的模式,错误地进入连续监听。

当前实现优先用运行时共享的 TurnEpoch 关联模式与播报,缺少 epoch 时再回退到任务标识匹配。回归测试故意给客户端和服务端不同名称,检查第一轮一次性指令只恢复唤醒词,第二轮普通对话才重新激活 ASR。

本次重跑回声保护的 4 个测试文件、42 项用例,以及 TurnEpoch 的 4 项基础用例,全部通过。覆盖尾部等待、旧定时器失效、取消恢复和轮次关联;测试使用模拟事件与计时器,没有测量真实环境的回声抑制率。

本文依据 Hanzora SDK 的 EchoGuardOrchestrator.ts、EchoGuardTransitionTable.ts、turnEpoch.ts 与对应测试整理,核对日期为 2026-10-10;项目见 Hanzora。