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]>
121 lines
4.0 KiB
Rust
121 lines
4.0 KiB
Rust
//! 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(())
|
||
}
|