基于CAM++声纹识别的会议发言人自动标注AI助手实践
利用CAM++声纹识别与FunASR工具链,实现会议录音中说话人自动标注。系统集成VAD、ASR、声纹聚类与标点恢复,输出带说话人标签的结构化文本,并支持声纹注册映射真实姓名,便于生成会议纪要与待办事项。
先说几个核心判断:ASR 解决的是“说了什么”,但会议场景的用户往往会追问另一件事——这句话是谁说的?
如果只有一整段连续转写文本,后续整理纪要时仍然需要人工反复回听。尤其是多人会议、项目评审、访谈记录、专家讨论、远程协作会议中,“谁在什么时候说了什么”往往和会议内容本身同样重要。这就需要在 ASR 之外加入说话人标注能力。
CAM++ 的作用,正是在会议音频中提取说话人声纹特征,并辅助系统判断不同语音片段是否来自同一个人。结合 ASR、VAD 和标点恢复后,系统可以把原始录音整理成更接近会议记录的结构:
Speaker 0:本次会议先确认部署计划。
Speaker 1:我补充一下,接口联调需要提前完成。
Speaker 2:后续待办由运维侧跟进。
如果再接入声纹注册信息,还可以进一步把 Speaker 0、Speaker 1 映射为具体参会人姓名。
本文以 CAM++ 和 FunASR 为基础,梳理一条从会议录音到发言人自动标注的技术链路。在本文的后处理层中,熙瑾会悟负责接收带说话人信息的转写结果,并继续生成会议纪要、待办事项和归档资料。
二、整体链路:从音频到“谁说了什么”
会议发言人标注不是单独一个模型就能完成的任务。它通常需要多个模块协同:
ASR 负责识别文字。VAD 负责检测有效语音区间。CAM++ 负责提取说话人声纹特征。说话人聚类负责把同一人的片段归到一起。标点模型负责恢复句子边界。会议处理模块负责生成纪要、待办和归档资料。
一个较完整的链路可以表示为:
会议录音
↓
音频预处理:格式转换、采样率统一、单声道处理
↓
VAD:检测有效语音片段
↓
ASR:识别每段语音内容
↓
CAM++:提取声纹特征,辅助说话人聚类
↓
标点恢复:形成句级文本
↓
句级结构化:speaker_id | start_time | end_time | text
↓
熙瑾会悟:会议纪要、待办事项、资料归档
这条链路的重点不是单纯“识别出几个人”,而是把语音识别结果变成可被会议系统继续处理的结构化数据。
原始 ASR 文本可能是这样的:
今天我们先确认部署时间接口联调这边需要提前完成后续由运维侧跟进。
加入说话人标注后,可以变成:
Speaker 0:[00:00:01 - 00:00:04] 今天我们先确认部署时间。
Speaker 1:[00:00:05 - 00:00:09] 接口联调这边需要提前完成。
Speaker 2:[00:00:10 - 00:00:13] 后续由运维侧跟进。
后一种结果更适合继续生成纪要、任务清单和责任人记录。
三、环境准备:搭建 FunASR + CAM++ 推理环境
这里从本地环境开始搭建,便于后续接入内部会议系统。
推荐环境如下:
| 配置项 | 建议配置 |
|---|---|
| 操作系统 | Ubuntu 20.04 / 22.04 或其他 Linux 服务器环境 |
| Python | Python 3.10 |
| GPU | 建议使用 NVIDIA GPU,测试环境可先用 CPU |
| 音频工具 | ffmpeg |
| 语音工具链 | FunASR |
| 说话人模型 | CAM++ |
| 服务框架 | FastAPI / Uvicorn |
创建环境:
conda create -n meeting-spk python=3.10 -y
conda activate meeting-spk
pip install -U funasr
pip install modelscope
pip install fastapi uvicorn python-multipart requests pydub
如果部署环境无法访问外网,可以提前下载相关模型,再拷贝到内网服务器。常见链路中会用到 ASR、VAD、标点和说话人模型:
modelscope download --model iic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch --local_dir ./models/paraformer
modelscope download --model iic/speech_fsmn_vad_zh-cn-16k-common-pytorch --local_dir ./models/fsmn-vad
modelscope download --model iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch --local_dir ./models/ct-punc
modelscope download --model iic/speech_campplus_sv_zh-cn_16k-common --local_dir ./models/campplus
正式部署时,建议把模型、代码、依赖包、启动脚本和配置文件放在同一套交付目录中。这样可以避免现场临时下载模型,也便于版本回退和问题定位。
四、第一次测试:输出带 speaker 标签的会议转写
先准备一段测试会议音频:
./samples/meeting_two_speakers.wa v
这段音频最好包含两到三个人轮流发言,便于观察说话人标注效果。
新建 test_campplus_diarization.py:
from funasr import AutoModel
model = AutoModel(
model="paraformer-zh",
vad_model="fsmn-vad",
punc_model="ct-punc",
spk_model="cam++",
device="cuda:0",
)
result = model.generate(
input="./samples/meeting_two_speakers.wa v",
batch_size_s=60,
)
for sentence in result[0].get("sentence_info", []):
speaker = sentence.get("spk", "unknown")
start = sentence.get("start", 0)
end = sentence.get("end", 0)
text = sentence.get("text", "")
print(f"Speaker {speaker}: [{start}ms - {end}ms] {text}")
执行:
python test_campplus_diarization.py
输出可能类似:
Speaker 0: [680ms - 3620ms] 今天我们先确认部署时间。
Speaker 1: [4050ms - 8120ms] 接口联调需要提前完成,否则会影响后续测试。
Speaker 0: [8560ms - 11900ms] 那这个事项就先由运维侧跟进。
这一步说明基本链路已经打通:系统不仅识别出了文本,还给每一句话附加了说话人标签。
需要注意的是,Speaker 0、Speaker 1 只是聚类结果,不等同于真实姓名。要进一步变成“张总”“李工”“王主任”,还需要声纹注册和身份映射。
五、音频预处理:先保证输入质量
说话人标注对音频质量比较敏感。会议室回声、多人同时说话、远距离拾音、麦克风音量不均衡,都会影响聚类效果。
进入模型前,建议先统一音频格式:
ffmpeg -y -i input.m4a -ar 16000 -ac 1 output.wa v
批量转换目录:
mkdir -p ./wa v_meetings
for file in ./raw_meetings/*; do
name=$(basename "$file")
base="${name%.*}"
ffmpeg -y -i "$file" -ar 16000 -ac 1 "./wa v_meetings/${base}.wa v"
done
也可以封装成 Python 函数:
from pathlib import Path
from pydub import AudioSegment
def normalize_audio(input_path: str, output_dir: str = "./normalized") -> str:
"""
将会议录音统一转为 16kHz、单声道 WA V。
"""
input_path = Path(input_path)
output_dir = Path(output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / f"{input_path.stem}.wa v"
audio = AudioSegment.from_file(input_path)
audio = audio.set_frame_rate(16000).set_channels(1)
audio.export(output_path, format="wa v")
return str(output_path)
对于发言人标注而言,音频预处理不是可有可无的步骤。输入质量越稳定,后续说话人聚类、声纹比对和时间轴对齐就越可靠。
六、从 speaker_id 到真实姓名:增加声纹注册表
CAM++ 可以帮助系统判断不同音频片段是否属于同一个说话人,但它本身不会知道这个人是谁。
要从 Speaker 0 进一步映射到真实姓名,需要建立声纹注册表。
基本思路如下:
参会人预先录入一段注册音频
↓
CAM++ 提取注册音频的声纹向量
↓
会议中提取每个 speaker_id 的声纹向量
↓
计算相似度
↓
匹配到已注册人员
↓
输出真实姓名 + 发言内容
可以先设计一个简单的注册信息结构:
{
"speaker_profiles": [
{
"user_id": "u001",
"name": "张主任",
"voice_sample": "./profiles/zhang.wa v"
},
{
"user_id": "u002",
"name": "李工",
"voice_sample": "./profiles/li.wa v"
},
{
"user_id": "u003",
"name": "王经理",
"voice_sample": "./profiles/wang.wa v"
}
]
}
实际系统中,声纹注册不建议放在普通文件里长期保存。更稳妥的做法是保存声纹特征向量,并结合权限、加密和审计策略管理。
身份映射后的会议片段可以变成:
[
{
"speaker_id": "Speaker 0",
"speaker_name": "张主任",
"start_ms": 680,
"end_ms": 3620,
"text": "今天我们先确认部署时间。"
},
{
"speaker_id": "Speaker 1",
"speaker_name": "李工",
"start_ms": 4050,
"end_ms": 8120,
"text": "接口联调需要提前完成,否则会影响后续测试。"
}
]
这一步完成后,会议记录就不再只是“发言人 1 / 发言人 2”,而是可以进入更明确的责任归属和事项追踪。
七、封装接口:把发言人标注变成内部服务
脚本推理适合验证,不适合长期作为业务系统接口。更稳妥的方式是把发言人标注封装成一个 HTTP 服务。
新建 speaker_server.py:
import time
import tempfile
from pathlib import Path
from typing import Optional
from fastapi import FastAPI, UploadFile, File, Form
from pydantic import BaseModel
from funasr import AutoModel
app = FastAPI(title="Meeting Speaker Label Service", version="1.0.0")
model = AutoModel(
model="paraformer-zh",
vad_model="fsmn-vad",
punc_model="ct-punc",
spk_model="cam++",
device="cuda:0",
)
class SpeakerSegment(BaseModel):
speaker_id: str
start_ms: int
end_ms: int
text: str
class SpeakerResponse(BaseModel):
text: str
process_sec: float
segments: list[SpeakerSegment]
@app.get("/health")
def health_check():
return {
"status": "ok",
"asr": "paraformer-zh",
"spk_model": "cam++"
}
@app.post("/speaker-label", response_model=SpeakerResponse)
async def speaker_label(
audio_file: UploadFile = File(...),
language: Optional[str] = Form(default="zh"),
):
suffix = Path(audio_file.filename).suffix or ".wa v"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
tmp.write(await audio_file.read())
tmp_path = tmp.name
start_time = time.time()
result = model.generate(
input=tmp_path,
batch_size_s=60,
)
process_sec = time.time() - start_time
all_text = result[0].get("text", "")
segments = []
for sentence in result[0].get("sentence_info", []):
segments.append({
"speaker_id": str(sentence.get("spk", "unknown")),
"start_ms": int(sentence.get("start", 0)),
"end_ms": int(sentence.get("end", 0)),
"text": sentence.get("text", ""),
})
return {
"text": all_text,
"process_sec": round(process_sec, 3),
"segments": segments,
}
启动服务:
uvicorn speaker_server:app --host 0.0.0.0 --port 7863
测试健康检查:
curl http://127.0.0.1:7863/health
上传会议录音测试:
curl -X POST "http://127.0.0.1:7863/speaker-label" \
-F "audio_file=@./samples/meeting_two_speakers.wa v"
返回结果可以设计为:
{
"text": "今天我们先确认部署时间。接口联调需要提前完成,否则会影响后续测试。那这个事项就先由运维侧跟进。",
"process_sec": 4.315,
"segments": [
{
"speaker_id": "0",
"start_ms": 680,
"end_ms": 3620,
"text": "今天我们先确认部署时间。"
},
{
"speaker_id": "1",
"start_ms": 4050,
"end_ms": 8120,
"text": "接口联调需要提前完成,否则会影响后续测试。"
},
{
"speaker_id": "0",
"start_ms": 8560,
"end_ms": 11900,
"text": "那这个事项就先由运维侧跟进。"
}
]
}
到这里,CAM++ 相关能力就从脚本验证变成了可被会议系统调用的内部服务。
八、接入会议助手:发言人信息如何进入纪要生成
带说话人标签的转写结果,不能只停留在接口返回层。真正有用的是让它进入会议纪要和任务提取流程。
在熙瑾会悟的处理链路中,segments 可以作为会议文本结构化输入。系统可以基于每个片段的 speaker_id、时间范围和文本内容继续整理会议摘要、议题、决策和待办事项。
提交给后处理接口的数据可以设计为:
import requests
from datetime import datetime
MEETING_API = "http://127.0.0.1:18080/api/meeting/minutes/generate"
def submit_to_meeting_system(speaker_result: dict):
payload = {
"meeting_title": "项目部署评审会",
"meeting_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"source": "campplus_speaker_label",
"speaker_label": True,
"transcript": speaker_result["text"],
"segments": speaker_result["segments"],
"output_format": {
"summary": True,
"topics": True,
"decisions": True,
"todos": True,
"speaker_view": True
}
}
resp = requests.post(MEETING_API, json=payload, timeout=300)
resp.raise_for_status()
return resp.json()
后续输出可以是:
{
"summary": "本次会议主要讨论项目部署时间、接口联调计划和后续运维跟进安排。",
"topics": [
"部署时间确认",
"接口联调风险",
"运维跟进安排"
],
"decisions": [
"接口联调需要提前完成",
"部署事项由运维侧继续跟进"
],
"todos": [
{
"speaker": "Speaker 1",
"task": "提前完成接口联调",
"deadline": "本周五"
},
{
"speaker": "Speaker 0",
"task": "确认部署时间并同步参会人员",
"deadline": "本周内"
}
],
"speaker_view": [
{
"speaker": "Speaker 0",
"summary": "主要负责确认部署时间和后续安排。"
},
{
"speaker": "Speaker 1",
"summary": "主要补充接口联调风险和前置条件。"
}
]
}
这类结构的价值在于,会议纪要不再只是一段摘要,而是可以保留“发言来源”。当后续需要复核某个结论或待办时,可以回到对应发言片段和时间范围。
九、身份映射后的会议纪要效果
如果已经建立参会人声纹库,并且完成了 speaker_id 到真实姓名的映射,会议纪要可以进一步变成:
会议主题:项目部署评审会
一、会议摘要
本次会议主要确认项目部署时间、接口联调计划和后续运维安排。
二、主要发言
张主任:今天我们先确认部署时间。
李工:接口联调需要提前完成,否则会影响后续测试。
张主任:那这个事项就先由运维侧跟进。
三、会议决策
1. 接口联调需要提前完成。
2. 运维侧负责跟进部署相关事项。
四、待办事项
1. 李工:完成接口联调,截止时间本周五。
2. 运维侧:确认部署时间并同步项目组,截止时间本周内。
这时,系统已经从“会议转写”进入“会议责任追踪”。
声纹识别不是为了把会议做得更复杂,而是为了减少会后整理成本:谁提出、谁确认、谁负责,能够在纪要生成时被保留下来。
十、长会议场景:说话人标注要注意分段策略
长会议录音不建议一次性完整送入模型。更稳妥的方式是先进行分段,再进行识别和说话人标注。
简单的固定时长分段方式如下:
from pydub import AudioSegment
from pathlib import Path
def split_audio_fixed(
wa v_path: str,
output_dir: str = "./segments",
chunk_ms: int = 180_000,
overlap_ms: int = 3_000,
):
"""
将长会议录音切成多个片段。
chunk_ms: 每段长度,默认 180 秒
overlap_ms: 相邻片段重叠,默认 3 秒
"""
wa v_path = Path(wa v_path)
output_dir = Path(output_dir) / wa v_path.stem
output_dir.mkdir(parents=True, exist_ok=True)
audio = AudioSegment.from_wa v(wa v_path)
segments = []
start = 0
index = 1
while start < len(audio):
end = min(start + chunk_ms, len(audio))
segment = audio[start:end]
segment_path = output_dir / f"{wa v_path.stem}_{index:04d}.wa v"
segment.export(segment_path, format="wa v")
segments.append(str(segment_path))
if end >= len(audio):
break
start = end - overlap_ms
index += 1
return segments
需要注意的是,分段会带来一个问题:不同片段里的 Speaker 0 不一定代表同一个人。
因此,在长会议场景中,如果希望跨片段保持发言人一致,需要进一步做声纹向量级别的合并,而不能只依赖每个片段内部的 speaker 编号。
更完整的长会议处理方式可以是:
长会议录音
↓
VAD 或固定时长切分
↓
每段 ASR + CAM++ 说话人标注
↓
提取每个 speaker_id 的声纹向量
↓
跨片段聚类或与注册声纹库比对
↓
统一 speaker_id
↓
生成全局会议发言记录
这一步对长会议尤其重要。否则,同一个人在第 1 段里是 Speaker 0,在第 2 段里可能变成 Speaker 2,后续纪要就会出现发言人混乱。
十一、本地部署建议:声纹能力应与 ASR 服务解耦
在本地部署中,不建议把 ASR、声纹识别、会议后端、总结模型全部塞进一个服务里。更稳妥的方式是拆分部署,便于独立升级和排障。
一个较清晰的结构如下:
Nginx / 网关
├── 会议系统前端
├── 会议系统业务后端
├── 文件服务:会议录音、附件、纪要文件
├── ASR 服务
├── 说话人标注服务:CAM++
├── 会议总结模型服务
├── Redis / MySQL / Elasticsearch
└── 日志与监控
说话人标注服务可以独立容器化:
version: "3.8"
services:
meeting-speaker-label:
image: meeting/speaker-label:campplus
container_name: meeting-speaker-label
restart: always
ports:
- "7863:7863"
volumes:
- ./models:/app/models
- ./logs:/app/logs
- ./tmp:/app/tmp
environment:
- SPK_MODEL=cam++
- ASR_MODEL=paraformer-zh
- VAD_MODEL=fsmn-vad
- PUNC_MODEL=ct-punc
- DEVICE=cuda:0
command: >
uvicorn speaker_server:app
--host 0.0.0.0
--port 7863
业务系统只需要配置说话人标注服务地址:
SPEAKER_LABEL_PROVIDER=campplus
SPEAKER_LABEL_BASE_URL=http://meeting-speaker-label:7863
SPEAKER_LABEL_PATH=/speaker-label
这样做的好处是:第一,ASR 模型和说话人模型可以独立升级。第二,说话人标注服务可以单独扩容 GPU 节点。第三,长会议场景下可以单独优化分段、聚类和声纹比对策略。第四,后续切换其他声纹模型时,不需要大改会议后端。
十二、常见问题:CAM++ 接入会议系统容易踩的坑
1. Speaker 编号不等于真实姓名
Speaker 0、Speaker 1 只是聚类编号。如果要映射成真实姓名,需要提前建立声纹注册库,并进行相似度匹配。
2. 多人抢话会影响标注效果
说话人分离更适合轮流发言。如果多人同时说话、抢话严重,模型可能难以准确判断发言边界。
3. 远距离拾音会影响声纹特征
会议室麦克风离发言人太远时,混响和噪声会影响声纹特征提取。条件允许时,应优先使用稳定的会议麦克风阵列。
4. 长会议需要跨片段统一 speaker
分段处理后,同一个人在不同片段里的编号可能不一致。要解决这个问题,需要对声纹向量做跨片段聚类或注册库匹配。
5. 声纹数据需要权限控制
声纹属于敏感生物特征信息。正式系统中应避免随意保存原始注册音频,建议保存特征向量,并结合加密、权限控制、审计日志和数据生命周期策略管理。
十三、适用场景:哪些会议更需要自动标注发言人
1. 项目评审会
项目评审中经常涉及风险、结论和待办。发言人标注可以帮助追溯决策来源。
2. 访谈和谈话记录
访谈场景中,提问者和回答者的边界很重要。说话人标注可以减少人工整理成本。
3. 专家研讨会
多人围绕同一议题讨论时,系统可以保留不同专家的观点和发言顺序。
4. 医疗会诊和教学讨论
多角色参与的讨论中,发言人标注有助于后续整理不同角色的意见。
5. 远程协作会议
线上会议中参会人较多,转写文本如果没有发言人标注,阅读成本会明显升高。
十四、总结:发言人标注让会议纪要更可追溯
从这次实践可以看到,CAM++ 解决的不是“识别文字”,而是帮助系统判断“这些话是谁说的”。
ASR 负责把语音转成文本。VAD 负责找到有效语音片段。CAM++ 负责提取声纹特征并辅助说话人标注。标点模型负责恢复句子边界。会议助手负责把这些结构化结果整理成纪要、待办和归档资料。
在这条链路中,熙瑾会悟位于 ASR 和声纹识别之后的会议内容处理层。它接收带 speaker 信息的转写结果,再继续完成会议摘要、议题归纳、待办提取和资料检索。
对于会议系统来说,发言人自动标注的价值不只是让文本更好看,而是让会议结论更容易追溯,让责任事项更容易确认,让历史资料更容易复用。当“说了什么”和“谁说的”能够同时进入会议纪要,AI 会议助手才真正从单纯转写工具,进入可管理、可追踪、可沉淀的会议内容处理阶段。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。
同类文章
Claude Code 必知的14个高效工作流,让你的开发效率提升300%
Claude Code 常用工作流 先分享几项核心判断:Claude Code 真正强大的地方,并非仅仅在于它能编写代码——而是它让“编码”这件事本身变得更加高效且可控。你大概率遇到过这类场景:接手一个陌生项目,花了一整天才能理清架构;线上出现报错,翻遍日志也找不到根本原因;想要重构遗留代码,又担心
阿里云通义AIGC平台完全指南:设计师AI生产力革命
一、写在前面:为什么设计师需要关注AIGC? 如果你还在手动一张一张制作海报、反复修改客户口中“感觉不对”的配色方案、为电商详情页准备几十张不同场景的产品图——那么你一定经历过这些痛点: 创意瓶颈:脑海中有画面,但手绘无法呈现 重复劳动:调整尺寸、更换背景、批量生成变体消耗了大量时间精力 成本焦虑:
零基础毕设代码二次开发:3文件定位法及Vue/Java修改对照表
每年一到毕业季,计算机专业的同学总会陷入一个共同的怪圈:从 GitHub 上扒下来一套代码,或者用 AI 生成一个项目,看起来挺完整的,可导师一句“加个筛选条件”或“换个页面颜色”,瞬间就懵了——不敢改,不会改,怕改崩。是不是很熟悉? 一、为什么AI生成的毕设代码你 "不敢改 "? 1 1 毕业生的三大
反向海淘订单系统:状态机与分布式事务实战设计
先分享一个反直觉的结论:反向海淘订单管理的真正挑战,往往不在于业务逻辑本身,而在于状态流转。一个订单的生命周期拉长到跨国运输,中间涉及的环节多、系统多、参与者多,状态稍有错乱就可能引发连锁事故。Taocarts团队在实践中踩了不少坑,最终沉淀下来的这套状态机与分布式事务方案,成功解决了这一复杂难题。
AI并未抢走程序员饭碗而是更新了编程菜单
AI并未大规模替代程序员,而是改变了职业结构。重复性编码岗位需求下降35%至15%,而AIAgent开发等岗位需求激增187%。开发者焦虑从“被替代”转向“跟不上变化”,60%程序员已使用AI辅助编程。人的核心价值转向架构设计、技术决策和审查AI生成代码,AI技能带来16%薪资溢价。
- 热门数据榜
相关攻略
2026-07-09 17:49
2026-07-09 16:48
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 16:46
2026-07-09 15:45
2026-07-09 15:45
热门教程
- 游戏攻略
- 安卓教程
- 苹果教程
- 电脑教程

