Zackriya-Solutions/meetily · error · anyhow::Error

VAD task panicked: {}

Error message

VAD task panicked: {}

What it means

The .await on tokio::task::spawn_blocking returned a JoinError, which means the thread that runs get_speech_chunks_with_progress panicked, or the Tokio runtime shut down and aborted the blocking task before it finished. The '{}' holds the panic payload text. This is not the VAD error itself: a VAD failure returns normally and produces 'VAD processing failed' at the second map_err (retranscription.rs:263).

Source

Thrown at frontend/src-tauri/src/audio/retranscription.rs:262

            VAD_REDEMPTION_TIME_MS,
            |vad_progress, segments_found| {
                // Map VAD progress (0-100) to overall progress (20-25)
                let overall_progress = 20 + (vad_progress as f32 * 0.05) as u32;
                emit_progress(
                    &app_for_vad,
                    &meeting_id_for_vad,
                    "vad",
                    overall_progress,
                    &format!("Detecting speech segments... {}% ({} found)", vad_progress, segments_found),
                );

                // Return false to cancel if cancellation requested
                !RETRANSCRIPTION_CANCELLED.load(Ordering::SeqCst)
            },
        )
    })
    .await
    .map_err(|e| anyhow!("VAD task panicked: {}", e))?
    .map_err(|e| anyhow!("VAD processing failed: {}", e))?;

    let total_segments = speech_segments.len();
    info!("VAD detected {} speech segments (redemption_time={}ms)", total_segments, VAD_REDEMPTION_TIME_MS);

    // Diagnostic: log segment duration distribution
    if !speech_segments.is_empty() {
        let durations_ms: Vec<f64> = speech_segments.iter()
            .map(|s| s.end_timestamp_ms - s.start_timestamp_ms)
            .collect();
        let total_speech_ms: f64 = durations_ms.iter().sum();
        let avg_duration = total_speech_ms / durations_ms.len() as f64;
        let min_duration = durations_ms.iter().cloned().fold(f64::INFINITY, f64::min);
        let max_duration = durations_ms.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
        info!(
            "VAD segment stats: avg={:.0}ms, min={:.0}ms, max={:.0}ms, total_speech={:.1}s/{:.1}s ({:.0}%)",
            avg_duration, min_duration, max_duration,
            total_speech_ms / 1000.0, duration_seconds,

View on GitHub (pinned to 0281737d87)

Solutions

  1. Re-run with RUST_LOG=debug and read the panic message in '{}' plus the stderr backtrace to find the exact panicking line in audio/vad.rs.
  2. If the panic occurs on app exit, call cancel_retranscription and wait until is_retranscription_in_progress() returns false before shutting the runtime down.
  3. Sanitize decoded samples before VAD: replace NaN/inf f32 values with 0.0 right after decoded.to_whisper_format().
  4. Fix the panicking invariant in the VAD processor (or wrap its body in catch_unwind and convert the panic into a Result error).

Example fix

// before
let audio_samples = tokio::task::spawn_blocking(move || {
    decoded.to_whisper_format()
}).await?;

// after: reject non-finite samples before they reach VAD
let audio_samples = tokio::task::spawn_blocking(move || {
    let mut s = decoded.to_whisper_format()?;
    if s.iter().any(|x| !x.is_finite()) {
        s.iter_mut().for_each(|x| {
            if !x.is_finite() { *x = 0.0; }
        });
    }
    Ok(s)
}).await??;
Defensive patterns

Strategy: try-catch

Validate before calling

// Rust: reject non-finite samples before handing audio to VAD
fn samples_are_finite(samples: &[f32]) -> bool {
    samples.iter().all(|x| x.is_finite())
}
// call before spawn_blocking:
// assert_or_bail!(samples_are_finite(&audio_samples), "audio contains non-finite samples");

Try / catch

// Distinguish JoinError (panic/abort) from the VAD Result
match tokio::task::spawn_blocking(move || get_speech_chunks_with_progress(...)).await {
    Ok(Ok(segments)) => { /* continue */ }
    Ok(Err(vad_err)) => log::warn!("VAD failed: {vad_err}"),
    Err(join_err) if join_err.is_panic() => log::error!("VAD panicked: {join_err}"),
    Err(_) => log::error!("VAD task aborted (runtime shutdown)"),
}

Prevention

When it happens

Trigger: Calling start_retranscription on audio whose decoded samples trigger a panic inside ContinuousVadProcessor::process_audio (for example non-finite f32 samples or an unexpected chunk shape), a bug in the progress callback, or exiting the app while VAD is still scanning a 35+ minute file so the runtime drops the task.

Common situations: User quits the app during the 'Detecting speech segments' stage; corrupted or mis-decoded audio containing NaN/inf samples; a VAD refactor that breaks an invariant only on edge-case audio lengths.

Related errors


AI-assisted analysis of Zackriya-Solutions/meetily@0281737d87 (2026-08-16). Data as JSON: /api/errors/083db86ecd5a19b7. Report an issue: GitHub.