diff --git a/desktop/src-tauri/src/dictation.rs b/desktop/src-tauri/src/dictation.rs index b5351caa4..b7d6b4b52 100644 --- a/desktop/src-tauri/src/dictation.rs +++ b/desktop/src-tauri/src/dictation.rs @@ -79,6 +79,7 @@ pub async fn start_dictation(state: State<'_, AppState>) -> Result tts_active: None, tts_cancel: None, ptt_active: None, + flush_on_shutdown: true, }; let (engine, text_rx) = SttEngine::new(config)?; diff --git a/desktop/src-tauri/src/huddle/stt.rs b/desktop/src-tauri/src/huddle/stt.rs index de2be1031..e91c0e68e 100644 --- a/desktop/src-tauri/src/huddle/stt.rs +++ b/desktop/src-tauri/src/huddle/stt.rs @@ -71,6 +71,7 @@ impl SttPipeline { tts_active: Some(tts_active), tts_cancel, ptt_active, + flush_on_shutdown: false, }; let (engine, text_rx) = SttEngine::new(config)?; diff --git a/desktop/src-tauri/src/stt_engine.rs b/desktop/src-tauri/src/stt_engine.rs index eefe950fa..02dba7b9a 100644 --- a/desktop/src-tauri/src/stt_engine.rs +++ b/desktop/src-tauri/src/stt_engine.rs @@ -72,6 +72,15 @@ pub struct SttEngineConfig { /// Optional: push-to-talk flag — when `Some`, speech is only accumulated /// while the flag is true. pub ptt_active: Option>, + /// When `true`, the worker drains any audio still queued at shutdown and + /// transcribes the trailing speech buffer before exiting. Dictation needs + /// this so the last words the user spoke before releasing the key still + /// land in the transcript. Set to `false` for huddle: leaving a huddle (or + /// disabling transcription) calls `shutdown()` then drops the pipeline, + /// whose `Drop` joins this worker — running the full drain + final decode + /// there would block huddle teardown, and the huddle generation guard + /// discards any transcript produced during teardown anyway. + pub flush_on_shutdown: bool, } // ── Public engine handle ────────────────────────────────────────────────────── @@ -265,6 +274,7 @@ fn stt_worker( // ── 5. Main loop ────────────────────────────────────────────────────────── let has_tts = config.tts_active.is_some(); + let flush_on_shutdown = config.flush_on_shutdown; let tts_active_flag = config .tts_active .unwrap_or_else(|| Arc::new(AtomicBool::new(false))); @@ -351,6 +361,15 @@ fn stt_worker( // enqueued batch, so the tail of speech would never reach `speech_buf` and // the final flush below would only transcribe older audio — dropping the // last words. Process everything still in the channel before flushing. + // + // Only dictation opts into this (`flush_on_shutdown`). Huddle shutdown must + // stay fast: its `Drop` joins this worker, so draining the queue + decoding + // the final buffer here would block huddle teardown, and any transcript + // produced during teardown is discarded by the huddle generation guard. + if !flush_on_shutdown { + return; + } + while let Ok(bytes) = audio_rx.try_recv() { let samples_48k = bytes_to_f32(&bytes); input_buf_48k.extend_from_slice(&samples_48k); diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index d52a81788..688ae69aa 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -621,11 +621,32 @@ export function AppShell() { } } + // If the WebView loses focus (Cmd-Tab, clicking another app, an OS focus + // change) or is hidden before ⌘D is released, the keyup never fires and the + // mic would keep recording. Release the push-to-talk key on blur/hide so + // the recording stops instead of running unattended. + function releaseDictationKey() { + if (dictationKeyHeld) { + dictationKeyHeld = false; + window.dispatchEvent(new CustomEvent("buzz:dictation-key-up")); + } + } + + function handleVisibilityChange() { + if (document.hidden) { + releaseDictationKey(); + } + } + window.addEventListener("keydown", handleKeyDown); window.addEventListener("keyup", handleKeyUp); + window.addEventListener("blur", releaseDictationKey); + document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.removeEventListener("keydown", handleKeyDown); window.removeEventListener("keyup", handleKeyUp); + window.removeEventListener("blur", releaseDictationKey); + document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [ handleOpenBrowseChannels,