FunASR 说话人与情感标签实战指南:声纹向量、匿名聚类与 SenseVoice 富标签的正确获取与保存
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
导读:本文围绕 FunASR 中"说话人与情感标签"这一核心主题,讲清四类极易混淆的输出——声纹向量(spk_embedding)、匿名聚类编号(spk)、已注册人员身份与情感/事件标签——之间的本质区别与适用场景。你将掌握:如何用 CAMPPlus / ERes2NetV2 从单段语音提取可序列化的声纹向量,如何用 SenseVoiceSmall 完整保留转写、情感与音频事件标签并安全落盘,以及为什么 VAD 说话人聚类、HTTP 转写服务与独立推理之间不能随意混用。全文以 docs/speaker_emotion.md 为主线,并结合仓库源码给出实现级佐证。
先选任务,再选模型:四类输出不可互相替代
在动手调用任何一个模型之前,必须明确你最终需要的是哪一种结果。声纹向量、匿名聚类标签、已注册人员身份与情感标签是四类不同性质的输出,任何一类都不能替代另一类:
| 任务 | 路径 |
|---|---|
| 从选定语音提取一个说话人向量 | CAMPPlus 或 ERes2NetV2;见下方示例 |
| 为转写片段分配匿名说话人 | ASR/VAD/说话人流水线 |
| 保留转写、情感和音频事件标签 | SenseVoiceSmall;见下方示例 |
| 联合转写与匿名说话人分离 | 第三方 OpenMOSS MOSS 指南 |
其中 CAMPPlus 与 ERes2NetV2 属于说话人识别(SV)模型,返回值是spk_embedding这个 Tensor,而不是人的姓名、匹配结论,也不是通用 ASR 的text字段。从源码看,两者的接口契约完全一致:
- CAMPPlus 模型实现 明确注释 "Output: 192-dimensional speaker embedding per utterance",其
inference方法返回[{"spk_embedding": Tensor of shape (1, 192)}]; - ERes2NetV2 模型实现 同样返回
{"spk_embedding": Tensor of shape (1, 192)},并在注释中说明它对短时音频(< 3 秒)的特征提取优于 CAM++——这是两者选型时的重要参考。
需要特别强调两个由源码结构可以确认的细节:
- 向量维度由检查点配置决定,不是接口永远保证 192 维。虽然默认
embedding_size=192,但该值来自模型构造参数,换一个检查点就可能不同,因此下游代码不应硬编码维度。 - 单条输入通常对应一行,但务必使用
batch_size=1。部分批处理路径会在一个字典内返回多行向量,而不是每个输入对应一个带key的结果,按"每输入一个结果"的假设取数会出错。
边界澄清:向量 ≠ 身份,标签 ≠ 人员 ID
- 向量本身不等于身份确认。注册(enrollment)、匹配(matching)、阈值校准、授权同意(consent)以及面向目标人群的评测,都需要应用层单独设计与实现,模型只负责输出向量。
spk=0或 MOSS 的S01只是录音内部的匿名标签,不是跨录音稳定的人员 ID。它只在某一条音频/会议内部有意义。spk_embedding_center是聚类均值,表示该聚类所有片段向量的中心点,不代表"已经完成身份注册"。
准备本地检查点:环境、模型与音频格式
环境与模型清单
开始之前,先完成安装检查,并准备一个完整的本地模型快照——包括配置文件、前端(frontend)、适用时的 tokenizer 以及权重文件。同时建议记录以下信息以备复现:解析后的模型 revision 或文件哈希、SDK 版本、实际导入的模块路径与源码 commit。本文对应的实现以文末列出的源码文件为准,不保证所有旧版 wheel、导出后端或模型变体接口一致。
本指南使用的三个模型路径:
- CAMPPlus(
embedding):iic/speech_campplus_sv_zh-cn_16k-common,其 ModelScope 别名为cam++; - ERes2NetV2(
embedding):iic/speech_eres2netv2_sv_zh-cn_16k-common; - SenseVoice(
sensevoice):iic/SenseVoiceSmall。
注意:embedding与sensevoice只是下方示例程序的两个运行模式选项,并不是 FunASR 新增的模型别名或 CLI 子命令。不要把 ASR 检查点传给embedding模式,也不要把 ERes2NetV2 的行为推广到所有 ERes2Net 变体。更多模型路径可查阅 Model Zoo。
音频输入要求
示例程序只接受非空、单声道、16 kHz 的 WAV 音频:它读取归一化的float32波形,对立体声、空音频或其它采样率的输入直接报错,而不是隐式转换。对于声纹提取,请使用选定说话人的单人有效语音——混合说话人、静音与极短片段都不能提供可靠的身份依据。需要说明的是,下面的输入校验只做格式检查,不是语音质量检测器。
完整示例程序:保存 SDK 结果且不丢失标签
下面这段独立程序接收task、model_dir、audio和一个新的 JSON 输出路径。运行方式(在脚本名之后):
python attributes.py embedding /models/campplus speaker.wav vector.json python attributes.py sensevoice /models/sensevoice utterance.wav tags.json两个模式都在 CPU 上处理完整片段:不添加 VAD、标点、配套说话人模型或流式缓存,程序内部也不会下载模型。完整代码即 docs/speaker_emotion.md 中的示例:
import argparse import json import os from pathlib import Path import soundfile as sf from funasr import AutoModel from funasr.utils.postprocess_utils import rich_transcription_postprocess def embedding_record(results): if not results: raise ValueError("No result from the speaker model") if len(results) != 1: raise ValueError("Expected one speaker result for one input") vector = results[0]["spk_embedding"] if vector.ndim != 2 or vector.shape[0] != 1 or vector.shape[1] == 0: raise ValueError("Expected a nonempty single-row speaker embedding") return {"spk_embedding": vector.detach().cpu().tolist()} def tagged_records(results): if not results: raise ValueError("No result from SenseVoice") records = [] for item in results: raw = item["text"] if not isinstance(raw, str): raise ValueError("Expected SenseVoice text with its original tags") records.append({ "key": item.get("key"), "raw_tagged_text": raw, "display_text": rich_transcription_postprocess(raw), }) return records def write_result(path, record): payload = json.dumps(record, ensure_ascii=False, allow_nan=False, indent=2) fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as stream: stream.write(payload + "\n") parser = argparse.ArgumentParser() parser.add_argument("task", choices=["embedding", "sensevoice"]) parser.add_argument("model_dir") parser.add_argument("audio") parser.add_argument("output") args = parser.parse_args() model_dir = Path(args.model_dir).expanduser().resolve(strict=True) if not model_dir.is_dir(): raise ValueError("Expected a complete local model directory") speech, sample_rate = sf.read(args.audio, dtype="float32") if sample_rate != 16000 or speech.ndim != 1 or len(speech) == 0: raise ValueError("Expected nonempty mono 16 kHz audio") model = AutoModel( model=str(model_dir), device="cpu", ncpu=1, disable_update=True, trust_remote_code=False, vad_model=None, punc_model=None, spk_model=None, ) if args.task == "embedding": results = model.generate(input=speech, fs=sample_rate, batch_size=1) result = embedding_record(results) else: results = model.generate( input=speech, fs=sample_rate, batch_size=1, language="auto", use_itn=True, output_timestamp=False, ) result = tagged_records(results) write_result(args.output, { "task": args.task, "model_dir": str(model_dir), "sample_rate": sample_rate, "result": result, })示例程序的设计要点(源码级解读)
- 外层 JSON、
raw_tagged_text与display_text是应用自己定义的字段,不是 SDK 新增的响应字段。AutoModel.generate本身返回的是 AutoModel 的标准结果列表。 - 向量序列化是显式的:
spk_embedding先detach()脱离计算图,再.cpu()移到 CPU,最后.tolist()转成嵌套列表才能写入 JSON——这与后文提到的"NumPy 聚类中心数组也需要显式序列化"是同一类问题。 - 非有限数值在创建文件前就被拒绝:
json.dumps(..., allow_nan=False)会抛出ValueError,杜绝 NaN/Inf 悄悄进入结果文件。 - 已有输出文件永远不会被覆盖:
os.O_EXCL标志保证如果目标路径已存在则直接失败(FileExistsError),每次运行需要选择新路径。 - 权限控制:在 POSIX 上文件以
0o600(仅所有者可读写)模式创建,此外仍需自行配置合适的目录权限与平台 ACL。
数据安全提醒
声纹向量和转写文本可能属于敏感个人信息:只保留应用确实需要的字段,不要公开私人音频样本或识别结果。
正确读取 SenseVoice 标签
原始标签在哪里
SenseVoice 的原始带标签字符串通常在result["text"]字段,不能假设一定存在raw_text、emotion或emotion_score之类的字段。标签区分大小写,例如<|zh|>、<|HAPPY|>、<|Speech|>、<|withitn|>;某个标签是否出现及其含义取决于具体检查点,一张展示映射表并不承诺每个模型都会输出表中全部标签。
从 postprocess_utils.py 的映射表 可以看到仓库实际支持的标签体系:
- 情感标签:
<|HAPPY|>、<|SAD|>、<|ANGRY|>、<|NEUTRAL|>、<|FEARFUL|>、<|DISGUSTED|>、<|SURPRISED|>; - 音频事件标签:
<|BGM|>、<|Speech|>、<|Applause|>、<|Laughter|>、<|Cry|>、<|Sneeze|>、<|Breath|>、<|Cough|>; - 语言标签:
<|zh|>、<|en|>、<|yue|>、<|ja|>、<|ko|>、<|nospeech|>; - 文本规范化开关:
<|withitn|>(开启逆文本规范化)与<|woitn|>(关闭)。
rich_transcription_postprocess是有损展示函数
rich_transcription_postprocess 是有损的展示处理,它做四件事:删除标签、按出现次数选择展示情感(见format_str_v2中"出现次数最多者胜出"的逻辑)、合并重复的展示标记,以及执行若干文本替换(例如把语言标签统一替换为<|lang|>、"The."替换为空格)。因此:
- 它不是结构化标签解析器,也不是概率计算器——不要从展示符号或模型标签推导置信概率、真实心理状态或诊断结论;
- 必须先把原始字符串原样保存(示例中的
raw_tagged_text正是为此设计),展示用display_text只是给人看的副产品; - 静音、短片段与噪声音频不能提供可靠的情感依据;
emotion2vec是另一类模型和接口,不是 SenseVoice 标签的别名,不要混淆。
生成参数的确切语义
示例中显式选择了language="auto"、use_itn=True、output_timestamp=False,这些参数并非全部必填。从 SenseVoice 模型 inference 实现 可以看到它们的底层语义:
language:语言提示,取值auto/zh/en/yue/ja/ko,默认auto。它会通过self.lid_dict映射为语言嵌入(language_query)拼接到输入中,从而引导解码;use_itn:控制逆文本规范化(Inverse Text Normalization),不是情感识别开关。源码中use_itn=True时textnorm取"withitn",否则取"woitn";但如果显式传入了text_norm,则以text_norm为准(优先级更高);output_timestamp:控制是否额外输出逐词时间戳,源码中只有在为True时才会走ctc_forced_align的强制对齐分支。
每次调用都需要重复传入需要固定的选项,不要依赖上次调用的状态。此外要明确:本例是完整片段推理,不是 KWS 的 EOS 协议,也不是实时情绪监控——不要把它当作流式或长时监测接口使用。
VAD、说话人分离与服务边界
不要把"聚类"和"直接推理"混为一谈
独立声纹提取和短片段 SenseVoice 推理不要求 VAD。但要注意:通用 AutoModel 的说话人聚类功能位于其 VAD 流水线内部——只设置spk_model或return_spk_res=True,并不会给直接推理增加说话人分离。换言之,model.generate直接推理不产出sentence_info[].spk;要走匿名说话人聚类,必须走完整的 SDK 流水线,组合兼容的 ASR/VAD/标点/说话人模型。
同时记住三点边界:
- VAD 边界不保证对应说话人切换——静音切段和说话人切换是两个不同的事件;
- 长音频分段会改变情感标签所依据的上下文——同一句话在整段上下文里与单独成段时,模型输出的标签可能不同;
sentence_info[].spk是录音内的匿名编号,SDK 的start/end时间字段单位是毫秒;NumPy 聚类中心数组也需要像 Tensor 一样显式序列化后才能写入 JSON。
第三方 MOSS 与内置 HTTP 服务
- OpenMOSS MOSS-Transcribe-Diarize(见 MOSS 指南)提供自己的联合转写与说话人分离路径,不需要外部
vad_model或spk_model。使用它时不要叠加第二套聚类流水线,也不要宣称其输出可识别已知人员身份;后端、显存与返回格式的细节以该指南为准。 - 当前内置的 HTTP 转写服务 中,SenseVoice 的配置附带 VAD,其 fallback 会剥离顶层及分段文本中的富标签,且不会补建情感分数字段。它的
spk=true属于服务自己的流水线,HTTP 分段start/end单位是秒(与 SDK 的毫秒不同)。因此,把本 SDK 示例平移到/v1/audio/transcriptions,不代表仍能读到同样的标签或使用同样的参数。 - 请查阅部署矩阵:不要假设 vLLM、llama.cpp、ONNX 或 WebSocket 等导出路径都保留与 Python 接口相同的内容结构。
源码与验证:契约从哪来,测试验什么
契约来源文件
本文描述的接口行为以仓库中以下实现为契约来源:
- CAMPPlus 模型:返回
spk_embedding(默认 192 维,维度来自检查点配置); - ERes2NetV2 模型:返回
spk_embedding,短时音频提取更优; - SenseVoice 模型:
language/use_itn/output_timestamp/text_norm的实际处理逻辑; - 后处理工具:
emo_dict/event_dict/lang_dict/emoji_dict映射与rich_transcription_postprocess有损展示逻辑; - AutoModel:
generate的入口与参数传递; - HTTP 适配层
funasr/bin/_server_app.py:HTTP 服务的标签剥离与spk流水线行为。
指南测试验证了什么
test_speaker_emotion_docs.py 使用记录调用参数的 SDK 替身(Recording Doubles)与真实的rich_transcription_postprocess函数,逐字执行本文公开的示例程序,重点验证:
- 字段保留:
raw_tagged_text与原始标签字符串逐字一致,display_text为经过真实有损处理的展示结果(例如<|zh|><|HAPPY|><|Speech|><|withitn|>你好→你好😊); - 参数传递:
batch_size=1、fs=16000、device="cpu"、trust_remote_code=False、vad_model/punc_model/spk_model=None,以及 SenseVoice 模式的language="auto"、use_itn=True、output_timestamp=False; - 序列化安全:向量必须经历
detach → cpu → tolist才允许写出;非有限数值、多行向量、空结果都会抛ValueError且不产生输出文件; - 错误输入处理:非 16 kHz、空音频、立体声、已有输出文件(不覆盖)、非字符串
text等场景均被正确拒绝。
需要明确测试的边界:这些测试验证的是接口契约与序列化正确性,不代表声学质量、身份匹配准确率、不同人群的表现差异或情感识别精度的评测结论。
写在最后:三个"先问自己"的检查清单
- 我要的是哪类结果?是向量(CAMPPlus/ERes2NetV2)、匿名聚类编号(VAD/说话人流水线)、富标签(SenseVoiceSmall)还是联合分离(MOSS)?四者不可互相替代。
- 我的结果会怎样被消费?向量需要显式 detach/cpu/tolist 才能 JSON 化;
spk是录音内匿名编号不是人员 ID;spk_embedding_center是聚类均值不是注册结果;展示文本经过了有损处理,原始标签必须先保存。 - 我的部署路径是谁?SDK 直接推理、VAD 聚类流水线、HTTP 转写服务、vLLM/llama.cpp/ONNX/WebSocket 导出路径之间的标签保留、时间单位(毫秒 vs 秒)与参数语义并不一致,务必以部署矩阵和对应指南为准。
【免费下载链接】FunASROpen-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.项目地址: https://gitcode.com/GitHub_Trending/fun/FunASR
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考