LabHub
开始
学习 学习路径 课程

语音 AI 智能体 — 会听、会查、会说的流水线

说完了吗 — 用 VAD 切分话轮并找出插话

在 LabHub 中继续学习

目标

把能量 VAD 和神经网络 VAD 用于同一段通话,统计误报,制作把 VAD 区间合并成轮次的规则,并在第一次见到的通话上测试。在混有回声的麦克风上,找出人插话的时刻。

为什么重要

端点判定的延迟,是即使把模型做得再快也不会减少的回答延迟。设得短,会打断说话的中途,设得长,就慢。本实验的通话(/opt/lab/fixtures/voice/vad/call_a.wav)是把合成的句子片段放在固定的时刻做成的,所以“实际说话的区间”就在 call_a.truth.json 中。评分器会在你没有见过的第二段通话上重新运行你的 turns.py——照着一个文件调阈值的代码,会在那里栽跟头。

步骤

  1. 把连接 30 ms(480 个采样点)帧的 dBFS 大于 -35 的区间得到的能量 VAD 结果,写入 /root/voice/vad/energy.json。
  2. 把 silero VAD(voicekit.models.vad(threshold=0.5, min_silence=0.1, min_speech=0.1),每次输入 512 个采样点)的区间,写入 /root/voice/vad/silero.json。
  3. 把两个结果与 call_a.truth.json 的 speech_segments 重叠,统计 false_alarms 和 missed,生成 /root/voice/vad/score.json。
  4. 创建带有 detect_turns(samples, sr, gap_s) 的 /root/voice/vad/turns.py——把 silero 区间中间隔短于 gap_s 的连接起来,返回 [(시작, 끝), …](占位符依次为开始、结束)。
  5. 创建在 gap 为 0.2 · 0.3 · 0.5 · 0.8 · 1.2 秒时统计 call_a 轮次数的 /root/voice/vad/sweep.json。
  6. 对轮次数正确的 gap 中最短的值,把每个轮次的回答延迟(VAD 看到的话尾 + gap)− 实际话尾,写入 /root/voice/vad/latency.json。
  7. 从 /opt/lab/fixtures/voice/bargein/mic.wav 中估计并减去 playback.wav 的回声延迟和比例,保存 /root/voice/vad/residual.wav,并把原始录音和减去之后的第一个说话时刻,写入 /root/voice/vad/bargein.json。
  8. 创建汇总了前面步骤数字的 /root/voice/vad/report.json。

参考

能量 VAD——大声音都算说话

把 /opt/lab/fixtures/voice/vad/call_a.wav 切成 30 ms(480 个采样点)的帧,把 dBFS 大于 -35 的帧连续出现的区间,以 [{"start_s": …, "end_s": …}](保留两位小数)写入 /root/voice/vad/energy.json。末尾不足的片段舍弃。

帧 i 的开始是 i×480/16000 秒。说话帧开始时记住帧号,并在结束帧的开始时刻关闭。voicekit.dbfs 会给出 RMS dBFS。

silero VAD 的区间

把 call_a 每次 512 个采样点输入 voicekit.models.vad(threshold=0.5, min_silence=0.1, min_speech=0.1),执行 flush() 之后,把得到的区间以相同的形式写入 /root/voice/vad/silero.json(开始 = front.start ÷ 16000,结束 = 开始 + len(front.samples) ÷ 16000)。

区间累积在 VAD 内部的队列里。读取 front 并执行 pop(),直到 empty() 为真。如果不调用 flush(),在文件末尾仍然打开的区间就不会出来。

与标准答案重叠,统计误报和漏检

把 call_a.truth.json 的 speech_segments 作为标准答案,把 energy 和 silero 各自的 false_alarms(与标准答案完全没有重叠的预测区间数)、missed(与任何预测都没有重叠的标准答案区间数),写入 /root/voice/vad/score.json。

两个区间 (a0,a1)·(b0,b1) 重叠 = a0 < b1 and b0 < a1。能量 VAD 是怎样处理 6 秒处的敲门声的呢?

把 VAD 区间合并成轮次

在 /root/voice/vad/turns.py 中创建 detect_turns(samples, sr=16000, gap_s=0.5)。依次查看 silero VAD(threshold 0.5 · min_silence 0.1 · min_speech 0.1)的区间,如果与前一个轮次结束的间隔短于 gap_s,就连接起来,否则开始新的轮次,返回 [(시작 초, 끝 초), …](占位符依次为开始秒数、结束秒数)。评分器会以 gap 0.5 同时运行 call_a 和第一次见到的通话。

把切细(VAD)和合并(gap)分开,就可以只改变 gap 来做实验。运行代码请放在 if name == 'main': 之下——评分器会把这个文件作为模块导入。

遍历 gap——切断与粘连

用 turns.py 在 gap 为 0.2 · 0.3 · 0.5 · 0.8 · 1.2 秒时统计 call_a 的轮次数,以 {"0.2": {"turns": n}, …} 的形式写入 /root/voice/vad/sweep.json(键是字符串)。

标准答案的轮次,在 truth 的 turns 中有 3 个。如果轮次比这更多,就是在说话中途的停顿处切断了;如果更少,就是把其他人的发言也连了进来。

等待交出轮次所花的时间

在第 05 步中,从轮次数与标准答案相同的 gap 中选出最短的值作为 gap_s,把每个轮次的 (VAD 가 본 턴 끝 + gap_s) − 실제 턴 끝(占位符依次为 VAD 看到的轮次结束、实际轮次结束)写入 per_turn_delay_s,把它们的平均值写入 mean_delay_s,生成 /root/voice/vad/latency.json。

判定轮次已经结束的那一刻,是 VAD 看到话尾之后又安静了 gap 那么久的时候。实际的轮次结束在 truth 的 turns 中。

去掉回声,找出插话的时刻

用 /opt/lab/fixtures/voice/bargein/mic.wav(用户 + 扬声器回声)和 playback.wav(我们发出的声音),在最初的 1.5 秒内求出互相关最大的延迟(0–200 ms)和最小二乘比例,保存减去回声之后的 /root/voice/vad/residual.wav。在 /root/voice/vad/bargein.json 中写入 echo_delay_ms、echo_gain、naive_first_speech_s(在原始麦克风上 silero 第一次判为说话的时刻)和 barge_in_s(在 residual 上第一次判为说话的时刻)。

对每个延迟 d 测量 np.dot(mic[d:], play[:n-d]),选出最大的 d。比例是 g = (mic·ref)/(ref·ref)。只使用用户开口之前的区间,人的声音才不会让估计变得模糊。

报告

在 /root/voice/vad/report.json 中抄写 energy_false_alarms、silero_false_alarms(score.json)、gap_s、mean_delay_s(latency.json)、turns_call_a(标准答案的轮次数)和 barge_in_s(bargein.json)。

读取前面步骤的文件,原样抄写即可。如果手写,四舍五入会不同而导致错位。