4.1.4 声纹
1. 模块概述
声纹组件提供说话人注册、识别和验证能力,用于判断一段语音来自哪个已注册说话人,或是否匹配指定说话人。组件位于 components/model_zoo/voiceprint,提供 C++ API 和两个命令行工具:register_speaker、identify_speaker。
当前后端为 CamP+(3D-Speaker)ONNX 模型,输出 192 维 L2 归一化 embedding,并通过余弦相似度做 1:N 识别或 1:1 验证。默认阈值为 0.6,默认数据库文件为 speakers.db。
典型数据链路:
注册 WAV/PCM -> CamP+ embedding -> speakers.db
测试 WAV/PCM -> CamP+ embedding -> 余弦相似度 -> Identify/Verify 结果
主要目录:
| 路径 | 说明 |
|---|---|
components/model_zoo/voiceprint/include/vp_service.h | C++ 对外 API。 |
components/model_zoo/voiceprint/src/ | 声纹引擎、后端工厂、WAV 读取和数据库逻辑。 |
components/model_zoo/voiceprint/tools/register_speaker.cpp | 注册工具源码。 |
components/model_zoo/voiceprint/tools/identify_speaker.cpp | 识别/验证工具源码。 |
2. 环境准备
前置条件
SDK 源码获取和基础编译环境配置统一参考 2.3-构建编译。完成 SDK 初始化后,回到本文继续执行“构建编译”。
后续命令默认在 spacemit_robot SDK 根目录执行。
构建编译
语音组件使用 target/k3-com260-omni-agent.json 目标配置。系统缺少依赖时先安装:
sudo apt install libcurl4-openssl-dev portaudio19-dev libsndfile1-dev
在 SDK 根目录加载环境后编译声纹:
source build/envsetup.sh
lunch k3-com260-omni-agent
cd components/model_zoo/voiceprint
mm
本文录音命令还依赖 audio 组件。如当前环境没有 audio_demo,继续编译 audio:
cd ../../multimedia/audio
mm
模型首次运行时默认下载或加载到 ~/.cache/models/vp/campplus/,文件名为 3dspeaker_speech_campplus_sv_zh-cn_16k-common.onnx。
构建后可执行文件安装到 output/staging/bin:
| 工具 | 用途 |
|---|---|
register_speaker | 从 WAV 文件或实时录音注册说话人,写入 speakers.db。 |
identify_speaker | 对输入 WAV 做 1:N 识别,或通过 -v 做 1:1 验证。 |
3. 示例使用
3.1 注册说话人
实时录音注册:
register_speaker -n muggle -t 2
工具会进入录音模式,默认录制 3 次,每次约 4 秒,并把平均 embedding 保存到 speakers.db。预期日志包含:
Embedding RTF: 0.1442
Successfully registered speaker 'muggle'
Database saved to: speakers.db
也可以从文件注册:
register_speaker -n muggle sample.wav
常用参数:
| 参数 | 说明 |
|---|---|
-n, --name NAME | 说话人名称,必填。 |
-d, --database FILE | 数据库文件,默认 speakers.db。 |
-t, --threads NUM | 推理线程数,默认 1。 |
-f, --force | 覆盖已有同名说话人。 |
-l, --list-devices | 列出录音设备。 |
-i, --input-device N | 指定录音设备。 |
3.2 识别说话人
先录制测试音频:
audio_demo record test.wav --duration 5 --rate 16000 --channels 2 --device -1
再识别:
identify_speaker ./test.wav
预期输出包含加载的说话人数量、阈值、RTF、是否识别成功和 Top N 匹配:
Loaded 1 speakers from database
Threshold: 0.6
IDENTIFIED: muggle (score: 0.802)
验证指定说话人:
identify_speaker -v muggle ./test.wav
4. 应用开发
本章面向应用开发者,说明如何在自己的 C++ 应用中集成声纹组件。完整接口以 components/model_zoo/voiceprint/include/vp_service.h 为准;本节只介绍常用公开接口和典型调用方式。声纹的 C++ 入口是 SpacemiT::VpEngine。当前模块仅提供 C++ SDK,无 Python 绑定。
4.1 接口说明
声纹组件的核心入口是 SpacemiT::VpEngine。应用侧通过该类创建引擎、注册说话人、做 1:N 识别(找最相似说话人)或 1:1 验证(确认是否为指定人),并持久化说话人数据库。
4.1.1 常用数据结构
| 类型 | 说明 |
|---|---|
VpConfig | 引擎配置。常用字段:backend、model_dir、num_threads=1、provider="cpu"、threshold=0.6(识别/验证判定阈值)、sample_rate=16000、db_path(数据库路径,构造时可指定)。可用 Preset(name) 创建;含链式 withThreshold/withNumThreads/withProvider/withDbPath/withSampleRate。 |
VpBackendType | 后端枚举:CAMPPLUS(默认)、ERES2NET、ECAPA_TDNN、HTTP(远端服务)、CUSTOM(保留)。 |
SpeakerMatch | 单条匹配结果:name(候选人)+ score(相似度,[0, 1])。 |
VpResult | 识别/验证/提取结果。提供 GetName()、GetScore()、IsIdentified()(1:N,分数 ≥ 阈值)、IsVerified()(1:1,分数 ≥ 阈值且姓名匹配)、GetMatches()(前 N 候选)、GetEmbedding()(特征向量,CAM++ 默认 192 维)、GetRTF()、GetProcessingTimeMs()、IsSuccess()、GetErrorMessage()。 |
4.1.2 引擎初始化与预设
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
VpEngine(VpConfig) | 用完整配置构造,可指定后端、模型路径、阈值、库路径。 | config:VpConfig 实例。 | 引擎实例。 |
VpEngine(backend, model_dir) | 通过后端枚举快速构造,模型路径为空时使用默认缓存。 | backend:VpBackendType;model_dir:可选模型目录。 | 引擎实例。 |
VpConfig::Preset(name) | 静态工厂,返回指定预设。 | name:目前仅注册 campplus;ERES2NET / ECAPA_TDNN 后端枚举已定义但未注册预设,传入会抛 std::invalid_argument。 | VpConfig。 |
VpConfig::AvailablePresets() | 静态方法,列出可用预设。 | 无。 | std::vector<std::string>。 |
IsInitialized() / GetEngineName() / GetBackendType() / GetEmbeddingDimension() / GetConfig() | 状态与配置查询;GetEmbeddingDimension() 取当前后端的特征维度。 | 无。 | 各自类型。 |
4.1.3 注册(Register)
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
Register(name, audio_path) | 用单段 WAV 文件注册说话人。 | name:说话人名;audio_path:WAV 路径(推荐 16kHz mono)。 | bool,成功为 true。 |
Register(name, vector<paths>) | 用同一说话人的多段 WAV 注册,内部聚合多段 embedding 取均值,鲁棒性更好。 | name:说话人名;audio_paths:多个 WAV 路径。 | bool。 |
Register(name, vector<float>, sample_rate=16000) | 用内存里的浮点 PCM 注册(来自麦克风录音或自己解码的 WAV)。 | name:说话人名;audio:PCM float;sample_rate:采样率。 | bool。 |
RegisterWithEmbedding(name, embedding) | 直接用预计算的 embedding 注册,跳过模型推理;适合从其它系统迁移特征库。 | name:说话人名;embedding:特征向量,长度需匹配 GetEmbeddingDimension()。 | bool。 |
4.1.4 识别与验证
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
Identify(audio_path) / Identify(vector<float>, sample_rate=16000) | 1:N:在数据库所有说话人中查找最相似的;IsIdentified() 表示分数过阈值。 | 输入音频。 | shared_ptr<VpResult>,GetMatches() 返回前 N 候选。 |
Verify(name, audio_path) / Verify(name, vector<float>, sample_rate=16000) | 1:1:仅与指定说话人比对;IsVerified() 表示通过。比 Identify 快得多,适合已知期望身份的鉴权场景。 | name:要验证的说话人;输入音频。 | shared_ptr<VpResult>。 |
ExtractEmbedding(audio_path) / ExtractEmbedding(vector<float>, sample_rate=16000) | 仅提取特征向量,不查库;用于自己实现自定义比对逻辑或导出特征。 | 输入音频。 | shared_ptr<VpResult>,通过 GetEmbedding() 取向量。 |
Identify vs Verify 选型对比:
| 场景 | 推荐接口 | 输入 | 输出 | 性能取舍 |
|---|---|---|---|---|
| 不知道说话人是谁,要找出最像的 | Identify | 音频 | 排序后的候选列表 + 是否过阈值 | 与库中说话人数线性相关 |
| 已知期望说话人,要确认是不是 | Verify | 说话人名 + 音频 | 是否匹配 + 分数 | 一次比对,最快 |
| 不入库,临时比较两段音频 | ExtractEmbedding × 2 + 余弦 | 两段音频 | 自己计算的相似度 | 完全可控,需自己写比对 |
4.1.5 数据库管理与运行时配置
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
SaveDatabase(path="") | 保存数据库到磁盘;path 为空时使用 VpConfig::db_path。 | path:可选目标路径。 | bool。 |
LoadDatabase(path="") | 从磁盘加载数据库;不存在时返回 false 但引擎仍可用,相当于空库。 | path:可选源路径。 | bool。 |
RemoveSpeaker(name) / ContainsSpeaker(name) / GetSpeakerCount() / GetAllSpeakers() | 数据库内成员管理与查询。 | name:说话人名。 | bool / int / vector<string>。 |
SetThreshold(threshold) / GetThreshold() | 运行时调整识别/验证阈值;过高漏识、过低误识。常用区间 0.5–0.7。 | threshold:[0, 1]。 | 无 / float。 |
4.2 C++ 调用示例
以下示例默认已完成 §2 构建;CAM++ 模型在 ~/.cache/models/vp/campplus/ 下;输入音频建议 16kHz mono,时长 2–5 秒,避免背景人声和过低音量。
声纹组件编译后会把 vp_service.h 和 libvoiceprint.a 安装到 output/staging(静态库)。下游组件链接声纹库的 CMake 写法:
add_executable(my_app main.cpp)
find_library(VP_LIB NAMES voiceprint
PATHS ${CMAKE_INSTALL_PREFIX}/lib NO_DEFAULT_PATH)
find_path(VP_INCLUDE_DIR NAMES vp_service.h
PATHS ${CMAKE_INSTALL_PREFIX}/include NO_DEFAULT_PATH)
if(NOT VP_LIB OR NOT VP_INCLUDE_DIR)
message(FATAL_ERROR "voiceprint not found. Build components/model_zoo/voiceprint first.")
endif()
target_include_directories(my_app PRIVATE ${VP_INCLUDE_DIR})
target_link_libraries(my_app PRIVATE ${VP_LIB})
包依赖建议在当前组件的 package.xml 中声明 <depend>voiceprint</depend>。
4.2.1 注册一个说话人(多段 WAV)
适用场景:批量从录音文件批准已有说话人;建议每个说话人收集 2–3 段干净 WAV(不同时间录的更好),让引擎内部聚合。
调用步骤:
- 用
VpConfig::Preset("campplus")创建预设并设阈值/线程/数据库路径。 - 构造引擎并
LoadDatabase(不存在则从空库开始)。 - 调
Register(name, vector<paths>)注册。 - 注册成功后
SaveDatabase落盘。
#include <iostream>
#include <vector>
#include "vp_service.h"
int main() {
auto config = SpacemiT::VpConfig::Preset("campplus")
.withThreshold(0.6f)
.withNumThreads(2)
.withDbPath("speakers.db");
SpacemiT::VpEngine engine(config);
if (!engine.IsInitialized()) {
std::cerr << "声纹引擎初始化失败" << std::endl;
return 1;
}
engine.LoadDatabase("speakers.db"); // 不存在则相当于空库
std::vector<std::string> samples = {
"muggle_sample_1.wav",
"muggle_sample_2.wav",
"muggle_sample_3.wav",
};
if (!engine.Register("muggle", samples)) {
std::cerr << "注册失败" << std::endl;
return 1;
}
engine.SaveDatabase();
std::cout << "已注册 muggle,库内共 "
<< engine.GetSpeakerCount() << " 人" << std::endl;
return 0;
}
完整版含麦克风录音注册(AudioRecorder + 多次提示用户说话),见 components/model_zoo/voiceprint/tools/register_speaker.cpp。
4.2.2 1:N 识别
适用场景:不知道当前说话人是谁,要在已注册说话人中找最相似的——典型用于会议成员识别、家庭场景中识别说话人。
调用步骤:
- 加载已有数据库。
- 调
Identify(audio)取VpResult。 - 用
IsIdentified()判定是否过阈值;用GetMatches()取前 N 候选辅助决策。
SpacemiT::VpEngine engine(SpacemiT::VpConfig::Preset("campplus")
.withThreshold(0.6f));
engine.LoadDatabase("speakers.db");
auto result = engine.Identify("test.wav");
if (!result || !result->IsSuccess()) {
std::cerr << "识别失败" << std::endl;
return;
}
if (result->IsIdentified()) {
std::cout << "识别为: " << result->GetName()
<< " (score=" << result->GetScore() << ")" << std::endl;
} else {
std::cout << "未识别(最高 score=" << result->GetScore()
<< " 低于阈值)" << std::endl;
}
std::cout << "前 N 候选:" << std::endl;
for (const auto& m : result->GetMatches()) {
std::cout << " " << m.name << " " << m.score << std::endl;
}
完整版含 verbose 模式、verify 子命令、列出所有说话人,见 components/model_zoo/voiceprint/tools/identify_speaker.cpp。
4.2.3 1:1 验证
适用场景:已知期望说话人(例如登录用户),只需确认当前音频是不是他;比 Identify 快得多,是 omni_agent --vp-verify 的核心调用。
auto result = engine.Verify("muggle", "test.wav");
if (result && result->IsVerified()) {
std::cout << "验证通过 (score=" << result->GetScore() << ")" << std::endl;
} else {
std::cout << "验证不通过" << std::endl;
}
4.2.4 提取 embedding 自定义对比
适用场景:不入库的临时鉴权(例如对话期间临时记住一个声音)、跨系统迁移特征库、自定义距离度量。
调用步骤:
- 对两段音频分别调
ExtractEmbedding取向量。 - 自己实现余弦相似度比较,按业务阈值判定。
#include <cmath>
#include <vector>
float Cosine(const std::vector<float>& a, const std::vector<float>& b) {
float dot = 0, na = 0, nb = 0;
for (size_t i = 0; i < a.size(); ++i) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (std::sqrt(na) * std::sqrt(nb) + 1e-8f);
}
auto e1 = engine.ExtractEmbedding("user_say_hello.wav");
auto e2 = engine.ExtractEmbedding("user_say_goodbye.wav");
if (e1 && e2) {
float sim = Cosine(e1->GetEmbedding(), e2->GetEmbedding());
std::cout << "相似度: " << sim
<< " (维度 " << engine.GetEmbeddingDimension() << ")" << std::endl;
}
4.2.5 数据库持久化
适用场景:服务重启后保留已注册说话人;多设备共享说话人库。
// 进程启动时
engine.LoadDatabase("speakers.db");
// 注册或修改后
engine.Register("alice", {"alice_1.wav", "alice_2.wav"});
engine.RemoveSpeaker("old_user");
engine.SaveDatabase(); // 等价 SaveDatabase("speakers.db")(用 VpConfig::db_path)
engine.SaveDatabase("backup_speakers.db"); // 也可指定其它路径
数据库文件是组件内部格式,跨版本不保证兼容;升级 SDK 后建议重建库或用 RegisterWithEmbedding 迁移特征。
5. 调试指南
调试声纹时应保留注册样本、识别样本和阈值配置,便于复现分数变化:
- 用
register_speaker -l或audio_demo list确认录音设备,保持注册和识别使用相同设备。 - 注册样本建议使用多段 2-5 秒清晰语音,避免背景人声和过低音量。
- 识别失败时记录 Top N 分数和当前
--threshold,先判断是样本问题还是阈值问题。 - 需要复现实验时固定
speakers.db路径,避免误用旧数据库。
6. 常见问题
| 现象 | 可能原因 | 处理 |
|---|---|---|
UNKNOWN: No match above threshold | 分数低于阈值、注册样本不足或环境不同 | 增加注册样本,降低噪声,必要时调整 --threshold。 |
| 同一人验证偶发失败 | 语音太短、音量过低或麦克风位置变化大 | 使用 2-5 秒清晰语音,并保持注册和识别设备一致。 |
| 不同人分数偏高 | 注册样本质量差或阈值过低 | 提高阈值,重新采集注册样本。 |
| 找不到录音设备 | PortAudio 默认设备不正确 | register_speaker -l 列出设备,再用 -i 指定。 |
附录:K3 实测数据
以下为 K3 平台实测数据。
| 场景 | 数值 | 说明 |
|---|---|---|
| 实时注册 embedding RTF | 约 0.14 | 三次录音分别为 0.1442、0.1435、0.1425。 |
| 匹配同一说话人 | score 0.802 | 阈值 0.6,输出 IDENTIFIED: muggle。 |
| 不同说话人样本 | score 0.282 | 阈值 0.6,输出 UNKNOWN。 |
identify_speaker RTF | 0.2522 | 5 秒测试 WAV。 |
测试方法:在 K3 板卡上用 register_speaker 录制并注册同一说话人的多段样本,再用 audio_demo record 采集测试 WAV,通过 identify_speaker 的 score、阈值判断和 RTF 日志记录结果。