4.1.3 VAD
1. 模块概述
VAD 组件提供语音活动检测能力,用于判断音频帧中是否有人声,并输出语音开始、语音中、语音结束等状态。组件位于 components/model_zoo/vad,当前默认后端为 Silero VAD,本地 ONNX 推理,提供 C++ API、Python 绑定和文件/单帧示例。
在端到端语音链路中,VAD 通常位于 ASR 前面:
麦克风 PCM -> VAD 检测语音段 -> 触发 ASR Flush/识别 -> LLM/TTS
在 omni_agent 中,VAD 还用于检测用户打断 TTS 播放的 barge-in 场景。
主要目录:
| 路径 | 说明 |
|---|---|
components/model_zoo/vad/include/vad_service.h | C++ 对外 API。 |
components/model_zoo/vad/src/backends/silero/ | Silero VAD 后端。 |
components/model_zoo/vad/examples/simple_demo.cpp | C++ 简单示例,产物为 vad_simple_demo。 |
components/model_zoo/vad/python/examples/vad_file_demo.py | Python 文件示例。 |
components/model_zoo/vad/python/spacemit_vad/ | Python 包。 |
演示视频

2. 环境准备
前置条件
SDK 源码获取和基础编译环境配置统一参考 2.3-构建编译。完成 SDK 初始化后,回到本文继续执行“构建编译”。
后续命令默认在 spacemit_robot SDK 根目录执行。
构建编译
语音组件使用 target/k3-com260-omni-agent.json 目标配置。系统缺少依赖时先安装:
sudo apt install libcurl4-openssl-dev pybind11-dev
在 SDK 根目录加载环境后编译 VAD:
source build/envsetup.sh
lunch k3-com260-omni-agent
cd components/model_zoo/vad
mm
构建产物会把 vad_simple_demo 安装到 output/staging/bin。需要 Python 示例时,在 SDK 根目录执行:
m -py
注意:m -py / mm -py 是构建 wheel,使用系统 Python 环境执行;不要在 ~/.comm-env 中执行。~/.comm-env 只用于后面的 pip install 和运行 Python 示例。
运行 Python 示例前,先安装虚拟环境依赖,并创建、激活 ~/.comm-env:
sudo apt install python3-venv python-is-python3 python3-pip
python3 -m venv ~/.comm-env
source ~/.comm-env/bin/activate
然后二选一安装 Python 包。方式一,直接安装发布包:
python -m pip install spacemit-vad \
--index-url https://git.spacemit.com/api/v4/projects/33/packages/pypi/simple
方式二,回到 SDK 根目录安装本地构建出的 wheel:
python -m pip install output/wheels/components_model_zoo_vad/spacemit_vad-*.whl \
--index-url https://git.spacemit.com/api/v4/projects/33/packages/pypi/simple
安装后可 import spacemit_vad。
Silero 模型默认路径为 ~/.cache/models/vad/silero/silero_vad.onnx,首次运行会按组件逻辑检查或下载。
Silero 默认音频配置:
| 参数 | 默认值 | 说明 |
|---|---|---|
sample_rate | 16000 | 输入采样率。 |
window_size | 512 | 16kHz 下约 32ms。 |
trigger_threshold | 0.5 | 语音开始阈值。 |
stop_threshold | 0.35 | 语音结束阈值。 |
smoothing_window | 10 | 平滑窗口大小。 |
3. 示例使用
3.1 C++ 简单示例
vad_simple_demo
该示例用于验证 VAD 引擎初始化和单帧检测能力。当前仓库存在 examples/simple_demo.cpp,因此文档只把 vad_simple_demo 作为固定产物说明。
3.2 Python 文件示例
安装 spacemit-vad 后运行:
cd components/model_zoo/vad
python python/examples/vad_file_demo.py
如果直接运行示例报 No module named 'spacemit_vad',需要确认已激活 ~/.comm-env,并已安装 spacemit-vad 或本地 wheel。
3.3 端到端验证
VAD 在 omni_agent 中会打印实际对话状态。启动语音对话后,关键日志包括:
[2/5] 初始化 VAD... OK (Silero VAD)
[VAD] 开始说话 (prob=0.86)...
[VAD] 停止说话,触发识别
[Barge-in] 用户打断 (连续5帧, prob=0.856),停止播放
上述 K3 实测日志可用于确认 VAD 与 ASR、TTS、barge-in 之间的端到端联动。
4. 应用开发
本章面向应用开发者,说明如何在自己的 C++ / Python 应用中集成 VAD 组件。完整接口以 components/model_zoo/vad/include/vad_service.h 为准;本节只介绍常用公开接口和典型调用方式。VAD 的 C++ 入口是 SpacemiT::VadEngine,Python 入口是 spacemit_vad.VadEngine。
4.1 接口说明
VAD 组件的核心入口是 SpacemiT::VadEngine。应用侧通过该类创建检测引擎、选择后端,并发起单帧检测或流式检测请求。注意 VAD 没有 Flush():流式 API 是 Start/SendAudioFrame/Stop;要清空内部状态用 Reset()。
4.1.1 常用数据结构
| 类型 | 说明 |
|---|---|
VadState | 状态机枚举:SILENCE、SPEECH_START(首次检测到语音边沿)、SPEECH(持续语音中)、SPEECH_END(语音段结束)。流式回调里用 IsSpeechStart() / IsSpeechEnd() 直接判断。 |
VadBackendType | 后端枚举:SILERO(默认,ONNX 模型)、ENERGY(能量阈值)、WEBRTC、FSMN、CUSTOM(保留)。 |
VadConfig | 引擎配置。常用字段:backend、model_dir、sample_rate=16000、window_size=512(约 32ms@16kHz)、trigger_threshold=0.5(开始判定阈值)、stop_threshold=0.35(结束判定阈值)、min_speech_duration_ms=250、min_silence_duration_ms=100、smoothing_window=10、use_smoothing=true。可用 Preset(name) 创建;含链式 withTriggerThreshold/withStopThreshold/withMinSpeechDuration/withMinSilenceDuration/withWindowSize/withSampleRate/withSmoothing 等。 |
VadResult | 检测结果。提供 GetProbability() / GetSmoothedProbability()、IsSpeech()、GetState()、IsSpeechStart() / IsSpeechEnd()、GetTimestampMs() / GetSpeechStartMs() / GetSpeechEndMs() / GetSpeechDurationMs()、GetProcessingTimeMs()、IsSuccess()、GetMessage()。 |
VadEngineCallback | 流式回调基类。覆写 OnOpen()、OnEvent(result)、OnSpeechStart(timestamp_ms)、OnSpeechEnd(timestamp_ms, duration_ms)、OnComplete()、OnError(message)、OnClose()。回调由 VAD 引擎内部线程触发,自有状态需自行加锁。 |
4.1.2 引擎初始化与预设
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
VadEngine(backend, model_dir) | 通过后端枚举快速构造,模型路径为空时使用默认缓存。 | backend:VadBackendType;model_dir:可选模型目录。 | 引擎实例。 |
VadEngine(VadConfig) | 用完整配置构造,可指定阈值、窗口、平滑参数。 | config:VadConfig 实例。 | 引擎实例。 |
VadConfig::Preset(name) | 静态工厂,返回指定预设的 VadConfig。 | name:silero / energy / webrtc 等。 | VadConfig。 |
VadConfig::AvailablePresets() | 静态方法,返回当前可用预设名称列表。 | 无。 | std::vector<std::string>。 |
IsInitialized() / IsStreaming() / GetEngineName() / GetBackendType() / GetCurrentState() / GetLastProbability() / IsInSpeech() / GetConfig() | 状态与配置查询。 | 无。 | 各自类型。 |
4.1.3 单帧检测
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
Detect(vector<float>, sample_rate=16000) | 检测一帧 PCM 浮点单声道音频,幅度 [-1.0, 1.0],长度建议为 window_size(512 默认)。 | audio:PCM float 单帧;sample_rate:采样率。 | shared_ptr<VadResult>,失败返回 nullptr。 |
Detect(float* data, size_t num_samples, sample_rate=16000) | 指针版本,避免数据复制;其它语义同上。 | data:浮点缓冲;num_samples:样本数。 | 同上。 |
4.1.4 流式检测
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
SetCallback(callback) | 注册流式回调对象,必须在 Start() 之前调用。 | callback:shared_ptr<VadEngineCallback>。 | 无。 |
Start() | 启动流式会话,进入接收音频帧状态。 | 无。 | bool。 |
SendAudioFrame(vector<float>) | 送入一帧 PCM 浮点单声道,长度建议是 window_size 的整数倍。 | data:PCM float。 | 无。 |
SendAudioFrame(float* data, size_t num_samples) | 指针版本。 | 同上。 | 无。 |
Stop() | 结束本次流式会话,触发 OnComplete。 | 无。 | 无。 |
Reset() | 清空内部状态机和概率历史,不结束会话;用于上下文切换或语音段间手动重置。 | 无。 | 无。 |
4.1.5 运行时阈值
| 接口 | 说明 | 参数 | 返回值 |
|---|---|---|---|
SetTriggerThreshold(threshold) | 运行时调高/降低开始判定阈值;阈值越高漏检越少、误检越多。 | threshold:[0, 1]。 | 无。 |
SetStopThreshold(threshold) | 运行时调结束判定阈值;通常比 trigger 低 0.1 左右形成滞回。 | threshold:[0, 1]。 | 无。 |
4.2 C++ 调用示例
以下示例默认已完成 §2 构建;Silero 模型在 ~/.cache/models/vad/silero/silero_vad.onnx。
VAD 组件编译后会把 vad_service.h、libvad.a 和 libsilero_vad.a 安装到 output/staging(均为静态库;vad 在内部 PUBLIC 链接 silero_vad,下游显式链接两者更稳)。下游组件链接 VAD 库的 CMake 写法:
add_executable(my_app main.cpp)
find_library(VAD_LIB NAMES vad
PATHS ${CMAKE_INSTALL_PREFIX}/lib NO_DEFAULT_PATH)
find_library(SILERO_VAD_LIB NAMES silero_vad
PATHS ${CMAKE_INSTALL_PREFIX}/lib NO_DEFAULT_PATH)
find_path(VAD_SERVICE_INCLUDE_DIR NAMES vad_service.h
PATHS ${CMAKE_INSTALL_PREFIX}/include NO_DEFAULT_PATH)
if(NOT VAD_LIB OR NOT SILERO_VAD_LIB OR NOT VAD_SERVICE_INCLUDE_DIR)
message(FATAL_ERROR "vad/silero_vad not found. Build components/model_zoo/vad first.")
endif()
target_include_directories(my_app PRIVATE ${VAD_SERVICE_INCLUDE_DIR})
target_link_libraries(my_app PRIVATE ${VAD_LIB} ${SILERO_VAD_LIB})
包依赖建议在当前组件的 package.xml 中声明 <depend>vad</depend>。
4.2.1 离线缓冲帧检测
适用场景:对一段已经在内存里的音频做语音/静音切分;用于离线脚本或回归测试。
调用步骤:
- 用
VadConfig::Preset("silero")创建预设,按需链式覆写阈值。 - 构造
VadEngine并通过IsInitialized()检查。 - 把音频按
window_size(默认 512)切片,循环Detect()取概率与状态。
#include <iostream>
#include <vector>
#include <memory>
#include "vad_service.h"
void DetectBuffer(const std::vector<float>& audio_16k_mono) {
auto config = SpacemiT::VadConfig::Preset("silero")
.withTriggerThreshold(0.5f)
.withStopThreshold(0.35f);
auto engine = std::make_shared<SpacemiT::VadEngine>(config);
if (!engine->IsInitialized()) {
std::cerr << "VAD 引擎初始化失败" << std::endl;
return;
}
constexpr size_t kWindow = 512;
for (size_t i = 0; i + kWindow <= audio_16k_mono.size(); i += kWindow) {
std::vector<float> frame(audio_16k_mono.begin() + i,
audio_16k_mono.begin() + i + kWindow);
auto result = engine->Detect(frame, 16000);
if (!result || !result->IsSuccess()) continue;
std::cout << "t=" << (i * 1000 / 16000) << "ms"
<< " prob=" << result->GetProbability()
<< " state=" << static_cast<int>(result->GetState());
if (result->IsSpeechStart()) std::cout << " [START]";
if (result->IsSpeechEnd()) std::cout << " [END]";
std::cout << std::endl;
}
}
完整版含合成正弦/静音测试和性能统计,见 components/model_zoo/vad/examples/simple_demo.cpp。
4.2.2 流式麦克风 VAD + 状态回调
适用场景:实时检测麦克风输入;与 audio 组件采集回调串联,得到 OnSpeechStart / OnSpeechEnd 事件。
调用步骤:
- 实现
VadEngineCallback子类,覆写OnSpeechStart/OnSpeechEnd处理边沿事件,覆写OnEvent处理逐帧概率。 - 构造引擎并
SetCallback()→Start()。 AudioCapture回调里把 PCM bytes 转 float([-1, 1])后调SendAudioFrame。- 退出前
Stop()。
#include <atomic>
#include <chrono>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include "vad_service.h"
#include "audio_base.hpp"
class VadCb : public SpacemiT::VadEngineCallback {
public:
void OnSpeechStart(int64_t ts_ms) override {
std::cout << "[VAD] 说话开始 @" << ts_ms << "ms" << std::endl;
}
void OnSpeechEnd(int64_t ts_ms, int duration_ms) override {
std::cout << "[VAD] 说话结束 @" << ts_ms
<< "ms (持续 " << duration_ms << "ms)" << std::endl;
}
void OnError(const std::string& msg) override {
std::cerr << "[VAD] 错误: " << msg << std::endl;
}
};
int main() {
auto config = SpacemiT::VadConfig::Preset("silero");
auto engine = std::make_shared<SpacemiT::VadEngine>(config);
auto cb = std::make_shared<VadCb>();
engine->SetCallback(cb);
engine->Start();
SpacemitAudio::AudioCapture capture(-1);
capture.SetCallback([&](const uint8_t* data, size_t size) {
const auto* pcm = reinterpret_cast<const int16_t*>(data);
size_t n = size / sizeof(int16_t);
std::vector<float> frame(n);
for (size_t i = 0; i < n; ++i) frame[i] = pcm[i] / 32768.0f;
engine->SendAudioFrame(frame);
});
capture.Start(16000, 1, 1024); // 1024 字节 = 512 个 int16
std::this_thread::sleep_for(std::chrono::seconds(30));
engine->Stop();
capture.Stop();
return 0;
}
4.2.3 配合 ASR 的端点检测
适用场景:典型语音助手链路——VAD 检测到 SPEECH_END 后把缓冲音频送 ASR.Recognize;这是 omni_agent voice_chat 的核心模式。
调用步骤:
- 在
OnSpeechStart时开始累积 PCM;OnSpeechEnd时把累积缓冲整段提交 ASR。 - 累积过程中也可结合
min_speech_duration_ms过滤过短的"假语音"。
class EndpointCb : public SpacemiT::VadEngineCallback {
public:
EndpointCb(SpacemiT::AsrEngine* asr) : asr_(asr) {}
void OnSpeechStart(int64_t) override {
std::lock_guard<std::mutex> lk(mu_);
buffer_.clear();
in_speech_ = true;
}
void OnSpeechEnd(int64_t, int duration_ms) override {
std::vector<int16_t> chunk;
{
std::lock_guard<std::mutex> lk(mu_);
chunk = std::move(buffer_);
in_speech_ = false;
}
if (chunk.empty()) return;
auto result = asr_->Recognize(chunk, 16000);
if (result && !result->IsEmpty())
std::cout << "[ASR] " << result->GetText() << std::endl;
}
void Append(const int16_t* data, size_t n) {
std::lock_guard<std::mutex> lk(mu_);
if (!in_speech_) return;
buffer_.insert(buffer_.end(), data, data + n);
}
private:
SpacemiT::AsrEngine* asr_;
std::mutex mu_;
std::vector<int16_t> buffer_;
bool in_speech_ = false;
};
AudioCapture 回调里把 int16 数据同时送 VAD(转 float)和 EndpointCb::Append(保留 int16 原始量)。完整链路示例可参考 application/native/omni_agent/src/voice_pipeline.cpp。
4.3 Python 示例
Python 包名为 spacemit_vad,安装方式见 §2 中 wheel 安装步骤。导入后直接使用:
import spacemit_vad
4.3.1 单帧检测
import numpy as np
import spacemit_vad
config = spacemit_vad.VadConfig.preset("silero") \
.with_trigger_threshold(0.5) \
.with_stop_threshold(0.35)
engine = spacemit_vad.VadEngine(config)
assert engine.is_initialized
frame = np.zeros(512, dtype=np.float32) # 用真实 PCM 替换
result = engine.detect(frame, 16000)
print(f"prob={result.probability:.3f}, "
f"is_speech={result.is_speech}, "
f"state={result.state}, "
f"start={result.is_speech_start}, end={result.is_speech_end}")
或者用模块级快捷函数:
import spacemit_vad
result = spacemit_vad.detect(audio_frame) # 默认 silero + 16kHz
4.3.2 引擎对象 + 回调流式
Python 端 VadCallback 不是 ABC 子类继承,而是用 setter 注册回调函数:
import numpy as np
import spacemit_vad
callback = spacemit_vad.VadCallback()
callback.on_speech_start(lambda ts: print(f"[VAD] 说话开始 @{ts}ms"))
callback.on_speech_end(lambda ts, dur: print(f"[VAD] 说话结束 @{ts}ms ({dur}ms)"))
callback.on_event(lambda r: None) # 逐帧事件,可选
callback.on_error(lambda msg: print(f"[VAD] 错误: {msg}"))
engine = spacemit_vad.VadEngine(spacemit_vad.VadConfig.preset("silero"))
engine.set_callback(callback)
engine.start()
# 把 16kHz mono float32 PCM 按 512 样本/帧送入;也可直接传 PCM16 bytes
for chunk in iter_audio_frames(): # 自行实现
engine.send_audio_frame(chunk) # numpy float32 或 bytes 都可
engine.stop()
4.3.3 C++ ↔ Python 接口对照
C++(SpacemiT::) | Python(spacemit_vad.) | 备注 |
|---|---|---|
VadConfig + Preset(name) | VadConfig.preset(name) | 链式方法名一致小写化(with_trigger_threshold 等);字段直接可读(config.trigger_threshold)。 |
VadEngine(config) + IsInitialized() | VadEngine(config) + engine.is_initialized 属性 | Python 构造时直接初始化。 |
Detect(vector<float>, sample_rate) | engine.detect(numpy_float32, sample_rate) 或模块级 spacemit_vad.detect(audio) | Python 端额外接受 int16 numpy。 |
SetCallback / Start / SendAudioFrame / Stop / Reset | engine.set_callback(cb) / start() / send_audio_frame(audio_or_bytes) / stop() / reset() | 流式 API 1:1 对应。 |
VadEngineCallback(继承 + 覆写虚函数) | VadCallback(实例 + setter 注册):cb.on_event(fn) / on_speech_start(fn) / on_speech_end(fn) / on_error(fn) / on_open(fn) / on_complete(fn) / on_close(fn) | 用法风格不同:Python 用回调函数注册,无需子类化 ABC。 |
SetTriggerThreshold / SetStopThreshold | engine.set_trigger_threshold(t) / set_stop_threshold(t) | |
VadResult::GetProbability/GetState/... | result.probability / state / is_speech / is_speech_start / is_speech_end / timestamp_ms / speech_duration_ms / processing_time_ms 属性 | Python 全部用属性,没有 getter 方法。 |
Python examples/ 当前仅有 vad_file_demo.py,未提供独立流式 demo;流式用法参考上面 §4.3.2,或对照 components/model_zoo/asr/python/examples/asr_stream_demo.py 的多进程采集骨架自行改写。
5. 调试指南
调试 VAD 时先确认输入音频,再观察状态机日志:
- 用
audio_demo record录制同一麦克风输入,并回放确认不是静音或严重削波。 - 单帧能力可用
vad_simple_demo独立验证;真实语音对话中的 VAD 状态以voice_chat日志为准。 - 阈值问题优先调整
trigger_threshold、stop_threshold和平滑窗口,避免先改业务逻辑。 - 回声导致误触发时记录播放音量、麦克风距离和是否启用硬件或软件 AEC。
6. 常见问题
| 现象 | 可能原因 | 处理 |
|---|---|---|
| 一直检测为静音 | 设备无输入、采样率不匹配或阈值过高 | 先用 audio_demo record 录音回放,再降低 trigger_threshold 验证。 |
| 背景噪声触发语音 | 阈值过低或环境噪声大 | 提高 trigger_threshold,增大平滑窗口,或在前级加入降噪。 |
| 语音结束太慢 | stop_threshold 或状态平滑导致拖尾 | 适当提高 stop_threshold 或缩短业务侧静音等待。 |
| barge-in 误触发 | TTS 回放泄漏到麦克风 | 使用外设硬件 AEC 或 voice_chat_aec 的 WebRTC AEC 模式。 |
附录:与 Agent 的联动结果
K3 端到端语音对话实测显示,VAD 能在 voice_chat 中完成以下动作:
| 动作 | 代表日志 | 作用 |
|---|---|---|
| 初始化 | [2/5] 初始化 VAD... OK (Silero VAD) | 确认模型和引擎可用。 |
| 语音开始 | [VAD] 开始说话 (prob=0.86)... | 开始缓存用户语音。 |
| 语音结束 | [VAD] 停止说话,触发识别 | 触发声纹验证和 ASR。 |
| 用户打断 | [Barge-in] 用户打断 ... 停止播放 | 中断 LLM/TTS 当前回复,开始新一轮输入。 |
测试方法:在 K3 板卡上构建 VAD 与 omni_agent 后直接运行 voice_chat,通过日志中的 VAD 初始化、语音开始、语音结束和 barge-in 事件确认真实对话状态。