Repository navigation
Add Rust API for VAD #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Rust API for VAD #3213
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||||||||||||||
| // Copyright (c) 2026 Xiaomi Corporation | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||
| // This file demonstrates how to use silero VAD with sherpa-onnx's | ||||||||||||||||||||||||||
| // Rust API to remove non-speech segments and save speech-only audio. | ||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||
| // See ../README.md for how to run it | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| use clap::Parser; | ||||||||||||||||||||||||||
| use sherpa_onnx::{self, SileroVadModelConfig, VadModelConfig, VoiceActivityDetector, Wave}; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Simple VAD example: remove non-speech segments from a WAV file | ||||||||||||||||||||||||||
| #[derive(Parser, Debug)] | ||||||||||||||||||||||||||
| #[command(author, version, about, long_about = None)] | ||||||||||||||||||||||||||
| struct Args { | ||||||||||||||||||||||||||
| /// Path to input WAV file | ||||||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||||||
| input: String, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Path to output WAV file | ||||||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||||||
| output: String, | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /// Path to Silero VAD ONNX model | ||||||||||||||||||||||||||
| #[arg(long)] | ||||||||||||||||||||||||||
| silero_vad_model: String, | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| fn main() -> anyhow::Result<()> { | ||||||||||||||||||||||||||
| let args = Args::parse(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Read WAV file | ||||||||||||||||||||||||||
| let wave = Wave::read(&args.input) | ||||||||||||||||||||||||||
| .ok_or_else(|| anyhow::anyhow!("Failed to read WAV file: {}", &args.input))?; | ||||||||||||||||||||||||||
| let sample_rate = wave.sample_rate(); | ||||||||||||||||||||||||||
| let input_num_samples = wave.num_samples(); | ||||||||||||||||||||||||||
| let input_duration = input_num_samples as f32 / sample_rate as f32; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||
| "Input WAV: sample rate: {}, num samples: {}, duration: {:.2}s", | ||||||||||||||||||||||||||
| sample_rate, input_num_samples, input_duration | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Configure VAD | ||||||||||||||||||||||||||
| let mut silero_config = SileroVadModelConfig::default(); | ||||||||||||||||||||||||||
| silero_config.model = Some(args.silero_vad_model); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // You can tune the values below | ||||||||||||||||||||||||||
| silero_config.threshold = 0.5; | ||||||||||||||||||||||||||
| silero_config.min_silence_duration = 0.25; | ||||||||||||||||||||||||||
| silero_config.min_speech_duration = 0.25; | ||||||||||||||||||||||||||
| silero_config.max_speech_duration = 5.0; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let vad_config = VadModelConfig { | ||||||||||||||||||||||||||
| silero_vad: silero_config, | ||||||||||||||||||||||||||
| ten_vad: Default::default(), | ||||||||||||||||||||||||||
| sample_rate, | ||||||||||||||||||||||||||
| num_threads: 1, | ||||||||||||||||||||||||||
| provider: Some("cpu".to_string()), | ||||||||||||||||||||||||||
| debug: false, | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let vad = VoiceActivityDetector::create(&vad_config, 30.0) | ||||||||||||||||||||||||||
| .expect("Failed to create VoiceActivityDetector"); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let mut speech_samples = Vec::new(); | ||||||||||||||||||||||||||
| const WINDOW_SIZE: usize = 512; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| for chunk in wave.samples().chunks(WINDOW_SIZE) { | ||||||||||||||||||||||||||
| vad.accept_waveform(chunk); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| while let Some(seg) = vad.front() { | ||||||||||||||||||||||||||
| speech_samples.extend_from_slice(seg.samples()); | ||||||||||||||||||||||||||
| vad.pop(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| vad.flush(); | ||||||||||||||||||||||||||
| while let Some(seg) = vad.front() { | ||||||||||||||||||||||||||
| speech_samples.extend_from_slice(seg.samples()); | ||||||||||||||||||||||||||
| vad.pop(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Write speech-only samples to output WAV | ||||||||||||||||||||||||||
| let ok = sherpa_onnx::write(&args.output, &speech_samples, sample_rate); | ||||||||||||||||||||||||||
| if ok { | ||||||||||||||||||||||||||
| println!("Saved speech-only audio to {}", args.output); | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| println!("Failed to save speech-only audio to {}", args.output); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+84
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The let ok = sherpa_onnx::write(&args.output, &speech_samples, sample_rate);
if !ok {
anyhow::bail!("Failed to save speech-only audio to {}", args.output);
}
println!("Saved speech-only audio to {}", args.output);
Comment on lines
+83
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n rust-api-examples/examples/silero_vad_remove_silence.rs | head -100Repository: k2-fsa/sherpa-onnx Length of output: 3855 🏁 Script executed: cat -n rust-api-examples/examples/silero_vad_remove_silence.rs | tail -20Repository: k2-fsa/sherpa-onnx Length of output: 816 🏁 Script executed: find . -name "Cargo.toml" -path "*/rust-api-examples/*" -o -name "Cargo.toml" -path "*" | head -5Repository: k2-fsa/sherpa-onnx Length of output: 210 🏁 Script executed: cat rust-api-examples/Cargo.tomlRepository: k2-fsa/sherpa-onnx Length of output: 586 Propagate write failures via non-zero exit. The function returns Suggested fix- let ok = sherpa_onnx::write(&args.output, &speech_samples, sample_rate);
- if ok {
- println!("Saved speech-only audio to {}", args.output);
- } else {
- println!("Failed to save speech-only audio to {}", args.output);
- }
+ if !sherpa_onnx::write(&args.output, &speech_samples, sample_rate) {
+ anyhow::bail!("Failed to save speech-only audio to {}", args.output);
+ }
+ println!("Saved speech-only audio to {}", args.output);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Summary | ||||||||||||||||||||||||||
| let output_num_samples = speech_samples.len(); | ||||||||||||||||||||||||||
| let output_duration = output_num_samples as f32 / sample_rate as f32; | ||||||||||||||||||||||||||
| println!("\n=== Summary ==="); | ||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||
| "Input: sample rate = {}, samples = {}, duration = {:.2}s", | ||||||||||||||||||||||||||
| sample_rate, input_num_samples, input_duration | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||
| "Output: sample rate = {}, samples = {}, duration = {:.2}s", | ||||||||||||||||||||||||||
| sample_rate, output_num_samples, output_duration | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| println!( | ||||||||||||||||||||||||||
| "Removed non-speech: {:.2}% of input removed", | ||||||||||||||||||||||||||
| 100.0 * (1.0 - output_duration / input_duration) | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,16 @@ | ||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||
| set -ex | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # https://k2-fsa.github.io/sherpa/onnx/vad/silero-vad.html | ||||||||||||||||||||||||||||||
| if [ ! -f "./silero_vad.onnx" ]; then | ||||||||||||||||||||||||||||||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if [ ! -f ./lei-jun-test.wav ]; then | ||||||||||||||||||||||||||||||
| curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/lei-jun-test.wav | ||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find the script file
find . -name "*run-silero-vad-remove-silence.sh" -type fRepository: k2-fsa/sherpa-onnx Length of output: 114 🏁 Script executed: # Check if the file exists and read its content
if [ -f "./rust-api-examples/run-silero-vad-remove-silence.sh" ]; then
cat -n "./rust-api-examples/run-silero-vad-remove-silence.sh"
fiRepository: k2-fsa/sherpa-onnx Length of output: 696 🌐 Web query:
💡 Result: By default, To make
Notes on your flags:
Citations:
🌐 Web query:
💡 Result:
Common patterns:
Sources: [1] [2] [3] Fail fast on download errors. 🔧 Suggested fix- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx
+ curl -fSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/silero_vad.onnx
...
- curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/lei-jun-test.wav
+ curl -fSL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/lei-jun-test.wav📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| cargo run --example silero_vad_remove_silence -- \ | ||||||||||||||||||||||||||||||
| --input ./lei-jun-test.wav \ | ||||||||||||||||||||||||||||||
| --output ./no-silence.wav \ | ||||||||||||||||||||||||||||||
| --silero-vad-model ./silero_vad.onnx | ||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1128,7 +1128,7 @@ struct SherpaOnnxVoiceActivityDetector { | |
| std::unique_ptr<sherpa_onnx::VoiceActivityDetector> impl; | ||
| }; | ||
|
|
||
| sherpa_onnx::VadModelConfig GetVadModelConfig( | ||
| static sherpa_onnx::VadModelConfig GetVadModelConfig( | ||
| const SherpaOnnxVadModelConfig *config) { | ||
| sherpa_onnx::VadModelConfig vad_config; | ||
|
|
||
|
|
@@ -1185,6 +1185,11 @@ sherpa_onnx::VadModelConfig GetVadModelConfig( | |
|
|
||
| const SherpaOnnxVoiceActivityDetector *SherpaOnnxCreateVoiceActivityDetector( | ||
| const SherpaOnnxVadModelConfig *config, float buffer_size_in_seconds) { | ||
| if (!config) { | ||
| SHERPA_ONNX_LOGE("vad config is nullptr"); | ||
| return nullptr; | ||
| } | ||
|
Comment on lines
+1188
to
+1191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OHOS path skips the same null guard when The new check correctly prevents 🛡️ Proposed fix for the OHOS path const SherpaOnnxVoiceActivityDetector *
SherpaOnnxCreateVoiceActivityDetectorOHOS(
const SherpaOnnxVadModelConfig *config, float buffer_size_in_seconds,
NativeResourceManager *mgr) {
if (mgr == nullptr) {
return SherpaOnnxCreateVoiceActivityDetector(config,
buffer_size_in_seconds);
}
+ if (!config) {
+ SHERPA_ONNX_LOGE("vad config is nullptr");
+ return nullptr;
+ }
+
auto vad_config = GetVadModelConfig(config);🤖 Prompt for AI Agents |
||
|
|
||
| auto vad_config = GetVadModelConfig(config); | ||
|
|
||
| if (!vad_config.Validate()) { | ||
|
|
@@ -1206,31 +1211,70 @@ void SherpaOnnxDestroyVoiceActivityDetector( | |
|
|
||
| void SherpaOnnxVoiceActivityDetectorAcceptWaveform( | ||
| const SherpaOnnxVoiceActivityDetector *p, const float *samples, int32_t n) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| if (!samples) { | ||
| SHERPA_ONNX_LOGE("samples is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| p->impl->AcceptWaveform(samples, n); | ||
| } | ||
|
|
||
| int32_t SherpaOnnxVoiceActivityDetectorEmpty( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return 1; // 1 means it is empty | ||
| } | ||
|
|
||
| return p->impl->Empty(); | ||
| } | ||
|
|
||
| int32_t SherpaOnnxVoiceActivityDetectorDetected( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return 0; | ||
| } | ||
|
|
||
| return p->impl->IsSpeechDetected(); | ||
| } | ||
|
|
||
| void SherpaOnnxVoiceActivityDetectorPop( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| p->impl->Pop(); | ||
| } | ||
|
|
||
| void SherpaOnnxVoiceActivityDetectorClear( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| p->impl->Clear(); | ||
| } | ||
|
|
||
| const SherpaOnnxSpeechSegment *SherpaOnnxVoiceActivityDetectorFront( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return nullptr; | ||
| } | ||
|
|
||
| if (SherpaOnnxVoiceActivityDetectorEmpty(p)) { | ||
| return nullptr; | ||
| } | ||
|
Comment on lines
+1274
to
+1276
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const sherpa_onnx::SpeechSegment &segment = p->impl->Front(); | ||
|
|
||
| SherpaOnnxSpeechSegment *ans = new SherpaOnnxSpeechSegment; | ||
|
|
@@ -1251,11 +1295,21 @@ void SherpaOnnxDestroySpeechSegment(const SherpaOnnxSpeechSegment *p) { | |
|
|
||
| void SherpaOnnxVoiceActivityDetectorReset( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| p->impl->Reset(); | ||
| } | ||
|
|
||
| void SherpaOnnxVoiceActivityDetectorFlush( | ||
| const SherpaOnnxVoiceActivityDetector *p) { | ||
| if (!p) { | ||
| SHERPA_ONNX_LOGE("vad is nullptr"); | ||
| return; | ||
| } | ||
|
|
||
| p->impl->Flush(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds a network-dependent step (downloads model/audio via
curl) to the primary Rust test script, which can make CI flaky and slower. Consider gating it behind an env flag (e.g.,RUN_NETWORK_TESTS=1), adding retries/checksums, and/or caching the assets in CI to keep the test pipeline deterministic.