Skip to main content

4.1.4 声纹

1. 模块概述

声纹组件提供说话人注册、识别和验证能力,用于判断一段语音来自哪个已注册说话人,或是否匹配指定说话人。组件位于 components/model_zoo/voiceprint,提供 C++ API 和两个命令行工具:register_speakeridentify_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.hC++ 对外 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引擎配置。常用字段:backendmodel_dirnum_threads=1provider="cpu"threshold=0.6(识别/验证判定阈值)、sample_rate=16000db_path(数据库路径,构造时可指定)。可用 Preset(name) 创建;含链式 withThreshold/withNumThreads/withProvider/withDbPath/withSampleRate
VpBackendType后端枚举:CAMPPLUS(默认)、ERES2NETECAPA_TDNNHTTP(远端服务)、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)用完整配置构造,可指定后端、模型路径、阈值、库路径。configVpConfig 实例。引擎实例。
VpEngine(backend, model_dir)通过后端枚举快速构造,模型路径为空时使用默认缓存。backendVpBackendTypemodel_dir:可选模型目录。引擎实例。
VpConfig::Preset(name)静态工厂,返回指定预设。name:目前仅注册 campplusERES2NET / ECAPA_TDNN 后端枚举已定义但未注册预设,传入会抛 std::invalid_argumentVpConfig
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_pathpath:可选目标路径。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.hlibvoiceprint.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(不同时间录的更好),让引擎内部聚合。

调用步骤:

  1. VpConfig::Preset("campplus") 创建预设并设阈值/线程/数据库路径。
  2. 构造引擎并 LoadDatabase(不存在则从空库开始)。
  3. Register(name, vector<paths>) 注册。
  4. 注册成功后 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 识别

适用场景:不知道当前说话人是谁,要在已注册说话人中找最相似的——典型用于会议成员识别、家庭场景中识别说话人。

调用步骤:

  1. 加载已有数据库。
  2. Identify(audio)VpResult
  3. 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 自定义对比

适用场景:不入库的临时鉴权(例如对话期间临时记住一个声音)、跨系统迁移特征库、自定义距离度量。

调用步骤:

  1. 对两段音频分别调 ExtractEmbedding 取向量。
  2. 自己实现余弦相似度比较,按业务阈值判定。
#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 -laudio_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 RTF0.25225 秒测试 WAV。

测试方法:在 K3 板卡上用 register_speaker 录制并注册同一说话人的多段样本,再用 audio_demo record 采集测试 WAV,通过 identify_speaker 的 score、阈值判断和 RTF 日志记录结果。