@@ -12,7 +12,7 @@ use crate::utils::system_monitor;
1212use crate :: whisper:: cache:: TranscriberCache ;
1313use crate :: whisper:: languages:: validate_language;
1414use crate :: whisper:: manager:: WhisperManager ;
15- use crate :: { emit_to_window, update_recording_state, AppState , RecordingState } ;
15+ use crate :: { emit_to_window, update_recording_state, AppState , RecordingMode , RecordingState } ;
1616use cpal:: traits:: { DeviceTrait , HostTrait } ;
1717use serde_json;
1818use std:: panic:: { RefUnwindSafe , UnwindSafe } ;
@@ -434,6 +434,12 @@ pub async fn start_recording(
434434 ] ,
435435 ) ;
436436
437+ // If we're stuck in Error, recover to Idle before attempting a new start
438+ let current_state = crate :: get_recording_state ( & app) ;
439+ if matches ! ( current_state, crate :: RecordingState :: Error ) {
440+ crate :: update_recording_state ( & app, crate :: RecordingState :: Idle , Some ( "recover" . to_string ( ) ) ) ;
441+ }
442+
437443 // Validate all requirements upfront
438444 let validation_start = Instant :: now ( ) ;
439445 match validate_recording_requirements ( & app) . await {
@@ -464,6 +470,10 @@ pub async fn start_recording(
464470 // All validation passed, update state to starting
465471 log_state_transition ( "RECORDING" , "idle" , "starting" , true , None ) ;
466472 update_recording_state ( & app, RecordingState :: Starting , None ) ;
473+ // Ensure transition actually happened; if blocked, abort early
474+ if !matches ! ( crate :: get_recording_state( & app) , crate :: RecordingState :: Starting ) {
475+ return Err ( "Cannot start recording in current state" . to_string ( ) ) ;
476+ }
467477
468478 // Load recording config once to avoid repeated store access
469479 let config = get_recording_config ( & app) . await . map_err ( |e| {
@@ -923,6 +933,22 @@ pub async fn stop_recording(
923933 }
924934 } ;
925935
936+ // Fast-path: handle header-only/empty WAV files before normalization
937+ if let Ok ( meta) = std:: fs:: metadata ( & audio_path) {
938+ // A valid WAV header is typically 44 bytes; <= 44 implies no audio samples were written
939+ if meta. len ( ) <= 44 {
940+ let _ = emit_to_window ( & app, "pill" , "recording-too-short" , "No audio captured" ) ;
941+ if let Err ( e) = std:: fs:: remove_file ( & audio_path) {
942+ log:: debug!( "Failed to remove empty audio file: {}" , e) ;
943+ }
944+ if let Err ( e) = crate :: commands:: window:: hide_pill_widget ( app. clone ( ) ) . await {
945+ log:: error!( "Failed to hide pill window: {}" , e) ;
946+ }
947+ update_recording_state ( & app, RecordingState :: Idle , None ) ;
948+ return Ok ( "" . to_string ( ) ) ;
949+ }
950+ }
951+
926952
927953 // Normalize captured audio to Whisper contract (WAV PCM s16, mono, 16k)
928954 let parent_dir = audio_path
@@ -946,7 +972,22 @@ pub async fn stop_recording(
946972 log:: debug!( "Failed to remove raw audio: {}" , e) ;
947973 }
948974
949- // Duration gate (min 5s) using normalized file
975+ // Determine min duration based on recording mode (PTT vs Toggle) once
976+ let ( min_duration_s_f32, min_duration_s_i32) = {
977+ let app_state = app. state :: < AppState > ( ) ;
978+ let mode = app_state
979+ . recording_mode
980+ . lock ( )
981+ . ok ( )
982+ . map ( |g| * g)
983+ . unwrap_or ( RecordingMode :: Toggle ) ;
984+ match mode {
985+ RecordingMode :: PushToTalk => ( 1.0f32 , 1i32 ) ,
986+ RecordingMode :: Toggle => ( 3.0f32 , 3i32 ) ,
987+ }
988+ } ;
989+
990+ // Duration gate (mode-specific) using normalized file
950991 let too_short = ( || -> Result < bool , String > {
951992 let reader = hound:: WavReader :: open ( & normalized_path)
952993 . map_err ( |e| format ! ( "Failed to open normalized wav: {}" , e) ) ?;
@@ -960,12 +1001,17 @@ pub async fn stop_recording(
9601001 ( "bits" , & spec. bits_per_sample . to_string ( ) . as_str ( ) ) ,
9611002 ( "duration_s" , & format ! ( "{:.2}" , duration) . as_str ( ) ) ,
9621003 ] ) ;
963- Ok ( duration < 5.0 )
1004+ Ok ( duration < min_duration_s_f32 )
9641005 } ) ( ) ;
9651006
9661007 if let Ok ( true ) = too_short {
9671008 // Emit friendly feedback and stop here
968- let _ = emit_to_window ( & app, "pill" , "recording-too-short" , "Recording shorter than 5 seconds" ) ;
1009+ let _ = emit_to_window (
1010+ & app,
1011+ "pill" ,
1012+ "recording-too-short" ,
1013+ format ! ( "Recording shorter than {} seconds" , min_duration_s_i32) ,
1014+ ) ;
9691015 if let Err ( e) = std:: fs:: remove_file ( & normalized_path) {
9701016 log:: debug!( "Failed to remove short normalized audio: {}" , e) ;
9711017 }
0 commit comments