@@ -4,6 +4,7 @@ use crate::managers::history::HistoryManager;
44use crate :: managers:: transcription:: TranscriptionManager ;
55use crate :: settings:: { get_settings, AppSettings } ;
66use crate :: shortcut;
7+ use crate :: streaming:: StreamingManager ;
78use crate :: tray:: { change_tray_icon, TrayIconState } ;
89use crate :: utils:: { self , show_recording_overlay, show_transcribing_overlay} ;
910use async_openai:: types:: {
@@ -219,12 +220,21 @@ impl ShortcutAction for TranscribeAction {
219220 show_recording_overlay ( app) ;
220221
221222 let rm = app. state :: < Arc < AudioRecordingManager > > ( ) ;
223+ let sm = app. state :: < Arc < StreamingManager > > ( ) ;
222224
223225 // Get the microphone mode to determine audio feedback timing
224226 let settings = get_settings ( app) ;
225227 let is_always_on = settings. always_on_microphone ;
226228 debug ! ( "Microphone mode - always_on: {}" , is_always_on) ;
227229
230+ // Set up streaming VAD callback BEFORE starting recording
231+ // This avoids recreating the microphone stream after recording starts
232+ let streaming_enabled = sm. is_streaming_enabled ( ) ;
233+ if streaming_enabled {
234+ sm. setup_vad_callback ( ) ;
235+ debug ! ( "Streaming VAD callback set up before recording" ) ;
236+ }
237+
228238 let mut recording_started = false ;
229239 if is_always_on {
230240 // Always-on mode: Play audio feedback immediately, then apply mute after sound finishes
@@ -267,6 +277,12 @@ impl ShortcutAction for TranscribeAction {
267277 if recording_started {
268278 // Dynamically register the cancel shortcut in a separate task to avoid deadlock
269279 shortcut:: register_cancel_shortcut ( app) ;
280+
281+ // Start streaming session if enabled (VAD callback already set up above)
282+ if streaming_enabled {
283+ sm. start_controller ( ) ;
284+ debug ! ( "Streaming session started" ) ;
285+ }
270286 }
271287
272288 debug ! (
@@ -286,6 +302,10 @@ impl ShortcutAction for TranscribeAction {
286302 let rm = Arc :: clone ( & app. state :: < Arc < AudioRecordingManager > > ( ) ) ;
287303 let tm = Arc :: clone ( & app. state :: < Arc < TranscriptionManager > > ( ) ) ;
288304 let hm = Arc :: clone ( & app. state :: < Arc < HistoryManager > > ( ) ) ;
305+ let sm = Arc :: clone ( & app. state :: < Arc < StreamingManager > > ( ) ) ;
306+
307+ // Check if streaming was active
308+ let streaming_was_active = sm. is_session_active ( ) ;
289309
290310 change_tray_icon ( app, TrayIconState :: Transcribing ) ;
291311 show_transcribing_overlay ( app) ;
@@ -313,34 +333,62 @@ impl ShortcutAction for TranscribeAction {
313333 samples. len( )
314334 ) ;
315335
316- let transcription_time = Instant :: now ( ) ;
336+ // If streaming was active, get the final text from streaming
337+ // (which does its own final transcription)
338+ let ( transcription, skip_paste) = if streaming_was_active {
339+ let samples_for_streaming = samples. clone ( ) ;
340+ let streaming_text = sm. stop_session ( Some ( samples_for_streaming) ) ;
341+ match streaming_text {
342+ Some ( text) if !text. is_empty ( ) => {
343+ debug ! ( "Streaming session final text: '{}'" , text) ;
344+ // Streaming already pasted incrementally, but we may need to
345+ // replace with post-processed version
346+ ( Ok ( text) , true )
347+ }
348+ _ => {
349+ // Streaming didn't produce output, fall back to normal transcription
350+ debug ! ( "Streaming produced no output, falling back to batch transcription" ) ;
351+ let transcription_time = Instant :: now ( ) ;
352+ let result = tm. transcribe ( samples. clone ( ) ) ;
353+ debug ! ( "Batch transcription completed in {:?}" , transcription_time. elapsed( ) ) ;
354+ ( result, false )
355+ }
356+ }
357+ } else {
358+ // Normal (non-streaming) mode
359+ let transcription_time = Instant :: now ( ) ;
360+ let result = tm. transcribe ( samples. clone ( ) ) ;
361+ debug ! ( "Transcription completed in {:?}" , transcription_time. elapsed( ) ) ;
362+ ( result, false )
363+ } ;
364+
317365 let samples_clone = samples. clone ( ) ; // Clone for history saving
318- match tm. transcribe ( samples) {
366+
367+ match transcription {
319368 Ok ( transcription) => {
320- debug ! (
321- "Transcription completed in {:?}: '{}'" ,
322- transcription_time. elapsed( ) ,
323- transcription
324- ) ;
369+ debug ! ( "Final transcription: '{}'" , transcription) ;
325370 if !transcription. is_empty ( ) {
326371 let settings = get_settings ( & ah) ;
327372 let mut final_text = transcription. clone ( ) ;
328373 let mut post_processed_text: Option < String > = None ;
329374 let mut post_process_prompt: Option < String > = None ;
375+ let mut needs_replacement = false ;
330376
331377 // First, check if Chinese variant conversion is needed
332378 if let Some ( converted_text) =
333379 maybe_convert_chinese_variant ( & settings, & transcription) . await
334380 {
335381 final_text = converted_text. clone ( ) ;
336382 post_processed_text = Some ( converted_text) ;
383+ needs_replacement = skip_paste; // Need to replace streaming output
337384 }
338385 // Then apply regular post-processing if enabled
339386 else if let Some ( processed_text) =
340387 maybe_post_process_transcription ( & settings, & transcription) . await
341388 {
342389 final_text = processed_text. clone ( ) ;
343390 post_processed_text = Some ( processed_text) ;
391+ needs_replacement = skip_paste; // Need to replace streaming output
344392
345393 // Get the prompt that was used
346394 if let Some ( prompt_id) = & settings. post_process_selected_prompt_id {
@@ -381,26 +429,55 @@ impl ShortcutAction for TranscribeAction {
381429 }
382430 } ) ;
383431
384- // Paste the final text (either processed or original)
385- let ah_clone = ah. clone ( ) ;
386- let paste_time = Instant :: now ( ) ;
387- ah. run_on_main_thread ( move || {
388- match utils:: paste ( final_text, ah_clone. clone ( ) ) {
389- Ok ( ( ) ) => debug ! (
390- "Text pasted successfully in {:?}" ,
391- paste_time. elapsed( )
392- ) ,
393- Err ( e) => error ! ( "Failed to paste transcription: {}" , e) ,
394- }
395- // Hide the overlay after transcription is complete
396- utils:: hide_recording_overlay ( & ah_clone) ;
397- change_tray_icon ( & ah_clone, TrayIconState :: Idle ) ;
398- } )
399- . unwrap_or_else ( |e| {
400- error ! ( "Failed to run paste on main thread: {:?}" , e) ;
432+ // Paste the final text
433+ // - If streaming was active and post-processing changed the text,
434+ // we need to replace what was streamed
435+ // - If streaming was active but no post-processing, skip pasting
436+ // (text already output)
437+ // - If not streaming, paste normally
438+ let should_paste = !skip_paste || needs_replacement;
439+
440+ if should_paste {
441+ let ah_clone = ah. clone ( ) ;
442+ let paste_time = Instant :: now ( ) ;
443+
444+ // If replacing streaming output, we need to delete old text first
445+ let chars_to_delete = if needs_replacement {
446+ transcription. chars ( ) . count ( )
447+ } else {
448+ 0
449+ } ;
450+
451+ ah. run_on_main_thread ( move || {
452+ // Delete streaming output if we're replacing
453+ if chars_to_delete > 0 {
454+ debug ! ( "Replacing streaming output ({} chars) with post-processed text" , chars_to_delete) ;
455+ if let Err ( e) = utils:: delete_chars ( chars_to_delete) {
456+ error ! ( "Failed to delete streaming output: {}" , e) ;
457+ }
458+ }
459+
460+ match utils:: paste ( final_text, ah_clone. clone ( ) ) {
461+ Ok ( ( ) ) => debug ! (
462+ "Text pasted successfully in {:?}" ,
463+ paste_time. elapsed( )
464+ ) ,
465+ Err ( e) => error ! ( "Failed to paste transcription: {}" , e) ,
466+ }
467+ // Hide the overlay after transcription is complete
468+ utils:: hide_recording_overlay ( & ah_clone) ;
469+ change_tray_icon ( & ah_clone, TrayIconState :: Idle ) ;
470+ } )
471+ . unwrap_or_else ( |e| {
472+ error ! ( "Failed to run paste on main thread: {:?}" , e) ;
473+ utils:: hide_recording_overlay ( & ah) ;
474+ change_tray_icon ( & ah, TrayIconState :: Idle ) ;
475+ } ) ;
476+ } else {
477+ // Streaming output is already there, just clean up
401478 utils:: hide_recording_overlay ( & ah) ;
402479 change_tray_icon ( & ah, TrayIconState :: Idle ) ;
403- } ) ;
480+ }
404481 } else {
405482 utils:: hide_recording_overlay ( & ah) ;
406483 change_tray_icon ( & ah, TrayIconState :: Idle ) ;
@@ -414,6 +491,10 @@ impl ShortcutAction for TranscribeAction {
414491 }
415492 } else {
416493 debug ! ( "No samples retrieved from recording stop" ) ;
494+ // Also stop streaming session if it was active
495+ if streaming_was_active {
496+ sm. stop_session ( None ) ;
497+ }
417498 utils:: hide_recording_overlay ( & ah) ;
418499 change_tray_icon ( & ah, TrayIconState :: Idle ) ;
419500 }
0 commit comments