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 thread and blocks on reply_rx.recv_timeout(STREAM_FINALIZE_REPLY_TIMEOUT) (30s). A Timeout means the worker accepted the command but did not reply in time — typically it is still draining queued Feed commands or is stuck inside stream.finalize() on very long buffered audio or a stalled GPU. Handy clears stream_active and returns this error; the doc comment warns the worker may still hold the engine, so callers must surface the error rather than immediately starting a batch fallback.

Source

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

    /// `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 98a4d80cce)

Solutions

  1. Retry stopping/finalizing once after a short pause — the worker often completes and a second attempt succeeds on the re-established stream
  2. Shorten live sessions and stop promptly so the finalize backlog stays small
  3. Switch the accelerator (or model) to one that keeps real-time factor comfortably below 1
  4. Update GPU drivers / check for Vulkan-Metal stalls (on Linux, VK_LOADER_DEBUG=error reveals loader faults)
  5. As a maintainer: raise STREAM_FINALIZE_REPLY_TIMEOUT or drain feeds before finalizing

Example fix

// before
let text = tm.finalize_stream()?; // single shot, 30s cap

// after — retry once, and never batch-fallback immediately on timeout
let text = match tm.finalize_stream() {
    Ok(Some(t)) => t,
    Ok(None) => tm.transcribe(audio)?, // no live stream: batch fallback is safe
    Err(e) if e.to_string().contains("Timed out") => {
        thread::sleep(Duration::from_secs(2));
        tm.finalize_stream()?.unwrap_or_default() // retry; surface if it fails again
    }
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Cheap pre-checks: only finalize when a stream router exists and is active
if !tm.stream_is_active() { return batch_fallback(); } // no live stream — skip the handshake

Try / catch

match tm.finalize_stream() {
    Ok(Some(text)) => text,
    Ok(None) => tm.transcribe(audio)?, // no usable stream — batch fallback is safe here
    Err(e) if e.to_string().contains("Timed out") => {
        // Worker may still hold the engine: surface to the user, retry once, do NOT batch-fallback immediately
        user_notify("Finalizing is taking longer than expected…");
        tm.finalize_stream()?.unwrap_or_default()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: User stops a long live-transcription session whose queued PCM backlog plus stream.finalize() compute exceeds 30s; stream.finalize() hangs on a GPU backend (Vulkan/Metal device lost, driver stall); the worker thread is starved by CPU contention while processing the Feed queue ahead of the Finalize command.

Common situations: Multi-minute dictations on CPU-only or low-end machines; GPU driver crash/hang mid-session; heavy parallel system load during finalize; very large feed backlog accumulated because feeds outpaced inference.

Understand the failure class

Related errors


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