@@ -86,11 +86,18 @@ def __init__(self, parent, current_settings, save_callback, close_callback, defa
8686 self .elevenlabs_voices_loaded = False
8787 self ._loading_voices = False
8888 self ._voices_need_update = False
89-
89+ self ._voices_update_in_progress = False # Protection contre les appels multiples
90+
9091 # Pour le chargement progressif des voix ElevenLabs
9192 self .elevenlabs_voice_offset = 0
9293 self .load_more_btn = None
9394 self .elevenlabs_scroll_frame = None # Pour stocker la référence au frame
95+ self ._elevenlabs_voices_displayed = False # Flag pour éviter les doublons
96+ self ._loading_more_voices = False # Protection contre les appels simultanés
97+
98+ # Pour éviter les doublons Gemini
99+ self ._gemini_voices_displayed = False
100+ self .gemini_scroll_frame = None
94101
95102 if isinstance (preloaded_elevenlabs_voices , list ) and preloaded_elevenlabs_voices :
96103 self .elevenlabs_voices = list (preloaded_elevenlabs_voices )
@@ -126,10 +133,25 @@ def _enable_play_buttons(self):
126133 button .configure (state = "normal" )
127134
128135 def check_voices_update (self ):
129- if self ._voices_need_update and self .elevenlabs_voices_loaded :
136+ """Vérifie périodiquement si les voix doivent être mises à jour."""
137+ if self ._voices_need_update and self .elevenlabs_voices_loaded and not self ._voices_update_in_progress :
130138 self ._voices_need_update = False
131- self .update_elevenlabs_comboboxes ()
132- self .after (200 , self .check_voices_update )
139+ self ._voices_update_in_progress = True
140+ try :
141+ self .update_elevenlabs_comboboxes ()
142+ # Déclencher le chargement des voix dans le guide si pas encore fait
143+ if not self ._elevenlabs_voices_displayed and self .elevenlabs_scroll_frame :
144+ self ._load_more_elevenlabs_voices ()
145+ finally :
146+ self ._voices_update_in_progress = False
147+
148+ # Continue à vérifier mais avec intervalle plus long sur macOS
149+ try :
150+ if self .winfo_exists ():
151+ interval = 500 if sys .platform == "darwin" else 200
152+ self .after (interval , self .check_voices_update )
153+ except tk .TclError :
154+ pass # Window destroyed
133155
134156 def create_interface (self ):
135157 main_frame = customtkinter .CTkFrame (self , fg_color = "transparent" )
@@ -158,14 +180,19 @@ def create_interface(self):
158180
159181 if self .gemini_api_configured :
160182 gemini_tab = notebook .add ("Gemini Voices" )
161- self ._populate_guide_tab (gemini_tab , "gemini" )
183+ # Créer le scrollable frame et le stocker pour éviter de recréer les voix
184+ self .gemini_scroll_frame = customtkinter .CTkScrollableFrame (gemini_tab , label_text = "" )
185+ self .gemini_scroll_frame .pack (fill = "both" , expand = True )
186+ self ._populate_guide_tab (self .gemini_scroll_frame , "gemini" )
162187
163188 if self .elevenlabs_api_configured :
164189 elevenlabs_tab = notebook .add ("ElevenLabs Voices" )
165190 # Créer le conteneur une seule fois
166191 self .elevenlabs_scroll_frame = customtkinter .CTkScrollableFrame (elevenlabs_tab , label_text = "" )
167192 self .elevenlabs_scroll_frame .pack (fill = "both" , expand = True )
168- self ._load_more_elevenlabs_voices () # Charger le premier lot
193+ # Le chargement sera déclenché par check_voices_update() quand les voix seront prêtes
194+ if self .elevenlabs_voices_loaded :
195+ self ._load_more_elevenlabs_voices ()
169196
170197 button_frame = customtkinter .CTkFrame (main_frame , fg_color = "transparent" )
171198 button_frame .pack (fill = tk .X , padx = 10 , pady = (15 , 10 ))
@@ -198,17 +225,23 @@ def _create_speaker_headers(self, parent_frame):
198225 font = customtkinter .CTkFont (weight = "bold" ), width = 220 ).pack (side = tk .LEFT ,
199226 padx = (0 , 10 ))
200227
201- def _populate_guide_tab (self , tab , provider ):
202- # Cette fonction ne gère plus que Gemini, qui est chargé une seule fois.
203- scrollable_frame = customtkinter .CTkScrollableFrame (tab , label_text = "" )
204- scrollable_frame .pack (fill = "both" , expand = True )
228+ def _populate_guide_tab (self , scrollable_frame , provider ):
229+ """Peuple l'onglet des voix Gemini. Ne doit être appelé qu'une seule fois."""
230+ # Protection contre les appels multiples (doublons)
231+ if provider == "gemini" and self ._gemini_voices_displayed :
232+ return
233+
205234 voices = list (AVAILABLE_VOICES .items ())
206235 for i , (name , desc ) in enumerate (voices ):
207236 self ._create_guide_row (scrollable_frame , provider , name , f"{ name } - { desc } " , name )
208237 if i < len (voices ) - 1 :
209238 separator = customtkinter .CTkFrame (scrollable_frame , height = 1 , fg_color = ("gray80" , "gray25" ))
210239 separator .pack (fill = 'x' , pady = 5 , padx = 5 )
211240
241+ # Marquer comme affiché
242+ if provider == "gemini" :
243+ self ._gemini_voices_displayed = True
244+
212245 def _create_guide_row (self , parent , provider , voice_id , display_name , play_identifier ):
213246 # Set a fixed height for each row and prevent it from resizing.
214247 # This makes layout calculations much faster and scrolling smoother.
@@ -260,26 +293,54 @@ def _load_more_elevenlabs_voices(self):
260293 if not self .elevenlabs_scroll_frame :
261294 return
262295
263- # Supprimer l'ancien bouton "Charger plus" s'il existe
264- if self . load_more_btn and self .load_more_btn . winfo_exists () :
265- self . load_more_btn . destroy ()
266- self .load_more_btn = None
296+ # Protection contre les appels multiples
297+ if hasattr ( self , '_loading_more_voices' ) and self ._loading_more_voices :
298+ return
299+ self ._loading_more_voices = True
267300
268- if self .elevenlabs_voices_loaded and self .elevenlabs_voices :
269- voices_to_display = self .elevenlabs_voices [self .elevenlabs_voice_offset : self .elevenlabs_voice_offset + 20 ]
270- for voice in voices_to_display :
271- self ._create_guide_row (self .elevenlabs_scroll_frame , "elevenlabs" , voice ['id' ], voice ['display_name' ], voice ['preview_url' ])
272- separator = customtkinter .CTkFrame (self .elevenlabs_scroll_frame , height = 1 , fg_color = ("gray80" , "gray25" ))
273- separator .pack (fill = 'x' , pady = 5 , padx = 5 )
274-
275- self .elevenlabs_voice_offset += len (voices_to_display )
301+ try :
302+ # Supprimer l'ancien bouton "Charger plus" s'il existe
303+ if self .load_more_btn :
304+ try :
305+ if self .load_more_btn .winfo_exists ():
306+ self .load_more_btn .destroy ()
307+ except tk .TclError :
308+ pass
309+ self .load_more_btn = None
310+
311+ if self .elevenlabs_voices_loaded and self .elevenlabs_voices :
312+ # Si c'est le premier chargement et que des voix sont déjà affichées, ne rien faire
313+ if self .elevenlabs_voice_offset == 0 and self ._elevenlabs_voices_displayed :
314+ return
276315
277- # S'il reste des voix à charger, recréer le bouton
278- if self .elevenlabs_voice_offset < len (self .elevenlabs_voices ):
279- self .load_more_btn = customtkinter .CTkButton (self .elevenlabs_scroll_frame , text = "Charger plus..." , command = self ._load_more_elevenlabs_voices )
280- self .load_more_btn .pack (pady = 10 )
281- else :
282- customtkinter .CTkLabel (self .elevenlabs_scroll_frame , text = "Loading ElevenLabs voices..." ).pack (pady = 20 )
316+ voices_to_display = self .elevenlabs_voices [self .elevenlabs_voice_offset : self .elevenlabs_voice_offset + 20 ]
317+
318+ if voices_to_display : # Seulement si on a des voix à afficher
319+ for voice in voices_to_display :
320+ self ._create_guide_row (self .elevenlabs_scroll_frame , "elevenlabs" , voice ['id' ],
321+ voice ['display_name' ], voice ['preview_url' ])
322+ separator = customtkinter .CTkFrame (self .elevenlabs_scroll_frame , height = 1 ,
323+ fg_color = ("gray80" , "gray25" ))
324+ separator .pack (fill = 'x' , pady = 5 , padx = 5 )
325+
326+ self .elevenlabs_voice_offset += len (voices_to_display )
327+ self ._elevenlabs_voices_displayed = True
328+
329+ # S'il reste des voix à charger, recréer le bouton
330+ if self .elevenlabs_voice_offset < len (self .elevenlabs_voices ):
331+ self .load_more_btn = customtkinter .CTkButton (
332+ self .elevenlabs_scroll_frame ,
333+ text = f"Load more... ({ self .elevenlabs_voice_offset } /{ len (self .elevenlabs_voices )} )" ,
334+ command = self ._load_more_elevenlabs_voices
335+ )
336+ self .load_more_btn .pack (pady = 10 )
337+ elif not self .elevenlabs_voices_loaded :
338+ # Afficher un message de chargement seulement si rien n'est affiché
339+ if not self ._elevenlabs_voices_displayed :
340+ customtkinter .CTkLabel (self .elevenlabs_scroll_frame ,
341+ text = "Loading ElevenLabs voices..." ).pack (pady = 20 )
342+ finally :
343+ self ._loading_more_voices = False
283344
284345 def safe_update_button (self , state , text ):
285346 try :
@@ -388,7 +449,18 @@ def save_and_close(self):
388449 new_settings .setdefault ('speaker_voices' , {})[speaker_name ] = row ['gemini_voice' ].get ()
389450 if row .get ('elevenlabs_voice' ):
390451 display = row ['elevenlabs_voice' ].get ()
391- voice_id = next ((v ['id' ] for v in self .elevenlabs_voices if v ['display_name' ] == display ), "" )
452+ # Try to find the voice_id from the loaded voices
453+ voice_id = next ((v ['id' ] for v in self .elevenlabs_voices if v ['display_name' ] == display ), None )
454+
455+ # If voice_id is not found (voices not loaded yet), preserve the existing ID from current_settings
456+ if voice_id is None :
457+ existing_data = self .current_settings .get ('speaker_voices_elevenlabs' , {}).get (speaker_name , {})
458+ if isinstance (existing_data , dict ) and existing_data .get ('display_name' ) == display :
459+ # Keep the existing voice_id if the display_name hasn't changed
460+ voice_id = existing_data .get ('id' , '' )
461+ else :
462+ voice_id = ''
463+
392464 new_settings .setdefault ('speaker_voices_elevenlabs' , {})[speaker_name ] = {'id' : voice_id ,
393465 'display_name' : display }
394466 if self .save_callback : self .save_callback (new_settings )
0 commit comments