cjpais/Handy · critical · anyhow::Error

Failed to create VAD

Error message

Failed to create VAD: {e}

What it means

Wraps a failure of vad_rs::Vad::new() to load the Silero VAD ONNX model from the given path at 16 kHz (constants::WHISPER_SAMPLE_RATE). It fails when the model file does not exist, is not valid ONNX, or the ONNX runtime cannot create a session for it. The threshold is validated before this call, so this error is purely about the model file itself.

Solutions

  1. Download the Silero model as documented: mkdir -p src-tauri/resources/models && curl -o src-tauri/resources/models/silero_vad_v4.onnx https://blob.handy.computer/silero_vad_v4.onnx
  2. Verify the file is a complete ONNX binary (size > 1 MB, not an HTML error page from a failed curl).
  3. Confirm the path passed to SileroVad::new resolves at runtime (resource dir in dev vs. bundled prod) and that tauri.conf.json includes it as a resource.
  4. Re-download if the file is corrupt; a bad ONNX will fail deterministically.

Example fix

// before
let vad = SileroVad::new(model_path, threshold)?;

// after — fail with an actionable message before hitting the ONNX loader
if !model_path.as_ref().exists() {
    anyhow::bail!(
        "Silero VAD model missing at {} — run the documented setup download",
        model_path.as_ref().display()
    );
}
let vad = SileroVad::new(model_path, threshold)?;
Defensive patterns

Strategy: validation

Validate before calling

// Before constructing the VAD, assert the model is present and non-trivial
let p = resource_dir.join("models/silero_vad_v4.onnx");
let meta = std::fs::metadata(&p)?;
if meta.len() < 1_000_000 {
    anyhow::bail!("Silero model at {} looks truncated ({} bytes)", p.display(), meta.len());
}
let vad = SileroVad::new(&p, threshold)?;

Try / catch

match SileroVad::new(&path, threshold) {
    Ok(v) => v,
    Err(e) => {
        error!("VAD init failed ({e}); run the setup download before starting the app");
        std::process::exit(1); // startup-critical resource missing
    }
}

Prevention

When it happens

Trigger: Constructing SileroVad::new(model_path, threshold) when silero_vad_v4.onnx is missing from src-tauri/resources/models/ (dev setup step not run), the file is truncated/corrupt (partial copy), or the path points at a non-ONNX file.

Common situations: Fresh clone without running the 'curl -o src-tauri/resources/models/silero_vad_v4.onnx' setup command; CI builds where resources are not fetched; antivirus/quarantine modifying the .onnx file; bundling the app without including the resource.

Related errors


AI-assisted analysis of cjpais/Handy@c6fa60da2f (2026-08-17). Data as JSON: /api/errors/601ac44cd569c1b9. Report an issue: GitHub.

Appendix: source

Thrown at src-tauri/src/audio_toolkit/vad/silero.rs:26

const SILERO_FRAME_MS: u32 = 30;
const SILERO_FRAME_SAMPLES: usize =
    (constants::WHISPER_SAMPLE_RATE * SILERO_FRAME_MS / 1000) as usize;

pub struct SileroVad {
    engine: Vad,
    threshold: f32,
}

impl SileroVad {
    pub fn new<P: AsRef<Path>>(model_path: P, threshold: f32) -> Result<Self> {
        if !(0.0..=1.0).contains(&threshold) {
            anyhow::bail!("threshold must be between 0.0 and 1.0");
        }

        Ok(Self {
            engine: Vad::new(&model_path, constants::WHISPER_SAMPLE_RATE as usize)
                .map_err(|e| anyhow::anyhow!("Failed to create VAD: {e}"))?,
            threshold,
        })
    }
}

impl VoiceActivityDetector for SileroVad {
    fn push_frame<'a>(&'a mut self, frame: &'a [f32]) -> Result<VadFrame<'a>> {
        if frame.len() != SILERO_FRAME_SAMPLES {
            anyhow::bail!(
                "expected {SILERO_FRAME_SAMPLES} samples, got {}",
                frame.len()
            );
        }

        let result = self
            .engine
            .compute(frame)
            .map_err(|e| anyhow::anyhow!("Silero VAD error: {e}"))?;

View on GitHub (pinned to c6fa60da2f)