Skip to content

Commit 10007d4

Browse files
committed
Improves UI stability by preventing voice loading race conditions.
1 parent 5626cd2 commit 10007d4

File tree

1 file changed

+101
-29
lines changed

1 file changed

+101
-29
lines changed

settings_window.py

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)