cjpais/Handy · critical · anyhow::Error

Transcription engine panicked: {}. The model has been unload

Error message

Transcription engine panicked: {}. The model has been unloaded and will reload on next attempt.

What it means

A panic inside the transcription engine call was caught by catch_unwind (used deliberately so a native crash cannot poison the manager's mutexes and hang the app). Handy recovers by NOT returning the engine (it is dropped, freeing native resources), clearing current_model_id, emitting a model-state-changed/unloaded event, and returning this error; the next transcription attempt re-loads the model automatically. The panic message identifies the native fault — typically a bug in the GGML/ONNX engine layer or bad state such as NaN audio reaching native code.

Source

Thrown at src-tauri/src/managers/transcription.rs:1444

                    {
                        let mut current_model = self
                            .current_model_id
                            .lock()
                            .unwrap_or_else(|e| e.into_inner());
                        *current_model = None;
                    }

                    let _ = self.app_handle.emit(
                        "model-state-changed",
                        ModelStateEvent {
                            event_type: "unloaded".to_string(),
                            model_id: None,
                            model_name: None,
                            error: Some(format!("Engine panicked: {}", panic_msg)),
                        },
                    );

                    return Err(anyhow::anyhow!(
                        "Transcription engine panicked: {}. The model has been unloaded and will reload on next attempt.",
                        panic_msg
                    ));
                }
            };

            let output_language = with_model_detected_language(
                resolve_output_language_evidence(
                    &settings,
                    applied_language_hint.as_deref(),
                    &model_languages,
                    output_was_translated,
                ),
                model_detected_language,
            );
            debug!("Output language evidence: {:?}", output_language);

            (text, output_language, model_languages)

View on GitHub (pinned to 98a4d80cce)

Solutions

  1. Simply retry — the model reloads on the next attempt and the panic is often non-deterministic
  2. Note the panic message; if it names a specific engine/backend, switch model or accelerator (e.g. CPU) to avoid the faulty path
  3. Re-download the implicated model in case of corruption
  4. Update Handy — engine panics are treated as bugs and patched
  5. Report the panic message plus model/OS/backend to the issue tracker if reproducible

Example fix

// before — caller treats all errors alike
let text = tm.transcribe(audio)?;

// after — recognize the panic-recovery error, notify, and let the next attempt reload
let text = match tm.transcribe(audio) {
    Ok(t) => t,
    Err(e) if e.to_string().contains("engine panicked") => {
        notify_user("Transcription engine crashed; retrying with a fresh model load");
        tm.initiate_model_load(); // expedite the reload instead of waiting
        wait_for_loading_completion(&app_handle);
        tm.transcribe(audio)?
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Panics are native and cannot be pre-validated; only input hygiene helps
anyhow::ensure!(audio.iter().all(|s| s.is_finite()), "refusing to feed NaN/inf audio to native engine");
anyhow::ensure!(!audio.is_empty(), "empty audio");

Try / catch

match tm.transcribe(audio.clone()) {
    Err(e) if e.to_string().contains("engine panicked") => {
        // Model was intentionally unloaded; a fresh load happens on next attempt.
        // Notify, expedite the reload, retry once — then report if it repeats.
        notify("Engine crashed — reloading model");
        tm.initiate_model_load();
        wait_for_loading_completion(&app_handle);
        tm.transcribe(audio)
    }
    other => other,
}

Prevention

When it happens

Trigger: The closure around the per-engine transcribe call panics: index/bounds panic in native binding code, assertion failures in transcribe-cpp or transcribe-rs, malformed audio (NaN/inf samples) triggering library assumptions, or an engine/arch-specific bug on a particular backend.

Common situations: Specific model+accelerator combinations hitting a native bug (fixed in later releases); corrupted model files driving native code into bad state; exotic audio input (all-NaN buffers after resampling); GPU driver crashes surfacing as panics in bindings.

Related errors


AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16). Data as JSON: /api/errors/2a1efe602e3f5eab. Report an issue: GitHub.