diff --git a/.github/scripts/test-rust.sh b/.github/scripts/test-rust.sh index adbd99b4a6..48a350fee5 100755 --- a/.github/scripts/test-rust.sh +++ b/.github/scripts/test-rust.sh @@ -81,6 +81,8 @@ rm -rf sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12-int8 ./run-silero-vad-remove-silence.sh +./run-ten-vad-remove-silence.sh + ./run-nemo-parakeet-en.sh ./run-zipformer-vi.sh ./run-zipformer-zh-en.sh diff --git a/rust-api-examples/README.md b/rust-api-examples/README.md index 4e51d9fe9c..4ed89c0594 100644 --- a/rust-api-examples/README.md +++ b/rust-api-examples/README.md @@ -91,6 +91,7 @@ in your own Cargo project, see | 46 | [zipformer_transducer_simulate_streaming_microphone](#example-46-simulated-streaming-asr-with-zipformer-transducer-japanese-and-vad-from-microphone) | Simulated streaming ASR with Zipformer transducer (Japanese) and VAD from microphone | | 47 | [qwen3_asr_simulate_streaming_microphone](#example-47-simulated-streaming-asr-with-qwen3-asr-and-vad-from-microphone) | Simulated streaming ASR with Qwen3 ASR and VAD from microphone | | 48 | [whisper](#example-48-asr-with-non-streaming-whisper) | Non-streaming ASR with Whisper (multilingual) | +| 49 | [ten_vad_remove_silence](#example-49-remove-silences-from-a-file-using-ten-vad) | Remove silences from an audio file using ten-vad | ## Run it @@ -422,3 +423,9 @@ Qwen3 ASR recognizer on each detected segment. ```bash ./run-whisper.sh ``` + +### Example 49: Remove silences from a file using ten-vad + +```bash +./run-ten-vad-remove-silence.sh +``` diff --git a/rust-api-examples/examples/ten_vad_remove_silence.rs b/rust-api-examples/examples/ten_vad_remove_silence.rs new file mode 100644 index 0000000000..897e67f86e --- /dev/null +++ b/rust-api-examples/examples/ten_vad_remove_silence.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2026 Xiaomi Corporation +// +// This file demonstrates how to use ten-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, TenVadModelConfig, VadModelConfig, VoiceActivityDetector, Wave}; + +/// Simple VAD example: remove non-speech segments from a WAV file using ten-vad +#[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 ten-vad ONNX model + #[arg(long)] + ten_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 + ); + + if sample_rate != 16000 { + anyhow::bail!( + "ten-vad expects a sample rate of 16000 Hz, but the input file has {} Hz", + sample_rate + ); + } + + // Configure VAD + let mut ten_vad_config = TenVadModelConfig::default(); + ten_vad_config.model = Some(args.ten_vad_model); + + // ten-vad expects a window size of 256 samples at 16 kHz. + // Please don't change it unless you know what you are doing. + ten_vad_config.window_size = 256; + + // You can tune the values below + ten_vad_config.threshold = 0.5; + ten_vad_config.min_silence_duration = 0.25; + ten_vad_config.min_speech_duration = 0.5; + ten_vad_config.max_speech_duration = 5.0; + + let window_size = ten_vad_config.window_size as usize; + + let vad_config = VadModelConfig { + silero_vad: Default::default(), + ten_vad: ten_vad_config, + sample_rate, + num_threads: 1, + provider: Some("cpu".to_string()), + debug: false, + }; + + let vad = VoiceActivityDetector::create(&vad_config, 30.0) + .ok_or_else(|| anyhow::anyhow!("Failed to create VoiceActivityDetector"))?; + + let mut speech_samples = Vec::with_capacity(input_num_samples as usize); + + 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 { + anyhow::bail!("Failed to save speech-only audio to {}", args.output); + } + println!("Saved speech-only audio to {}", args.output); + + // 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 + ); + if input_duration > 0.0 { + println!( + "Removed non-speech: {:.2}% of input removed", + 100.0 * (1.0 - output_duration / input_duration) + ); + } + + Ok(()) +} diff --git a/rust-api-examples/run-ten-vad-remove-silence.sh b/rust-api-examples/run-ten-vad-remove-silence.sh new file mode 100755 index 0000000000..c3b0e206df --- /dev/null +++ b/rust-api-examples/run-ten-vad-remove-silence.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -ex + +# https://k2-fsa.github.io/sherpa/onnx/vad/index.html +if [ ! -f "./ten-vad.onnx" ]; then + curl -SLf -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/ten-vad.onnx +fi + +if [ ! -f ./lei-jun-test.wav ]; then + curl -SLf -O https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/lei-jun-test.wav +fi + +cargo run --example ten_vad_remove_silence -- \ + --input ./lei-jun-test.wav \ + --output ./no-silence-ten-vad.wav \ + --ten-vad-model ./ten-vad.onnx