从录音中去除'um'比听起来更难
摘要
一个本地CLI工具,利用OpenAI的Whisper检测并去除音频录音中的填充词(um、uh、erm),采用技术避免点击声和背景嘶嘶声等音频伪影。
暂无内容
查看缓存全文
缓存时间: 2026/06/12 05:51
# "erm:一个本地 CLI,用于去除语音中的嗯、呃、啊等填充词"
来源:https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/
2026年5月2日·1547 字·8 分钟阅读
语言学家给英语口语中那些填充的`嗯`、`呃`、`啊`以及它们的延长形式(`嗯——`、`呃——`)起了一个名字:**非流利性**。
我自己并不录制很多语音,但有几位朋友会录制,他们告诉我手动剪辑掉这些填充词非常痛苦。所以我构建了`erm`(https://github.com/dougcalobrisi/erm)来完成这项工作。
这就是常见情况的全部界面。它会在输入文件旁边生成一个清理后的`.wav`文件和一个 JSON 格式的剪切列表。本篇文章将介绍它的工作原理,因为显而易见的做法听起来效果并不好,而大部分代码正是为了解决这个问题。
## 朴素的版本行不通🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#the-naive-version-doesnt-work)
你可能预期的工作方式是:用 Whisper 进行转录并获得每个词的时间戳,找到像`um`和`uh`这样的标记,然后用 ffmpeg 剪掉这些时间范围。
这样做大概只能完成 60% 的工作,而且结果听起来比原始音频还差。原因有三:
- Whisper 常常悄无声息地把许多填充词从转写中去掉,导致一开始就没有`um`标记可匹配。
- 在任意时间点切割音频会在波形上产生一个微小阶跃。你的耳朵会把它听成一个咔嗒声。
- 即使拼接点本身是干净的,切割前后背景噪音的细微不匹配也会让你在每次编辑处听到微弱的噪声变化。
`erm` 的大部分工作就是解决这三个问题。
## 关于 Whisper 的简要说明🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#a-quick-word-on-whisper)
Whisper(https://github.com/openai/whisper)是 OpenAI 的开源代码语音转文本模型。你给它一段音频,它返回一份转写稿,并且如果设置正确的参数,它还能告诉你每个词的开始和结束时间戳。它在本地运行,这正是这类工具无需发送录音也能实现的关键。
`erm` 使用了 `faster-whisper`(https://github.com/SYSTRAN/faster-whisper),这是一个重新实现版本,速度比原始版本快数倍且内存占用更少。相同的模型权重,相同的输出,只是运行时更优。默认使用的是 `medium.en` 模型,在速度与准确性之间取得了很好的平衡。如果你想要更快的速度,可以通过 `--model` 参数覆盖为 `small.en`,但我个人会倾向使用 `large-v3`。它在识别填充词方面明显更出色,值得消耗额外的算力。
## 检测🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#detection)
首先运行 Whisper。`erm` 要求 Whisper 输出单词级别的时间戳,并在开始时给出一个小指令,告诉它不要清理转写稿。如果不加干预,Whisper 会编辑掉填充词,因为它的大部分训练数据都是干净的书面文本。任何被识别为已知填充词(如 `um`、`uh`、`er` 等)的单词都会被标记为需要剪切。像 `ummmm` 这样的延长形式会实时与 `um` 词干进行匹配。
Whisper 仍然会遗漏一些内容,因此还有三个额外的步骤直接对音频进行分析:
**间隙填充词。** 如果两个转写单词之间存在异常长的停顿(默认超过 350 毫秒),`erm` 会检查这段“停顿”期间是否真的有人在发声。如果一段声音出现在 Whisper 标记为静音的区域,那就说明这是一个被 Whisper 完全删除的填充词——*它真的会直接丢弃这些词。没有任何标记,仅仅在转写稿中留下一个空洞,就像那里曾经有个“嗯”一样。*
**隐藏在单词内部的填充词。** Whisper 有时会将填充词粘附到相邻的单词上,因此像 `"in, uhhhhh"` 这样的内容可能会被返回为单一的 `in` 标记。`erm` 会检查那些很长的单标记单词,在音频短暂的低谷处将其分割,找出哪个片段才是实际的单词(基于该单词通常发音所需的时间),并将其余部分视为填充词。
**发音时间过长的单词。** 如果一个单词的持续时间远超其文本理应发音的长度,那么尾部就值得怀疑。`erm` 会扫描尾部是否存在有声发音,并可选地用音高测试做二次确认:可疑片段听起来像是有人在持续一个元音(如 `uhhhhh`),还是只是说话速度慢?维持一个元音会呈现出稳定、简单的声学形态;真正的语音在音素间转换时会不断变化。音高测试可以防止工具误减语速较慢的人。
所有四个步骤(Whisper 步骤和三个音频步骤)各自产生候选剪切片段,这些列表会在下一步骤之前合并。
## 微调剪切点🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#refining-the-cut-points)
在精确的 `t = 1.234s` 处进行剪切,会落到波形的某个任意瞬间,几乎永远不会落在零值上。将两个任意点拼接在一起会在波形上产生一个阶跃,而这个阶跃正是你听到的咔嗒声。
按顺序进行两个小的修正。首先,每个剪切端点可以稍微滑动一点(最多 60 毫秒),以落在附近最安静的位置。如果原始剪切点前后恰好有短暂的音频低潮,就滑动到那里。滑动范围是有界限制的,不能侵入相邻的单词,否则会删掉真正的语音。其次,从那个安静位置出发,端点会吸附到波形恰好穿过零点的最近时刻。拼接两个零点会形成一个连续的波形,没有阶跃,也没有咔嗒声。
完成所有这些之后,会清理掉非常短的残留片段:如果两个相邻的剪切会在它们之间留下一个短于约 120 毫秒的音频细丝,那么该细丝会被合并成一个更大的剪切。一个如此小的片段无论如何都无法承受两侧的平滑处理,只会听起来像一声啪响。
## 拼接🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#splicing)
ffmpeg 使用**交叉淡入淡出**进行实际的拼接。它不是简单地将两段音频首尾相接,而是短暂地让它们重叠,并在一个淡出的同时另一个淡入。这可以平滑掉任何剩余的不匹配。
关键在于选择重叠的**时长**。固定长度(大多数教程建议 80 毫秒左右)无论哪种情况听起来都不好:短的剪切被糊在一起,长的剪切仍然会砰砰作响。`erm` 会根据剪切的大小来缩放时长:一个微小的 `uh` 片段使用较短的交叉淡入淡出,一个长久的 `ummmmm` 则使用较长的交叉淡入淡出。存在下限和上限(50 毫秒到 120 毫秒),并且交叉淡入淡出绝不允许回退到真实单词的开始位置,以免模糊两侧的语音。
## 环境音🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#room-tone)
即便经过了以上所有处理,录音的背景嘶嘶声(房间在无人说话时的环境声音)在剪切处仍不能完美匹配。每个房间的“寂静”都略有不同,即使拼接两段近乎静音的部分,也会产生可听见的微弱变化。
解决方案很笨但很有效。从原始录音中找一段安静的部分(一段真实的“这个房间无人说话时的声音”),然后以低音量在整个输出音频下方循环播放。这样一来,背景音在任何地方都是相同的,因为到处是同一段循环。每个拼接点处的微小不匹配都会被叠加在底部的稳定底噪所掩盖。
默认情况下,安静片段会自动寻找。如果你知道一段好的安静区间,也可以指向特定的时间范围。
## 降噪器很隐蔽🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#the-denoiser-is-sneaky)
ffmpeg 内置了降噪功能,你可以在管道的不同阶段对音频运行它。但问题是:降噪会平滑掉检测器依赖的那些细节(音量波动和音高变化)。因此,**何时**进行降噪至关重要。
`erm` 有四种模式:
| 模式 | 检测基于 ... | 输出的剪切来自 ... |
|------|--------------|-------------------|
| `none` | 原始音频 | 原始音频 |
| `pre` | 降噪后的副本 | 降噪后的副本 |
| `post` | 原始音频 | 原始音频;最后阶段进行降噪 |
| `hybrid` | 原始音频 | 降噪后的副本 |
`hybrid` 是默认模式,也是你想要的模式:检测在原始音频上进行(以便捕捉所有线索),但实际的剪切来自干净、降噪后的副本(从而让拼接处听起来更舒服)。
`pre` 看起来合理,但却是最差的选项,因为在降噪后的音频上运行检测器会隐藏掉它们所寻找的对象。
## 验证🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#validation)
音频渲染可能以微妙的方式出错,因此有一个 `validate` 子命令:
```
uvx erm validate input.wav cleaned.wav --cuts cuts.json
```
它会执行三项检查:
- 输出文件可以正常打开。
- 输出文件比输入文件短,缩短的长度大致等于所有剪切的总时长(在允许的误差范围内)。
- 当将清理后的文件重新转写为文本时,不再出现填充词。
最后一项是最有用的。它是端到端的测试:可以验证工具确实实现了它所声称的功能。
## 它不会碰的内容🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#what-it-wont-touch)
它会把 `like`、`you know` 和 `I mean` 等词保留。这些听起来像填充词,但在句子中承担着实际功能,自动剪切它们会改变说话者的原意。`erm` 遵守的规则是:只移除属于声音而非语言的成分。
它也不会处理重复的单词、话头中断或长时间的思考停顿。这些不是叠加在语音上的噪声,它们**就是**语音本身,只是比说话者希望的更杂乱。清理它们属于编辑决策——决定保留哪个版本——而 `erm` 对此没有意见。
## 试试看🔗 (https://doug.sh/posts/erm-a-local-cli-that-strips-ums-uhs-and-erms-from-speech/#try-it)
最快的方式是使用 `uv`(https://github.com/astral-sh/uv),它可以在不永久安装的情况下一步获取并运行工具:
```
uvx erm input.wav --dry-run # 查看将被剪切的内容
uvx erm input.wav # 生成结果
```
如果你想按常规方式安装:
```
pip install erm # 或者:pipx install erm
erm input.wav
```
你还需要将 `ffmpeg` 和 `ffprobe` 添加到你的 `PATH` 中(在 macOS 上使用 `brew install ffmpeg`)。
github.com/dougcalobrisi/erm(https://github.com/dougcalobrisi/erm)。音频始终保留在本地。如果你录制语音笔记或播客,并且每两个词就有一个是“嗯”,不妨试试它。
相似文章
@FeitengLi: 其实这些问题都能很好的解决了 1. 扔掉 whisper,换 ASR 模型,Qwen3-ASR 就很不错幻觉很少、也有一些别的ASR选择,whisper 幻觉多也要求 30s片段,Qwen3-ASR 塞更长的音频识别越准确,最大支持 20…
推荐使用Qwen3-ASR替代Whisper以减少幻觉,使用LattifAI工具进行精确的音文本对齐和字幕生成,并介绍自己的OmniVAD-Kit项目用于语音活动检测。
Hush
Hush 是一个开源的噪声抑制工具,专为语音AI代理设计,可提升实时交互中的音频清晰度。
vaibhavs10/incredibly-fast-whisper
一个高度优化的OpenAI Whisper Large v3版本,使用Transformers、Optimum和Flash Attention 2,能够在Replicate上在2分钟内转录150分钟的音频。
@tom_doerr: 以70倍实时速度转录音频 https://github.com/m-bain/whisperX
WhisperX是一个用于快速自动语音识别的工具,提供词级时间戳和说话人分离,使用Whisper large-v2实现70倍实时转录。
Whisper 介绍
OpenAI 推出 Whisper,这是一个端到端的编码器-解码器 Transformer 模型,在大规模多样化音频数据上进行训练,可提供强大的多语言语音识别、语言识别和语音到英文翻译功能。Whisper 在多样化数据集上的错误率比专业模型低 50%,并且在语音翻译方面优于有监督基准,尽管未针对特定数据集进行微调。