个人电脑玩AI-14让5060 Ti给你打工——给 Whisper 字幕工具加上说话人分离:pyannote.audio 实战与踩坑记

by 雪隐_上班了 from juejin.cn/user/143341... 欢迎分享与聚合,全文转载就不必了,尊重版权,圈子就这么大,若急用可联系授权。

专栏主旨 :用我那台 RTX 5060 Ti 16G + 64GB 内存 的"丐帮战车",做点不枉费电费的新奇事情。

上一章我们用 MOSS-Transcribe-Diarize 做了会议纪要,端到端一把梭,能跑通。但是------那个"但是"你们懂的------太慢了。36 分钟录音要等 31 分钟,慢到怀疑人生。而且 MOSS 和 gemma 在 16G 显存里谁也容不下谁,只能两班倒。

所以这一章,我换了一条路:用 Whisper Large V3 + pyannote.audio,各干各的,再按时间轴对齐。 效果怎么样?快了很多。有多快?往下看。

背景

我有一个基于 faster-whisper(Large V3)的本地字幕生成工具,能把视频/音频转成带时间戳的 SRT 字幕。最近想给它加一个「会议纪要」功能,而纪要的前提是回答一个问题:这句话是谁说的?

Whisper 本身只做语音转文字,输出的片段没有任何说话人信息。要区分说话人,就需要引入说话人分离(Speaker Diarization)技术。这篇文章记录我用 pyannote.audio 实现这一功能的过程,以及在 Windows 环境下踩到的几个坑------每一个坑都够写一集《今日说法》

方案选型

主流的说话人分离方案有两个:

  • pyannote.audio :业界事实标准,speaker-diarization-3.1 模型效果好,社区活跃。缺点是模型在 HuggingFace 上是受限(gated)的,需要申请授权------得跟 HF 签个"君子协定"
  • FunASR(阿里):从 ModelScope 下载,国内网络友好,中文场景效果不错。缺点是不用魔搭的人得重新学一套 API。

考虑到通用性和识别质量,我最终选了 pyannote.audio。如果你的网络访问 HuggingFace 不方便,FunASR 是更省事的选择。

整体 pipeline 设计如下:

markdown 复制代码
音视频文件 → ffmpeg 提取 16kHz 单声道 wav
          → faster-whisper 转录(带时间戳的文本片段)
          → pyannote 说话人分离(带时间戳的说话人区间)
          → 按时间重叠对齐,给每个片段打上说话人标签
          → 相邻同说话人片段合并 → 带说话人的转写稿

关键思路:转录和分离是两个独立的模型,各干各的,最后靠时间轴对齐。就像两个侦探分别查案,最后对笔记------一个负责"说了什么",一个负责"谁说的"。这样不需要替换现有的 Whisper 流程,侵入最小。

核心实现

1. 加载分离管线(比下载模型还简单)

python 复制代码
from pyannote.audio import Pipeline

pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-3.1",
    token=HF_TOKEN
)
pipeline.to(torch.device("cuda"))

支持传入 num_speakers 指定人数(知道参会人数时能明显提高准确度),不传则自动检测------但自动检测有时候会多送你一两个不存在的"说话人",比如猫叫、门响、空调声。 所以知道人数最好还是传一下。

2. 分离:得到说话人时间区间

python 复制代码
diarization = pipeline(audio_input, num_speakers=num_speakers)

turns = []
for turn, _, speaker in diarization.itertracks(yield_label=True):
    turns.append({
        "start": turn.start,
        "end": turn.end,
        "speaker": speaker   # 形如 SPEAKER_00 / SPEAKER_01
    })

输出是一系列「谁、从第几秒、说到第几秒」的区间。像剧本一样干净:张三 0-5 秒,李四 5-10 秒,张三 10-15 秒------吵架的时候切换尤其快

3. 对齐:把 Whisper 片段分给说话人

这是整个功能的核心算法。对每个 Whisper 片段,计算它与所有说话人区间的时间重叠长度,分配给重叠最大的那个说话人;完全没有重叠时(比如 Whisper 的时间戳略有偏差),退而求其次取时间上最近的:

python 复制代码
for seg in segments:
    best_speaker, best_overlap = None, 0.0
    for turn in turns:
        overlap = min(seg["end"], turn["end"]) - max(seg["start"], turn["start"])
        if overlap > best_overlap:
            best_overlap = overlap
            best_speaker = turn["speaker"]

    if best_speaker is None:  # 无重叠时取最近者
        seg_mid = (seg["start"] + seg["end"]) / 2
        best_speaker = min(turns, key=lambda t: min(
            abs(seg_mid - t["start"]), abs(seg_mid - t["end"])))["speaker"]

    seg["speaker"] = friendly_name(best_speaker)

friendly_name 按说话人首次出现的顺序把 SPEAKER_00 重命名为「说话人1/2/3...」,可读性更好------不然开会纪要用 SPEAKER_00 来指代老板,总觉得哪里不对。

如果有 word 级别的时间戳(faster-whisper 支持 word_timestamps=True),可以做更精细的词级对齐,片段边界处串人的概率会更低------目前的片段级对齐对会议场景已经够用。毕竟开会不会有人每隔 0.5 秒就抢一次话,除非你们公司文化比较特别。

4. 合并:让转写稿像「对话」

Whisper 的片段很碎(一两秒一条),直接标注会满屏都是说话人切换,读起来像精神分裂患者的日记。把相邻、同说话人、间隔小于 2 秒的片段合并成发言块:

python 复制代码
if seg["speaker"] == current["speaker"] and seg["start"] - current["end"] <= 2.0:
    current["end"] = seg["end"]
    current["text"] += " " + seg["text"]

最终格式化成这样的转写稿:

csharp 复制代码
[00:00:02] 说话人1: there you go ... so we got kind of a weird one ...
[00:00:47] 说话人2: yeah the problem is we need more wine ...
[00:00:59] 说话人3: congratulations thank you ...
[00:01:19] 说话人4: you realize you really got me ...

这份转写稿既可以直接阅读,也可以喂给 LLM 生成会议纪要------顺便还能看出谁在会上话最多,谁一直在"嗯""对""好"。

踩坑记录(重点,敲黑板)

坑一:403 Gated Repo,而且不止一个

pyannote 的模型在 HuggingFace 上是受限的,光有 token 不够,还必须在网页上逐个点击"同意使用协议"。报错长这样:

bash 复制代码
Cannot access gated repo for url https://huggingface.co/pyannote/speaker-diarization-3.1/...
Access to model pyannote/speaker-diarization-3.1 is restricted...

我在代码里对未配置 token 的情况给出了明确指引,但即便配了 token,还是连续撞了三次 403------因为需要的授权不止一个

  1. pyannote/segmentation-3.0
  2. pyannote/speaker-diarization-3.1
  3. pyannote/speaker-diarization-community-1最容易漏掉的一个

第三个是 pyannote.audio 4.x 内部依赖的模型,报错信息里才会暴露出来。三个页面都点完"同意"之后,世界终于清净了。

教训:gated 模型的依赖链可能藏得很深,授权时把报错里提到的每个仓库都点一遍。 就像填表格,你以为填完了,结果下一页还有三个。

坑二:Windows 上 torchcodec 解码失败

pyannote 4.x 用 torchcodec 解码音频,但在我的 Windows 环境(torch 2.8 + cu128)下 libtorchcodec 的 DLL 死活加载不了:

css 复制代码
torchcodec is not installed correctly so built-in audio decoding will fail.
* use audio preloaded in-memory as a {'waveform': ..., 'sample_rate': ...} dictionary;

好在报错信息直接给了出路:不喂文件路径,改成喂内存中的波形。 用 soundfile 读音频,soundfile 不支持的格式(部分 m4a/wma)先用 ffmpeg 转一道:

python 复制代码
import soundfile as sf

data, sample_rate = sf.read(audio_path, dtype='float32')
waveform = torch.from_numpy(data)
if waveform.ndim == 1:
    waveform = waveform.unsqueeze(0)   # (time,) -> (1, time)
else:
    waveform = waveform.T              # (time, channel) -> (channel, time)

diarization = pipeline({"waveform": waveform, "sample_rate": sample_rate})

绕开 torchcodec 之后世界清静了。这就像开车上不了高速,那就走国道,反正能到。 还顺带统一了输入格式------不管你是 mp3、m4a、flac、wma,ffmpeg 先归一化再读,pyannote 只管"吃"。

坑三:pyannote 4.x 返回值结构变了

speaker-diarization-3.1 在 pyannote.audio 3.x 里直接返回 Annotation 对象;而 4.x 返回的是一个 DiarizeOutput 数据类,真正的分离结果在它的 speaker_diarization 属性里。直接对 DiarizeOutputitertracks 会报 AttributeError------就像打开快递盒,发现里面还有一个盒子,再打开才是你要的东西。

兼容两个版本的写法:

python 复制代码
diarization = pipeline(audio_input)
# pyannote.audio >= 4.0 返回 DiarizeOutput,取其中的 Annotation
if hasattr(diarization, "speaker_diarization"):
    diarization = diarization.speaker_diarization

顺带一提,4.x 里 Pipeline.from_pretrained 的认证参数也从 use_auth_token 改成了 tokenAPI 变更是程序员永远躲不掉的债。

效果:终于快起来了

用一个 100 秒的英文短片实测:

  • Whisper 转录:约 15 秒
  • pyannote 分离:约 8 秒(GPU)
  • 对齐 + 合并:不到 1 秒
  • 总计:约 23 秒

对比上一章的 MOSS:36 分钟录音 → 31 分钟转写。速度差距大概是 80 倍 。Whisper + pyannote 组合拳在 100 秒音频上的总耗时不超过 30 秒,处理 1 小时会议录音大约需要 8-12 分钟------这才是正常人能接受的等待时间。

自动检测出 4 个说话人,发言块合并合理。对于真实的双人/多人会议录音,如果提前知道参会人数,传入 num_speakers 可以明显减少串人现象。

完整代码约 200 行,核心就是一个 SpeakerDiarizer 类:diarize()(分离)→ assign_speakers()(对齐)→ merge_consecutive()(合并)→ format_transcript()(格式化),四个静态/成员方法各司其职,也方便单独测试------对齐和合并逻辑不依赖模型,用模拟数据就能单测。

最关键的是:这个方案在 5060 Ti 16G 上跑得稳稳的,不会把显存榨干。 不像 MOSS 那样动不动就要 49.5G,还不给商量。

总结与可改进点

「转录 + 分离 + 时间轴对齐」是一条性价比很高的路线:两个成熟模型各司其职,中间只用几十行对齐代码粘合,就能让纯文本转写升级成带角色的会议记录。

对比上一章 MOSS 的端到端方案:

对比项 MOSS 0.9B Whisper + pyannote
速度 慢(约 1:1 耗时) 快(约 5-8 倍实时)
显存占用 动态增长,长音频危险 稳定可控
灵活性 固定流程,难以拆分 模块独立,可替换
模型授权 开箱即用 需手动点 3 个授权页面

没有完美的方案,只有适合的方案。MOSS 胜在省事,Whisper+pyannote 胜在灵活和速度------成年人当然是两个都要,不同场景换着用。

如果继续打磨,几个方向:

  • 词级对齐 :用 word_timestamps 按词分配说话人,处理说话人快速交替的场景------比如辩论节目,两人互怼时每半秒换一次人。
  • exclusive diarization :pyannote 4.x 还提供 exclusive_speaker_diarization(不重叠的分离结果),对重叠语音的处理更干净------适合那种大家同时说话的混乱会议。
  • 说话人命名:分离只能给出「说话人1/2/3」,结合声纹库或简单的人工标注界面,可以替换成真实姓名------"老板说"比"说话人1说"有说服力多了。

如果你之前用 MOSS 跑会议纪要觉得太慢,试试这套组合拳。代码量不大,改造成本低,速度提升肉眼可见。

如果这篇文章让你成功避开了一个坑,或者省了一下午的调试时间,点赞、评论、转发都行。

谢谢大家 🙏

祝你们的模型一次加载成功,token 永不超时,说话人永远不串。

相关推荐
颜酱15 小时前
15 | 安全执行 SQL 并返回查询结果
人工智能
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
新芒15 小时前
海尔洗衣机智慧洗护:AI赋能洗烘护全面进化
人工智能
二月龙15 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家15 小时前
Windows 上同时安装多个 node 版本
前端
颜酱15 小时前
14 | 验证并修正 LLM 生成的 SQL
人工智能·python
AI创界者15 小时前
AIGC进阶】Sulphur-2 视频生成大模型离线实战:文生视频/图生视频本地一键部署整合包解压即用与调优指南
人工智能·aigc·音视频
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端