Whisper本地部署会议转写MLX
用 Mac mini 本地 Whisper 转写中日双语会议录音(含小音量修复)
Apple M4 上用 mlx-whisper 把 81 分钟中日混说的会议录音在 10 分钟内转成带时间戳的双语文字稿:小音量修复、按静音切窗、双语种对照择优、温度回退为何帮倒忙。
目标
把开会录的视频转成带时间戳的中日双语文字稿,全程在内网 Mac mini(Apple M4)上跑,音频不出内网,不花 API 费用。
实测:81 分钟录音 ≈ 10 分钟跑完(107 个窗口 × 2 个语种)。
为什么不用云 API,也不用主力 Mac
| 方案 | 判断 |
|---|---|
| 火山方舟 / 云 ASR | 会议内容要上传第三方;且单次音频有 25MB 上限,81 分钟必须切片 |
| 主力机(Intel i9-9980HK) | ❌ 无 Metal 可用,large 模型约 4–8 小时;且 Python 3.14 缺 ctranslate2 / torch 轮子 |
| Mac mini(M4) | ✅ MLX 直接吃 Metal GPU,11.7 倍速,81 分钟约 10 分钟 |
关键差别在芯片,不在内存
M4 只有 16GB,反而比 64GB 的 Intel i9 快一个数量级——因为 MLX 走统一内存 + Metal, Intel Mac 上这条路完全不通。有 Apple Silicon 就用它,别在 Intel 上硬跑。
环境(Mac mini 上已装好,只需确认)
ssh mac-mini '~/whisper-env/bin/python -c "import mlx_whisper, mlx.core as mx; print(mx.metal.is_available())"'
输出 True 即可用。已缓存 whisper-large-v3-turbo 与 whisper-large-v3 两个模型。
如果要在新机器重建环境
python3 -m venv ~/whisper-env
~/whisper-env/bin/pip install mlx-whisper
不需要 Homebrew,不需要 sudo,不需要 ffmpeg(见下方“坑 1”)。 Xcode CLT 自带的 Python 3.9 就够。
第一步:提取音频并修复小音量
会议录音普遍偏小。先测原始电平:
ffmpeg -i 录音.mov -vn -af volumedetect -f null - 2>&1 | grep -E "mean_volume|max_volume"
正常语音 mean_volume 应在 −20 dB 左右。这次实测 −33.0 dB,但 max_volume 已经是 0.0 dB。
不能直接加增益
均值很低但峰值已到顶,说明有个别爆音。简单
volume=+14dB会把那些峰值削平失真。 必须用语音动态归一化——把弱音抬起来,同时压住峰值。
ffmpeg -i 录音.mov -vn \
-af "highpass=f=70,speechnorm=p=0.95:e=12.5:r=0.0005:l=1,loudnorm=I=-16:TP=-1.5:LRA=11" \
-ar 16000 -ac 1 -c:a pcm_s16le meeting_16k_mono.wav
三级链路各管一段:
| 滤镜 | 作用 |
|---|---|
highpass=f=70 |
滤掉空调、桌面震动这类低频隆隆声 |
speechnorm |
语音动态归一化,逐帧抬升弱音、跟随峰值 |
loudnorm |
广播级响度标准化(EBU R128) |
实测结果:−33.0 dB → −18.6 dB(+14.4 dB),峰值 −0.8 dB 无削波。
顺手再出一份给人听的增强版:
ffmpeg -i 录音.mov -vn -af "<同上滤镜>" -ar 48000 -ac 2 -c:a aac -b:a 128k 音频_增强版.m4a
第二步:按静音点切成语句窗口
不要整段丢给模型——Whisper 只在开头做一次语种判定,中日混说会全程按一种语言硬解。
先找静音点:
ffmpeg -i meeting_16k_mono.wav -af "silencedetect=noise=-20dB:d=0.3" -f null - 2>&1 \
| grep -E "silence_start|silence_end" > sil.txt
阈值要试
归一化之后噪声底被抬高了,
-32dB只能找到 3 个点(81 分钟!),-20dB:d=0.3才有 806 个(约 6 秒一个)。 先数一下点的个数再决定,太少就把阈值调宽松。
然后按目标 45 秒、贴着静音中点切(脚本 plan_chunks.py)。实测切出 107 个窗口,平均 45.4 秒,全部落在静音处,不切断句子。
第三步:双语种对照转写(核心)
每个窗口分别按中文和日语各解码一次,比较置信度择优。
for lang in ("zh", "ja"):
r = mlx_whisper.transcribe(
seg, path_or_hf_repo="mlx-community/whisper-large-v3-turbo",
language=lang, initial_prompt=PROMPT[lang],
condition_on_previous_text=False, temperature=0.0, verbose=None)
打分不能只看 avg_logprob,要对重复度加惩罚:
score = mean(avg_logprob) - 0.5 * max(0, mean(compression_ratio) - 2.4)
实测有效性:某段中文,zh 得 −0.743、ja 得 −0.880,正确判为中文;ja 版本是一堆硬凑的幻觉日语。
再加一道邻域平滑——会议里语种是局部连贯的,孤立的低置信翻转基本是误判:
if 前后窗口语种相同 and 本窗口不同 and margin < 0.6:
改判为前后一致的语种
这次修正了 2 处(35:43、41:00 被误判成日语)。
第四步:后处理
四件事,缺一不可:
-
繁简转换 — Whisper 中文输出会混繁体
pip install opencc-python-reimplemented # 装在 venv 里opencc.OpenCC('t2s').convert(text) -
折叠重复短语 — ABABAB… → AB(n-gram 扫描,连续 ≥4 次即折叠)
-
丢弃套话幻觉 — Whisper 训练数据里的字幕组残留,出现即为幻觉:
请不吝点赞|订阅|转发|打赏|明镜|字幕组|Amara.org ご視聴ありがとう|次の動画|チャンネル登録|お会いしましょう -
输出 SRT + 可读全文 — 按语种块和时间间隔合并成段落
踩过的坑
坑 1:mlx-whisper 内部调 ffmpeg,但机器上没有
报 FileNotFoundError: 'ffmpeg'。不用装 ffmpeg——本机已经转好 16kHz 单声道 WAV 了,直接用标准库读成数组喂进去:
import wave, numpy as np
def load_wav(p):
with wave.open(p, 'rb') as w:
assert w.getframerate() ** 16000 and w.getnchannels() ** 1
d = w.readframes(w.getnframes())
return np.frombuffer(d, np.int16).astype(np.float32) / 32768.0
mlx_whisper.transcribe(load_wav(path), ...) # 传数组,不传路径
坑 2:温度回退在嘈杂音频上帮倒忙 ⚠️
temperature=0.0 会关掉 Whisper 的防重复机制,于是我改成 temperature=(0.0, 0.2, ..., 1.0) 重跑——
结果重复幻觉从 5 处涨到 13 处,最严重的重复了 223 次。
原因:这段音频信噪比差,logprob_threshold 频繁触发回退,而高温采样比贪心更容易陷入循环。
结论
嘈杂录音就用贪心解码(
temperature=0.0)。 想两头兼顾就两版都跑,逐窗口比较重复度择优——这次 107 个窗口里 103 个取贪心版、只有 4 个取回退版。
坑 3:initial_prompt 自己会诱发循环
有一个窗口两种语种都输出垃圾(日语套话循环 + 中文「约片片片…」),但音量正常(−19.2 dB),不是静音段。
去掉 initial_prompt 重跑就干净了(cr 从 4.2 降到 1.3)。领域词 prompt 能提升专有名词准确率,但偶尔会把模型带进循环——遇到死活解不出的窗口,先试试把 prompt 去掉。
坑 4:模型之间会犯同一个错
日方说的一个以长音「イー」结尾的公司名,large-v3 和 turbo 六次输出全部吞掉了尾音,一致地识别成短一截的写法。
模型一致 ≠ 正确
两个模型共享训练数据偏置,会犯同样的错。专有名词一定要人工核对或外部查证, 不要因为多个模型输出一致就当成确认。
坑 5:环境琐碎问题
- Python 3.9 不支持 f-string 里嵌套同种引号(3.12+ 才行)→ 别写内联
python -c,写成脚本文件传过去 - macOS 系统 Python 受 PEP 668 保护,
pip install报 externally-managed → 建 venv,别用--break-system-packages - macOS 自带 rsync 是 2.6.9,不支持
--info=progress2这类新选项 timeout命令 macOS 没有,用gtimeout(coreutils)或改用后台任务
完整脚本
全套脚本五个文件,职责如下:
脚本/
├── plan_chunks.py 切分窗口
├── run_asr.py 双语种对照转写(主脚本,支持断点续跑)
├── postprocess.py 繁简转换 / 折叠重复 / 过滤套话 / 出 SRT 和全文
├── appendix.py 追加校对清单与处理说明
└── rescue.py 抢救解不出的窗口(多模型多配置对比)
run_asr.py 会把每个窗口的结果逐条追加写入 jsonl,中断后重跑会自动跳过已完成的——81 分钟的任务不怕断。
效率参考(Apple M4,10 核 / 16GB)
| 环节 | 耗时 |
|---|---|
| 音频提取 + 归一化(在 Intel 主力机上做) | 约 3 分钟 |
| 传输 155MB WAV 到 mini(局域网) | 35 秒 |
| 语种普查(26 个 30 秒窗口) | 130 秒 |
| 双语种转写(107 窗口 × 2) | 10 分钟 |
| 后处理 | 数秒 |
转写倍速 11.7×。若开温度回退会掉到约 3.4×(34.5 分钟)——又慢又差,别开。
已知局限
- 没有说话人分离——本流程不做声纹聚类,多人快速交替会连成一段
- 同句内中日混说会被整体归入某一语种,另一语种的零星词汇可能失真
- 数字与型号(容量、日期、金额)ASR 易错,涉及决策前务必对照原音频核实
- 专有名词必须人工核对,见坑 4