cjpais/Handy · error · anyhow::Error

Model failed to load after auto-load attempt. Please check…

Error message

Model failed to load after auto-load attempt. Please check your model settings.

What it means

Inside the transcription block, engine_guard.take() moves the engine out of the mutex before the lock is dropped for the inference call. If take() returns None — engine present at the earlier check (line 1181) but gone by now — this error fires. It is a race guard, not a literal auto-load failure: between the two lock acquisitions another thread switched or unloaded the model, or a prior panicking transcription left the slot empty.

Solutions

  1. Retry the transcription after the model switch/unload completes — the new model loads and takes the slot
  2. Avoid issuing transcribe calls while changing models (serialize load/unload and transcribe in calling code)
  3. If it recurs without switching, look for a preceding 'Transcription engine panicked' error (the panic path leaves the slot empty)
  4. Check that only one component (overlay vs CLI vs tests) drives the manager at a time
Defensive patterns

Strategy: validation

Validate before calling

// serialize model lifecycle changes against transcribe()
let _guard = manager.lifecycle_lock(); // mutual-excludes load/unload/switch
let text = manager.transcribe(audio)?;

Type guard

fn engine_resident(m: &TranscriptionManager) -> bool {
    m.lock_engine().is_some() // non-generic check right before take()
}

Try / catch

match manager.transcribe(audio) {
    Err(e) if e.to_string().starts_with("Model failed to load after auto-load attempt") => {
        // transient race: wait for the switch to settle, then retry once
        manager.wait_until_model_settled();
        manager.transcribe(audio)
    }
    other => other,
}

Prevention

When it happens

Trigger: Model switched or unloaded concurrently with transcribe() (settings change, --transcribe-file with a different --model, idle-unload timer firing in the window); a previous engine panic removed the engine and the state checks raced.

Common situations: Recording while changing the selected model in the UI; CLI file-transcription with an explicit --model racing a live recording; test harnesses driving transcribe() and load/unload from parallel threads.

Related errors


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

Appendix: source

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

        // Non-whisper archs (e.g. Voxtral Small) can advertise
        // Feature::InitialPrompt yet reject the whisper-kind run extension
        // with INVALID_ARG, so the whisper extension must be gated on the
        // arch, not on the feature (see #1601).
        let mut model_is_whisper = false;

        // Perform transcription with the appropriate engine.
        // We use catch_unwind to prevent engine panics from poisoning the mutex,
        // which would make the app hang indefinitely on subsequent operations.
        let (result, output_language, model_languages) = {
            let mut engine_guard = self.lock_engine();

            // Take the engine out so we own it during transcription.
            // If the engine panics, we simply don't put it back (effectively unloading it)
            // instead of poisoning the mutex.
            let mut engine = match engine_guard.take() {
                Some(e) => e,
                None => {
                    return Err(anyhow::anyhow!(
                        "Model failed to load after auto-load attempt. Please check your model settings."
                    ));
                }
            };

            // Release the lock before transcribing — no mutex held during the engine call
            drop(engine_guard);

            // Probe live transcribe-cpp capabilities once (cheap GGUF-metadata
            // reads); the loaded session is the source of truth, not the
            // ModelManager copy. The whisper run extension is kind-tagged, so
            // non-whisper archs (parakeet, voxtral, …) reject it with
            // INVALID_ARG; attach it — and translate — only where supported.
            let mut model_supports_translate = false;
            let mut model_languages = self
                .model_manager
                .get_model_info(&active_model)
                .map(|info| info.supported_languages)

View on GitHub (pinned to ba10ce1943)