ASR、VAD 与端点检测
正在读取学习记录…
ASR 是自动语音识别,把声音变成文字;VAD 是语音活动检测,判断这一小段声音里有没有人在说话;端点检测判断一句话什么时候说完。三者连起来,语音 Agent 才知道何时收集输入、何时开始回答。像面对面交谈时等待对方停顿:停顿太短就抢话,太长则反应慢。
把连续音频切成20毫秒小帧,某帧能量超过阈值就标为有声。毫秒常写作 ms;1000毫秒等于1秒,所以20毫秒是0.02秒。发现有声后,连续两帧静音触发端点。本例有五帧:静音、有声、有声、静音、静音,累计时间是5 × 20 = 100毫秒,也就是0.1秒。读完第五帧后结束收音,再把这段音频交给ASR识别文字。
从输入到可核对结果
示例生成具有标准 WAV 头和16位单声道样本的人工振幅夹具;样本用于能量分段,不含人声词句。wave 读帧,struct 把二进制字节还原为有符号整数。每帧160个样本、采样率8000次每秒,帧长为160÷8000秒,即20毫秒。
energy 取绝对振幅的平均值,大于500时重置静音计数。active 记住前面是否出现有声,避免开场静音就误触发句子结束;只有active后才累计silence。实际VAD还要区分背景噪声与人声,简单能量门限在嘈杂环境里容易误判。Python wave解释WAV读写参数;实时VAD文档说明静音检测与语义端点的取舍。
| 帧结束时刻 | 平均振幅 | 判断 | 连续静音计数 |
|---|---|---|---|
| 20ms | 0 | 开场静音 | 0 |
| 40ms | 1200 | 有声 | 0 |
| 60ms | 1200 | 有声 | 0 |
| 80ms | 0 | 停顿 | 1 |
| 100ms | 0 | 触发端点 | 2 |
保存并运行这两个文件
在 PowerShell 创建练习目录:
New-Item -ItemType Directory -Force l9-asr
cd l9-asrmacOS 终端:
mkdir -p l9-asr
cd l9-asr把下面第一段完整代码保存为 endpoint_lab.py,第二段保存为 test_endpoint_lab.py,两份文件放在这个目录。
import io
import struct
import wave
SILENCE_FRAMES = 2
RATE = 8000
FRAME_SAMPLES = 160
def fixture_wav():
# 人工振幅夹具:静音、有声能量、有声能量、静音、静音。
# 生成真正的16位单声道WAV,数值用于测试分段,不包含人声词句。
amplitudes = [0, 1200, 1200, 0, 0]
memory = io.BytesIO()
with wave.open(memory, "wb") as output:
output.setparams((1, 2, RATE, 0, "NONE", "not compressed"))
for amplitude in amplitudes:
output.writeframes(struct.pack("<h", amplitude) * FRAME_SAMPLES)
return memory.getvalue()
def endpoint(data, silence_frames):
if silence_frames < 1:
raise ValueError("静音帧数至少为1")
with wave.open(io.BytesIO(data), "rb") as audio:
if (audio.getnchannels(), audio.getsampwidth(), audio.getframerate()) != (1, 2, RATE):
raise ValueError("需要8kHz单声道16位音频")
active = False
silence = 0
index = 0
while raw := audio.readframes(FRAME_SAMPLES):
samples = struct.unpack("<" + "h" * (len(raw) // 2), raw)
energy = sum(abs(value) for value in samples) / len(samples)
# 简单能量VAD:把超过阈值的帧标为有声。
if energy > 500:
active, silence = True, 0
elif active:
silence += 1
if silence >= silence_frames:
return (index + 1) * 20
index += 1
return None
if __name__ == "__main__":
result = endpoint(fixture_wav(), SILENCE_FRAMES)
print("音频:5帧,每帧20ms")
print("端点:" + (str(result) + "ms" if result is not None else "等待更多静音"))
print("下一步:把分段音频交给ASR识别文字")import unittest
import endpoint_lab as lab
class MechanismTests(unittest.TestCase):
# 正常、边界和失败输入都由这里提供,直接运行即可。
def test_normal(self):
self.assertEqual(lab.endpoint(lab.fixture_wav(), 2), 100)
def test_boundary_one_frame(self):
self.assertEqual(lab.endpoint(lab.fixture_wav(), 1), 80)
self.assertIsNone(lab.endpoint(lab.fixture_wav(), 3))
def test_failure(self):
with self.assertRaises(ValueError):
lab.endpoint(lab.fixture_wav(), 0)
with self.assertRaises(Exception):
lab.endpoint(b"broken wav", 2)
if __name__ == "__main__":
unittest.main()打开 PowerShell,逐行输入下列命令。cd 是进入文件夹,dir 是列出文件;确认列表里有刚保存的两个 .py 文件,再运行 Python。
# 在刚才创建的练习目录检查文件,再运行程序。
ls
python -X utf8 endpoint_lab.py
python -X utf8 -m unittest -v test_endpoint_lab.py程序输出:
音频:5帧,每帧20ms
端点:100ms
下一步:把分段音频交给ASR识别文字测试末尾应显示 Ran 3 tests 与 OK。若找不到文件,检查当前目录与扩展名;若导入失败,确认测试和主程序在同一文件夹。测试描述的三组材料就在上方代码,不需要另外准备输入。
改一处,再看结果
输出中的端点为100ms,来自读过五个真实WAV帧后的计算。最后一行标出后续步骤:分段结束后再把音频传给ASR,得到可供Agent使用的文本。
把 SILENCE_FRAMES = 2 改成 SILENCE_FRAMES = 1,保存并重跑。端点变成80ms,提前20ms。恢复为2后重跑。门限调短能降低等候时间,也更容易把话中停顿当作结尾;调长能容忍停顿,却会拖慢首个回答。
如果始终没有端点,先检查采样率、声道和振幅,再看是否存在足够长的静音。真实录音还需要麦克风权限;上传给识别服务前应取得录音与上传授权,并按音频长度控制费用。ASR返回的文字可能漏词或误识别数字,付款和联系人等关键信息应回读确认。
改动后仍在刚才的 PowerShell 目录运行:
python -X utf8 endpoint_lab.py确认上述变化后,把参数恢复成原值,保存并再次执行这条命令。
把机制接回 Agent
实时ASR接入按采集音频、VAD分段、提交片段、接收部分转写、确认最终转写推进。部分转写可能在后续音频到达后修订,Agent应区分临时文字与稳定结果。把端点时间、转写结束时间和回答首音时间分别记录,才能找到慢在收音还是识别。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“ASR、VAD 与端点检测”,哪种做法更稳妥?
对应知识点:L9-asr
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L9-asr
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L9-asr
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L9-asr
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。