diff --git a/crates/buzz-relay/src/api/transcribe.rs b/crates/buzz-relay/src/api/transcribe.rs index 954c24607..0c7716278 100644 --- a/crates/buzz-relay/src/api/transcribe.rs +++ b/crates/buzz-relay/src/api/transcribe.rs @@ -189,18 +189,18 @@ async fn can_mint_session( fn build_session_payload(model: &str) -> Value { let uses_manual_commit = model.contains("realtime-whisper"); - let mut audio_input = serde_json::json!({ - "transcription": { - "model": model, - } - }); - - if !uses_manual_commit { - audio_input.as_object_mut().unwrap().insert( - "turn_detection".to_string(), - serde_json::json!({ "type": "server_vad" }), - ); - } + let audio_input = if uses_manual_commit { + // Manual commit mode — omit turn_detection entirely. + serde_json::json!({ + "transcription": { "model": model } + }) + } else { + // Server VAD mode — include turn_detection. + serde_json::json!({ + "transcription": { "model": model }, + "turn_detection": { "type": "server_vad" } + }) + }; serde_json::json!({ "session": { diff --git a/desktop/src/features/dictation/hooks/useRealtimeDictation.ts b/desktop/src/features/dictation/hooks/useRealtimeDictation.ts index 1b40953b1..b56c8eb9a 100644 --- a/desktop/src/features/dictation/hooks/useRealtimeDictation.ts +++ b/desktop/src/features/dictation/hooks/useRealtimeDictation.ts @@ -102,6 +102,11 @@ export function useRealtimeDictation({ if (needsCommit && dc) { commitAudioBuffer(dc); + // Mark the run stale immediately so late transcript events from the + // data channel are rejected by handleRealtimeEvent. This prevents + // transcripts from writing back into the composer after a send, + // edit-save, or navigation that triggered this cleanup. + activeRunIdRef.current += 1; // Stop the mic immediately so no new audio is sent after commit. for (const track of streamRef.current?.getTracks() ?? []) { track.stop(); @@ -109,16 +114,12 @@ export function useRealtimeDictation({ streamRef.current = null; audioCaptureRef.current?.close(); audioCaptureRef.current = null; - // Delay full teardown to allow the final transcript to arrive. + // Delay WebRTC teardown briefly for clean protocol shutdown (the + // commit message needs to reach OpenAI before the channel closes). const pc = peerConnectionRef.current; peerConnectionRef.current = null; dataChannelRef.current = null; - const runId = activeRunIdRef.current; setTimeout(() => { - // Only tear down if no new run started in the meantime. - if (activeRunIdRef.current === runId) { - activeRunIdRef.current += 1; - } dc.close(); pc?.close(); }, 3000);