ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Pyannote+Whisper实现带说话人标签的会议录音自动转写

Pyannote+Whisper实现带说话人标签的会议录音自动转写 最近整理会议录音的时候我遇到一个特别头疼的问题一段录音里三四个人在说话有人提问有人补充还有人插嘴打断手动标注“这段是谁说的”“那几句是回答谁的”光是听写就耗掉整个下午。后来我干脆把整段音频直接丢给 Whisper 转写结果更乱两个人同时开口的部分被揉成一团出来的文字根本分不清哪句是哪个人说的。折腾了几天之后我定下来一套本地处理方案Pyannote 负责把不同说话人从时间轴上分开Whisper 负责把每一段声音转写成文字。两者一前一后串成管道输入一个录音文件输出就是带说话人标签和精确时间戳的逐字稿。实测下来一小时左右的音频在普通显卡上十几分钟就能跑完关键是完全本地运行适合会议记录、访谈整理、播客后期这些场景。这篇文章会把整套实现思路、完整可运行的代码、参数调优经验以及我实际踩过的坑全部写下来。你不需要很强的音频基础只要会装 Python 包、能看懂基本代码就可以照着我这套流程复现。1. 项目背景与方案选型1.1 需求场景当一段录音里有多个人说话带说话人分离的语音转文字跟普通的语音转文字完全是两个难度级别。普通转写字幕只需要做 ASR自动语音识别模型拿到音频后直接吐文字但如果要在转写结果里区分“谁说了什么”就必须先做说话人分离Speaker Diarization也就是“这一段是哪个人在说、那一段是哪个人在说”然后再对每一段分别做语音识别。实际使用中这类需求最常见的就是会议纪要。公司内部讨论会、客户访谈、播客录音、法院庭审记录甚至记者的采访录音都会碰上一段声音文件里有多个人轮番发言的情况。如果只是想把录音变文字稿那么任何一款语音转文字工具都能做到但如果想直接生成一份“发言人A内容”、“发言人B内容”这样格式清晰的会议纪要就绕不开说话人分离。而且这里要澄清一个概念说话人分离并不等于说话人识别。说话人识别要做的是“声纹比对”告诉你这位发言人是张三还是李四而分离只做“按声纹归类”把相同音色的片段归为同一个标签输出结果往往是 speaker_0、speaker_1 这样的代号。对大多数转写场景来说先得到代号级别的分离再由人来对照填写身份已经足够用了。1.2 技术方案对比为什么选择 Pyannote Whisper市面上的说话人分离方案不算少但我最终选择 Pyannote Whisper 的组合主要有三个原因。第一个原因是Pyannote 是目前开源社区里效果最稳的说话人分离工具。它基于声纹嵌入embedding和聚类算法实现背后是大量真实音频数据集训练出来的模型在常见的中文会议录音上表现不错。官方提供的speaker-diarization-3.1模型一行代码就能加载不需要自己训练模型大大降低了使用门槛。第二个原因是Whisper 对中文的支持足够好。Whisper 是 OpenAI 开源的语音识别模型训练数据覆盖了多种语言包括中文即使不用任何特殊微调日常会议、访谈录音里的中文识别准确率也能达到实用水平。更重要的是它在长音频上不会像一些轻量模型那样出现严重漏字、错字的问题。第三个原因是整套方案都可以本地运行。录音内容往往涉及隐私直接上传到云端 API 转写有风险而 Pyannote 和 Whisper 都可以完全离线运行录音文件不出本机对隐私保护更友好。当然Pyannote 也有替代方案比如 Kaldi 体系的 diarization 工具效果也不错但配置复杂度高了一截还有商业化的声纹分离 SaaS按分钟计费对高频用户来讲成本可观。Pyannote 的优势就是“零训练 开源 效果稳定”和 Whisper 搭在一起正好补足了 Whisper 没有说话人信息的短板。1.3 核心处理流程总览整个系统的处理流程可以理解为一条流水线原始音频 - 预处理格式转换、重采样 - Pyannote 说话人分离 - 得到多段带说话人标签的时间片段 - Whisper 逐段转写 - 合并结果输出用生活里的话说Pyannote 负责“分座位”Whisper 负责“记笔记”。Pyannote 先把一整段音频切成不同说话人的片段生成一个包含start_time / end_time / speaker的记录表Whisper 拿到这些片段后把每一小段音频转成文字最后再做一个合并步骤按照时间排序把同一说话人的连续发言拼接起来输出干净、可读的文字稿。这个流程看起来简单实际操作里涉及细节不少比如 Pyannote 的时间戳精度如何控制、Whisper 一次转写多长时间的音频效果最好、两个工具之间如何互相传递音频数据这些我都会在后面的代码解读里逐一说明。2. 环境准备与依赖安装2.1 基础环境与硬件建议先说硬件。带说话人分离的语音转文字属于计算密集任务有 NVIDIA 显卡会舒服很多。我的主力机器是一块 8GB 显存的 RTX 3060跑 Pyannote 3.1 模型和 Whisper small 模型都没有压力如果是 4GB 显存的老显卡建议 Whisper 用 tiny 或 base 模型或者干脆用 CPU 跑多花点时间而已。没有显卡也不用怕CPU 完全可以运行只是速度会慢几倍。实测下来一段 40 秒的音频在 CPU 上用 Whisper small 模型转写大约需要 30 秒左右勉强还能接受一小时的录音就比较折磨人了可能得跑半个小时以上。操作系统上Windows、Linux、macOS 都支持。我个人更推荐 Linux 或 WSL因为在处理 ffmpeg 等依赖时更方便但如果你已经习惯 Windows装好对应依赖也能跑后面会给出处理方法。Python 版本建议 3.9 及以上安装时最好用 conda 或 venv 新建一个独立的环境避免和项目里的其他依赖打架。conda create -n whisper_pyannote python3.10 conda activate whisper_pyannote2.2 安装 Pyannote、Whisper 与 ffmpeg依赖安装是整个项目里最琐碎的环节我按顺序一个个来。先安装 PyTorch。如果你有 NVIDIA 显卡要按你的 CUDA 版本安装对应的 PyTorch例如 CUDA 12.1pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121这里提醒一句PyTorch 和 CUDA 版本要匹配不然后面的 CUDA out of memory、驱动版本报错会特别频繁。不确定自己 CUDA 版本的在终端跑一下nvidia-smi看右上角版本号就行。接着安装 Pyannotepip install pyannote.audio然后是 Whisper。这里我用的官方版openai-whisperpip install openai-whisper最后是 ffmpeg。Whisper 依赖 ffmpeg 来解码音频文件这一步很容易被忽略。Windows 上我建议直接用包管理器安装或者把 ffmpeg 的 exe 下载后放进 PATHLinux/macOS 上用 apt/brew 一行搞定sudo apt update sudo apt install ffmpeg装完之后在命令行里输入ffmpeg -version能正常输出版本号说明解码环境没问题。辅助工具这边我还会用到pydub来切分音频片段它底层还是依赖 ffmpeg但接口更友好pip install pydub soundfilesoundfile用于直接读写 WAV 文件对后面做音频切片很有帮助。2.3 Hugging Face 模型授权与 Token 配置Pyannote 的预训练模型托管在 Hugging Face 上加载之前需要做两件事注册一个 Hugging Face 账号在模型页面同意使用条款然后生成一个 Access Token 供本地代码读取。具体步骤如下访问 Hugging Face 官网并登录账号。打开pyannote/speaker-diarization-3.1和pyannote/segmentation-3.0两个模型页面点击同意条款。在个人设置里创建一个 Access TokenRead 权限即可复制保存。拿到 Token 后可以直接在代码里通过use_auth_token参数传入也可以先用命令行登录huggingface-cli login然后粘贴 Token 回车。很多问题比如报401 Unauthorized或403 Forbidden十有八九就是这一步没完成。我最初调试时就是漏掉了同意模型使用条款卡了好一阵子。Token 配置好之后首次运行会从 Hugging Face 下载模型权重体积大概几百 MB视网速可能要点时间。网络状况不好的话下载会比较痛苦这时候可以考虑在国内镜像站点手动下载模型包然后放到本地缓存目录。这个属于环境优化会在常见问题部分再展开。3. 完整代码实现与关键参数说明3.1 用 Pyannote 提取说话人时间轴核心第一步是用 Pyannote 的Pipeline加载说话人分离模型然后对音频文件做 diarization。话不多说先看代码import torch from pyannote.audio import Pipeline # 从 Hugging Face 加载预训练模型 pipeline Pipeline.from_pretrained( pyannote/speaker-diarization-3.1, use_auth_tokenhf_你的token, ) # 如果有 GPU使用 GPU 加速 pipeline.to(torch.device(cuda)) # 执行说话人分离 diarization pipeline( meeting.wav, min_speakers2, max_speakers4, ) # 遍历分离结果拿到每个说话人的时间区间 for turn, _, speaker in diarization.itertracks(yield_labelTrue): print(fstart{turn.start:.1f}s stop{turn.end:.1f}s speaker{speaker})代码里几个要重点解释的参数min_speakers和max_speakers是给 Pyannote 的先验说话人数量范围。如果你提前知道这段录音里有多少人发言设置范围之后分离准确率会明显提高。比如我知道这是一场两人对话直接设min_speakers2, max_speakers2模型就不会把一个人因为不同情绪的变化误分成两个说话人如果完全不清楚人数就设宽一点比如min_speakers1, max_speakers8。itertracks(yield_labelTrue)是 Pyannote 的遍历方式返回的内容包含三个部分turn是一个时间段对象有start和end属性单位秒、_是轨道信息这里不用、speaker是说话人编号。这里特别注意Pyannote 的输出时间戳精度不是绝对的即便模型对说话人切换很敏锐在说话人重叠、有停顿、有环境噪声时边界也可能有零点几秒的误差。如果拿来做字幕级别的精确时间轴建议在代码里加入一个“边界扩展”的逻辑让每段识别区间略微重叠一部分避免把语音片段切断导致转写丢字。接下来保存分离结果到本地方便后续处理with open(diarization.txt, w, encodingutf-8) as f: for turn, _, speaker in diarization.itertracks(yield_labelTrue): f.write(f{turn.start:.2f} {turn.end:.2f} {speaker}\n)这一步生成的diarization.txt就是整个说话人分离的核心产物每一行代表一段“某人在某个时间段内说话”。有了它后面的 Whisper 转写就变成了一个“拿着时间轴去切音频、再逐段识别”的过程。3.2 用 Whisper 逐段转写并合并结果拿到说话人时间轴之后下一步要做的是把原始音频按这些时间段切割出来再交给 Whisper 逐段识别。这里有一个实现上的取舍直接对整段长音频调用 Whisper 也可以但我们已经在 Pyannote 阶段得到了时间信息所以最好按片段分别传给 Whisper这样既能避免长音频模型上下文长度限制又方便把识别结果挂到一个具体的说话人下面。我采用的方案是先把整个音频读入内存转换成 WAV 格式然后用时间戳切片这样比反复读写临时文件更快更稳。直接看代码import whisper import soundfile as sf import numpy as np # 读取音频soundfile 会把音频转为 numpy 数组 audio, sample_rate sf.read(meeting.wav) # 如果音频是双声道先取单声道Whisper 会自己处理单声道更保险 if audio.ndim 1: audio audio.mean(axis1) # 加载 Whisper 模型 model whisper.load_model(small) def transcribe_segment(start, end): start_sample int(start * sample_rate) end_sample int(end * sample_rate) segment_audio audio[start_sample:end_sample] result model.transcribe( segment_audio, languagezh, fp16False, # CPU 跑必须 FalseGPU 可开 True 加速 temperature0.0, # 固定温度减少随机性导致的文本飘忽 ) return result[text].strip() # 假设这是上一步 Pyannote 输出的结果列表 segments [ {start: 0.0, end: 5.2, speaker: speaker_0}, {start: 5.8, end: 12.6, speaker: speaker_1}, # ... ] transcript [] for seg in segments: text transcribe_segment(seg[start], seg[end]) if text: transcript.append((seg[speaker], seg[start], seg[end], text)) for speaker, start, end, text in transcript: print(f[{speaker}] {start:.1f}s - {end:.1f}s{text})这里的核心参数解释一下languagezh明确告诉 Whisper 识别中文避免它在开始的几十秒里通过静默或语气词猜语言浪费上下文窗口。temperature0.0Whisper 的采样温度。日常转写建议设为 0让它每次都输出“最可能”的文本而不是产生随机变体遇到重复文本或幻觉严重的音频再适当调高温度试试。fp16False如果跑在 CPU 上必须设成 False否则会报类型错误GPU 上设为 True 能明显提速显存占用也更小。上一步的 Pyannote 输出格式和这里的segments列表结构是匹配的实际使用时你只需要把diarization.itertracks(yield_labelTrue)的循环结果塞进这个列表即可。3.3 让结果更好用导出带说话人标签的文本和 SRT识别结果仅仅打印到终端还不够实际项目里往往需要保存成文件。我会同时输出两种格式。第一种是纯文本格式适合直接放进会议纪要或作为邮件正文。实现时同一个说话人的连续发言会被合并成一段中间没有其他说话人打断from collections import OrderedDict def save_plain_text(transcript, output_path): merged [] for speaker, start, end, text in transcript: if merged and merged[-1][0] speaker: # 同一个说话人连续发言直接拼接到上一段 merged[-1][2] end merged[-1][3] text else: merged.append([speaker, start, end, text]) with open(output_path, w, encodingutf-8) as f: for speaker, start, end, text in merged: f.write(f[{speaker}] ({start:.1f}s - {end:.1f}s)\n{text}\n\n)第二种是 SRT 字幕格式。如果后续需要做视频字幕或者想在播放器里同步显示说话人这种格式更方便def save_srt(transcript, output_path): def format_ts(seconds): ms int((seconds - int(seconds)) * 1000) s int(seconds) m, s divmod(s, 60) h, m divmod(m, 60) return f{h:02d}:{m:02d}:{s:02d},{ms:03d} lines [] idx 1 for speaker, start, end, text in transcript: lines.append(str(idx)) lines.append(f{format_ts(start)} -- {format_ts(end)}) lines.append(f{speaker}: {text}) lines.append() idx 1 with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines))一个实际的处理建议如果在导出时发现同一说话人的多个片段之间的间隔非常短比如只有 0.2 秒这往往是 Pyannote 把一句完整的、但中间有短暂停顿的话切成了多段。此时可以把间隔小于 0.3 秒的相邻片段合并再交给 Whisper 转写识别出来的中文会更连贯不会出现一句话被硬生生从中间切开的别扭感。我在实际使用时的经验是把整段说话人分离结果先做一次“间隔合并”再做 Whisper 转写最后再合并同一个说话人的连续片段。这样经过两层合并之后输出的文本质量明显要比直接按原始切分转写好很多。到这里最核心的代码逻辑已经完整了。你只需要把自己录音文件的路径替换到代码里跑完就能得到带说话人的文字稿。4. 实操测试与效果调优4.1 一次真实会议录音的测试过程为了验证整套流程我拿了一段三人线上会议的测试音频时长约 40 秒内容是典型的“一人提问、两人回答”模式。测试环境是 RTX 3060 8GB、CUDA 12.1Pyannote 3.1 Whisper small。跑 Pyannote 分离时我设置了min_speakers2, max_speakers4因为会议中可能会出现第四个人的零星发言。分离结果如下start0.8s stop8.2s speakerspeaker_0 start8.7s stop15.1s speakerspeaker_1 start15.8s stop22.6s speakerspeaker_0 start23.2s stop34.5s speakerspeaker_2 start35.1s stop40.0s speakerspeaker_0可以看到三人分离效果基本正确。speaker_0 出现了三次发言中间被 speaker_1 和 speaker_2 打断这正是真实对话里常见的“轮换发言”模式。紧接着用 Whisper small 逐段转写温度设为 0语言指定为中文。转写结果里只有 speaker_0 的最后一段出现了一个字的口误“这个方案”被识别成“那个方案”其余内容准确率很高。整段处理耗时大概 10 秒左右速度还是让人满意的。测试中也发现如果直接把 40 秒音频整体丢给 Whisper 转写虽然也能输出文字但没有任何说话人信息几个人轮流发言时文字内容表面看起来连贯实际串了多个人的话。经过 Pyannote 分离之后再逐段转每个片段内部都是同一个人的声音识别结果也更专注于单一说话风格和内容。4.2 影响准确率的关键参数与调优经验在使用过程中我总结出几个影响最终输出质量的关键参数。第一个是Pyannote 的说话人数量范围。这个参数直接决定聚类算法的行为。预设太窄模型可能硬把两个混在一起的说话人归为一个人预设太宽模型可能把同一个人在不同情绪下的声音误拆成两个人。我的经验是在能确定人数的情况下把范围收窄到最小。比如知道是两人访谈就min_speakers2, max_speakers2不确定时宁可放宽也不要漏人。第二个是Whisper 的模型大小。Whisper 提供 tiny、base、small、medium、large 多个档位。中文场景下我实测 small 已经能覆盖绝大多数会议、访谈内容准确率与 medium 差距不大但速度快很多遇到专业术语、方言、背景噪声较大的音频再换 medium 甚至 large。有一点要注意模型越大对显存要求越高而且 large 在 8GB 显存上已经比较紧张容易 OOM。第三个是输入音频的质量。这可能听起来不是“参数”但效果影响很大。Pyannote 和 Whisper 都对较干净、较少混响的音频更友好。如果是多人通过不同设备接入的线上会议录音每个人的音量、音色差异大反而是优势分离更容易但如果是单麦克风近距离录音、周围有风扇或电视噪声建议先做降噪处理否则分离边界会变模糊。第四个是时间戳边界处理。前面提到过Pyannote 的边界不一定完全贴着字边界如果对时间要求严格可以尝试在切分音频时前后各扩展 0.2 到 0.3 秒。比如 Pyannote 说是 8.0 秒到 12.0 秒实际切片时切 7.8 秒到 12.3 秒这样能避免语音开头和结尾被切掉。4.3 长音频与批处理优化处理完整的一小时会议录音时直接把整个文件丢给 Pyannote 和 Whisper 虽然也能工作但有几个隐藏问题一是 Pyannote 在超长音频上处理时间会线性增加二是 Whisper 的转写上下文有限三是如果中途某个片段出错就得全量重跑。我建议在进入 Pyannote 之前先用 ffmpeg 或 pydub 对音频做一个“静音切分”。具体做法是检测幅度低于某个阈值的静音区间把长音频切成 5 到 10 分钟的块然后逐块处理最后合并结果。from pydub import AudioSegment from pydub.silence import split_on_silence audio AudioSegment.from_wav(long_meeting.wav) chunks split_on_silence( audio, min_silence_len800, # 静音持续 800ms 以上视为断点 silence_thresh-40, # 低于 -40dBFS 视为静音 keep_silence500, # 保留边缘 500ms 静音防止切断说话 ) for i, chunk in enumerate(chunks): chunk.export(fchunk_{i}.wav, formatwav)切出来的块数如果偏多还可以用faster-whisper替换官方 Whisper。faster-whisper 是 Whisper 模型的 CTranslate2 实现推理速度通常是官方版本的两到四倍显存占用也更低接口基本兼容。切换方法很简单from faster_whisper import WhisperModel model WhisperModel(small, devicecuda, compute_typefloat16) segments, info model.transcribe(segment.wav, languagezh, beam_size5) text .join(seg.text.strip() for seg in segments)我自己在长音频批处理场景下已经切换到了 faster-whisper速度提升非常明显唯一的代价是输出格式和官方版本略有差异需要多写几行代码拼接结果。如果只是偶尔处理几个文件官方版也完全够用。5. 常见问题排查与经验总结5.1 高频报错速查表这几天调试下来我整理了开发中最高频的报错及对应排查手段放在下面这张表里基本能覆盖 90% 的启动阶段问题。报错信息常见原因解决办法401 Unauthorized或403 ForbiddenHugging Face 未登录、Token 错误或未同意模型协议检查use_auth_token到模型主页点击同意条款重新生成 Read TokenModuleNotFoundError: No module named pyannote没有安装 Pyannote 或环境不匹配执行pip install pyannote.audio确认当前 Python 环境是 3.9 以上CUDA out of memory显存不足 / 模型过大 / batch 设置过大换用更小的 Whisper 模型fp16True或用 CPU 跑RuntimeError: ffmpeg not found系统缺少 ffmpeg 解码器安装 ffmpeg 并确保能在终端直接调用ffmpeg -versionOMP: Error #15OpenMP 多线程初始化冲突在代码开头加os.environ[KMP_DUPLICATE_LIB_OK]TRUETypeError: __init__() got an unexpected keyword argument use_auth_tokenPyannote 版本与代码示例不匹配确认pyannote.audio版本是 3.x如果是 2.x 需要升级或调整 API 写法转写结果为空字符串静音过多、音量过小、切片太短检查切片区间是否太短小于 0.5 秒适当增加麦克风音量后再录音其中401 Unauthorized是新手最容易踩的坑。很多人以为注册了 Hugging Face 账号、生成了 Token 就万事大吉实际上 Pyannote 的模型是 gated model必须在模型页面上单独点击同意条款否则模型文件下载没权限。这个坑比较隐蔽因为报错往往不会提示“你同意一下协议”只会说“Unauthorized”。5.2 避坑心得与使用建议最后分享几个实际项目里磨出来的使用建议。一是先跑小模型验证整套流程再换大模型。第一次复现时先用 Whisper tiny 跑通 30 秒的测试音频确认 Pyannote 分离、音频切片、Whisper 转写、结果导出每个环节都没问题再切换到 small 或 medium 处理长音频。这样能极大减少调参和排错的成本。二是音频格式统一转成 16kHz 或 22kHz 的单声道 WAV。Pyannote 和 Whisper 内部都有重采样逻辑但前置步统一转好可以减少不确定性。在这方面ffmpeg 是万能的ffmpeg -i input.m4a -ac 1 -ar 16000 output.wav-ac 1表示单声道-ar 16000表示 16kHz 采样率。录音文件是 m4a、mp3、ogg 都没关系提前转成标准 WAV 能省掉很多后续兼容问题。三是同一段录音不要反复跑分离把时间轴缓存下来。Pyannote 分离的时间轴结果本身就是文本保存下来之后后续更换 Whisper 模型、调整转写参数时不需要重新跑分离直接复用这份时间轴即可。我在批量处理系列录音时通常会把每段录音的 diarization 结果存成一个 JSON 或 TXT下次再处理就是纯文本加载大幅缩短迭代时间。四是如果内容里有大量专业术语人名、产品名、代码关键词通过initial_prompt提示 Whisper。Whisper 支持一个初始提示参数可以在转写前把“可能会出现的词汇”塞进去帮助模型在解码时倾向输出这些词。示例result model.transcribe( segment_audio, languagezh, initial_prompt以下是一些专有名词Whisper、Pyannote、Transformer、CUDA、gated model。, )这个技巧尤其适合技术会议和产品评审录音能明显减少人名和术语的错字率。五是我个人的经验处理长录音时不要试图一次性转写所有片段先看 Pyannote 的分离结果里有没有异常比如单个 speaker 时间占比高得不正常、某段音频时长超过 30 秒还没切换。如果发现异常优先处理这些有问题的片段再跑全量。这套预处理习惯帮我避免过很多次“跑了一晚上最后发现结果串人”的尴尬局面。现在我的日常流程已经固定成一套脚本输入一个文件夹路径脚本自动查找所有录音文件依次做格式转换、说话人分离、分段转写、导出文本和 SRT。跑完一小时的会议录音通常十几分钟后就有一份干净、带说话人标签的初稿剩下的工作就是人工核对一遍时间和错字。如果你经常处理多人录音这套组合方案值得花半小时搭一下后续的效率提升完全是值得的。
返回列表