3 秒 16 kHz 音频切成 298 帧,门限取峰值能量的 25%(0.1063),检测出 2 段语音, 与埋进去的两段相比起终点各差 1~4 帧。语音总时长 1.45 秒,只占 48%——一半是静音。
上一节处理文本,这一节处理声音。 语音聊天看着最像「AI」,其实它是本章链条最长的一个:四段接力,任何一段塌了后面全废。
一、语音聊天是四段接力
语音识别 → 语言理解 → 生成回答 → 语音合成。四段,缺一段都聊不起来。 第一段之前还有一道门:端点检测。它把静音切掉,只把真正有声音的部分送进识别模型。 这道门看着不起眼,却决定两件事:省多少算力,以及开头结尾的字会不会被削掉。 Demo 只做这一道门。 后面三段都要模型,沙箱里装不了,也调不到在线接口——这一点我在本节末尾会再说一次。
二、分帧:把连续的声音切成小片
声音是一串采样点。16 kHz 就是每秒 16000 个点,3 秒就是 48000 个点。 帧长 25 毫秒(400 点),帧移 10 毫秒(160 点)。相邻帧重叠 15 毫秒。 为什么要重叠?声音的特征在帧边界上会突变,重叠能让特征平滑过渡,也让短促的音不会被切在缝里丢掉。 3 秒按 10 毫秒一帧算,看着像 300 帧,实际算出 298 帧。差的两帧是首尾凑不满一整个窗被丢掉的。 每帧算一个能量(采样点平方的均值),就得到一条能量曲线。这一步是纯算术,不需要任何模型。
三、门限怎么定:峰值的 25%
门限不能写死一个绝对数。换一支麦克风、录音音量差十倍,绝对门限就全废。 所以取峰值能量的 25%,本例算出来是 0.1063。 25% 这个比例是我定的。定高了,说话声轻的地方会被当静音切掉;定低了,呼吸和底噪会被当成语音留下来。 Demo 还算了过零率,中位数是 0.029。 过零率是波形穿过零点的频繁程度:浊音(a、o 这类)波形慢,过零率低; 清音(s、f 这类)接近噪声,过零率高。 工业上的端点检测不会只用能量。能量加过零率,再叠一个几十 KB 的小网络,才在嘈杂环境里稳得住。
四、检测结果:差几个帧,够用
埋进去的两段是 0.40~1.05 秒和 1.55~2.45 秒。检测出来是 0.41~1.02 和 1.57~2.41。 起点差 1~2 帧,终点差 3~4 帧,也就是 10 到 40 毫秒。这个误差对人耳没影响,对识别模型也够用。 终点普遍偏早,是这套方法的固有偏差:能量掉到门限以下就判结束,而人说话结尾的音往往很轻,先掉下去。 语音总时长 1.45 秒,占 48%。 也就是说一半时间在静音——砍掉这一半,后面的识别模型就省了一半的活。 这个取舍没有标准答案:门限调低会多留静音(多算但安全),调高会削字头字尾(省算但丢内容)。 要看后面接的是什么模型、能容忍多少延迟。 最后把话说清:这个 Demo 只跑到「切出语音段」。 真要做语音聊天,还需要声学模型做识别、语言模型做理解与生成、声码器做合成,这三样沙箱里都没有, 我也不会假装跑过。
五、动手 Demo:语音端点检测:把静音切掉,只留说话段
合成一段 3 秒 16 kHz 音频,在两段指定区间里填带包络的谐波、其余填低幅噪声; 按 25 毫秒窗、10 毫秒移分帧,算每帧能量; 门限取峰值能量的 25%,把连续超门限的帧合并成段,再把帧号换算回秒,最后和埋进去的真实区间对比。
win, hop = int(0.025 * sr), int(0.010 * sr) # 400 / 160
n = 1 + (len(x) - win) // hop # 298 帧
e = np.array([np.mean(x[i * hop:i * hop + win] ** 2)
for i in range(n)])
thr = 0.25 * e.max()
on = e > thr
# 连续为 True 的帧并成一段,再按帧号换算回秒
实跑输出:
--------------------------------------------------------------------
语音端点检测:把静音切掉,只留说话段
--------------------------------------------------------------------
采样率 16000 Hz,时长 3.0 秒
分帧 窗 25 毫秒,帧移 10 毫秒,共 298 帧
能量门限 峰值能量的 25% = 0.1063
过零率中位数 0.029
真实语音段(我埋的):
0.40 ~ 1.05 秒
1.55 ~ 2.45 秒
检测到的段:
起点(秒) 终点(秒) 时长(秒)
-----------------------------
0.41 1.02 0.61
1.57 2.41 0.84
段数 检测 2,真实 2
语音总时长 1.45 秒(占 48%)
能量曲线(每 10 毫秒一帧,按帧序号):
* **
* * * *
*
* * *
*
*
**** **** ****
^ 高 0.4240 低 0.0110
端点检测是所有语音系统的第一道门:它砍掉静音,
后面的识别模型就只需要处理真正有声音的那几秒。
砍得准,能省一半算力;砍错了,开头结尾的字就丢了。
这个 Demo 只到「切出语音段」。后面的语音识别(ASR)和
语音合成(TTS)都要模型,沙箱里装不了,也调不到在线接口。
所以「语音聊天」这个实例,能真跑的是前面这一段。
298 帧不是 300。 算法是 1 加 (48000 减 400) 整除 160,得 1 加 297 等于 298,最后一帧凑不满一个窗就被丢了。 门限 0.1063 正好落在语音段和静音段之间,所以两段都检出来了。 这个数不是调出来的,是峰值乘 0.25 算出来的。 两段时长 0.61 秒和 0.84 秒,加起来 1.45 秒,占全长 48%。另一半是静音,被砍掉。 能量曲线那张图能看出两团高能量区,中间掉下去一段——那就是两句之间的停顿。 曲线是用字符画的,一列一帧,横向缩过,只表示形状不表示精确值。
30 秒实验
- 把门限比例从 25% 改成 10% 和 50%,看检测出几段、语音总时长怎么变。
- 把帧移从 10 毫秒改成 25 毫秒,看段边界精度差多少。
- 想一想:为什么终点误差比起点误差大?
常见误区
- 「语音聊天就是语音识别」——它是四段接力,识别只是第一段。
- 「门限可以写死一个数」——录音音量差十倍就全废,得按峰值取比例。
- 「静音切掉不影响结果」——切早了削字头,切晚了白算,两头都疼。
自测三问(点开对答案)
1. 端点检测在整条语音链路里起什么作用? —— 砍掉静音,只把有声段送进识别模型,省算力也省延迟。 2. 为什么门限要用峰值的比例,而不是一个固定值? —— 录音音量差异很大,固定值换支麦克风就失效。 3. 本例的终点为什么普遍偏早? —— 能量掉到门限以下就判结束,而句尾的字往往很轻,先掉下去。第五章到此结束。下一章进自然语言处理,把这一章零散摸到的文本处理,系统地讲一遍。
- ☐ (本章完,下一节 6.1.1 概念解读:什么是自然语言处理?)