11use crate :: managers:: model:: ModelManager ;
2- use crate :: settings:: get_settings;
2+ use crate :: settings:: { get_settings, ModelUnloadTimeout } ;
33use anyhow:: Result ;
4+ use log:: debug;
45use natural:: phonetics:: soundex;
56use serde:: Serialize ;
7+ use std:: sync:: atomic:: { AtomicBool , AtomicU64 , Ordering } ;
68use std:: sync:: { Arc , Mutex } ;
9+ use std:: thread;
10+ use std:: time:: { Duration , SystemTime } ;
711use strsim:: levenshtein;
812use tauri:: { App , AppHandle , Emitter , Manager } ;
913use whisper_rs:: {
@@ -18,12 +22,16 @@ pub struct ModelStateEvent {
1822 pub error : Option < String > ,
1923}
2024
25+ #[ derive( Clone ) ]
2126pub struct TranscriptionManager {
22- state : Mutex < Option < WhisperState > > ,
23- context : Mutex < Option < WhisperContext > > ,
27+ state : Arc < Mutex < Option < WhisperState > > > ,
28+ context : Arc < Mutex < Option < WhisperContext > > > ,
2429 model_manager : Arc < ModelManager > ,
2530 app_handle : AppHandle ,
26- current_model_id : Mutex < Option < String > > ,
31+ current_model_id : Arc < Mutex < Option < String > > > ,
32+ last_activity : Arc < AtomicU64 > ,
33+ shutdown_signal : Arc < AtomicBool > ,
34+ watcher_handle : Arc < Mutex < Option < thread:: JoinHandle < ( ) > > > > ,
2735}
2836
2937fn apply_custom_words ( text : & str , custom_words : & [ String ] , threshold : f64 ) -> String {
@@ -139,21 +147,133 @@ impl TranscriptionManager {
139147 let app_handle = app. app_handle ( ) . clone ( ) ;
140148
141149 let manager = Self {
142- state : Mutex :: new ( None ) ,
143- context : Mutex :: new ( None ) ,
150+ state : Arc :: new ( Mutex :: new ( None ) ) ,
151+ context : Arc :: new ( Mutex :: new ( None ) ) ,
144152 model_manager,
145153 app_handle : app_handle. clone ( ) ,
146- current_model_id : Mutex :: new ( None ) ,
154+ current_model_id : Arc :: new ( Mutex :: new ( None ) ) ,
155+ last_activity : Arc :: new ( AtomicU64 :: new (
156+ SystemTime :: now ( )
157+ . duration_since ( SystemTime :: UNIX_EPOCH )
158+ . unwrap ( )
159+ . as_millis ( ) as u64 ,
160+ ) ) ,
161+ shutdown_signal : Arc :: new ( AtomicBool :: new ( false ) ) ,
162+ watcher_handle : Arc :: new ( Mutex :: new ( None ) ) ,
147163 } ;
148164
165+ // Start the idle watcher
166+ {
167+ let app_handle_cloned = app_handle. clone ( ) ;
168+ let manager_cloned = manager. clone ( ) ;
169+ let shutdown_signal = manager. shutdown_signal . clone ( ) ;
170+ let handle = thread:: spawn ( move || {
171+ while !shutdown_signal. load ( Ordering :: Relaxed ) {
172+ thread:: sleep ( Duration :: from_secs ( 10 ) ) ; // Check every 10 seconds
173+
174+ // Check shutdown signal again after sleep
175+ if shutdown_signal. load ( Ordering :: Relaxed ) {
176+ break ;
177+ }
178+
179+ let settings = get_settings ( & app_handle_cloned) ;
180+ let timeout_seconds = settings. model_unload_timeout . to_seconds ( ) ;
181+
182+ if let Some ( limit_seconds) = timeout_seconds {
183+ // Skip polling-based unloading for immediate timeout since it's handled directly in transcribe()
184+ if settings. model_unload_timeout == ModelUnloadTimeout :: Immediately {
185+ continue ;
186+ }
187+
188+ let last = manager_cloned. last_activity . load ( Ordering :: Relaxed ) ;
189+ let now_ms = SystemTime :: now ( )
190+ . duration_since ( SystemTime :: UNIX_EPOCH )
191+ . unwrap ( )
192+ . as_millis ( ) as u64 ;
193+
194+ if now_ms. saturating_sub ( last) > limit_seconds * 1000 {
195+ // idle -> unload
196+ if manager_cloned. is_model_loaded ( ) {
197+ let unload_start = std:: time:: Instant :: now ( ) ;
198+ debug ! ( "Starting to unload model due to inactivity" ) ;
199+
200+ if let Ok ( ( ) ) = manager_cloned. unload_model ( ) {
201+ let _ = app_handle_cloned. emit (
202+ "model-state-changed" ,
203+ ModelStateEvent {
204+ event_type : "unloaded_due_to_idle" . to_string ( ) ,
205+ model_id : None ,
206+ model_name : None ,
207+ error : None ,
208+ } ,
209+ ) ;
210+ let unload_duration = unload_start. elapsed ( ) ;
211+ debug ! (
212+ "Model unloaded due to inactivity (took {}ms)" ,
213+ unload_duration. as_millis( )
214+ ) ;
215+ }
216+ }
217+ }
218+ }
219+ }
220+ debug ! ( "Idle watcher thread shutting down gracefully" ) ;
221+ } ) ;
222+ * manager. watcher_handle . lock ( ) . unwrap ( ) = Some ( handle) ;
223+ }
224+
149225 // Try to load the default model from settings, but don't fail if no models are available
150226 let settings = get_settings ( & app_handle) ;
151227 let _ = manager. load_model ( & settings. selected_model ) ;
152228
153229 Ok ( manager)
154230 }
155231
232+ pub fn is_model_loaded ( & self ) -> bool {
233+ let state = self . state . lock ( ) . unwrap ( ) ;
234+ state. is_some ( )
235+ }
236+
237+ pub fn unload_model ( & self ) -> Result < ( ) > {
238+ let unload_start = std:: time:: Instant :: now ( ) ;
239+ debug ! ( "Starting to unload model" ) ;
240+
241+ {
242+ let mut state = self . state . lock ( ) . unwrap ( ) ;
243+ * state = None ; // Dropping state frees GPU/CPU memory
244+ }
245+ {
246+ let mut context = self . context . lock ( ) . unwrap ( ) ;
247+ * context = None ; // Dropping context frees additional memory
248+ }
249+ {
250+ let mut current_model = self . current_model_id . lock ( ) . unwrap ( ) ;
251+ * current_model = None ;
252+ }
253+
254+ // Emit unloaded event
255+ let _ = self . app_handle . emit (
256+ "model-state-changed" ,
257+ ModelStateEvent {
258+ event_type : "unloaded_manually" . to_string ( ) ,
259+ model_id : None ,
260+ model_name : None ,
261+ error : None ,
262+ } ,
263+ ) ;
264+
265+ let unload_duration = unload_start. elapsed ( ) ;
266+ debug ! (
267+ "Model unloaded manually (took {}ms)" ,
268+ unload_duration. as_millis( )
269+ ) ;
270+ Ok ( ( ) )
271+ }
272+
156273 pub fn load_model ( & self , model_id : & str ) -> Result < ( ) > {
274+ let load_start = std:: time:: Instant :: now ( ) ;
275+ debug ! ( "Starting to load model: {}" , model_id) ;
276+
157277 // Emit loading started event
158278 let _ = self . app_handle . emit (
159279 "model-state-changed" ,
@@ -252,7 +372,12 @@ impl TranscriptionManager {
252372 } ,
253373 ) ;
254374
255- println ! ( "Successfully loaded transcription model: {}" , model_id) ;
375+ let load_duration = load_start. elapsed ( ) ;
376+ debug ! (
377+ "Successfully loaded transcription model: {} (took {}ms)" ,
378+ model_id,
379+ load_duration. as_millis( )
380+ ) ;
256381 Ok ( ( ) )
257382 }
258383
@@ -262,6 +387,15 @@ impl TranscriptionManager {
262387 }
263388
264389 pub fn transcribe ( & self , audio : Vec < f32 > ) -> Result < String > {
390+ // Update last activity timestamp
391+ self . last_activity . store (
392+ SystemTime :: now ( )
393+ . duration_since ( SystemTime :: UNIX_EPOCH )
394+ . unwrap ( )
395+ . as_millis ( ) as u64 ,
396+ Ordering :: Relaxed ,
397+ ) ;
398+
265399 let st = std:: time:: Instant :: now ( ) ;
266400
267401 let mut result = String :: new ( ) ;
@@ -272,10 +406,34 @@ impl TranscriptionManager {
272406 return Ok ( result) ;
273407 }
274408
409+ // Check if model is loaded, if not try to load it
410+ {
411+ let state_guard = self . state . lock ( ) . unwrap ( ) ;
412+ if state_guard. is_none ( ) {
413+ // Model not loaded, try to load the selected model from settings
414+ let settings = get_settings ( & self . app_handle ) ;
415+ println ! (
416+ "Model not loaded, attempting to load: {}" ,
417+ settings. selected_model
418+ ) ;
419+
420+ // Drop the guard before calling load_model to avoid deadlock
421+ drop ( state_guard) ;
422+
423+ // Try to load the model
424+ if let Err ( e) = self . load_model ( & settings. selected_model ) {
425+ return Err ( anyhow:: anyhow!(
426+ "Failed to auto-load model '{}': {}. Please check that the model is downloaded and try again." ,
427+ settings. selected_model, e
428+ ) ) ;
429+ }
430+ }
431+ }
432+
275433 let mut state_guard = self . state . lock ( ) . unwrap ( ) ;
276434 let state = state_guard. as_mut ( ) . ok_or_else ( || {
277435 anyhow:: anyhow!(
278- "No model loaded. Please download and select a model from settings first ."
436+ "Model failed to load after auto-load attempt. Please check your model settings ."
279437 )
280438 } ) ?;
281439
@@ -333,6 +491,34 @@ impl TranscriptionManager {
333491 } ;
334492 println ! ( "\n took {}ms{}" , ( et - st) . as_millis( ) , translation_note) ;
335493
494+ // Check if we should immediately unload the model after transcription
495+ if settings. model_unload_timeout == ModelUnloadTimeout :: Immediately {
496+ println ! ( "⚡ Immediately unloading model after transcription" ) ;
497+ // Drop the state guard first to avoid deadlock
498+ drop ( state_guard) ;
499+ if let Err ( e) = self . unload_model ( ) {
500+ eprintln ! ( "Failed to immediately unload model: {}" , e) ;
501+ }
502+ }
503+
336504 Ok ( corrected_result. trim ( ) . to_string ( ) )
337505 }
338506}
507+
508+ impl Drop for TranscriptionManager {
509+ fn drop ( & mut self ) {
510+ debug ! ( "Shutting down TranscriptionManager" ) ;
511+
512+ // Signal the watcher thread to shutdown
513+ self . shutdown_signal . store ( true , Ordering :: Relaxed ) ;
514+
515+ // Wait for the thread to finish gracefully
516+ if let Some ( handle) = self . watcher_handle . lock ( ) . unwrap ( ) . take ( ) {
517+ if let Err ( e) = handle. join ( ) {
518+ eprintln ! ( "Failed to join idle watcher thread: {:?}" , e) ;
519+ } else {
520+ debug ! ( "Idle watcher thread joined successfully" ) ;
521+ }
522+ }
523+ }
524+ }
0 commit comments