说话人分离为什么会认错人
这个 bug 我查错了方向,前两周一直在怀疑声纹模型。
会议转录里说话人对不上号,通常是哪一步出的错?
大多数时候不是声纹模型分不清人,是切分粒度太粗。把一整段包含多人接话的语音当成一个单位交给后面,无论用什么聚类算法,错误在那之前就已经定死了。
- iOS 到 26 为止没有原生的说话人分离 API
- 早期用 VAD 段落加段级聚类,一段里多人接话就会整段归给一人
- 现在用 pyannote segmentation-3.0 在音频层切话轮,再逐话轮提声纹聚类
- 声纹模型是 CAM++ zh_en advanced,sherpa-onnx 库里唯一的中英联合模型
先确认没有现成的可以用
做这个功能之前我先把苹果的原生框架翻了一遍,想看看能不能不引第三方模型。结论是到 iOS 26 为止都没有。SpeechAnalyzer 和 SpeechTranscriber 支持长音频离线转写,但输出里没有说话人标签;SFVoiceAnalytics 给的是 Jitter、Shimmer、Pitch 这类声音质量指标,不能用来区分人;老的 SFSpeechRecognizer 从来就没支持过。
当时调研的结论是用 FluidAudio,一个 Swift SDK,底层是 pyannote 加 WeSpeaker 转成 CoreML,跑在神经网络引擎上,模型三十多兆,实时倍速能到 8 到 15 倍。看起来是最省事的选择。
最后没用它。畅译的语音识别本来就跑在 sherpa-onnx 上,说话人分离如果换一套 CoreML 的栈,就要同时维护两种模型格式、两套加载和回退逻辑、两份资产包下载。sherpa-onnx 自己带了离线说话人分离管线,模型换成 pyannote segmentation-3.0 加 CAM++ 就能跑,整个链路上只有一个运行时。
最早的做法,和它在真会议上的表现
第一版是最直觉的做法:先用 VAD 把录音切成有人说话和静音两类区间,每个说话区间提一条声纹,最后跑层次聚类把相似的归成同一个人。VAD 是语音活动检测,判断一小段音频里有没有人在出声。
这套在我自己造的测试音频上跑得很好。一个人念稿子,或者找同事一人一句轮流说,结果都对。
然后我拿了一段真的团队周会进去,三个人,四十多分钟。转出来一看,中间有一千多字整块挂在同一个人名下,但我记得很清楚那段是两个人在来回讨论一个方案。
我查错了方向
第一反应是声纹模型分不清这两个人的音色。他们俩确实都是男声,音高接近。于是我去换声纹模型的参数、调聚类的合并阈值,前后大概弄了两周。每次调完,总有一批录音变好、另一批变差,始终没有一组参数能同时把手上几十段测试音频都做对。
后来我把 VAD 的切分结果单独打出来看,才发现问题根本不在声纹这一层。那一千多字对应的是一个完整的 VAD 区间。两个人你一句我一句接得很紧,中间的停顿短到 VAD 认为整段都是有人在说话,于是把这一整块当成一个单位交给了后面。
到这一步,无论声纹提得多准、聚类多聪明,这一整块也只能返回一个说话人。错误在进入聚类之前就写死了,我调的那两周全在下游打转。
把切分交给模型
现在切分这一步单独交给 pyannote segmentation-3.0。它训练的目标就是判断谁在什么时刻开始说、什么时刻结束,也包括两个人重叠说话的区间。和 VAD 的区别在于它给的是话轮边界,不是有没有人在说话。同一个 VAD 区间里如果发生了三次说话人切换,它会切成三段。
切完话轮再逐段提声纹、做全局聚类。说话人切换点由模型在音频上定位,不需要等文本转出来之后再回头猜这句话的措辞更像谁。那段周会重新跑,一千多字被拆成十几个话轮,归属基本对上了。
有一点值得说清楚:聚类的合并阈值现在还是写死的 0.7,我并没有解决掉固定阈值这个问题。变化在于切分准了之后,进入聚类的每一段都只包含一个人,阈值偏一点造成的后果从整段归错人变成了个别段落合并得保守一些。同一个参数,误差的量级不一样了。
双语这块没得选
声纹模型定的是 CAM++ zh_en advanced,理由很简单,sherpa-onnx 库里只有这一个中英联合的声纹模型。
畅译的用户里中英混说不是边缘情况。技术会议、留学场景下大量对话本身就夹着英文。如果中文用一个声纹模型、英文用另一个,同一个人在中英之间切换时,两条声纹向量落在不同的空间里,聚类会把他当成两个人。所以这里其实不存在选型,只有一个能用。
还有一次,跟算法没关系
聚类和段落拆分的逻辑,历史上在两个地方各写了一遍:会议详情页的 ViewModel 一份,批量导入队列一份。写的时候没觉得有问题,两边看着是一样的。
后来出现同一段音频走录音路径和走导入路径结果不一致的情况,其中一次还丢了字。查了很久才定位到是两份实现在某个边界条件上处理不同。
现在这套逻辑收在一个 Service 里,所有调用方都必须经它执行。这个坑跟算法准不准没关系,同一个算法有两份实现,出问题时你不知道该怀疑哪一份,查起来比算法本身不够好麻烦多了。