ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

FastWhisper+Pyannote:ASR与说话人分离离线转写实战

FastWhisper+Pyannote:ASR与说话人分离离线转写实战 前阵子帮一个做会议纪要的朋友收拾烂摊子他们的产品要在一小时的多人会议录音里产出逐句文字还得标清楚每一句是谁说的。最开始他们用的是某个在线转写接口单次时长限制、按分钟计费、还得把录音传到别人的服务器上成本和合规两头都难受后来索性改成自己本地跑。折腾了两周最后的方案就是FastWhisper 做 ASRPyannote 做说话者识别Speaker Diarization两条链路各干各的中间用时间轴把它们缝起来。整套东西跑在一张 12G 显存的消费级显卡上一小时的会议录音十几分钟出结果全程离线说话人编号也能对得上。这篇就按我当时的实施顺序,把选型理由、代码实现、参数调优和踩过的坑完整写一遍代码都能直接抄适合有一定 Python 基础、想自己搭一套多人语音转写流水线的同学。顺便说一句很多人搜ASR 的 AT 指令最小的 ASR 模型那多半是在玩串口语音模块那条路子跟本文这套软件流水线不是一个赛道后面我会顺带对照一下两种方案的取舍。1. 先把需求拆干净ASR 和说话者识别是两条独立链路1.1 转写文字和分辨谁在说本来就是两件事刚接触这块的人容易有个误区觉得语音转文字是一个整体能力找个模型一把梭就完事。实际上这是两个完全独立的子任务输入都是音频输出却完全不同。ASRAutomatic Speech Recognition的任务是把声学信号映射成文字序列它关心的是这句话的字面内容是什么而说话者识别更准确地叫说话人日志Speaker Diarization的任务是回答这段话是第几个人说的它根本不需要理解内容只关心声纹特征和聚类结果。为什么这个区分这么重要因为它们的失败模式完全不一样。ASR 出错表现为错字、漏字、幻觉句子说话者识别出错表现为两个人被合成一个编号或者一个人被切成三个。你在调优的时候如果不知道错误来自哪条链路就会像我当时一样先去调 Whisper 的 beam_size折腾半天发现文字明明是对的乱的是编号。还有一类需求叫说话人确认Speaker Verification比如声纹解锁那是 1:1 比对的判定任务和多人场景下的聚类任务又是两码事。我们这里要做的是N 个人混在一起谁说了哪句属于聚类问题说话人数量事先可能都不知道。1.2 为什么选 Whisper 系而不是自研或商用接口Whisper 是 OpenAI 开源的多语种 ASR 模型最大的好处是中文识别开箱即用不用自己标注数据训模型。我实测过几套方案从零训练一个中文 ASR哪怕只做到勉强可用没有几百小时标注数据和几周调参下不来对个人项目来说完全不现实。但直接跑官方的openai-whisper有两个明显的痛点一是推理慢官方实现里 Python 层的开销和注意力计算没做太多优化二是显存占用高large 模型在消费级卡上跑起来紧张。FasterWhisper 是基于 CTranslate2 的重新实现把模型转成 CTranslate2 格式做了算子融合、量化支持和批处理优化同样的模型同样精度下速度能有数倍提升显存也降下来了。更关键的是它原生支持word_timestampsTrue能给出词级时间戳这正是后面做说话人对齐的基础。1.3 Pyannote 在这套方案里的定位Pyannote 是一个专门做说话人日志的开源工具链其中pyannote/speaker-diarization-3.1是目前业界用得比较多的预训练流水线。它的内部大致分三步走先用一个分割模型segmentation在时间轴上找出有人在说话的片段然后用声纹嵌入模型embedding把每个片段压成一个向量最后用聚类算法把向量分组同一组的片段归为同一个说话人。选它而不是其他方案主要是三个考虑预训练模型在公开数据集上的 DERDiarization Error Rate说话人日志错误率通常落在 15% 到 20% 这个量级具体数字以官方模型卡为准对会议场景够用了它支持用min_speakers/max_speakers约束聚类这个开关对结果影响巨大还有就是它的输出是标准的pyannote.core.Annotation对象能直接按时间区间遍历方便和 Whisper 的输出做对齐。至于嵌入式那边搜AT 指令的朋友你们面对的是另一套东西多数离线语音模块只做关键词唤醒或固定命令词识别通过串口发 AT 指令配置优点是功耗低、无需联网、响应快缺点是只能认有限几条命令不能做通用转写。如果你要做的是录音转文字加区分说话人那就别在这条路上耗了。2. 环境准备先把音频和依赖理顺再写代码2.1 音频预处理是绕不过去的第一步我踩过的第一个坑就是直接拿 mp3 喂给 Pyannote结果报解码错误。Pyannote 内部用 torchaudio 加载音频虽然理论上支持 ffmpeg 后端但版本组合一乱就容易出问题。最稳妥的做法是在流水线外面用 ffmpeg 统一转成 16kHz 单声道 WAV这一步做完后面所有环节的采样率假设都统一了。ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le -vn output.wav参数的含义我解释一下-ac 1强制单声道因为 Whisper 和 Pyannote 都在单声道上训练双声道不但浪费算力还可能因为左右声道微小的时间差影响特征-ar 16000统一到 16kHz这是 Whisper 的原生采样率重采样交给成熟的 ffmpeg 做比交给推理库做更可控-vn是丢掉视频流有时候录屏文件里带画面不丢会报错。注意如果你的原始录音是 44.1kHz 或 48kHz 的务必先重采样。我见过一次没重采样直接跑Whisper 内部做了重采样但 Pyannote 用的是原始采样率读入的时长两边时间轴差了 3 倍对齐结果全乱。2.2 安装依赖和版本组合我用的环境是 Python 3.10 PyTorch 2.x CUDA 12.x这套组合比较稳。安装命令pip install faster-whisper pip install pyannote.audio pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121这里有个容易翻车的地方pyannote.audio会带一个自己依赖的 torch 版本如果你的 torch 是单独装的高版本 CUDA 版pip 有可能把它降级掉。装完之后一定要验证python -c import torch; print(torch.__version__, torch.cuda.is_available())如果打印出来是 False说明 CUDA 版本不匹配得回去检查显卡驱动和 torch 的对应关系。这一步不解决后面所有模型都会默默跑在 CPU 上速度差十倍以上。2.3 模型授权和下载Pyannote 有个隐藏门槛Pyannote 的预训练模型不是下载即用需要先在模型页面接受使用条款再去 Hugging Face 申请一个 Access Token。我第一次跑的时候直接报 401以为是网络问题查了半天才发现是没接受条款。这个环节很多人第一次都会卡住步骤是注册账号打开pyannote/speaker-diarization-3.1和它依赖的两个子模型页面分别点同意然后在设置里生成一个 read 权限的 token。代码里传递 token 的写法在不同版本间有变化# pyannote.audio 3.x 的写法 # pipeline Pipeline.from_pretrained(pyannote/speaker-diarization-3.1, use_auth_tokenHF_TOKEN) # 较新版本的写法 from pyannote.audio import Pipeline pipeline Pipeline.from_pretrained(pyannote/speaker-diarization-3.1, tokenHF_TOKEN)如果你用use_auth_token报参数错误就换成token反过来也一样。这是纯 API 变更不是你的问题。2.4 Whisper 模型怎么选从 tiny 到 large-v3 的取舍FasterWhisper 支持的模型规格和官方一致tiny、base、small、medium、large-v3还有社区蒸馏版 distil 系列。搜最小的 ASR 模型的朋友多半是在意体积我把常见规格的实际情况列一下体积是 CTranslate2 格式的大致数值具体以你下载的仓库为准模型规格参数量级float16 体积int8 体积中文效果适用场景tiny约 39M约 75MB约 40MB明显吃力演示、极低资源设备base约 74M约 145MB约 75MB能出字错字多快速预览small约 244M约 484MB约 245MB勉强可用边缘设备、准实时medium约 769M约 1.5GB约 770MB较好显存有限时的折中large-v3约 1550M约 3.1GB约 1.6GB最好追求准确率的离线批处理我的建议很直接追求准确率就上 large-v3显存不够就用 int8 量化版本别在 tiny 和 base 上浪费时间。中文的声调和同音字太多小模型出来的结果错得离谱后处理修错的成本比自己跑大模型高得多。如果你确实需要极小的模型可以考虑 distil 系列的英文蒸馏版它在英文上的速度和精度平衡做得不错但中文支持没优势。3. 核心实现把两条链路拼成一条流水线3.1 先跑说话人日志拿到时间轴整个流水线的执行顺序我建议是先做说话人日志再做转写。原因是 Pyannote 的模型小、跑得快如果它挂了或者授权不对你能立刻发现不用等 Whisper 跑完十分钟才发现白跑。另外一个原因是显存两个模型不用同时驻留跑完一个释放掉再跑下一个12G 卡完全够用。import torch from pyannote.audio import Pipeline def get_diarization_turns(audio_path, hf_token, num_speakersNone, min_speakersNone, max_speakersNone): pipeline Pipeline.from_pretrained( pyannote/speaker-diarization-3.1, tokenhf_token, ) if torch.cuda.is_available(): pipeline.to(torch.device(cuda)) output pipeline( audio_path, num_speakersnum_speakers, min_speakersmin_speakers, max_speakersmax_speakers, ) # 关键兼容点新版 pyannote.audio 返回 DiarizeOutput不是 Annotation annotation getattr(output, speaker_diarization, output) turns [] for seg, _, speaker in annotation.itertracks(yield_labelTrue): turns.append({ start: round(seg.start, 3), end: round(seg.end, 3), speaker: speaker, }) # 及时释放显存给 Whisper 腾地方 del pipeline torch.cuda.empty_cache() return sorted(turns, keylambda x: x[start])itertracks(yield_labelTrue)返回的是三元组时间段、轨道号、说话人标签。轨道号我们不需要说话人标签形如SPEAKER_00、SPEAKER_01注意这个编号是每次运行都可能变的它只是聚类结果的一个内部标识不要拿它当固定 ID 用跨文件对比时要重新做映射。这里那个兼容点值得单独强调新版 pyannote.audio 的 pipeline 返回的是一个DiarizeOutput数据类里面同时包含speaker_diarization和exclusive_speaker_diarization两个字段直接对它调用itertracks会报 AttributeError。用getattr兜一下两个版本都能跑。这是我卡了半小时才查出来的问题网上很多老教程还停留在旧返回值的写法上。3.2 再做词级转写拿到带时间戳的文字下一步是转写。关键参数是word_timestampsTrue打开之后每个 segment 里会带一个words列表每个词有自己的起止时间和置信度。from faster_whisper import WhisperModel def transcribe_words(audio_path, model_sizelarge-v3, languageNone): model WhisperModel(model_size, devicecuda, compute_typefloat16) segments, info model.transcribe( audio_path, languagelanguage, # 已知语种就写死避免短音频误判 beam_size5, word_timestampsTrue, vad_filterTrue, vad_parametersdict(min_silence_duration_ms500), condition_on_previous_textFalse, ) words [] for seg in segments: for w in (seg.words or []): words.append({ start: w.start, end: w.end, word: w.word, prob: w.probability, }) del model torch.cuda.empty_cache() return words, infosegments是个生成器所以model.transcribe调用本身几乎瞬间返回真正耗时发生在你遍历它的时候。这个特性在做进度条的时候有用但也容易让人误判哦这么快结果卡在 for 循环里。languageNone让模型自动检测语种但对于只有几十秒的短音频检测很容易翻车把中文识别成日文的情况我遇到过不止一次。如果你明确知道音频语种就写死languagezh省得模型自己猜。3.3 时间轴对齐把词挂到说话人身上这是整套方案最核心的一步。Whisper 给你一串带时间戳的词Pyannote 给你一串带说话人标签的时间区间现在要把词分配下去。我的做法是按重叠时长最多的那个区间来判定而不是简单地看词的中点落在哪个区间里。为什么用重叠时长因为说话人切换的瞬间Pyannote 的边界和 Whisper 的词边界几乎不可能严丝合缝。一个词跨越了两段看中点可能判错但按重叠时长加权取占比大的那一边鲁棒性明显更好。def assign_speaker(word, turns): s, e word[start], word[end] best_speaker, best_overlap None, 0.0 for t in turns: overlap min(e, t[end]) - max(s, t[start]) if overlap best_overlap: best_overlap overlap best_speaker t[speaker] if best_speaker is None: # 完全没重叠说明落在一段静音或边界缝隙里退回用中点判断 mid (s e) / 2 for t in turns: if t[start] mid t[end]: return t[speaker] return best_speaker or UNKNOWN接下来把这些词合并成一句一句的话。合并规则是同一个说话人、且和上一句的间隔小于阈值就并成一句。这个阈值我一般设 0.6 到 1.0 秒太小会把一句话切得稀碎太大则会把两个人的交替发言粘在一起。def merge_into_utterances(words, turns, gap_threshold0.8): utterances [] for w in words: speaker assign_speaker(w, turns) if (utterances and utterances[-1][speaker] speaker and w[start] - utterances[-1][end] gap_threshold): utterances[-1][text] w[word] utterances[-1][end] w[end] utterances[-1][probs].append(w[prob]) else: utterances.append({ speaker: speaker, start: w[start], end: w[end], text: w[word], probs: [w[prob]], }) for u in utterances: u[text] u[text].strip() u[avg_prob] round(sum(u.pop(probs)) / len(u[probs]), 3) return utterances有个细节要提醒Whisper 输出的word字段对英文是带前导空格的比如 hello直接拼接是对的中文则是按 token 切分的不一定一个字一个 token所以中文的词级时间戳粒度是不均匀的有时候一个 token 覆盖好几个字。这不影响拼接结果的正确性但如果你要拿它做逐字高亮字幕粒度会显得很怪。这种情况下我建议退回按 segment 级别做对齐反而更整齐。3.4 输出成 SRT 和结构化 JSON对齐完就要落盘了。我一般同时输出两种格式带说话人前缀的 SRT 给人看结构化 JSON 给系统消费。def format_timestamp(seconds, sep,): h int(seconds // 3600) m int(seconds % 3600 // 60) s int(seconds % 60) ms int(round((seconds - int(seconds)) * 1000)) return f{h:02d}:{m:02d}:{s:02d}{sep}{ms:03d} def to_srt(utterances, speaker_namesNone): speaker_names speaker_names or {} lines [] for i, u in enumerate(utterances, start1): name speaker_names.get(u[speaker], u[speaker]) lines.append(str(i)) lines.append(f{format_timestamp(u[start])} -- {format_timestamp(u[end])}) lines.append(f[{name}] {u[text]}) lines.append() return \n.join(lines)speaker_names这个映射表是给人用的SPEAKER_00这种标签交付出去没人看得懂我会先跑一遍人工听一下开头几十秒确认哪个编号是谁然后填进去替换。这一步目前没什么好办法自动做除非接入声纹库做比对。4. 参数调优把错误率再往下压一压4.1 说话人数量约束性价比最高的一个开关如果你知道这场会议有几个人一定要把num_speakers传进去。这是所有参数里对结果影响最大的一个。不传的时候Pyannote 会用一个基于阈值的聚类自动决定分几类结果就是人数经常判多或判少。我实测过同一段四人会议不约束的时候分出了六个说话人把num_speakers4写死之后直接降到四个DER 肉眼可见地改善。如果人数不确定可以退一步用范围约束turns get_diarization_turns( meeting.wav, hf_tokenHF_TOKEN, min_speakers2, max_speakers6, )这两个参数不能和num_speakers同时用同时传会报错。我的经验是能确定就写死确定不了就给个合理的范围哪怕范围宽一点也比完全不约束强。另外一个技巧是用你已知的信息反推。比如电话客服录音必然是两个人那你直接写num_speakers2比任何聚类调参都管用。4.2 VAD 与断句参数决定时间轴颗粒度Whisper 的vad_filterTrue是用一个内置的 VAD 模型先切掉静音再送进 ASR这个开关主要作用是抑制幻觉。Whisper 在遇到长段静音或者噪音时会凭空编出一些句子中文场景下常见的是编出谢谢观看请点赞订阅这种训练数据里的高频短句。打开 VAD 之后这类问题能减少一大半。vad_parameters里最常调的是min_silence_duration_ms默认值是 2000 毫秒也就是静音超过两秒才断开。会议场景里我习惯调到 500 毫秒左右因为人与人对话的停顿往往不到两秒调大之后容易出现一段音频里塞进好几分钟的连续内容时间轴就不准了。Pyannote 那边也有对应的参数可以通过instantiate覆盖pipeline Pipeline.from_pretrained(pyannote/speaker-diarization-3.1, tokenHF_TOKEN) pipeline.instantiate({ segmentation: { min_duration_off: 0.0, # 允许更短的静音间隔 }, clustering: { method: centroid, min_cluster_size: 12, }, })min_duration_off调小会让切分更碎但说话人切换的边界会更精确调大会让片段更连续但快速交替的对话可能被合并。这个需要拿你自己的数据试没有万能值。4.3 Whisper 解码参数与幻觉抑制除了 VAD还有几个解码参数值得动beam_size默认是 5调大能略微提升准确率但耗时线性增长。我一般在离线批处理时用 5准实时场景降到 1 或 2。condition_on_previous_text默认是 True意思是把上一段的结果作为下一段的上下文提示。这个机制能提升连贯性但一旦模型开始幻觉错误会被不断放大形成循环重复。我现在的默认做法是设成 False牺牲一点连贯性换稳定。temperature可以传一个列表做回退策略默认就是[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]当某段的压缩率或对数概率不达标时自动升温重试。这个机制大部分时候有用但偶尔会引入不必要的随机性追求可复现的时候我会固定成[0.0]。还有一个很少人提但很有用的参数是initial_prompt。它可以给模型一段提示文本引导输出风格。比如你的录音里全是专业术语把术语表塞进initial_prompt能明显减少错字。但注意提示文本不能太长太长会挤占上下文窗口反而让模型开始重复。4.4 显存和速度的实际估算我把测试环境说清楚单张 12G 显存的消费级显卡音频是 16kHz 单声道 WAV一小时长度。以下是我实测的大致量级硬件不同差别会很大仅供参考。环节模型显存占用一小时音频耗时说话人日志pyannote 3.1约 1.5-2GB约 1-2 分钟转写large-v3 float16约 4-5GB约 8-15 分钟转写medium float16约 2.5-3GB约 4-6 分钟转写small int8约 1GB约 2-3 分钟可以看到说话人日志的耗时可忽略不计瓶颈全在 Whisper 上。所以如果整体太慢优先考虑降 Whisper 的规格或者上量化而不是去动 Pyannote。显存方面两个模型我都做了顺序执行加显存释放峰值占用出现在 Whisper 阶段。如果你要并发处理多个文件记得控制并发数large-v3 同时跑两个就可能 OOM。5. 常见问题与排查实录5.1 时间轴错位、句子被切碎最常见的一类问题。症状是输出的句子时间戳明显偏移或者一句话被切成了七八段。原因通常有三个一是音频没做重采样两边采样率假设不一致这个前面说过了二是 VAD 的min_silence_duration_ms太大段落粘在一起导致时间戳累积漂移三是合并阈值gap_threshold设得太小。排查顺序我一般这样走先单独打印 Pyannote 的输出人工对照音频听几个时间点确认它的时间轴是对的再单独打印 Whisper 的词级时间戳同样抽查几个点。两个都对问题就在对齐逻辑上如果其中一个不对那就不是对齐的锅。还有一个隐蔽的情况音频里存在长时间的音乐或环境噪声VAD 会把这些当成语音切出来送进 Whisper模型在这些片段上输出的时间戳会非常离谱。解决办法是在预处理阶段做一次能量检测把明显的非语音片段剔掉。5.2 说话人编号串了、两个人合成一个第二个高频问题。最直接的原因是没做人数约束前面讲过了。但即使做了约束还是可能出现串号这时候通常是声纹本身太接近比如两个人音色相似或者录音质量差导致嵌入向量区分度不够。可以尝试的补救手段有把min_cluster_size调大抑制小簇的产生检查音频里是不是有重叠说话两个人同时说重叠语音是说话人日志的老大难目前没有特别好的通用解法再就是做后处理统计每个说话人的总时长如果某个编号只出现了两秒钟大概率是误判可以把它合并到相邻编号。提示不要指望编号在多次运行之间保持一致。Pyannote 的编号是聚类时顺序生成的同一段音频跑两次编号可能互换。如果你需要一个稳定的说话人 ID得自己接一套声纹比对来做映射。5.3 幻觉、重复、语气词刷屏Whisper 的幻觉在中文场景下的典型表现是在静音段编出字幕由某某提供请不吝点赞这类句子或者把同一句话重复输出十几遍。我的处理是三重防护一起上vad_filterTrue先切静音condition_on_previous_textFalse切断错误传播再加一层输出后处理检测重复。重复检测的逻辑很简单把连续 N 句完全相同的文本合并或者删掉def dedup_utterances(utterances, max_repeat2): result [] for u in utterances: recent [x[text] for x in result[-max_repeat:]] if len(recent) max_repeat and all(t u[text] for t in recent): result[-1][end] u[end] # 只延长时间不再追加文本 continue result.append(u) return result语气词刷屏是另一回事那是模型如实转写了嗯啊那个这个不算错误。要做纪要的话可以在后处理里删掉但从转写准确度角度讲它是对的别去调参数惩罚它。5.4 常见问题速查表把上面这些整理成一张表方便对照排查现象最可能的原因优先尝试的解决方式401 / 403 报错未接受 Pyannote 模型条款或 token 无效去模型页点同意重新生成 read 权限 tokenAttributeError: itertracks新版返回 DiarizeOutput 对象用 getattr 取 speaker_diarization 字段mp3 加载失败torchaudio 后端缺失先用 ffmpeg 转成 16kHz 单声道 WAV说话人数明显偏多未约束聚类数量传 num_speakers 或 min/max_speakers时间戳整体偏移采样率未统一加重采样步骤两端统一 16kHz出现幻觉句子静音段被送进模型开 vad_filter关 condition_on_previous_text中文识别成日文语种自动检测误判transcribe 时写死 languagezh跑在 CPU 上很慢torch 的 CUDA 版本不匹配验证 torch.cuda.is_available()重装对应版本显存爆掉两个模型同时驻留顺序执行跑完 del empty_cache6. 工程化落地上的几个经验6.1 长音频分片和批处理一小时的音频直接整段跑没问题但如果你的输入是三四小时的会议或者多文件批量就得考虑分片。我的分片策略是按静音点切每片 10 到 15 分钟片与片之间留 1 秒重叠。留重叠是因为切点可能正好落在一个词中间重叠一小段能保证这个词至少在某一片里是完整的后面去重的时候按时间戳把重复部分删掉就行。分片还有个额外好处是能并行。多个进程各自跑一片最后按时间顺序合并总耗时能压到接近单片的水平。但注意别开太多并发Whisper large-v3 一开多显存就顶不住了一般并发数等于显存容量除以单实例占用再减一。另外Pyannote 的说话人日志建议在整段音频上跑而不是分片跑。因为说话人聚类依赖全局的声纹分布分片跑会导致同一个人的编号在每片里都不一样合并的时候完全对不上。这是个很容易踩的坑我第一版就是这么写的结果合并出来的结果里同一个人有三四个编号。6.2 想做到接近实时该怎么做纯离线批处理这套流程没法做到实时因为 Pyannote 的聚类需要看到足够长的上下文才能判断说话人数量。如果你要做成准实时的思路是分两步先用一个短窗口做在线聚类给每个新片段分配一个临时的说话人标签同时维护一个声纹库随着音频推进不断更新声纹库标签可能会在前期发生修正。这套东西实现复杂度比离线高一个量级除非产品明确要求我一般不建议一上来就做。如果只是想把延迟压到几秒可以用小模型加流式解码的方式。FasterWhisper 本身支持对音频块做增量转写配合小规格模型延迟能压到秒级代价是准确率下降。这个取舍要看你的场景纪要场景我强烈建议老老实实做离线。6.3 效果到底怎么评估调了半天参数总得有个量化的东西来判断好坏。ASR 侧用WER词错误率把人工标注的文本和模型输出对比用 jiwer 这类库直接算from jiwer import wer score wer(这是参考文本, 这是模型输出文本)中文算 WER 的时候通常按字算不用分词这样更接近实际观感。说话人侧用DER说话人日志错误率它由三部分组成漏检该说话的地方没检测到、误检不该说话的地方检测成说话、说话人混淆检测到了但分错人。用 pyannote 自带的评估工具from pyannote.metrics.diarization import DiarizationErrorRate from pyannote.core import Annotation, Segment metric DiarizationErrorRate() der metric(reference, hypothesis)实际做的时候我一般标个十分钟的测试集就够用了标太多不现实。标完之后固定这套测试集每次改参数都跑一遍看 DER 有没有改善避免凭感觉调参。顺便说一个我自己的感受DER 从 20% 降到 15%主观上会觉得结果靠谱多了但从 15% 再往下降到 10%对使用体验的改善就没那么明显了。所以如果资源有限先把 ASR 的准确率做上去性价比比死磕说话人日志高。最后分享一个小技巧也是我在实际项目里用得最多的一个别急着上大模型先用 small 加 int8 跑通整条流水线把数据流和对齐逻辑全部验证正确再换成 large-v3 重跑一遍。我第一版直接上 large-v3 调试每次改一行代码要等十分钟才能看到结果一天下来效率极低。换成小模型之后迭代速度提升十倍逻辑确认无误了再上大模型整个开发周期缩短了一大截。这个顺序上的小调整省下来的时间比我调所有参数加起来都多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进