Skip to content

Commit 4467999

Browse files
committed
fixing macOS TTS
1 parent fa7eed3 commit 4467999

File tree

2 files changed

+93
-50
lines changed

2 files changed

+93
-50
lines changed

pyradio/radio.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7728,6 +7728,7 @@ def keypress(self, char):
77287728
self.selection = self.startPos
77297729
self.refreshBody()
77307730
self._do_display_notify()
7731+
self._speak_selection()
77317732
return
77327733

77337734
elif (char == kbkey['screen_middle'] or \
@@ -7742,6 +7743,7 @@ def keypress(self, char):
77427743
self.selection = self.startPos + int((self.bodyMaxY - 1) / 2)
77437744
self.refreshBody()
77447745
self._do_display_notify()
7746+
self._speak_selection()
77457747
return
77467748

77477749
elif (char == kbkey['screen_bottom'] or \
@@ -7756,6 +7758,7 @@ def keypress(self, char):
77567758
self.selection = self.startPos + self.bodyMaxY - 1
77577759
self.refreshBody()
77587760
self._do_display_notify()
7761+
self._speak_selection()
77597762
return
77607763

77617764
elif ( char in (kbkey['t'], ) or \

pyradio/tts.py

Lines changed: 90 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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):
360365
class 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

Comments
 (0)