cjpais/Handy · error · anyhow::Error

Timed out waiting for live transcription to finalize

Error message

Timed out waiting {:?} for live transcription to finalize

What it means

finalize_stream sends StreamCmd::Finalize to the streaming worker and blocks on reply_rx.recv_timeout for STREAM_FINALIZE_REPLY_TIMEOUT (30s). A Timeout means the worker did not reply in time — typically because flushing the accumulated stream and running final inference took longer than 30s. On timeout the code clears stream_active and returns this error; the worker may still hold the engine, so callers must surface the error rather than immediately starting a batch fallback.

Solutions

  1. Retry the recording — the timed-out worker eventually finishes or exits and the engine state recovers on subsequent loads
  2. Shorten live sessions or check the stream perf logs (logged every STREAM_PERF_LOG_INTERVAL = 5s) to see how far behind the worker is
  3. Switch the accelerator setting (GPU to CPU or back) or use a lighter model if finalize is consistently slow
  4. Rebuild with a larger STREAM_FINALIZE_REPLY_TIMEOUT if long sessions are the norm
  5. Check system logs for GPU driver resets if the timeout correlates with display issues

Example fix

// before: 30s hard limit
const STREAM_FINALIZE_REPLY_TIMEOUT: Duration = Duration::from_secs(30);

// after: allow long sessions to finalize
const STREAM_FINALIZE_REPLY_TIMEOUT: Duration = Duration::from_secs(120);
Defensive patterns

Strategy: fallback

Try / catch

match manager.finalize_stream() {
    Ok(Some(text)) => insert_text(text),
    Ok(None) => { /* no stream was active: batch-transcribe the buffer */ }
    Err(e) if e.to_string().contains("Timed out waiting") => {
        // worker may still hold the engine — surface to the user, do NOT
        // immediately batch-fallback (per finalize_stream's contract)
        notify_user_timeout();
    }
    Err(e) => notify_user(e),
}

Prevention

When it happens

Trigger: User stops a long live-transcription session and the streaming model's finalize inference exceeds 30s: very long buffered audio, CPU starvation (heavy foreground app), GPU hang/stall in the ONNX provider, or the worker thread busy draining a large Feed backlog before the FIFO-ordered Finalize command is even reached.

Common situations: Long dictation sessions on slow hardware; system under load (builds, games); GPU driver reset mid-inference; default quantization of the streaming model being too heavy for the machine.

Understand the failure class

Related errors


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

Appendix: source

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

    /// `Ok(None)` means no usable stream was active and the caller may fall back
    /// to batch transcription. `Err` means finalize itself failed or timed out.
    /// A timeout may still leave the worker holding the engine, so callers
    /// should surface it instead of immediately starting a batch fallback.
    pub fn finalize_stream(&self) -> Result<Option<String>> {
        let Some(tx) = self.router.take() else {
            return Ok(None);
        };
        let (reply_tx, reply_rx) = mpsc::channel();
        if tx.send(StreamCmd::Finalize(reply_tx)).is_err() {
            return Ok(None);
        }
        let finalized = match reply_rx.recv_timeout(STREAM_FINALIZE_REPLY_TIMEOUT) {
            Ok(Some(finalized)) => finalized,
            Ok(None) => return Ok(None),
            Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(None),
            Err(mpsc::RecvTimeoutError::Timeout) => {
                self.stream_active.store(false, Ordering::Release);
                return Err(anyhow::anyhow!(
                    "Timed out waiting {:?} for live transcription to finalize",
                    STREAM_FINALIZE_REPLY_TIMEOUT
                ));
            }
        };

        let settings = get_settings(&self.app_handle);
        // Streaming models do not receive a decode prompt, so custom words
        // always go through the shared fuzzy post-correction path.
        let filtered = post_process_transcription_text(
            finalized.text,
            &settings,
            false,
            &finalized.output_language,
            &finalized.supported_languages,
        );

        self.maybe_unload_immediately("streaming transcription");

View on GitHub (pinned to ba10ce1943)