@@ -302,7 +302,8 @@ def __init__(self, config):
302302 self .speaker .Volume = 100
303303 self .current_stream = None
304304 self ._lock = threading .RLock ()
305- logger .info ("Windows TTS initialized with SAPI.SpVoice" )
305+ if logger .isEnabledFor (logging .INFO ):
306+ logger .info ("Windows TTS initialized with SAPI.SpVoice" )
306307
307308 def _execute_speech (self , text , priority = Priority .NORMAL ):
308309 """Execute speech with proper priority handling"""
@@ -312,13 +313,16 @@ def _execute_speech(self, text, priority=Priority.NORMAL):
312313 self .stop ()
313314
314315 # Speak the new text (flags=1 for async)
316+ self .speaker .Volume = 50
317+ self .speaker .Rate = 0
315318 self .current_stream = self .speaker .Speak (text , 1 )
316319 self .state = TTSState .SPEAKING
317320
318321 if priority == Priority .HIGH :
319322 # For HIGH priority, wait for completion with shutdown check
320- logger .debug ("Waiting for HIGH priority speech completion" )
321- while not self .speaker .WaitUntilDone (self .current_stream ): # 100ms intervals
323+ if logger .isEnabledFor (logging .DEBUG ):
324+ logger .debug ("Waiting for HIGH priority speech completion" )
325+ while not self .speaker .WaitUntilDone (self .current_stream ):
322326 if self .state == TTSState .SHUTTING_DOWN :
323327 self .stop ()
324328 return False
@@ -331,7 +335,8 @@ def _execute_speech(self, text, priority=Priority.NORMAL):
331335 return True
332336
333337 except Exception as e :
334- logger .error (f"Windows TTS error: { e } " )
338+ if logger .isEnabledFor (logging .ERROR ):
339+ logger .error (f"Windows TTS error: { e } " )
335340 return False
336341
337342 def stop (self ):
@@ -360,62 +365,97 @@ def wait_for_shutdown(self, timeout=2.0):
360365class TTSMacOS (TTSBase ):
361366 """macOS TTS implementation"""
362367
368+ def __init__ (self , config ):
369+ super ().__init__ (config )
370+ self ._current_process = None
371+ self ._lock = threading .RLock ()
372+
363373 def _execute_speech (self , text , priority = Priority .NORMAL ):
364- """Execute speech on macOS"""
365- try :
366- cmd = self .config .get_command (self .system , text )
367- if isinstance (cmd , str ):
368- cmd = shlex .split (cmd )
374+ """Execute speech on macOS with proper interruption"""
375+ with self ._lock :
376+ try :
377+ # Stop any current speech first
378+ self ._stop_current_speech ()
379+
380+ cmd = self .config .get_command (self .system , text )
381+ if isinstance (cmd , str ):
382+ cmd = shlex .split (cmd )
383+
384+ # Start new speech process
385+ self ._current_process = subprocess .Popen (
386+ cmd ,
387+ stdout = subprocess .DEVNULL ,
388+ stderr = subprocess .DEVNULL
389+ )
390+ self .state = TTSState .SPEAKING
369391
370- # Use Popen for process control
371- self ._current_process = subprocess .Popen (
372- cmd ,
373- stdout = subprocess .DEVNULL ,
374- stderr = subprocess .DEVNULL
375- )
392+ if priority == Priority .HIGH :
393+ # For HIGH priority, wait for completion
394+ return self ._wait_for_completion ()
395+ else :
396+ # For NORMAL priority, return immediately
397+ # The process will continue in background
398+ return True
376399
377- # Wait for completion
378- self ._current_process .wait (timeout = 30 )
379- return True
400+ except Exception as e :
401+ if logger .isEnabledFor (logging .ERROR ):
402+ logger .error (f"macOS TTS error: { e } " )
403+ return False
404+
405+ def _stop_current_speech (self ):
406+ """Stop any currently running speech process"""
407+ if self ._current_process and self ._current_process .poll () is None :
408+ self ._current_process .terminate ()
409+ try :
410+ self ._current_process .wait (timeout = 0.5 )
411+ except subprocess .TimeoutExpired :
412+ self ._current_process .kill ()
413+ self ._current_process .wait ()
414+ # Additional cleanup: kill any stray say processes
415+ subprocess .run (['pkill' , '-9' , 'say' ],
416+ stdout = subprocess .DEVNULL ,
417+ stderr = subprocess .DEVNULL ,
418+ timeout = 5 )
419+
420+ def _wait_for_completion (self ):
421+ """Wait for current process to complete with shutdown checking"""
422+ try :
423+ while self ._current_process and self ._current_process .poll () is None :
424+ if self .state == TTSState .SHUTTING_DOWN :
425+ self ._stop_current_speech ()
426+ return False
427+ time .sleep (0.1 )
428+
429+ return self ._current_process .returncode == 0 if self ._current_process else False
380430
381- except subprocess .TimeoutExpired :
382- if logger .isEnabledFor (logging .WARNING ):
383- logger .warning ("macOS TTS timeout" )
384- if self ._current_process :
385- self ._current_process .terminate ()
386- return False
387431 except Exception as e :
388432 if logger .isEnabledFor (logging .ERROR ):
389- logger .error (f"macOS TTS error : { e } " )
433+ logger .error (f"Error waiting for speech completion : { e } " )
390434 return False
391435
392436 def stop (self ):
393437 """Stop current speech"""
394438 with self ._lock :
395- if self ._current_process and self ._current_process .poll () is None :
396- self ._current_process .terminate ()
397- try :
398- self ._current_process .wait (timeout = 1 )
399- except subprocess .TimeoutExpired :
400- self ._current_process .kill ()
401- # Kill any say processes that might be stuck
402- subprocess .run (
403- ['pkill' , '-9' , 'say' ],
404- capture_output = True
405- )
439+ self ._stop_current_speech ()
406440 time .sleep (self .speech_delay )
441+ self .state = TTSState .IDLE
407442
408443 def shutdown (self ):
409444 """Phase 1: Immediate shutdown (non-blocking)"""
410445 with self ._lock :
411446 self .state = TTSState .SHUTTING_DOWN
412- self .stop ()
447+ self ._stop_current_speech ()
413448
414449 def wait_for_shutdown (self , timeout = 2.0 ):
415450 """Phase 2: Wait for complete shutdown (blocking)"""
451+ start_time = time .time ()
416452 with self ._lock :
417- if self ._current_process and self ._current_process .poll () is None :
418- self ._current_process .terminate ()
453+ while (self ._current_process and
454+ self ._current_process .poll () is None and
455+ (time .time () - start_time ) < timeout ):
456+ time .sleep (0.1 )
457+
458+ self ._stop_current_speech ()
419459 self ._current_process = None
420460 self .state = TTSState .IDLE
421461 return True
@@ -471,7 +511,6 @@ def _initialize_tts(self):
471511 self .available = self ._check_macos_availability ()
472512
473513 if self .available :
474- logger .error ('1' )
475514 self .engine = self ._create_engine ()
476515 if logger .isEnabledFor (logging .INFO ):
477516 logger .info (f"TTS initialized successfully for { self .system } " )
@@ -532,8 +571,8 @@ def _check_windows_availability(self):
532571 speaker .Speak ("" , 1 )
533572 return True
534573 except Exception as e :
535- logger .error ( '5' )
536- logger .error (f"Windows TTS availability check failed: { e } " )
574+ if logger .isEnabledFor ( logging . ERROR ):
575+ logger .error (f"Windows TTS availability check failed: { e } " )
537576 return False
538577
539578 def _check_macos_availability (self ):
@@ -574,9 +613,6 @@ def _process_queues(self):
574613 try :
575614 high_request = self .high_priority_queue .get (timeout = 0.1 )
576615 self ._execute_request (high_request )
577-
578- # After HIGH completes, check for pending title
579- self ._process_pending_title ()
580616 continue
581617 except queue .Empty :
582618 pass
@@ -585,17 +621,21 @@ def _process_queues(self):
585621 try :
586622 normal_request = self .normal_priority_queue .get (timeout = 0.1 )
587623
588- # PLATFORM-SPECIFIC: Anti-stutter delay
589- if self .system == "Linux" :
590- # Linux: No delay needed - speech-dispatcher handles interruptions
624+ # PLATFORM-SPECIFIC: Anti-stutter and interruption
625+ if self .system == "Darwin" : # macOS
626+ # For macOS, always stop current speech before new NORMAL
627+ if self ._wait_with_interruption (0.1 ):
628+ self ._execute_request (normal_request )
629+ elif self .system == "Linux" :
630+ # Linux: No delay needed
591631 self ._execute_request (normal_request )
592632 else :
593- # Windows/macOS : Apply anti-stutter delay with interruption check
633+ # Windows: Apply anti-stutter delay
594634 if self ._wait_with_interruption (0.3 ):
595635 self ._execute_request (normal_request )
596636
597637 except queue .Empty :
598- time .sleep (0.01 ) # Small sleep to prevent busy waiting
638+ time .sleep (0.01 )
599639
600640 except Exception as e :
601641 if logger .isEnabledFor (logging .ERROR ):
0 commit comments