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
- Retry stopping/finalizing once after a short pause — the worker often completes and a second attempt succeeds on the re-established stream
- Shorten live sessions and stop promptly so the finalize backlog stays small
- Switch the accelerator (or model) to one that keeps real-time factor comfortably below 1
- Update GPU drivers / check for Vulkan-Metal stalls (on Linux, VK_LOADER_DEBUG=error reveals loader faults)
- 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
- Stop live sessions promptly instead of letting multi-minute backlogs accumulate
- Pick an accelerator/model whose real-time factor stays well below 1 for streaming
- Keep GPU drivers current; on Linux use VK_LOADER_DEBUG=error to catch Vulkan stalls early
- Watch stream perf logs (feed/compute timings) and end sessions when compute lags feed persistently
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
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- Failed to load moonshine streaming model {}: {}
- Moonshine streaming transcription failed: {}
- threshold must be between 0.0 and 1.0
- Failed to create VAD: {e}
- Failed to create SileroVad: {}
AI-assisted analysis of cjpais/Handy@98a4d80cce (2026-08-16).
Data as JSON: /api/errors/ec8bc7d2a0202039.
Report an issue: GitHub.