gnosticdev commited on
Commit
ee6df46
·
verified ·
1 Parent(s): 30e9b85

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +124 -69
app.py CHANGED
@@ -61,6 +61,36 @@ except Exception as e:
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def buscar_videos_pexels(query, api_key, per_page=5):
65
  if not api_key:
66
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
@@ -129,53 +159,63 @@ def generate_script(prompt, max_length=150):
129
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
130
 
131
  cleaned_text = text.strip()
 
132
  try:
133
- instruction_end_idx = text.find(instruction_phrase)
134
- if instruction_end_idx != -1:
135
- cleaned_text = text[instruction_end_idx + len(instruction_phrase):].strip()
136
- logger.debug("Instrucción inicial encontrada y eliminada del guión generado.")
 
 
137
  else:
 
138
  instruction_start_idx = text.find(instruction_phrase_start)
139
  if instruction_start_idx != -1:
140
- prompt_in_output_idx = text.find(prompt, instruction_start_idx)
141
- if prompt_in_output_idx != -1:
142
- cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
143
- logger.debug("Instrucción base y prompt encontrados y eliminados del guión generado.")
144
- else:
145
- cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
146
- logger.debug("Instrucción base encontrada, eliminada del guión generado (sin prompt detectado).")
 
147
 
148
  except Exception as e:
149
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
150
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
151
 
152
- if not cleaned_text or len(cleaned_text) < 10:
153
- 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).")
154
- cleaned_text = re.sub(r'<[^>]+>', '', text).strip()
 
155
 
 
156
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
157
- cleaned_text = cleaned_text.lstrip(':').strip()
158
- cleaned_text = cleaned_text.lstrip('.').strip()
 
159
 
 
160
  sentences = cleaned_text.split('.')
161
  if sentences and sentences[0].strip():
162
  final_text = sentences[0].strip() + '.'
163
- if len(sentences) > 1 and sentences[1].strip() and len(final_text.split()) < max_length * 0.7:
 
164
  final_text += " " + sentences[1].strip() + "."
165
- final_text = final_text.replace("..", ".")
166
 
167
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
168
  return final_text.strip()
169
 
170
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
171
- return cleaned_text.strip()
172
 
173
  except Exception as e:
174
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
175
  logger.warning("Usando prompt original como guion debido al error de generación.")
176
  return prompt.strip()
177
 
178
- # Función TTS con voz especificada
179
  async def text_to_speech(text, output_path, voice):
180
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
181
  if not text or not text.strip():
@@ -346,10 +386,12 @@ def extract_visual_keywords_from_script(script_text):
346
  logger.info(f"Palabras clave finales: {top_keywords}")
347
  return top_keywords
348
 
349
- def crear_video(prompt_type, input_text, musica_file=None):
 
350
  logger.info("="*80)
351
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
352
  logger.debug(f"Input: '{input_text[:100]}...'")
 
353
 
354
  start_time = datetime.now()
355
  temp_dir_intermediate = None
@@ -380,35 +422,39 @@ def crear_video(prompt_type, input_text, musica_file=None):
380
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
381
  temp_intermediate_files = []
382
 
383
- # 2. Generar audio de voz con reintentos y voz de respaldo
384
  logger.info("Generando audio de voz...")
385
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
386
 
387
- primary_voice = "es-ES-JuanNeural"
388
- fallback_voice = "es-ES-ElviraNeural" # Otra voz en español
 
 
 
 
 
 
389
  tts_success = False
390
- retries = 3
 
 
 
 
391
 
392
- for attempt in range(retries):
393
- current_voice = primary_voice if attempt == 0 else fallback_voice
394
- if attempt > 0: logger.warning(f"Reintentando TTS ({attempt+1}/{retries})...")
395
- logger.info(f"Intentando TTS con voz: {current_voice}")
396
  try:
397
  tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
398
  if tts_success:
399
- logger.info(f"TTS exitoso en intento {attempt + 1} con voz {current_voice}.")
400
- break
401
  except Exception as e:
402
- pass
403
-
404
- if not tts_success and attempt == 0 and primary_voice != fallback_voice:
405
- logger.warning(f"Fallo con voz {primary_voice}, intentando voz de respaldo: {fallback_voice}")
406
- elif not tts_success and attempt < retries - 1:
407
- logger.warning(f"Fallo con voz {current_voice}, reintentando...")
408
 
409
 
 
410
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
411
- 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.")
412
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
413
 
414
  temp_intermediate_files.append(voz_path)
@@ -843,8 +889,8 @@ def crear_video(prompt_type, input_text, musica_file=None):
843
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
844
 
845
 
846
- # La función run_app ahora recibe todos los inputs de texto y el archivo de música
847
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
848
  logger.info("="*80)
849
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
850
 
@@ -860,17 +906,27 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
860
  # Retornar None para video y archivo, actualizar estado con mensaje de error
861
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
862
 
 
 
 
 
 
 
 
 
 
863
  logger.info(f"Tipo de entrada: {prompt_type}")
864
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
865
  if musica_file:
866
  logger.info(f"Archivo de música recibido: {musica_file}")
867
  else:
868
  logger.info("No se proporcionó archivo de música.")
 
869
 
870
  try:
871
  logger.info("Llamando a crear_video...")
872
- # Pasar el input_text elegido y el archivo de música a crear_video
873
- video_path = crear_video(prompt_type, input_text, musica_file)
874
 
875
  if video_path and os.path.exists(video_path):
876
  logger.info(f"crear_video retornó path: {video_path}")
@@ -890,12 +946,11 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
890
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
891
  finally:
892
  logger.info("Fin del handler run_app.")
893
- # Retornar las tres salidas esperadas por el evento click
894
  return output_video, output_file, status_msg
895
 
896
 
897
  # Interfaz de Gradio
898
- with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
899
  .gradio-container {max-width: 800px; margin: auto;}
900
  h1 {text-align: center;}
901
  """) as app:
@@ -912,18 +967,16 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
912
  )
913
 
914
  # Contenedores para los campos de texto para controlar la visibilidad
915
- # visible=True/False está aquí en la COLUMNA
916
  with gr.Column(visible=True) as ia_guion_column:
917
  prompt_ia = gr.Textbox(
918
  label="Tema para IA",
919
  lines=2,
920
- placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer...",
921
  max_lines=4,
922
  value=""
923
- # visible=... <--- NO DEBE ESTAR AQUÍ
924
  )
925
 
926
- # visible=True/False está aquí en la COLUMNA
927
  with gr.Column(visible=False) as manual_guion_column:
928
  prompt_manual = gr.Textbox(
929
  label="Tu Guion Completo",
@@ -931,7 +984,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
931
  placeholder="Ej: En este video exploraremos los misterios del océano. Veremos la vida marina fascinante y los arrecifes de coral vibrantes. ¡Acompáñanos en esta aventura subacuática!",
932
  max_lines=10,
933
  value=""
934
- # visible=... <--- NO DEBE ESTAR AQUÍ
935
  )
936
 
937
  musica_input = gr.Audio(
@@ -939,7 +992,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
939
  type="filepath",
940
  interactive=True,
941
  value=None
942
- # visible=... <--- NO DEBE ESTAR AQUÍ
943
  )
944
 
945
  # --- COMPONENTE: Selección de Voz ---
@@ -948,7 +1001,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
948
  choices=AVAILABLE_VOICES,
949
  value=DEFAULT_VOICE,
950
  interactive=True
951
- # visible=... <--- NO DEBE ESTAR AQUÍ
952
  )
953
  # --- FIN COMPONENTE ---
954
 
@@ -960,12 +1013,13 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
960
  label="Previsualización del Video Generado",
961
  interactive=False,
962
  height=400
963
- # visible=... <--- NO DEBE ESTAR AQUÍ
964
  )
965
  file_output = gr.File(
966
  label="Descargar Archivo de Video",
967
  interactive=False,
968
- visible=False # <-- ESTA BIEN AQUÍ porque su visibilidad se controla solo por el lambda posterior
 
969
  )
970
  status_output = gr.Textbox(
971
  label="Estado",
@@ -973,7 +1027,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
973
  show_label=False,
974
  placeholder="Esperando acción...",
975
  value="Esperando entrada..."
976
- # visible=... <--- NO DEBE ESTAR AQUÍ
977
  )
978
 
979
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
@@ -981,8 +1035,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
981
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
982
  gr.update(visible=x == "Usar Mi Guion")),
983
  inputs=prompt_type,
984
- # APUNTAR A LAS COLUMNAS, esto está correcto
985
- outputs=[ia_guion_column, manual_guion_column]
986
  )
987
 
988
  # Evento click del botón de generar video
@@ -994,29 +1047,31 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file):
994
  ).then(
995
  # Acción 2 (asíncrona): Llamar a la función principal
996
  run_app,
997
- # PASAR TODOS LOS INPUTS RELEVANTES a run_app
998
- # Esto parece que ahora debería coincidir con la definición de run_app con 5 argumentos
999
- inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown],
1000
  # run_app retornará los 3 outputs esperados
1001
  outputs=[video_output, file_output, status_output]
1002
  ).then(
1003
  # Acción 3 (síncrona): Hacer visible el enlace de descarga
 
1004
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
 
1005
  inputs=[video_output, file_output, status_output],
 
1006
  outputs=[file_output]
1007
  )
1008
 
 
1009
  gr.Markdown("### Instrucciones:")
1010
  gr.Markdown("""
1011
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1012
- 2. **Selecciona el tipo de entrada**:
1013
- - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
1014
- - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
1015
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
1016
- 4. **Haz clic en "✨ Generar Video"**.
1017
- 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
1018
- 6. La previsualización del video aparecerá arriba (puede fallar para archivos grandes), y un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1019
- 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
1020
  """)
1021
  gr.Markdown("---")
1022
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
@@ -1028,7 +1083,7 @@ if __name__ == "__main__":
1028
  try:
1029
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1030
  temp_clip.close()
1031
- logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
1032
  except Exception as e:
1033
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1034
 
 
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
64
+ # --- Obtener voces de Edge TTS al inicio ---
65
+ async def get_available_voices():
66
+ logger.info("Obteniendo lista de voces disponibles de Edge TTS...")
67
+ try:
68
+ voices = await edge_tts.VoicesManager.create()
69
+ # Retornar solo voces en español si prefieres, o dejar todas
70
+ es_voices = [voice.Name for voice in voices.Voices if voice.Locale.startswith('es-')]
71
+ if es_voices:
72
+ logger.info(f"Encontradas {len(es_voices)} voces en español.")
73
+ return es_voices
74
+ else:
75
+ # Si no hay español, retornar todas las voces
76
+ all_voices = [voice.Name for voice in voices.Voices]
77
+ logger.warning(f"No se encontraron voces en español. Retornando {len(all_voices)} voces en todos los idiomas.")
78
+ return all_voices if all_voices else ["en-US-AriaNeural"] # Fallback si no hay ninguna
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error obteniendo voces de Edge TTS: {str(e)}", exc_info=True)
82
+ # Retornar una lista de voces por defecto si falla la API de Edge TTS
83
+ logger.warning("No se pudieron obtener voces de Edge TTS. Usando lista de voces por defecto.")
84
+ return ["es-ES-JuanNeural", "es-ES-ElviraNeural", "en-US-AriaNeural"]
85
+
86
+ # Obtener las voces al inicio del script (esto puede tardar un poco)
87
+ logger.info("Inicializando lista de voces disponibles...")
88
+ AVAILABLE_VOICES = asyncio.run(get_available_voices())
89
+ # Establecer una voz por defecto inicial
90
+ DEFAULT_VOICE = "es-ES-JuanNeural" if "es-ES-JuanNeural" in AVAILABLE_VOICES else (AVAILABLE_VOICES[0] if AVAILABLE_VOICES else "en-US-AriaNeural")
91
+ logger.info(f"Voz por defecto seleccionada: {DEFAULT_VOICE}")
92
+
93
+
94
  def buscar_videos_pexels(query, api_key, per_page=5):
95
  if not api_key:
96
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
 
159
  text = tokenizer.decode(outputs[0], skip_special_tokens=True)
160
 
161
  cleaned_text = text.strip()
162
+ # Limpieza mejorada de la frase de instrucción
163
  try:
164
+ # Buscar el índice de inicio del prompt original dentro del texto generado
165
+ prompt_in_output_idx = text.lower().find(prompt.lower())
166
+ if prompt_in_output_idx != -1:
167
+ # Tomar todo el texto DESPUÉS del prompt original
168
+ cleaned_text = text[prompt_in_output_idx + len(prompt):].strip()
169
+ logger.debug("Texto limpiado tomando parte después del prompt original.")
170
  else:
171
+ # Fallback si el prompt original no está exacto en la salida: buscar la frase de instrucción base
172
  instruction_start_idx = text.find(instruction_phrase_start)
173
  if instruction_start_idx != -1:
174
+ # Tomar texto después de la frase base (puede incluir el prompt)
175
+ cleaned_text = text[instruction_start_idx + len(instruction_phrase_start):].strip()
176
+ logger.debug("Texto limpiado tomando parte después de la frase de instrucción base.")
177
+ else:
178
+ # Si ni la frase de instrucción ni el prompt se encuentran, usar el texto original
179
+ logger.warning("No se pudo identificar el inicio del guión generado. Usando texto generado completo.")
180
+ cleaned_text = text.strip() # Limpieza básica
181
+
182
 
183
  except Exception as e:
184
  logger.warning(f"Error durante la limpieza heurística del guión de IA: {e}. Usando texto generado sin limpieza adicional.")
185
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Limpieza básica como fallback
186
 
187
+ # Asegurarse de que el texto resultante no sea solo la instrucción o vacío
188
+ if not cleaned_text or len(cleaned_text) < 10: # Umbral de longitud mínima
189
+ 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).")
190
+ cleaned_text = re.sub(r'<[^>]+>', '', text).strip() # Fallback al texto original limpio
191
 
192
+ # Limpieza final de caracteres especiales y espacios sobrantes
193
  cleaned_text = re.sub(r'<[^>]+>', '', cleaned_text).strip()
194
+ cleaned_text = cleaned_text.lstrip(':').strip() # Quitar posibles ':' al inicio
195
+ cleaned_text = cleaned_text.lstrip('.').strip() # Quitar posibles '.' al inicio
196
+
197
 
198
+ # Intentar obtener al menos una oración completa si es posible para un inicio más limpio
199
  sentences = cleaned_text.split('.')
200
  if sentences and sentences[0].strip():
201
  final_text = sentences[0].strip() + '.'
202
+ # Añadir la segunda oración si existe y es razonable
203
+ 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
204
  final_text += " " + sentences[1].strip() + "."
205
+ final_text = final_text.replace("..", ".") # Limpiar doble punto
206
 
207
  logger.info(f"Guion generado final (Truncado a 100 chars): '{final_text[:100]}...'")
208
  return final_text.strip()
209
 
210
  logger.info(f"Guion generado final (sin oraciones completas detectadas - Truncado): '{cleaned_text[:100]}...'")
211
+ return cleaned_text.strip() # Si no se puede formar una oración, devolver el texto limpio tal cual
212
 
213
  except Exception as e:
214
  logger.error(f"Error generando guion con GPT-2 (fuera del bloque de limpieza): {str(e)}", exc_info=True)
215
  logger.warning("Usando prompt original como guion debido al error de generación.")
216
  return prompt.strip()
217
 
218
+ # Función TTS ahora recibe la voz a usar
219
  async def text_to_speech(text, output_path, voice):
220
  logger.info(f"Convirtiendo texto a voz | Caracteres: {len(text)} | Voz: {voice} | Salida: {output_path}")
221
  if not text or not text.strip():
 
386
  logger.info(f"Palabras clave finales: {top_keywords}")
387
  return top_keywords
388
 
389
+ # crear_video ahora recibe la voz seleccionada
390
+ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
391
  logger.info("="*80)
392
  logger.info(f"INICIANDO CREACIÓN DE VIDEO | Tipo: {prompt_type}")
393
  logger.debug(f"Input: '{input_text[:100]}...'")
394
+ logger.info(f"Voz seleccionada para TTS: {selected_voice}")
395
 
396
  start_time = datetime.now()
397
  temp_dir_intermediate = None
 
422
  logger.info(f"Directorio temporal intermedio creado: {temp_dir_intermediate}")
423
  temp_intermediate_files = []
424
 
425
+ # 2. Generar audio de voz usando la voz seleccionada, con reintentos si falla
426
  logger.info("Generando audio de voz...")
427
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
428
 
429
+ tts_voices_to_try = [selected_voice] # Intentar primero la voz seleccionada
430
+ # Añadir voces de respaldo si no están ya en la lista y son diferentes a la seleccionada
431
+ if "es-ES-JuanNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-JuanNeural")
432
+ if "es-ES-ElviraNeural" not in tts_voices_to_try: tts_voices_to_try.append("es-ES-ElviraNeural")
433
+ # Si la lista de voces disponibles es fiable, podrías usar un subconjunto ordenado para reintentos más amplios
434
+ # Ejemplo: for voice_id in [selected_voice] + sorted([v for v in AVAILABLE_VOICES if v.startswith('es-') and v != selected_voice]) + sorted([v for v in AVAILABLE_VOICES if not v.startswith('es-') and v != selected_voice]):
435
+
436
+
437
  tts_success = False
438
+ tried_voices = set() # Usar un set para rastrear voces intentadas de forma eficiente
439
+
440
+ for current_voice in tts_voices_to_try:
441
+ if current_voice in tried_voices: continue # Evitar intentar la misma voz dos veces
442
+ tried_voices.add(current_voice)
443
 
444
+ logger.info(f"Intentando TTS con voz: {current_voice}...")
 
 
 
445
  try:
446
  tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
447
  if tts_success:
448
+ logger.info(f"TTS exitoso con voz '{current_voice}'.")
449
+ break # Salir del bucle de reintentos si tiene éxito
450
  except Exception as e:
451
+ logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
452
+ pass # Continuar al siguiente intento
 
 
 
 
453
 
454
 
455
+ # Verificar si el archivo fue creado después de todos los intentos
456
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
457
+ 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.")
458
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
459
 
460
  temp_intermediate_files.append(voz_path)
 
889
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
890
 
891
 
892
+ # CAMBIO CRÍTICO: run_app ahora recibe TODOS los inputs que Gradio le pasa desde el evento click
893
+ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
894
  logger.info("="*80)
895
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
896
 
 
906
  # Retornar None para video y archivo, actualizar estado con mensaje de error
907
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
908
 
909
+ # Validar la voz seleccionada. Si no es válida, usar la por defecto.
910
+ # AVAILABLE_VOICES se obtiene al inicio.
911
+ if selected_voice not in AVAILABLE_VOICES:
912
+ logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
913
+ selected_voice = DEFAULT_VOICE
914
+ else:
915
+ logger.info(f"Voz seleccionada validada: {selected_voice}")
916
+
917
+
918
  logger.info(f"Tipo de entrada: {prompt_type}")
919
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
920
  if musica_file:
921
  logger.info(f"Archivo de música recibido: {musica_file}")
922
  else:
923
  logger.info("No se proporcionó archivo de música.")
924
+ logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final que se usará
925
 
926
  try:
927
  logger.info("Llamando a crear_video...")
928
+ # Pasar el input_text elegido, la voz seleccionada y el archivo de música a crear_video
929
+ video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
930
 
931
  if video_path and os.path.exists(video_path):
932
  logger.info(f"crear_video retornó path: {video_path}")
 
946
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
947
  finally:
948
  logger.info("Fin del handler run_app.")
 
949
  return output_video, output_file, status_msg
950
 
951
 
952
  # Interfaz de Gradio
953
+ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="""
954
  .gradio-container {max-width: 800px; margin: auto;}
955
  h1 {text-align: center;}
956
  """) as app:
 
967
  )
968
 
969
  # Contenedores para los campos de texto para controlar la visibilidad
 
970
  with gr.Column(visible=True) as ia_guion_column:
971
  prompt_ia = gr.Textbox(
972
  label="Tema para IA",
973
  lines=2,
974
+ placeholder="Ej: Un paisaje natural con montañas y ríos al amanecer, mostrando la belleza de la naturaleza...",
975
  max_lines=4,
976
  value=""
977
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
978
  )
979
 
 
980
  with gr.Column(visible=False) as manual_guion_column:
981
  prompt_manual = gr.Textbox(
982
  label="Tu Guion Completo",
 
984
  placeholder="Ej: En este video exploraremos los misterios del océano. Veremos la vida marina fascinante y los arrecifes de coral vibrantes. ¡Acompáñanos en esta aventura subacuática!",
985
  max_lines=10,
986
  value=""
987
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
988
  )
989
 
990
  musica_input = gr.Audio(
 
992
  type="filepath",
993
  interactive=True,
994
  value=None
995
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
996
  )
997
 
998
  # --- COMPONENTE: Selección de Voz ---
 
1001
  choices=AVAILABLE_VOICES,
1002
  value=DEFAULT_VOICE,
1003
  interactive=True
1004
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1005
  )
1006
  # --- FIN COMPONENTE ---
1007
 
 
1013
  label="Previsualización del Video Generado",
1014
  interactive=False,
1015
  height=400
1016
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1017
  )
1018
  file_output = gr.File(
1019
  label="Descargar Archivo de Video",
1020
  interactive=False,
1021
+ visible=False # <-- ESTÁ BIEN AQUÍ porque su visibilidad se controla por el último then()
1022
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ si ya está visible=False arriba!
1023
  )
1024
  status_output = gr.Textbox(
1025
  label="Estado",
 
1027
  show_label=False,
1028
  placeholder="Esperando acción...",
1029
  value="Esperando entrada..."
1030
+ # visible=... <-- ¡NO DEBE ESTAR AQUÍ!
1031
  )
1032
 
1033
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
 
1035
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1036
  gr.update(visible=x == "Usar Mi Guion")),
1037
  inputs=prompt_type,
1038
+ outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas contenedoras
 
1039
  )
1040
 
1041
  # Evento click del botón de generar video
 
1047
  ).then(
1048
  # Acción 2 (asíncrona): Llamar a la función principal
1049
  run_app,
1050
+ # PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
1051
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- Pasar los 5 inputs a run_app
 
1052
  # run_app retornará los 3 outputs esperados
1053
  outputs=[video_output, file_output, status_output]
1054
  ).then(
1055
  # Acción 3 (síncrona): Hacer visible el enlace de descarga
1056
+ # Recibe las salidas de la Acción 2
1057
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
1058
+ # Inputs para esta lambda son los outputs del .then() anterior
1059
  inputs=[video_output, file_output, status_output],
1060
+ # Actualizamos la visibilidad del componente file_output
1061
  outputs=[file_output]
1062
  )
1063
 
1064
+
1065
  gr.Markdown("### Instrucciones:")
1066
  gr.Markdown("""
1067
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1068
+ 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
1069
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
1070
+ 4. **Selecciona la voz** deseada del desplegable.
1071
+ 5. **Haz clic en "✨ Generar Video"**.
1072
+ 6. Espera a que se procese el video. Verás el estado.
1073
+ 7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** se mostrará si la generación fue exitosa.
1074
+ 8. Revisa `video_generator_full.log` para detalles si hay errores.
 
1075
  """)
1076
  gr.Markdown("---")
1077
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
1083
  try:
1084
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1085
  temp_clip.close()
1086
+ logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
1087
  except Exception as e:
1088
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1089