会议发言人如何自动标注?CAM++ 声纹识别接入AI会议助手实践

📅 2026/7/9 4:24:38
会议发言人如何自动标注?CAM++ 声纹识别接入AI会议助手实践
一、引言会议转写之后为什么还要标注发言人在会议转写系统中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 服务器环境PythonPython 3.10GPU建议使用 NVIDIA GPU测试环境可先用 CPU音频工具ffmpeg语音工具链FunASR说话人模型CAM服务框架FastAPI / Uvicorn创建环境conda create-nmeeting-spkpython3.10-yconda activate meeting-spk pipinstall-Ufunasr pipinstallmodelscope pipinstallfastapi uvicorn python-multipart requests pydub如果部署环境无法访问外网可以提前下载相关模型再拷贝到内网服务器。常见链路中会用到 ASR、VAD、标点和说话人模型modelscope download--modeliic/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-pytorch--local_dir./models/paraformer modelscope download--modeliic/speech_fsmn_vad_zh-cn-16k-common-pytorch--local_dir./models/fsmn-vad modelscope download--modeliic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch--local_dir./models/ct-punc modelscope download--modeliic/speech_campplus_sv_zh-cn_16k-common--local_dir./models/campplus正式部署时建议把模型、代码、依赖包、启动脚本和配置文件放在同一套交付目录中。这样可以避免现场临时下载模型也便于版本回退和问题定位。四、第一次测试输出带 speaker 标签的会议转写先准备一段测试会议音频./samples/meeting_two_speakers.wav这段音频最好包含两到三个人轮流发言便于观察说话人标注效果。新建test_campplus_diarization.pyfromfunasrimportAutoModel modelAutoModel(modelparaformer-zh,vad_modelfsmn-vad,punc_modelct-punc,spk_modelcam,devicecuda:0,)resultmodel.generate(input./samples/meeting_two_speakers.wav,batch_size_s60,)forsentenceinresult[0].get(sentence_info,[]):speakersentence.get(spk,unknown)startsentence.get(start,0)endsentence.get(end,0)textsentence.get(text,)print(fSpeaker{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-iinput.m4a-ar16000-ac1output.wav批量转换目录mkdir-p./wav_meetingsforfilein./raw_meetings/*;doname$(basename$file)base${name%.*}ffmpeg-y-i$file-ar16000-ac1./wav_meetings/${base}.wavdone也可以封装成 Python 函数frompathlibimportPathfrompydubimportAudioSegmentdefnormalize_audio(input_path:str,output_dir:str./normalized)-str: 将会议录音统一转为 16kHz、单声道 WAV。 input_pathPath(input_path)output_dirPath(output_dir)output_dir.mkdir(parentsTrue,exist_okTrue)output_pathoutput_dir/f{input_path.stem}.wavaudioAudioSegment.from_file(input_path)audioaudio.set_frame_rate(16000).set_channels(1)audio.export(output_path,formatwav)returnstr(output_path)对于发言人标注而言音频预处理不是可有可无的步骤。输入质量越稳定后续说话人聚类、声纹比对和时间轴对齐就越可靠。六、从 speaker_id 到真实姓名增加声纹注册表CAM 可以帮助系统判断不同音频片段是否属于同一个说话人但它本身不会知道这个人是谁。要从Speaker 0进一步映射到真实姓名需要建立声纹注册表。基本思路如下参会人预先录入一段注册音频 ↓ CAM 提取注册音频的声纹向量 ↓ 会议中提取每个 speaker_id 的声纹向量 ↓ 计算相似度 ↓ 匹配到已注册人员 ↓ 输出真实姓名 发言内容可以先设计一个简单的注册信息结构{speaker_profiles:[{user_id:u001,name:张主任,voice_sample:./profiles/zhang.wav},{user_id:u002,name:李工,voice_sample:./profiles/li.wav},{user_id:u003,name:王经理,voice_sample:./profiles/wang.wav}]}实际系统中声纹注册不建议放在普通文件里长期保存。更稳妥的做法是保存声纹特征向量并结合权限、加密和审计策略管理。身份映射后的会议片段可以变成[{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.pyimporttimeimporttempfilefrompathlibimportPathfromtypingimportOptionalfromfastapiimportFastAPI,UploadFile,File,FormfrompydanticimportBaseModelfromfunasrimportAutoModel appFastAPI(titleMeeting Speaker Label Service,version1.0.0)modelAutoModel(modelparaformer-zh,vad_modelfsmn-vad,punc_modelct-punc,spk_modelcam,devicecuda:0,)classSpeakerSegment(BaseModel):speaker_id:strstart_ms:intend_ms:inttext:strclassSpeakerResponse(BaseModel):text:strprocess_sec:floatsegments:list[SpeakerSegment]app.get(/health)defhealth_check():return{status:ok,asr:paraformer-zh,spk_model:cam}app.post(/speaker-label,response_modelSpeakerResponse)asyncdefspeaker_label(audio_file:UploadFileFile(...),language:Optional[str]Form(defaultzh),):suffixPath(audio_file.filename).suffixor.wavwithtempfile.NamedTemporaryFile(deleteFalse,suffixsuffix)astmp:tmp.write(awaitaudio_file.read())tmp_pathtmp.name start_timetime.time()resultmodel.generate(inputtmp_path,batch_size_s60,)process_sectime.time()-start_time all_textresult[0].get(text,)segments[]forsentenceinresult[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--host0.0.0.0--port7863测试健康检查curlhttp://127.0.0.1:7863/health上传会议录音测试curl-XPOSThttp://127.0.0.1:7863/speaker-label\-Faudio_file./samples/meeting_two_speakers.wav返回结果可以设计为{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、时间范围和文本内容继续整理会议摘要、议题、决策和待办事项。提交给后处理接口的数据可以设计为importrequestsfromdatetimeimportdatetime MEETING_APIhttp://127.0.0.1:18080/api/meeting/minutes/generatedefsubmit_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}}resprequests.post(MEETING_API,jsonpayload,timeout300)resp.raise_for_status()returnresp.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. 运维侧确认部署时间并同步项目组截止时间本周内。这时系统已经从“会议转写”进入“会议责任追踪”。声纹识别不是为了把会议做得更复杂而是为了减少会后整理成本谁提出、谁确认、谁负责能够在纪要生成时被保留下来。十、长会议场景说话人标注要注意分段策略长会议录音不建议一次性完整送入模型。更稳妥的方式是先进行分段再进行识别和说话人标注。简单的固定时长分段方式如下frompydubimportAudioSegmentfrompathlibimportPathdefsplit_audio_fixed(wav_path:str,output_dir:str./segments,chunk_ms:int180_000,overlap_ms:int3_000,): 将长会议录音切成多个片段。 chunk_ms: 每段长度默认 180 秒 overlap_ms: 相邻片段重叠默认 3 秒 wav_pathPath(wav_path)output_dirPath(output_dir)/wav_path.stem output_dir.mkdir(parentsTrue,exist_okTrue)audioAudioSegment.from_wav(wav_path)segments[]start0index1whilestartlen(audio):endmin(startchunk_ms,len(audio))segmentaudio[start:end]segment_pathoutput_dir/f{wav_path.stem}_{index:04d}.wavsegment.export(segment_path,formatwav)segments.append(str(segment_path))ifendlen(audio):breakstartend-overlap_ms index1returnsegments需要注意的是分段会带来一个问题不同片段里的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.8services:meeting-speaker-label:image:meeting/speaker-label:camppluscontainer_name:meeting-speaker-labelrestart:alwaysports:-7863:7863volumes:-./models:/app/models-./logs:/app/logs-./tmp:/app/tmpenvironment:-SPK_MODELcam-ASR_MODELparaformer-zh-VAD_MODELfsmn-vad-PUNC_MODELct-punc-DEVICEcuda:0command:uvicorn speaker_server:app --host 0.0.0.0 --port 7863业务系统只需要配置说话人标注服务地址SPEAKER_LABEL_PROVIDERcampplusSPEAKER_LABEL_BASE_URLhttp://meeting-speaker-label:7863SPEAKER_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 会议助手才真正从单纯转写工具进入可管理、可追踪、可沉淀的会议内容处理阶段。