实时语音交互 SDK
Hanzora · 可嵌入的数字人 SDK
面向数字孪生大屏的实时语音交互 SDK,本地唤醒、回声保护与工具编排驱动三维场景。
一个面向数字孪生大屏的实时语音交互数字人系统:用户说话,数字人应答,并驱动飞渡三维场景(导航到某栋楼、切换天气特效、打开业务面板)。从浏览器端 SDK、数字人推理运行时到多 MCP 智能体平台,由我一人主导——下面重点讲我负责的 SDK,后端平台见技术笔记两篇。
wake-activated(唤醒)、pre-roll-flush(预录缓冲,1500ms)、echo-guard(回声保护的三态流转)、speak-start / playback-end(说话生命周期)。这些事件就是本文提到的状态机在真实运行时吐出来的东西。三层接入,一条内核
对外发布三个入口:hanzora/vue 与 hanzora/react 两个框架组件,以及底层的 RuntimeEngine(供自定义渲染器与高级集成使用)。组件模式只需要 baseUrl 加 agentId,其余配置在本地同步装配完成——挂载之前没有任何网络请求。
规模:TypeScript 约 1.95 万行(143 个模块),测试约 1.8 万行(98 个文件、773 个用例),CI 强制四项 80% 覆盖率门禁。
把不可信数据挡在协议边界
后端会随时新增遥测字段而不通知 SDK。所以每条流式消息都经过运行时校验,并把结果分成四路互斥的形态:已知 / 契约漂移 / 未知类型 / 格式错误,消费方按 kind 强制分流,绕不过去。
严格校验会让客户端因为后端的善意升级而失败;完全不管又会把脏数据带进业务。允许未知字段透传、但强制显式分流,是这两者之间的中间解。
用类型系统消灭状态类 bug
六台声明式状态机共用一套查表式抽象:非法迁移是 no-op(由上层决定记日志还是忽略,而不是抛异常炸掉整条链路)。所有状态写入收敛到唯一入口,且迁移原因必须是强类型联合的成员——新增一个原因却不同步改类型,编译就报错。
对外暴露的视图状态由一个纯函数从五台底层状态机投影成七个值(空闲 / 连接中 / 聆听 / 思考 / 说话 / 取消中 / 错误),UI 只订阅这个投影,不直接读底层状态。
浏览器里的语音唤醒
关键词唤醒完全跑在浏览器端:ONNX 模型经 WASM 在独立 Worker 内推理,配合 AudioWorklet 与共享内存传递音频,模型随 npm 包一起分发。唤醒延迟低于 200 毫秒、音频不出本机、服务端零依赖——这是选型时的硬约束(不引入任何商用语音 SaaS)。启动时检测跨源隔离条件,不满足则标记为「不支持」并优雅降级,不抛错也不影响其他功能。
交互上做了两件容易被忽略的事:终止性打断(随时可以打断正在说话的数字人)与回声保护(数字人自己的声音不会被自己的麦克风当成新指令),二者都不依赖麦克风音量阈值,而是由播放时序确定性驱动。
平台侧的语音指令理解用一个小模型完成(见技术笔记《用一个 1.7B 模型替掉 27B》)。
竞态不靠运气解决
实时语音交互里最贵的是竞态:打断与流式响应并发、播放器连接与断开交错、音频上下文重复创建导致麦克风指示灯关不掉。这些都有对应的确定性防护——跨模块统一的轮次标识、并发打断聚合后只触发一次、停止操作幂等且合并并发调用、音频上下文按引用计数释放。每一条在变更记录里都能找到对应的回归测试。
它身后的后端
SDK 对接两层后端:一层是数字人推理运行时(音频驱动口型视频、语音合成、语音识别、微调后的小模型对话),一层是多 MCP 智能体平台(意图理解、工具编排、把"说一句话"变成三维场景里的动作)。两层都由我实现,共约 4.8 万行 Python,其中智能体内核占 1.6 万行、编排图有 33 个节点。
其中两个设计我单独写了笔记:《五层意图漏斗》讲怎么在 100 毫秒内判断用户想干什么;《当框架不报错,只是什么都不做》讲一次"功能上线了但全部未命中"的排查。
一次编排式代码审计
2026 年 6 月的 SDK 审计先分层测绘,再做多维度评审,最后逐条读代码验证。报告记录原始 80 条发现中保留 74 条、0 项 critical,同时列出组件接入面、无障碍和边界健壮性的后续改进项。并发打断、麦克风释放与媒体重连等已完成修复,可在当前源码和回归测试中核对。
四个问题,展开看实现与验证
- 两路语音打断,为什么只能关闭一次:KWS 与流式识别并发到达时,合并关闭流程并约束副作用次数。
- 数字人说完之后,什么时候重新开麦:播放尾部缓冲、服务端取消与轮次关联怎样共同决定监听恢复。
- 页面离开了,麦克风为什么还亮着:采集释放、共享音频上下文与旧媒体连接的异步清理。
- 服务端多了一个字段,SDK 应该怎么办:协议校验的四路分流,以及原始消息透传的兼容边界。
(SDK 以 MIT 协议发布;本文只描述通用技术架构,不含任何客户信息。)
本文数据来自生产实践,已隐去客户与内网信息。