Anarlog 多说话人识别全解:一段会议录音如何被拆成"谁在什么时候说了什么"
【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog
Anarlog 是一个开源的 AI 会议记录工具(定位类似 Granola),其中最能体现工程含量的部分,是多说话人识别——也就是语音行业常说的 speaker diarization(说话人分离):把一条混在一起的录音,切分成"0:00–0:12 是说话人 A,0:12–0:30 是说话人 B"这样的时间轴,再挂到转写的文字上。有意思的是,Anarlog 同时维护了两条管线:一条跑在本地的 ONNX 模型上,完全离线;另一条通过自建网关代理 pyannote 云服务。下面把这两条路径拆开讲。
两条管线:本地离线推理与云端 API 代理
先看仓库里的模块分工,两个 crate 承担了全部 diarization 逻辑:
- crates/pyannote-local/:本地离线管线。代码注释里写得很直白——"Mirrors the structure of the pyannote.audio 3.1 pipeline, tuned for CPU batch use",即复刻了 pyannote.audio 3.1 的算法结构,但针对 CPU 批处理场景做了调优,在录音结束后运行,而不是边录边算。
- crates/api-pyannote/:云端管线的代理层。暴露
/v1/diarize、/v1/identify、/v1/voiceprint三个接口,把请求转发给 pyannote 云 API。
云代理层看似只是转发,实际上做了几件安全上很关键的事:媒体的 URL 只接受media://users/{用户ID}/前缀、且逐段校验禁止..路径穿越,确保用户只能提交自己的文件;上游返回的 jobId 会被重新签名为一个带 HMAC-SHA256 签名的"job handle",绑定到发起调用的用户,别人拿到这个句柄也查不到任务结果。这些细节在 crates/api-pyannote/src/routes.rs 里都有对应的测试用例。
十秒滑窗:语音分离(Segmentation)到底在做什么
本地管线的第一步是分割。它加载的 ONNX 分割模型(segmentation-3.0)有两个硬约束:每次只能吃10 秒的 16kHz 单声道音频;每个窗口内最多区分3 个本地说话人。
模型的输出不是逐帧概率,而是 7 类"幂集"分类:静音、3 个单人、3 种双人组合。每个输出帧覆盖 270 个采样点,模型在 7 个类别里取 argmax,解码回"这个时刻哪些本地说话人在发声"。代码在 crates/pyannote-local/src/segmentation.rs,幂集解码逻辑就十几行。
长录音靠滑窗覆盖:默认步进 2 秒,每个时刻最多被约 5 个重叠窗口同时观测到——这个"少数服从多数"的冗余正是后面抑制误检的基础。同时管线对窗口总数设了上限(默认 1800 个),录音越长,步进自动增大,保证计算量有界,不会让两小时的录音把 CPU 打满。
声纹嵌入与聚类:从"窗口内的 3 个人"到"全场 N 个人"
分割模型只知道"这个窗口里有 3 个人在说话",但窗口 A 的"本地 1 号"和窗口 B 的"本地 2 号"很可能是同一个人。桥接两者的工具是声纹嵌入(embedding):把一段某人说话时的音频压缩成一个固定长度的向量,向量越接近代表声音越像。
这里的细节是整条管线里最见功力的部分:
- 掩码(masking)提取:只对"该说话人独自发声"的帧提取嵌入。两人重叠说话的片段会污染向量,所以代码专门统计每个本地说话人的 clean frames(独占帧),不足 1 秒的短促发言不配当"聚类锚点",只会被归入最近的簇。嵌入计算在 crates/embedding/ 中完成。
- 凝聚式聚类:先用锚点向量做质心链接(centroid-linkage)聚类,切割阈值默认 0.7045(这是对着 pyannote 官方参数调出来的);支撑时长过短的簇会被邻居吸收,避免把一个人的声音拆成两半。
- 全局投票重建:每个窗口的活动图带着聚类标签投到全局帧网格上,某帧获得过半票数才判为该说话人"正在说"。最后按首次出现顺序给说话人编号,再经过
min_duration_on/off的后处理合并碎片、桥接短停顿。 - 对齐文字:
assign_words函数把转写的每个词按"时间重叠最大"原则分配给某个说话人,落在空档里的词归给最近的发言段——这样 diarization 结果就能和 Whisper 转写逐词对齐了。
声纹库与"已知说话人":让匿名簇挂上真名
前两步产出的是"说话人 1、说话人 2"。要变成"张伟、李娜",靠的是声纹库。
crates/voiceprint/ 负责从已有转写里挑选干净的单人片段来建立声纹候选:合并间隔小于 400ms 的词、剔除同通道上的重叠语音(重叠段直接整段切掉)、每段保留 1.5 到 10 秒(超长段只保留中段,因为句首句尾的犹豫声更多)、每人最多取 3 段最长的。
有了声纹后,crates/pyannote-local/src/pipeline.rs 里的apply_known_speakers会做两件事:
- 合并碎片簇:本地 diarization 最常见的失败模式是同一个人的声音被拆成两个簇。如果两个簇都和同一个已知声纹高度相似,它们会被强制合并——这是声纹库对质量最大的贡献。
- 命名:合并后,每个簇与声纹库做余弦相似度匹配,只有匹配唯一且超过阈值时才会把名字挂上去,宁缺毋滥。
云端路径上,/v1/voiceprint负责生成声纹、/v1/identify负责用声纹库识别人,两者都走前面提到的安全代理。
用 DER 和 RTF 度量:这套算法被怎么验收
本地管线有一个专门的自动化评估协议(crates/pyannote-local/eval/PROGRAM.md),值得新手借鉴:
- 指标是 DER(Diarization Error Rate,说话人分离错误率),collar 设为 0.25 秒,即标注边界允许 0.25 秒的容差;数据集用的是公开的 AMI 和 VoxConverse 会议语料。
- 同时盯 RTF(实时率,处理 1 秒音频花多少秒),验收线是 0.15 以内——意味着 1 小时的录音在参考机器上约 1 分半内跑完,这是"本地 CPU 可用"的硬约束。
- 工作流是棘轮式的:一次只改一个参数(聚类阈值、锚点时长、后处理等),跑一遍 dev 集,DER 没有改善超过 0.1 个点就回滚,test 集留到最后才碰。
这套协议也解释了代码里那些"魔法数字"的来源:2 秒步进是为了让每个帧被约 5 个窗口覆盖;12 秒的最小簇时长对应 pyannote 官方的 12 个锚点;0.7045 的阈值直接沿用了 pyannote 3.1 的调优值。
边界在哪:重叠语音与本地/云端的取舍
读代码能看出作者对失效场景的清醒认知:每个窗口只能分辨 3 个说话人,所以 5 人以上同时密集插话时,窗口内就会出现"本地标签混叠";重叠发言越多,可用于嵌入的 clean frames 越少,簇的质量随之下降——这是滑窗式离线管线的结构性上限,不是调参能解决的。云端管线对超长、超高密度会议的鲁棒性更好,代价是音频要经过自建代理转发到第三方服务;而本地图谱的卖点正是录音不离开你的设备,这也是 Anarlog "privacy-first" 定位的直接体现。
从 10 秒窗口分割、掩码嵌入、凝聚聚类到投票重建和声纹命名,pyannote-local用大约两千行 Rust 代码把学术界的标准管线搬进了 CPU 上的批处理流程,并且每一个默认参数都有评估协议背书。想验证效果的读者,克隆仓库后直接跑cargo test -p pyannote-local就能看到它在内置双人测试音频上的断言通过情况。
【免费下载链接】anarlogOpen source Granola AI Alternative项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考