MiniMind-O实时打断是如何实现的?VAD驱动的近似双工交互全解析
【免费下载链接】minimind-o🎙️ A 0.1B Omni model trained from scratch, capable of listening, speaking, and seeing!项目地址: https://gitcode.com/gh_mirrors/mi/minimind-o
MiniMind-O 是一个仅 0.1B 参数的端到端 Omni 模型,支持文本、语音、图像输入与流式语音输出。它的实时打断(barge-in)机制并不复杂:用 Silero VAD 以 64ms 为窗口持续监听麦克风,一旦检测到"模型正在说话 + 用户正在说话",就立即中止当前生成、停止本地播放,让系统从 speaking 状态退回 listening 状态——整个过程无需语义理解,纯工程闭环,延迟极低。这就是所谓"近似双工":模型在说话的同时始终"听得见"你。
为什么"能被打断"是语音助手的关键体验?
传统语音助手是回合制的:你说完 → 等模型答完 → 再轮到你。这种模式下你无法中途改口、追问或说"停"。
真正自然的对话要求双工能力,即双方可以同时收发语音。完整双工(如 Moshi 的内外双通路)实现复杂,而 MiniMind-O 选择了一条更务实的路线:
- 听:VAD 始终在线,持续接收麦克风音频
- 说:Talker 流式生成 24 kHz 语音并即时播放
- 打断:两者冲突时(用户插话),立刻 abort 生成
官方实时交互时序图完整展示了这一闭环:
上图下半部分 "Barge-in turn-taking" 正是本文要拆解的核心:用户第 1 句说完后进入 silence,模型 prefill 并开始 reply;当用户在模型说话期间再次开口(speech_start),系统触发 interrupt、abort 当前回复,随后重新 prefill 生成新回复。
架构定位:VAD 是"零耦合"的纯工程层
在 model/model_omni.py 中,实时打断相关代码被明确标注为"与模型本体零耦合,纯工程层":
SileroVAD(model/model_omni.py):用 ONNX Runtime 加载 model/vad/silero_vad.onnx,输入 16 kHz 音频块,输出"当前块是语音"的概率RealtimeSession(model/model_omni.py):状态机,管理 listening / speaking / generating / interrupt 四个标志位,并缓存用户语音
模型本体的 Thinker–Talker 双路径架构(Thinker 负责语义理解,Talker 通过 MTP 预测 8 层 Mimi codes)见下图,打断逻辑完全发生在模型之外:
实时打断是如何实现的?4 个关键步骤
① 以 64ms 小窗口持续喂给 VAD
浏览器端把麦克风音频重采样到 16 kHz,按 4096 采样(256ms)打包成 Int16 二进制帧,通过 WebSocket 发送到服务端/ws/realtime路由。服务端RealtimeSession.push_chunk再把数据切成1024 采样 ≈ 64ms的小窗口,逐块跑 VAD:
概率 > 0.8(threshold)→ 判定为语音;否则视为静音
小窗口是低延迟打断的关键——用户开口后最多 ~64ms 就能被检测到。
② 环形缓冲:保住语音开头的每一毫秒
人在开口瞬间,第一两个窗口往往还没超过阈值(音量渐强)。RealtimeSession用一个 1 帧的ring环形缓冲记住"开口前"的最后 64ms 音频,一旦判定开始说话,就把它拼回缓冲头部(self.buffer = self.ring + self.buffer)。这避免了每个句子的第一个音节被切掉。
③ 打断判定:generating × speaking = interrupt
打断逻辑就一行核心判断(model/model_omni.py):
如果
self.generating and self.speaking(模型正在生成语音,且用户正在说话)→ 置interrupt = True,返回'interrupt'
服务端生成循环在每个 token 步调用poll_interrupt(),一旦发现队列里有音频块触发打断,立即break退出run_generate,不再 flush 剩余音频,并通过{'type': 'done', 'interrupted': True}通知前端。
④ 语音结束判定:800ms 静音才交棒
反过来,用户说完话时,VAD 需要连续800ms(min_silence_ms)静音才返回speech_end,避免句内自然停顿被误判为说完;同时tail_silence会把尾部冗余静音从缓冲中裁掉,让送入模型的音频更紧凑。之后RealtimeSession.get_audio()交出完整语音,进入 prefill–reply 流程。
前端配合:打断"听起来"像真的
打断只靠服务端 break 是不够的——扬声器里已经播出的声音必须也立刻停掉。前端 webui/web_demo.html 用一条 WebSocket 消息流串起完整状态机:
| 服务端消息 | 前端动作 |
|---|---|
vad+ speaking=true | 立即stopPcm()停止助手播放,切到 listening 状态 |
generating | 进入 thinking,清空本轮缓冲 |
pcm | 追加播放 24 kHz 语音块 |
done+ interrupted=true | 再次stopPcm(),彻底掐断残留声音 |
配合 UI 上的 idle → listening → thinking → speaking 四态切换,用户能清晰感受到"一开口,它就闭嘴了"。
近似双工的另一半:低延迟流式语音
打断之所以能"无缝",还因为语音链路本身够快:
- TTFT ≈ 140ms / TTFA ≈ 260ms(见实时交互时序图上半场)
- 文本 token 一边生成,Talker 一边通过 MTP + 延迟调度(+7)补齐 8 层 Mimi codes
- Mimi 解码器按4 帧(约 320ms)一小块增量解码出波形,重叠 2 帧缓解块边界断裂,播放不必等回答结束
可调参数与当前局限
想调优打断体验,主要动这几个参数:
threshold = 0.8(model/model_omni.py):VAD 语音概率阈值,调高更不容易误打断,调低更灵敏min_speech_ms = 128/min_silence_ms = 800:最短语音 / 判停静音时长--audio_chunk_frames(webui/web_demo.py):播放块大小,4 为低延迟默认值,播放卡顿可调大到 8/12
需要说明的是,当前打断检测仍是简单 VAD 阈值,还谈不上语义级打断——模型不会判断"用户这句话是不是冲自己说的",背景人声也可能触发 abort。这正是官方在实时交互章节中承认的改进方向。但就工程闭环而言,从 speaking 退回 listening 再处理下一轮输入的路径已经完整跑通,这也是理解 Moshi、Mini-Omni 等双工系统前一个足够透明、可以逐行读懂的基线。
【免费下载链接】minimind-o🎙️ A 0.1B Omni model trained from scratch, capable of listening, speaking, and seeing!项目地址: https://gitcode.com/gh_mirrors/mi/minimind-o
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考