跳到主要内容

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.hC++ 对外 API。
components/model_zoo/vad/src/backends/silero/Silero VAD 后端。
components/model_zoo/vad/examples/simple_demo.cppC++ 简单示例,产物为 vad_simple_demo
components/model_zoo/vad/python/examples/vad_file_demo.pyPython 文件示例。
components/model_zoo/vad/python/spacemit_vad/Python 包。

演示视频

Silero VAD 演示 GIF

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_rate16000输入采样率。
window_size51216kHz 下约 32ms。
trigger_threshold0.5语音开始阈值。
stop_threshold0.35语音结束阈值。
smoothing_window10平滑窗口大小。

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状态机枚举:SILENCESPEECH_START(首次检测到语音边沿)、SPEECH(持续语音中)、SPEECH_END(语音段结束)。流式回调里用 IsSpeechStart() / IsSpeechEnd() 直接判断。
VadBackendType后端枚举:SILERO(默认,ONNX 模型)、ENERGY(能量阈值)、WEBRTCFSMNCUSTOM(保留)。
VadConfig引擎配置。常用字段:backendmodel_dirsample_rate=16000window_size=512(约 32ms@16kHz)、trigger_threshold=0.5(开始判定阈值)、stop_threshold=0.35(结束判定阈值)、min_speech_duration_ms=250min_silence_duration_ms=100smoothing_window=10use_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)通过后端枚举快速构造,模型路径为空时使用默认缓存。backendVadBackendTypemodel_dir:可选模型目录。引擎实例。
VadEngine(VadConfig)用完整配置构造,可指定阈值、窗口、平滑参数。configVadConfig 实例。引擎实例。
VadConfig::Preset(name)静态工厂,返回指定预设的 VadConfigname: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() 之前调用。callbackshared_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.hlibvad.alibsilero_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 离线缓冲帧检测

适用场景:对一段已经在内存里的音频做语音/静音切分;用于离线脚本或回归测试。

调用步骤:

  1. VadConfig::Preset("silero") 创建预设,按需链式覆写阈值。
  2. 构造 VadEngine 并通过 IsInitialized() 检查。
  3. 把音频按 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 事件。

调用步骤:

  1. 实现 VadEngineCallback 子类,覆写 OnSpeechStart / OnSpeechEnd 处理边沿事件,覆写 OnEvent 处理逐帧概率。
  2. 构造引擎并 SetCallback()Start()
  3. AudioCapture 回调里把 PCM bytes 转 float([-1, 1])后调 SendAudioFrame
  4. 退出前 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 的核心模式。

调用步骤:

  1. OnSpeechStart 时开始累积 PCM;OnSpeechEnd 时把累积缓冲整段提交 ASR。
  2. 累积过程中也可结合 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 / Resetengine.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 / SetStopThresholdengine.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_thresholdstop_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 事件确认真实对话状态。