Files
rustytorch/crates/models/rtx-csm/examples/silero_vad_smoke.rs
T
osobhandClaude Opus 4.7 a5cedfb46a rtx-csm: emotional_speech_guide — CREMA-D vs RAVDESS firdhokk verdict
8-gen bench (4 emotions × 2 corpora) at seed=42 against firdhokk
Whisper-LV3:

  target    RAVDESS              CREMA-D
  happy     happy (0.999) ✓      happy (0.999) ✓
  angry     neutral (0.92)       sad (0.99)
  fearful   happy (0.998)        fearful (0.984) ✓
  sad       angry (0.99)         fearful (0.99)

CREMA-D 2/4 vs RAVDESS 1/4. Larger / more naturalistic corpus
produces more class-pure fearful direction. Neither corpus solves
angry or sad — recipe shifts into 'vague expressivity' rather than
class-specific corners.

Practical: prefer CREMA-D when available; A/B both per emotion if
class precision matters.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-30 00:01:02 -07:00

121 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Phase 11.1 smoke test for Silero V5. Runs the model on the
//! synthetic 50/50 silence/speech WAV (created by `make_silence_test`)
//! and verifies that speech windows give high probability, silence
//! windows give low.
//!
//! Usage:
//! ```bash
//! # First create the test WAV (silence + speech + silence + silence):
//! cargo run -p rtx-csm --release --example make_silence_test
//!
//! # Then run the VAD smoke:
//! cargo run -p rtx-csm --release --features metal --example silero_vad_smoke
//! ```
use anyhow::{Context, Result};
use candle_core::Device;
use rtx_csm::{audio_io, silero_vad::SileroVad};
use std::time::Instant;
fn main() -> Result<()> {
let device = if candle_core::utils::metal_is_available() {
Device::new_metal(0)?
} else {
Device::Cpu
};
eprintln!("device: {device:?}");
let load_t = Instant::now();
let vad = SileroVad::load_default(&device).context("load Silero VAD")?;
eprintln!("model loaded in {} ms", load_t.elapsed().as_millis());
// Test path: prefer the silence-heavy synthetic WAV (50% silence)
// if available; fall back to the LibriSpeech sample (mostly speech).
let path = if std::path::Path::new("/tmp/asr_silence_heavy.wav").exists() {
std::path::Path::new("/tmp/asr_silence_heavy.wav")
} else {
std::path::Path::new("/tmp/asr_test.flac")
};
eprintln!("test audio: {}", path.display());
let samples = audio_io::load_mono_at_rate(path, 16_000).context("load + resample to 16 kHz")?;
eprintln!(
" {} samples ({:.2}s @ 16 kHz)",
samples.len(),
samples.len() as f32 / 16_000.0
);
let inf_t = Instant::now();
let probs = vad.forward_audio(&samples, &device)?;
let inf_ms = inf_t.elapsed().as_millis();
let audio_secs = samples.len() as f32 / 16_000.0;
eprintln!(
"VAD sweep: {inf_ms} ms over {} chunks ({:.4}× realtime)",
probs.len(),
inf_ms as f32 / (audio_secs * 1000.0)
);
// Each chunk = 512 samples = 32 ms. Print probabilities every 250 ms
// (~8 chunks).
println!();
println!("=== speech probability over time ===");
println!("(each chunk is 32 ms; printed every 8 chunks ~= every 256 ms)");
let mut speech_count = 0;
let mut silence_count = 0;
for (i, &p) in probs.iter().enumerate() {
if p > 0.5 {
speech_count += 1;
} else {
silence_count += 1;
}
if i % 8 == 0 {
let ms = i * 32;
let bar = (p * 40.0) as usize;
let bar_str: String = std::iter::repeat('█').take(bar).collect();
println!(" [{ms:>5} ms] p={p:.3} {bar_str}");
}
}
println!();
println!("=== summary ===");
println!(
"speech chunks: {speech_count} ({:.1}%)",
100.0 * speech_count as f32 / probs.len() as f32
);
println!(
"silence chunks: {silence_count} ({:.1}%)",
100.0 * silence_count as f32 / probs.len() as f32
);
println!(
"total: {} chunks ({:.2} s of audio)",
probs.len(),
audio_secs
);
if path.ends_with("asr_silence_heavy.wav") {
// Synthetic layout: 1 s silence + 4 s speech + 5 s silence.
// Expect ~40 % speech.
let speech_pct = speech_count as f32 / probs.len() as f32;
if (0.30..0.55).contains(&speech_pct) {
println!("PASS: discrimination matches 50/50 synthetic layout");
} else {
println!(
"WARN: expected ~40% speech, got {:.1}% (model may be miscalibrated)",
speech_pct * 100.0
);
}
} else if path.ends_with("asr_test.flac") {
// LibriSpeech is mostly speech. Expect ≥ 80 % speech.
let speech_pct = speech_count as f32 / probs.len() as f32;
if speech_pct > 0.80 {
println!("PASS: LibriSpeech sample classified mostly as speech");
} else {
println!(
"WARN: expected > 80% speech, got {:.1}%",
speech_pct * 100.0
);
}
}
Ok(())
}