gnosticdev commited on
Commit
744ab6c
·
verified ·
1 Parent(s): 38d44ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -69
app.py CHANGED
@@ -9,7 +9,7 @@ import gradio as gr
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
- # Importación correcta: Solo 'concatenate_videoclips'
13
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
14
  import re
15
  import math
@@ -32,6 +32,7 @@ logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
32
  logger.info("="*80)
33
 
34
  # Diccionario de voces TTS disponibles organizadas por idioma
 
35
  VOCES_DISPONIBLES = {
36
  "Español (España)": {
37
  "es-ES-JuanNeural": "Juan (España) - Masculino",
@@ -99,9 +100,32 @@ def get_voice_choices():
99
  choices = []
100
  for region, voices in VOCES_DISPONIBLES.items():
101
  for voice_id, voice_name in voices.items():
102
- choices.append((voice_name, voice_id))
 
103
  return choices
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  # Clave API de Pexels
106
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
107
  if not PEXELS_API_KEY:
@@ -200,53 +224,63 @@ def generate_script(prompt, max_length=150):
200
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
201
 
202
  cleaned_text = text.strip()
 
203
  try:
204
- instruction_end_idx = text.find(instruction_phrase)
205
- if instruction_end_idx != -1:
206
- cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
207
- logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
 
 
208
  else:
 
209
  instruction_start_idx = text.find(instruction_phrase_start)
210
  if instruction_start_idx != -1:
211
- prompt_in_output_idx = text.find(prompt, instruction_start_idx)
212
- if prompt_in_output_idx != -1:
213
- cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
214
- logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
215
- else:
216
- cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
217
- logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
 
218
 
219
  except Exception as e:
220
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
221
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
222
 
223
- if not cleaned_text or len(cleaned_text) < 10:
224
- logger.warning("El guión generado parece muy corto o vacío después de la limpieza. Usando el texto generado original (sin limpieza heurística).")
225
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
226
 
 
227
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
228
- cleaned_text = cleaned_text.lstrip(':').strip()
229
- cleaned_text = cleaned_text.lstrip('.').strip()
 
230
 
 
231
  sentences = cleaned_text.split('.')
232
  if sentences and sentences[0].strip():
233
  final_text = sentences[0].strip() + '.'
234
- if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
 
235
  final_text += " " + sentences[1].strip() + "."
236
- final_text = final_text.replace("..", ".")
237
 
238
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
239
  return final_text.strip()
240
 
241
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
242
- return cleaned_text.strip()
243
 
244
  except Exception as e:
245
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
246
  logger.warning("Usando prompt original como guion debido al error de generación.")
247
  return prompt.strip()
248
 
249
- # Función TTS con voz especificada
250
  async def text_to_speech(text, output_path, voice):
251
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
252
  if not text or not text.strip():
@@ -417,6 +451,7 @@ def extract_visual_keywords_from_script(script_text):
417
  logger.info(f"Palabras clave finales: {top_keywords}")
418
  return top_keywords
419
 
 
420
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
421
  logger.info("="*80)
422
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
@@ -452,35 +487,40 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
452
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
453
  temp_intermediate_files = []
454
 
455
- # 2. Generar audio de voz con reintentos y voz de respaldo
456
  logger.info("Generando audio de voz...")
457
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
458
 
459
- primary_voice = selected_voice
460
- fallback_voice = "es-ES-ElviraNeural" if selected_voice != "es-ES-ElviraNeural" else "es-ES-JuanNeural"
 
 
 
 
 
 
 
 
461
  tts_success = False
462
- retries = 3
463
 
464
- for attempt in range(retries):
465
- current_voice = primary_voice if attempt == 0 else fallback_voice
466
- if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
467
- logger.info(f"Intentando TTS con voz: {current_voice}")
 
468
  try:
469
  tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
470
  if tts_success:
471
- logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
472
- break
473
  except Exception as e:
474
- pass
475
-
476
- if not tts_success and attempt == 0 and primary_voice != fallback_voice:
477
- logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
478
- elif not tts_success and attempt < retries - 1:
479
- logger.warning(f"Fallo con voz {current_voice}, reintentando...")
480
-
481
 
 
482
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
483
- logger.error(f"Fallo en la generación de voz después de {retries} intentos. Archivo de audio no creado o es muy pequeño.")
484
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
485
 
486
  temp_intermediate_files.append(voz_path)
@@ -530,20 +570,6 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
530
  except Exception as e:
531
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
532
 
533
- if len(videos_data) < total_desired_videos / 2:
534
- logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
535
- generic_keywords = ["nature", "city", "background", "abstract"]
536
- for keyword in generic_keywords:
537
- if len(videos_data) >= total_desired_videos:
538
- break
539
- try:
540
- videos = buscar_videos_pexels(keyword, PEXELS_API_KEY, per_page=2)
541
- if videos:
542
- videos_data.extend(videos)
543
- logger.info(f"Encontrados {len(videos)} videos para '{keyword}' (genérico). Total data: {len(videos_data)}")
544
- except Exception as e:
545
- logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
546
-
547
  if len(videos_data) < total_desired_videos / 2:
548
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
549
  generic_keywords = ["nature", "city", "background", "abstract"]
@@ -929,7 +955,7 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
929
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
930
 
931
 
932
- # CAMBIO CRÍTICO: run_app ahora recibe TODOS los inputs que Gradio le pasa desde el evento click
933
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
934
  logger.info("="*80)
935
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
@@ -947,10 +973,11 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
947
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
948
 
949
  # Validar la voz seleccionada. Si no es válida, usar la por defecto.
950
- # AVAILABLE_VOICES se obtiene al inicio.
951
- if selected_voice not in AVAILABLE_VOICES:
952
- logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
953
- selected_voice = DEFAULT_VOICE
 
954
  else:
955
  logger.info(f"Voz seleccionada validada: {selected_voice}")
956
 
@@ -961,12 +988,12 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
961
  logger.info(f"Archivo de música recibido: {musica_file}")
962
  else:
963
  logger.info("No se proporcionó archivo de música.")
964
- logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final que se usará
965
 
966
  try:
967
  logger.info("Llamando a crear_video...")
968
- # Pasar el input_text elegido, la voz seleccionada y el archivo de música a crear_video
969
- video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
970
 
971
  if video_path and os.path.exists(video_path):
972
  logger.info(f"crear_video retornó path: {video_path}")
@@ -1038,8 +1065,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1038
  # --- COMPONENTE: Selección de Voz ---
1039
  voice_dropdown = gr.Dropdown(
1040
  label="Seleccionar Voz para Guion",
1041
- choices=AVAILABLE_VOICES,
1042
- value=DEFAULT_VOICE,
1043
  interactive=True
1044
  # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1045
  )
@@ -1058,7 +1085,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1058
  file_output = gr.File(
1059
  label="Descargar Archivo de Video",
1060
  interactive=False,
1061
- visible=False # <-- ESTÁ BIEN AQUÍ porque su visibilidad se controla por el último then()
1062
  # visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
1063
  )
1064
  status_output = gr.Textbox(
@@ -1093,11 +1120,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1093
  outputs=[video_output, file_output, status_output]
1094
  ).then(
1095
  # Acción 3 (síncrona): Hacer visible el enlace de descarga
1096
- # Recibe las salidas de la Acción 2
1097
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
1098
- # Inputs para esta lambda son los outputs del .then() anterior
1099
  inputs=[video_output, file_output, status_output],
1100
- # Actualizamos la visibilidad del componente file_output
1101
  outputs=[file_output]
1102
  )
1103
 
 
9
  import torch
10
  from transformers import GPT2Tokenizer, GPT2LMHeadModel
11
  from keybert import KeyBERT
12
+ # Importación correcta
13
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
14
  import re
15
  import math
 
32
  logger.info("="*80)
33
 
34
  # Diccionario de voces TTS disponibles organizadas por idioma
35
+ # Puedes expandir esta lista si conoces otros IDs de voz de Edge TTS
36
  VOCES_DISPONIBLES = {
37
  "Español (España)": {
38
  "es-ES-JuanNeural": "Juan (España) - Masculino",
 
100
  choices = []
101
  for region, voices in VOCES_DISPONIBLES.items():
102
  for voice_id, voice_name in voices.items():
103
+ # Formato: (Texto a mostrar en el dropdown, Valor que se pasa)
104
+ choices.append((f"{voice_name} ({region})", voice_id))
105
  return choices
106
 
107
+ # Obtener las voces al inicio del script
108
+ # Usamos la lista predefinida por ahora para evitar el error de inicio con la API
109
+ # Si deseas obtenerlas dinámicamente, descomenta la siguiente línea y comenta la que usa get_voice_choices()
110
+ # AVAILABLE_VOICES = asyncio.run(get_available_voices())
111
+ AVAILABLE_VOICES = get_voice_choices() # <-- Usamos la lista predefinida y aplanada
112
+ # Establecer una voz por defecto inicial
113
+ DEFAULT_VOICE_ID = "es-ES-JuanNeural" # ID de Juan
114
+
115
+ # Buscar el nombre amigable para la voz por defecto si existe
116
+ DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
117
+ for text, voice_id in AVAILABLE_VOICES:
118
+ if voice_id == DEFAULT_VOICE_ID:
119
+ DEFAULT_VOICE_NAME = text
120
+ break
121
+ # Si Juan no está en la lista (ej. lista de fallback), usar la primera voz disponible
122
+ if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
123
+ DEFAULT_VOICE_ID = AVAILABLE_VOICES[0][1] if AVAILABLE_VOICES else "en-US-AriaNeural"
124
+ DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Aria (United States) - Female" # Fallback name
125
+
126
+ logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
127
+
128
+
129
  # Clave API de Pexels
130
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
131
  if not PEXELS_API_KEY:
 
224
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
225
 
226
  cleaned_text = text.strip()
227
+ # Limpieza mejorada de la frase de instrucción
228
  try:
229
+ # Buscar el índice de inicio del prompt original dentro del texto generado
230
+ prompt_in_output_idx = text.lower().find(prompt.lower())
231
+ if prompt_in_output_idx != -1:
232
+ # Tomar todo el texto DESPUÉS del prompt original
233
+ cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
234
+ logger.debug("Texto limpiado tomando parte después del prompt original.")
235
  else:
236
+ # Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
237
  instruction_start_idx = text.find(instruction_phrase_start)
238
  if instruction_start_idx != -1:
239
+ # Tomar texto después de la frase base (puede incluir el prompt)
240
+ cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
241
+ logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
242
+ else:
243
+ # Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
244
+ logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
245
+ cleaned_text = text.strip() # Limpieza básica
246
+
247
 
248
  except Exception as e:
249
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
250
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
251
 
252
+ # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
253
+ if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
254
+ logger.warning("El guión generado parece muy corto o vacío después de la limpieza heurística. Usando el texto generado original (sin limpieza adicional).")
255
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
256
 
257
+ # Limpieza final de caracteres especiales y espacios sobrantes
258
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
259
+ cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
260
+ cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
261
+
262
 
263
+ # Intentar obtener al menos una oración completa si es posible para un inicio más limpio
264
  sentences = cleaned_text.split('.')
265
  if sentences and sentences[0].strip():
266
  final_text = sentences[0].strip() + '.'
267
+ # Añadir la segunda oración si existe y es razonable
268
+ if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7: # Usar un 70% de max_length como umbral
269
  final_text += " " + sentences[1].strip() + "."
270
+ final_text = final_text.replace("..", ".") # Limpiar doble punto
271
 
272
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
273
  return final_text.strip()
274
 
275
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
276
+ return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
277
 
278
  except Exception as e:
279
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
280
  logger.warning("Usando prompt original como guion debido al error de generación.")
281
  return prompt.strip()
282
 
283
+ # Función TTS ahora recibe la voz a usar
284
  async def text_to_speech(text, output_path, voice):
285
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
286
  if not text or not text.strip():
 
451
  logger.info(f"Palabras clave finales: {top_keywords}")
452
  return top_keywords
453
 
454
+ # crear_video ahora recibe la voz seleccionada
455
  def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
456
  logger.info("="*80)
457
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
 
487
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
488
  temp_intermediate_files = []
489
 
490
+ # 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
491
  logger.info("Generando audio de voz...")
492
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
493
 
494
+ tts_voices_to_try = [selected_voice] # Intentar primero la voz seleccionada
495
+ # Añadir voces de respaldo si no están ya en la lista y son diferentes a la seleccionada
496
+ # Nos aseguramos de no añadir None o IDs vacíos a la lista de reintento
497
+ if "es-ES-JuanNeural" not in tts_voices_to_try and "es-ES-JuanNeural" is not None: tts_voices_to_try.append("es-ES-JuanNeural")
498
+ if "es-ES-ElviraNeural" not in tts_voices_to_try and "es-ES-ElviraNeural" is not None: tts_voices_to_try.append("es-ES-ElviraNeural")
499
+ # Si la lista de voces disponibles es fiable, podrías usar un subconjunto ordenado para reintentos más amplios
500
+ # Opcional: si AVAILABLE_VOICES es fiable, podrías usar un subconjunto ordenado para reintentos
501
+ # Ejemplo: for voice_id in [selected_voice] + sorted([v[1] for v in AVAILABLE_VOICES if v[1].startswith('es-') and v[1] != selected_voice]) + sorted([v[1] for v in AVAILABLE_VOICES if not v[1].startswith('es-') and v[1] != selected_voice]):
502
+
503
+
504
  tts_success = False
505
+ tried_voices = set() # Usar un set para rastrear voces intentadas de forma eficiente
506
 
507
+ for current_voice in tts_voices_to_try:
508
+ if not current_voice or current_voice in tried_voices: continue # Evitar intentar IDs None/vacíos o duplicados
509
+ tried_voices.add(current_voice)
510
+
511
+ logger.info(f"Intentando TTS con voz: {current_voice}...")
512
  try:
513
  tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
514
  if tts_success:
515
+ logger.info(f"TTS exitoso con voz '{current_voice}'.")
516
+ break # Salir del bucle de reintento si tiene éxito
517
  except Exception as e:
518
+ logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
519
+ pass # Continuar al siguiente intento
 
 
 
 
 
520
 
521
+ # Verificar si el archivo fue creado después de todos los intentos
522
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
523
+ logger.error("Fallo en la generación de voz después de todos los intentos. Archivo de audio no creado o es muy pequeño.")
524
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
525
 
526
  temp_intermediate_files.append(voz_path)
 
570
  except Exception as e:
571
  logger.warning(f"Error buscando videos para '{keyword}': {str(e)}")
572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  if len(videos_data) < total_desired_videos / 2:
574
  logger.warning(f"Pocos videos encontrados ({len(videos_data)}). Intentando con palabras clave genéricas.")
575
  generic_keywords = ["nature", "city", "background", "abstract"]
 
955
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
956
 
957
 
958
+ # run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
959
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
960
  logger.info("="*80)
961
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
 
973
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
974
 
975
  # Validar la voz seleccionada. Si no es válida, usar la por defecto.
976
+ # AVAILABLE_VOICES se obtiene al inicio. Hay que buscar si el voice_id existe en la lista de pares (nombre, id)
977
+ voice_ids_disponibles = [v[1] for v in AVAILABLE_VOICES]
978
+ if selected_voice not in voice_ids_disponibles:
979
+ logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE_ID}.")
980
+ selected_voice = DEFAULT_VOICE_ID # <-- Usar el ID de la voz por defecto
981
  else:
982
  logger.info(f"Voz seleccionada validada: {selected_voice}")
983
 
 
988
  logger.info(f"Archivo de música recibido: {musica_file}")
989
  else:
990
  logger.info("No se proporcionó archivo de música.")
991
+ logger.info(f"Voz final a usar (ID): {selected_voice}") # Loguear el ID de la voz final
992
 
993
  try:
994
  logger.info("Llamando a crear_video...")
995
+ # Pasar el input_text elegido, la voz seleccionada (el ID) y el archivo de música a crear_video
996
+ video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice (ID) a crear_video
997
 
998
  if video_path and os.path.exists(video_path):
999
  logger.info(f"crear_video retornó path: {video_path}")
 
1065
  # --- COMPONENTE: Selección de Voz ---
1066
  voice_dropdown = gr.Dropdown(
1067
  label="Seleccionar Voz para Guion",
1068
+ choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
1069
+ value=DEFAULT_VOICE_ID, # Usar el ID de la voz por defecto calculada
1070
  interactive=True
1071
  # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1072
  )
 
1085
  file_output = gr.File(
1086
  label="Descargar Archivo de Video",
1087
  interactive=False,
1088
+ visible=False # <-- ESTÁ BIEN AQUÍ
1089
  # visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
1090
  )
1091
  status_output = gr.Textbox(
 
1120
  outputs=[video_output, file_output, status_output]
1121
  ).then(
1122
  # Acción 3 (síncrona): Hacer visible el enlace de descarga
 
1123
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
 
1124
  inputs=[video_output, file_output, status_output],
 
1125
  outputs=[file_output]
1126
  )
1127