Skip to content

Commit 5626cd2

Browse files
committed
Improves robust termination of playback processes and UI stability.
1 parent e804ae9 commit 5626cd2

File tree

1 file changed

+77
-29
lines changed

1 file changed

+77
-29
lines changed

gui.py

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -160,16 +160,21 @@ def __init__(self, root: tk.Tk, generate_func, logger, api_key: str, default_scr
160160
self.update_provider_menu_state()
161161
self.update_voice_settings_enabled()
162162

163-
# Schedule initial quota fetch and theme watcher
163+
# Schedule initial quota fetch with longer delay on macOS ARM
164164
if self.app_settings.get("tts_provider", "elevenlabs").lower() == "elevenlabs":
165-
self._schedule_provider_label_refresh(delay_ms=2000, retries=5)
165+
# Increase delay on macOS ARM to avoid blocking UI initialization
166+
delay = 3000 if sys.platform == "darwin" else 2000
167+
self._schedule_provider_label_refresh(delay_ms=delay, retries=5)
166168

167-
# Démarrer le watcher seulement si on utilise les menus Tkinter
169+
# Démarrer le watcher seulement si on utilise les menus Tkinter, avec intervalle plus long sur macOS
168170
if not HAS_CTK_MENUBAR:
169-
self._start_theme_watcher()
171+
# Use longer interval on macOS to reduce event loop interference
172+
interval = 5000 if sys.platform == "darwin" else 2000
173+
self._start_theme_watcher(interval_ms=interval)
170174

171-
# Schedule the pre-fetch of voices after the UI is stable to avoid startup race conditions.
172-
self.root.after(500, self._prefetch_elevenlabs_voices)
175+
# Schedule the pre-fetch of voices with longer delay on macOS ARM to avoid startup race conditions
176+
prefetch_delay = 1500 if sys.platform == "darwin" else 500
177+
self.root.after(prefetch_delay, self._prefetch_elevenlabs_voices)
173178

174179
self.logger.info("Main interface initialized.")
175180

@@ -575,20 +580,30 @@ def _update_all_menu_themes(self):
575580

576581
def _configure_button_state(self, button: customtkinter.CTkButton, enabled: bool):
577582
"""Configures the state and color of a button."""
578-
if not button or not button.winfo_exists():
583+
if not button:
579584
return
580585

581-
if enabled:
582-
# Restore default theme color for the enabled state
583-
normal_fg_color = customtkinter.ThemeManager.theme["CTkButton"]["fg_color"]
584-
normal_text_color = customtkinter.ThemeManager.theme["CTkButton"]["text_color"]
585-
button.configure(state='normal', fg_color=normal_fg_color, text_color=normal_text_color)
586-
else:
587-
# Apply a custom, more opaque color for the disabled state
588-
disabled_fg_color = ("gray75", "gray30")
589-
# Use a readable text color for the disabled state
590-
disabled_text_color = ("gray10", "gray70") # Dark gray for light mode, a less bright gray for dark mode
591-
button.configure(state='disabled', fg_color=disabled_fg_color, text_color_disabled=disabled_text_color)
586+
try:
587+
if not button.winfo_exists():
588+
return
589+
except (tk.TclError, AttributeError):
590+
return
591+
592+
try:
593+
if enabled:
594+
# Restore default theme color for the enabled state
595+
normal_fg_color = customtkinter.ThemeManager.theme["CTkButton"]["fg_color"]
596+
normal_text_color = customtkinter.ThemeManager.theme["CTkButton"]["text_color"]
597+
button.configure(state='normal', fg_color=normal_fg_color, text_color=normal_text_color)
598+
else:
599+
# Apply a custom, more opaque color for the disabled state
600+
disabled_fg_color = ("gray75", "gray30")
601+
# Use a readable text color for the disabled state
602+
disabled_text_color = ("gray10", "gray70") # Dark gray for light mode, a less bright gray for dark mode
603+
button.configure(state='disabled', fg_color=disabled_fg_color, text_color_disabled=disabled_text_color)
604+
except (tk.TclError, AttributeError) as e:
605+
# Widget might be in an invalid state on macOS ARM
606+
self.logger.debug(f"Error configuring button state: {e}")
592607

593608
def on_provider_selected(self):
594609
"""Handles selection from the TTS Provider radio button menu."""
@@ -850,7 +865,12 @@ def fetch_and_update():
850865
if not requests:
851866
self.logger.error("'requests' library not found. Cannot fetch ElevenLabs quota.")
852867
self.elevenlabs_quota_text = "TTS Provider: ElevenLabs v3 - 'requests' missing"
853-
self.root.after(0, self._update_provider_label)
868+
# Use try/except to ensure UI thread safety on macOS
869+
try:
870+
if self.root and self.root.winfo_exists():
871+
self.root.after(0, self._update_provider_label)
872+
except (tk.TclError, RuntimeError):
873+
pass
854874
return
855875
try:
856876
# Use the new v3-compatible quota function
@@ -866,8 +886,13 @@ def fetch_and_update():
866886
self.elevenlabs_quota_text = "TTS Provider: ElevenLabs v3 - Network error"
867887
_save_quota_cache(self.elevenlabs_quota_text)
868888
finally:
869-
# Always schedule the UI update from the main thread
870-
self.root.after(0, self._update_provider_label)
889+
# Always schedule the UI update from the main thread with safety check
890+
try:
891+
if self.root and self.root.winfo_exists():
892+
self.root.after(0, self._update_provider_label)
893+
except (tk.TclError, RuntimeError):
894+
# Window may have been destroyed, ignore
895+
pass
871896

872897
threading.Thread(target=fetch_and_update, daemon=True).start()
873898

@@ -939,13 +964,27 @@ def poll_log_queue(self):
939964
elif msg_type == 'UPDATE_PLAY_BUTTON':
940965
is_enabled = message[2] == 'normal'
941966
self._configure_button_state(self.play_button, enabled=is_enabled)
942-
if self.play_button and self.play_button.winfo_exists():
943-
self.play_button.configure(text=message[1])
967+
try:
968+
if self.play_button and self.play_button.winfo_exists():
969+
self.play_button.configure(text=message[1])
970+
except (tk.TclError, AttributeError):
971+
pass # Widget might be in an invalid state
944972
else:
945973
self._update_log(message)
946974
except queue.Empty:
947975
pass # The queue is empty, do nothing
948-
self.root.after(100, self.poll_log_queue) # Check the queue every 100 ms
976+
except Exception as e:
977+
# Log but don't crash if there's an issue processing the queue
978+
self.logger.debug(f"Error processing log queue: {e}")
979+
980+
# Schedule next poll, with safety check for window existence
981+
try:
982+
if self.root and self.root.winfo_exists():
983+
# Slightly longer interval on macOS to reduce event loop contention
984+
interval = 150 if sys.platform == "darwin" else 100
985+
self.root.after(interval, self.poll_log_queue)
986+
except (tk.TclError, RuntimeError):
987+
pass # Window destroyed, stop polling
949988

950989
def _update_log(self, message):
951990
self.log_text.configure(state='normal')
@@ -1268,8 +1307,13 @@ def _play_in_thread(self):
12681307
self.log_status(f"Audio playback error: {e}")
12691308
finally:
12701309
self.playback_obj = None
1271-
if self.root.winfo_exists():
1272-
self.log_queue.put(('UPDATE_PLAY_BUTTON', '▶', 'normal'))
1310+
# Check if window still exists before updating UI
1311+
try:
1312+
if self.root and self.root.winfo_exists():
1313+
self.log_queue.put(('UPDATE_PLAY_BUTTON', '▶', 'normal'))
1314+
except (tk.TclError, RuntimeError):
1315+
# Window destroyed, ignore
1316+
pass
12731317

12741318
def _reset_active_button(self):
12751319
"""Resets the currently active play button to its default state."""
@@ -1477,7 +1521,7 @@ def perform_startup_tasks(self):
14771521

14781522
def _prefetch_elevenlabs_voices(self):
14791523
"""Prefetches ElevenLabs voices in a background thread.
1480-
This is started with a delay to avoid startup race conditions on Windows."""
1524+
This is started with a delay to avoid startup race conditions on Windows and macOS."""
14811525

14821526
def _run_fetch():
14831527
try:
@@ -1492,7 +1536,9 @@ def _run_fetch():
14921536
return
14931537

14941538
headers = {"xi-api-key": key}
1495-
resp = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers, timeout=15)
1539+
# Increase timeout on macOS ARM to avoid connection issues
1540+
timeout = 20 if sys.platform == "darwin" else 15
1541+
resp = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers, timeout=timeout)
14961542

14971543
if resp.status_code != 200:
14981544
self.elevenlabs_voices_cache = []
@@ -1512,7 +1558,9 @@ def _run_fetch():
15121558
'labels': labels, 'preview_url': voice.get('preview_url', '')})
15131559
voices.sort(key=lambda x: x.get('name', ''))
15141560
self.elevenlabs_voices_cache = voices
1515-
except Exception:
1561+
self.logger.info(f"Successfully pre-fetched {len(voices)} ElevenLabs voices.")
1562+
except Exception as e:
1563+
self.logger.warning(f"Failed to pre-fetch ElevenLabs voices: {e}")
15161564
self.elevenlabs_voices_cache = []
15171565

15181566
threading.Thread(target=_run_fetch, daemon=True).start()

0 commit comments

Comments
 (0)