@@ -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