说话人分离(Diarization)技术实践:让 AI 知道一段录音里究竟是谁在说话

举报
yd_54922183 发表于 2026/10/28 09:06:36 2026/10/28
【摘要】 本文以开源 pyannote.audio Community-1 为例,通过会议录音分析和多语言客服通话转写两个案例,介绍如何使用 Python 为录音添加说话人标签。

摘要: ASR 可以把语音转成文字,但面对多人会议、客服通话时,还有一个问题:这些话分别是谁说的?说话人分离(Speaker Diarization)就是为解决这个问题而设计的。它利用语音分割、声纹特征提取与聚类,将不同说话人的发言划分出来。本文以开源 pyannote.audio Community-1 为例,通过会议录音分析和多语言客服通话转写两个案例,介绍如何使用 Python 为录音添加说话人标签。

一、什么是说话人分离?

假设我们录制了一场三人会议:

张三:我们下个月准备上线新系统。

李四:测试工作已经完成了吗?

张三:目前还差最后一轮验证。

王五:那我来跟进剩下的问题。

普通 ASR 可能只会输出连续的文字,无法告诉我们每句话由谁说出。

Diarization 要做的,就是把它们拆开:

00:01 - 00:04  SPEAKER_00
00:05 - 00:07  SPEAKER_01
00:08 - 00:11  SPEAKER_00
00:12 - 00:15  SPEAKER_02

注意,模型输出的 SPEAKER_00、SPEAKER_01 只是匿名说话人编号。

Diarization 能判断哪些话来自同一个人,但不知道这个人叫张三还是李四。

如果需要识别真实身份,还要结合已授权的声纹注册数据或其他身份信息。

1.1 它和 VAD、ASR 有什么区别?

模型 核心问题 输出
VAD 什么时候有人说话? 语音时间段
ASR 说了什么? 文字
Diarization 谁在什么时候说话? 时间段 + 说话人编号
Speaker Recognition 说话人是否为已知人员? 身份匹配结果

这些任务经常被组合使用,但并不是同一种技术。

1.2 核心原理

目前比较典型的说话人分离流水线可以分为四个阶段:

原始录音
   │
   ▼
语音活动检测与分割
   │
   ▼
提取说话人 Embeddings
   │
   ▼
相似度比较与聚类
   │
   ▼
生成说话人时间轴

这里又遇到了之前介绍过的 Embeddings。

但这次并不是文本 Embeddings,而是 Speaker Embeddings(说话人声纹向量)。

模型会从语音中提取音色、声道共振特征、发声习惯等声学信息,将其映射成向量。

随后,利用聚类算法把相似的语音片段归为同一个说话人。

这也解释了为什么 Diarization 可以支持多语言。

它主要比较的是“声音像不像同一个人”,而不是“这个人说了什么词”。

即使一位用户上一句说中文、下一句说英文,模型理论上仍然可以将其归为同一个说话人。

不过,多语言兼容并不代表在所有语言、口音和录音环境中具有相同准确率。


二、选择哪个开源模型?

目前可以关注两条主要技术路线:

方案 技术特点 适合场景
pyannote Community-1 语音分割、Speaker Embeddings、聚类流水线 多语言会议、客服录音
NVIDIA Sortformer 端到端 Transformer 说话人分离 多人语音、实时应用研究

本文选择 pyannote/speaker-diarization-community-1。

它于 2025 年随 pyannote.audio 4.0 发布,具有几个值得关注的特点:

  • 不要求输入指定语言,支持跨语言说话人分离。
  • 支持普通说话人时间轴与重叠发言检测。
  • 可以指定说话人数量或数量范围。
  • 支持 CPU、GPU 和离线部署。
  • 提供专门用于 ASR 时间戳对齐的 exclusive_speaker_diarization。

官方在中文语音数据集上公布过以下评估结果:

中文数据集 旧版 3.1 DER Community-1 DER
AISHELL-4 12.2% 11.7%
AliMeeting(单通道) 24.5% 20.3%

DER(Diarization Error Rate)是说话人分离错误率,越低越好。这里的数据来自官方公开基准测试,包含重叠语音评估,并不代表所有实际录音都能达到同样效果。

官方项目:

Community-1 模型采用 CC-BY-4.0 许可,首次下载需要登录 Hugging Face 并接受模型页面的访问条件。

三、实践案例一:自动分析多人会议录音

3.1 业务场景

假设我们有一段 30 分钟的中文会议录音,希望知道:

  • 会议中有几个人发言?
  • 每个人什么时候开始和停止发言?
  • 哪些语音片段属于同一个人?

这类场景不需要立即运行 ASR,先完成说话人分离即可。

3.2 环境准备

建议使用 Python 3.11 或更高版本。

安装 FFmpeg,并确认 PyTorch 与自己的 CPU 或 CUDA 环境兼容,然后安装:

pip install "pyannote.audio>=4,<5"

到 Hugging Face 的 Community-1 模型页面接受访问条件,然后创建一个具有模型读取权限的 Access Token。

设置环境变量:

HF_TOKEN=你的HuggingFace访问令牌

假设准备的录音是 meeting.wav。

如果是 MP3,可以先转成 16kHz 单声道:

ffmpeg -i meeting.mp3 -ar 16000 -ac 1 meeting.wav

实际上 Community-1 可以自动完成重采样和声道混合,这里只是让输入更统一。

3.3 Python 完整实现

import os
import torch
from pyannote.audio import Pipeline

# 1. 加载开源说话人分离模型
pipeline = Pipeline.from_pretrained(
    "pyannote/speaker-diarization-community-1",
    token=os.environ["HF_TOKEN"]
)

# 2. 有GPU则使用GPU
if torch.cuda.is_available():
    pipeline.to(torch.device("cuda"))

# 3. 处理会议录音
# 假设会议有2~5名发言人
output = pipeline(
    "meeting.wav",
    min_speakers=2,
    max_speakers=5
)

# 4. 输出说话人时间轴
for turn, _, speaker in (
    output.speaker_diarization.itertracks(
        yield_label=True
    )
):
    print(
        f"{turn.start:.2f}s - "
        f"{turn.end:.2f}s | {speaker}"
    )

# 5. 导出标准RTTM文件
with open("meeting.rttm", "w", encoding="utf-8") as f:
    output.speaker_diarization.write_rttm(f)

假设这段会议确实有三个人,理想输出可能如下:

1.20s - 4.50s  | SPEAKER_00
4.80s - 7.30s  | SPEAKER_01
7.50s - 11.20s | SPEAKER_00
11.50s - 15.80s| SPEAKER_02

以上为演示结果,真实输出由录音决定。

这里有两个比较实用的参数:

# 已知只有两名说话人
output = pipeline("meeting.wav", num_speakers=2)

# 不确定人数,但估计有2~5人
output = pipeline(
    "meeting.wav",
    min_speakers=2,
    max_speakers=5
)

对于只有客服和客户两人的电话录音,明确传入 num_speakers=2 往往是值得尝试的优化,但前提是录音确实只有两名说话人。

输出的 RTTM 是说话人分离领域常用的标注格式,可以用于后续可视化或评测。


四、实践案例二:给中英文客服通话自动标注说话人

4.1 业务场景

第二个案例更接近完整的语音 AI 应用。

假设客服通话中出现了中英文混合:

客服:您好,请问有什么可以帮助您?

客户:I want to check my refund status.

客服:好的,请提供您的订单编号。

我们希望最后得到一份这样的记录:

[00:01] SPEAKER_00:您好,请问有什么可以帮助您?

[00:04] SPEAKER_01:I want to check my refund status.

[00:08] SPEAKER_00:好的,请提供您的订单编号。

只使用 Diarization 不够,因为它不会识别文字。

只使用 ASR 也不够,因为它不一定知道不同句子属于谁。

因此需要把两者结合起来。

4.2 整体架构

中英文客服录音
       │
       ├─────────────┐
       ▼             ▼
  pyannote       Whisper ASR
       │             │
       ▼             ▼
说话人时间轴      文字 + 时间戳
       │             │
       └──────┬──────┘
              ▼
         时间轴对齐
              │
              ▼
       带说话人的转写记录

这里有一个难点:ASR 的文字时间戳与 Diarization 的说话人分段,并不一定完全一致。

Community-1 专门提供了一个 exclusive_speaker_diarization 结果。

它与普通分离结果的区别是:每个时刻最多只保留一个说话人。

这样更方便把 ASR 识别出的字词归属到一个人身上。

代价是:两个人同时说话时,互斥结果会丢失部分重叠发言信息。

4.3 Python 实现

为了方便复现,使用开源的 Faster-Whisper 进行 ASR:

pip install faster-whisper

相关项目:Faster-Whisper GitHub

下面的代码沿用前一个案例已经加载的 pipeline。

from faster_whisper import WhisperModel

audio = "call.wav"

# 1. 执行说话人分离
output = pipeline(audio, num_speakers=2)

# 获取互斥说话人时间段
turns = [
    (turn.start, turn.end, speaker)
    for turn, _, speaker in
    output.exclusive_speaker_diarization.itertracks(
        yield_label=True
    )
]

# 2. 加载多语言ASR
# CPU INT8便于复现,可按设备更换为CUDA
asr = WhisperModel(
    "small",
    device="cpu",
    compute_type="int8"
)

# 3. 识别文字和词级时间戳
segments, info = asr.transcribe(
    audio,
    word_timestamps=True,
    beam_size=5
)

# 4. 按时间重叠程度匹配说话人
def find_speaker(start, end):
    scores = {}

    for s, e, speaker in turns:
        overlap = max(
            0.0,
            min(end, e) - max(start, s)
        )

        scores[speaker] = (
            scores.get(speaker, 0.0) + overlap
        )

    if not scores or max(scores.values()) == 0:
        return "UNKNOWN"

    return max(scores, key=scores.get)


# 5. 按词的时间戳分配说话人
records = []

for segment in segments:
    for word in segment.words or []:
        speaker = find_speaker(
            word.start,
            word.end
        )

        records.append({
            "start": word.start,
            "speaker": speaker,
            "text": word.word
        })

# 6. 合并相邻同一说话人的文字
groups = []

for record in records:
    if groups and groups[-1]["speaker"] == record["speaker"]:
        groups[-1]["text"] += record["text"]
    else:
        groups.append(record.copy())

for item in groups:
    print(
        f'[{item["start"]:.2f}s] '
        f'{item["speaker"]}: {item["text"].strip()}'
    )

这里使用的是最基础的词级时间重叠匹配方法,并不是一个经过独立评测的完整对齐算法。

如果客户和客服同时讲话、ASR 时间戳出现偏移,仍然可能发生说话人归属错误。此外,small 模型对频繁中英混说的识别能力有限,可以根据实际效果换用更大的多语言 ASR。

在正式系统里,还可以把结果进一步交给 LLM,生成客服质检报告、客户诉求摘要,或者统计客服与客户各自的发言时间。


五、实际使用时,有哪些坑?

第一,Diarization 不等于声纹身份认证。

SPEAKER_00 只能说明这些片段被模型判断为同一个说话人。

不同录音中的 SPEAKER_00 并不自动代表同一个真实的人。

第二,多人同时说话是最难处理的情况之一。

当两个人抢话、打断、同时发声时,分离错误率通常会升高。

原始说话人时间轴可以表示重叠发言;但上面的互斥对齐方法会将同一时刻分配给一个人,因此不适合要求完整还原交叠发言的场景。

第三,多语言兼容不等于多语言 ASR。

pyannote 本身主要依靠声学特征,不需要根据中文、英文分别配置模型。

但如果要输出文字,ASR 仍然需要覆盖相应语言。特别是中英文在一句话内交替时,转写效果可能下降。

第四,离线分离与实时分离是不同难度的任务。

本文使用的是完整录音的离线处理方式。

如果想在实时语音客服中判断当前是哪位用户说话,还需要考虑增量聚类、说话人状态保持、历史修正,以及实时延迟。不能把离线处理代码直接当成低延迟流式方案。

5.1 如何评估模型?

最常用的指标是 DER:

[
DER=\frac{FA+MISS+CONF}{TOTAL}
]

其中:

  • FA:把非语音错误识别成语音的时长。
  • MISS:漏掉真实语音的时长。
  • CONF:将语音归属到错误说话人的时长。
  • TOTAL:真实说话人语音总时长。

DER 越低,代表整体说话人分离效果越好。

但评估时应同时记录是否计算重叠语音、是否使用边界容忍区间,否则不同系统的 DER 不能简单横向比较。

对于客服录音,建议额外关注说话人数量准确率、换人时间点检测,以及最终带说话人标签的转写准确率。


六、总结

说话人分离是一个看起来不起眼,但在实际语音应用中非常关键的技术。

它与前面研究过的模型可以形成如下流程:

       多人语音
          │
          ├─────────────┐
          ▼             ▼
      Diarization      ASR
          │             │
          ▼             ▼
      说话人时间轴    转写文字
          │             │
          └──────┬──────┘
                 ▼
         带说话人标签的文本
                 │
                 ▼
            LLM / 决策模型
                 │
                 ▼
        会议纪要 / 客服质检

本文两个案例说明了它的主要价值:

  • 会议录音分析: 自动区分不同发言人的时间段。
  • 多语言客服转写: 结合 ASR,为中文、英文等语音内容添加说话人标签。

和上一期介绍的 VAD 不同,Diarization 不只关心有没有人说话,还要尝试判断前后两段话是不是来自同一个人。

最后,可以用三个问题概括这套语音处理体系:

VAD:有人在说话吗?

ASR:他说了什么?

Diarization:这句话是谁说的?

把这三个问题分别解决,再交给 LLM 进行语义理解,才更容易构建一个真正能处理多人对话的语音 AI 系统。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。