gnosticdev commited on
Commit
4c089c2
·
verified ·
1 Parent(s): 0e14a87

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +72 -126
app.py CHANGED
@@ -35,7 +35,7 @@ logger.info("="*80)
35
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
36
  if not PEXELS_API_KEY:
37
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
38
- # raise ValueError("API key de Pexels no configurada") # Descomentar para forzar fallo si no está
39
 
40
  # Inicialización de modelos
41
  MODEL_NAME = "datificate/gpt2-small-spanish"
@@ -61,36 +61,6 @@ except Exception as e:
61
  logger.error(f"FALLA al cargar KeyBERT: {str(e)}", exc_info=True)
62
  kw_model = None
63
 
64
- # --- NUEVA FUNCIÓN: Obtener voces de Edge TTS ---
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
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,63 +129,53 @@ def generate_script(prompt, max_length=150):
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,12 +346,10 @@ def extract_visual_keywords_from_script(script_text):
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,37 +380,35 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=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 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 conocida y contiene otras voces españolas, añadirlas también
434
- # Opcional: si AVAILABLE_VOICES es fiable, podrías usar un subconjunto ordenado para reintentos
435
- # Ejemplo: for voice_id in [selected_voice] + [v for v in AVAILABLE_VOICES if v != selected_voice][:2]:
436
-
437
  tts_success = False
438
- tried_voices = []
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 si está duplicada
442
- tried_voices.append(current_voice)
443
- logger.info(f"Intentando TTS con voz: {current_voice}...")
444
  try:
445
  tts_success = asyncio.run(text_to_speech(guion, voz_path, voice=current_voice))
446
  if tts_success:
447
- logger.info(f"TTS exitoso con voz '{current_voice}'.")
448
- break # Salir del bucle de reintentos si tiene éxito
449
  except Exception as e:
450
- logger.warning(f"Fallo al generar TTS con voz '{current_voice}': {str(e)}", exc_info=True)
451
- pass # Continuar al siguiente intento
 
 
 
 
 
452
 
453
- # Verificar si el archivo fue creado después de todos los intentos
454
  if not tts_success or not os.path.exists(voz_path) or os.path.getsize(voz_path) <= 100:
455
- 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.")
456
  raise ValueError("Error generando voz a partir del guion (fallo de TTS).")
457
 
458
  temp_intermediate_files.append(voz_path)
@@ -887,8 +843,8 @@ def crear_video(prompt_type, input_text, selected_voice, musica_file=None):
887
  logger.info(f"Directorio temporal intermedio {temp_dir_intermediate} persistirá para que Gradio lea el video final.")
888
 
889
 
890
- # run_app ahora recibe todos los inputs, incluyendo la voz seleccionada
891
- def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice): # <-- Recibe el valor del Dropdown
892
  logger.info("="*80)
893
  logger.info("SOLICITUD RECIBIDA EN INTERFAZ")
894
 
@@ -901,29 +857,20 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
901
 
902
  if not input_text or not input_text.strip():
903
  logger.warning("Texto de entrada vacío.")
 
904
  return None, None, gr.update(value="⚠️ Por favor, ingresa texto para el guion o el tema.", interactive=False)
905
 
906
- # Validar la voz seleccionada. Si no es válida, usar la por defecto.
907
- # AVAILABLE_VOICES se obtiene al inicio.
908
- if selected_voice not in AVAILABLE_VOICES:
909
- logger.warning(f"Voz seleccionada inválida o no encontrada en la lista: '{selected_voice}'. Usando voz por defecto: {DEFAULT_VOICE}.")
910
- selected_voice = DEFAULT_VOICE
911
- else:
912
- logger.info(f"Voz seleccionada validada: {selected_voice}")
913
-
914
-
915
  logger.info(f"Tipo de entrada: {prompt_type}")
916
  logger.debug(f"Texto de entrada: '{input_text[:100]}...'")
917
  if musica_file:
918
  logger.info(f"Archivo de música recibido: {musica_file}")
919
  else:
920
  logger.info("No se proporcionó archivo de música.")
921
- logger.info(f"Voz final a usar: {selected_voice}") # Loguear la voz final que se usará
922
 
923
  try:
924
  logger.info("Llamando a crear_video...")
925
- # Pasar el input_text elegido, la voz seleccionada y el archivo de música
926
- video_path = crear_video(prompt_type, input_text, selected_voice, musica_file) # <-- PASAR selected_voice a crear_video
927
 
928
  if video_path and os.path.exists(video_path):
929
  logger.info(f"crear_video retornó path: {video_path}")
@@ -943,6 +890,7 @@ def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
943
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}", interactive=False)
944
  finally:
945
  logger.info("Fin del handler run_app.")
 
946
  return output_video, output_file, status_msg
947
 
948
 
@@ -964,6 +912,7 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
964
  )
965
 
966
  # Contenedores para los campos de texto para controlar la visibilidad
 
967
  with gr.Column(visible=True) as ia_guion_column:
968
  prompt_ia = gr.Textbox(
969
  label="Tema para IA",
@@ -989,16 +938,6 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
989
  value=None
990
  )
991
 
992
- # --- COMPONENTE: Selección de Voz ---
993
- voice_dropdown = gr.Dropdown(
994
- label="Seleccionar Voz para Guion",
995
- choices=AVAILABLE_VOICES, # Usar la lista obtenida al inicio
996
- value=DEFAULT_VOICE, # Usar la voz por defecto calculada
997
- interactive=True
998
- )
999
- # --- FIN COMPONENTE ---
1000
-
1001
-
1002
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
1003
 
1004
  with gr.Column():
@@ -1021,31 +960,37 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1021
  )
1022
 
1023
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
 
1024
  prompt_type.change(
1025
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
1026
  gr.update(visible=x == "Usar Mi Guion")),
1027
  inputs=prompt_type,
1028
- outputs=[ia_guion_column, manual_guion_column] # Apuntar a las Columnas
 
1029
  )
1030
 
1031
  # Evento click del botón de generar video
1032
  generate_btn.click(
1033
- # Acción 1 (síncrona): Resetear salidas y establecer estado
 
1034
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
1035
  outputs=[video_output, file_output, status_output],
1036
- queue=True, # Usar la cola de Gradio
1037
  ).then(
1038
- # Acción 2 (asíncrona): Llamar a la función principal
1039
  run_app,
1040
- # PASAR TODOS LOS INPUTS RELEVANTES
1041
- inputs=[prompt_type, prompt_ia, prompt_manual, musica_input, voice_dropdown], # <-- PASAR el dropdown de voz
1042
- # run_app retornará los 3 outputs esperados
1043
  outputs=[video_output, file_output, status_output]
1044
  ).then(
1045
- # Acción 3 (síncrona): Hacer visible el enlace de descarga
1046
- # Recibe las salidas de la Acción 2
 
1047
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
 
1048
  inputs=[video_output, file_output, status_output],
 
1049
  outputs=[file_output]
1050
  )
1051
 
@@ -1053,13 +998,14 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft(), css="
1053
  gr.Markdown("### Instrucciones:")
1054
  gr.Markdown("""
1055
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1056
- 2. **Selecciona el tipo de entrada**: "Generar Guion con IA" o "Usar Mi Guion".
1057
- 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.).
1058
- 4. **Selecciona la voz** deseada del desplegable.
1059
- 5. **Haz clic en "✨ Generar Video"**.
1060
- 6. Espera a que se procese el video. Verás el estado.
1061
- 7. La previsualización aparecerá si es posible, y siempre un enlace **Descargar Archivo de Video** si la generación fue exitosa.
1062
- 8. Revisa `video_generator_full.log` para detalles si hay errores.
 
1063
  """)
1064
  gr.Markdown("---")
1065
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
@@ -1071,7 +1017,7 @@ if __name__ == "__main__":
1071
  try:
1072
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1073
  temp_clip.close()
1074
- logger.info("Clips base de MoviePy creados y cerrados exitosamente. FFmpeg parece accesible.")
1075
  except Exception as e:
1076
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1077
 
 
35
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
36
  if not PEXELS_API_KEY:
37
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
38
+ # raise ValueError("API key de Pexels no configurada")
39
 
40
  # Inicialización de modelos
41
  MODEL_NAME = "datificate/gpt2-small-spanish"
 
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
  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
  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
  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
  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
 
 
857
 
858
  if not input_text or not input_text.strip():
859
  logger.warning("Texto de entrada vacío.")
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
  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
 
 
912
  )
913
 
914
  # Contenedores para los campos de texto para controlar la visibilidad
915
+ # Nombrados para que coincidan con los outputs del evento change
916
  with gr.Column(visible=True) as ia_guion_column:
917
  prompt_ia = gr.Textbox(
918
  label="Tema para IA",
 
938
  value=None
939
  )
940
 
 
 
 
 
 
 
 
 
 
 
941
  generate_btn = gr.Button("✨ Generar Video", variant="primary")
942
 
943
  with gr.Column():
 
960
  )
961
 
962
  # Evento para mostrar/ocultar los campos de texto según el tipo de prompt
963
+ # Apuntar a los componentes Column padre para controlar la visibilidad
964
  prompt_type.change(
965
  lambda x: (gr.update(visible=x == "Generar Guion con IA"),
966
  gr.update(visible=x == "Usar Mi Guion")),
967
  inputs=prompt_type,
968
+ # Pasar los componentes Column
969
+ outputs=[ia_guion_column, manual_guion_column]
970
  )
971
 
972
  # Evento click del botón de generar video
973
  generate_btn.click(
974
+ # Acción 1 (síncrona): Resetear salidas y establecer estado a procesando
975
+ # Retorna None para los 3 outputs iniciales
976
  lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar varios minutos.", interactive=False)),
977
  outputs=[video_output, file_output, status_output],
978
+ queue=True, # Usar la cola de Gradio para tareas largas
979
  ).then(
980
+ # Acción 2 (asíncrona): Llamar a la función principal de procesamiento
981
  run_app,
982
+ # PASAR TODOS LOS INPUTS DE LA INTERFAZ que run_app espera
983
+ inputs=[prompt_type, prompt_ia, prompt_manual, musica_input],
984
+ # run_app retornará los 3 outputs esperados aquí
985
  outputs=[video_output, file_output, status_output]
986
  ).then(
987
+ # Acción 3 (síncrona): Hacer visible el enlace de descarga si se retornó un archivo
988
+ # Esta función recibe las salidas de la Acción 2 (video_path, file_path, status_msg)
989
+ # Solo necesitamos video_path o file_path para decidir si mostrar el enlace
990
  lambda video_path, file_path, status_msg: gr.update(visible=file_path is not None),
991
+ # Inputs son las salidas de la función .then() anterior
992
  inputs=[video_output, file_output, status_output],
993
+ # Actualizamos la visibilidad del componente file_output
994
  outputs=[file_output]
995
  )
996
 
 
998
  gr.Markdown("### Instrucciones:")
999
  gr.Markdown("""
1000
  1. **Clave API de Pexels:** Asegúrate de haber configurado la variable de entorno `PEXELS_API_KEY` con tu clave.
1001
+ 2. **Selecciona el tipo de entrada**:
1002
+ - "Generar Guion con IA": Describe brevemente un tema (ej. "La belleza de las montañas"). La IA generará un guion corto.
1003
+ - "Usar Mi Guion": Escribe el guion completo que quieres para el video.
1004
+ 3. **Sube música** (opcional): Selecciona un archivo de audio (MP3, WAV, etc.) para usar como música de fondo.
1005
+ 4. **Haz clic en "✨ Generar Video"**.
1006
+ 5. Espera a que se procese el video. El tiempo de espera puede variar. Verás el estado en el cuadro de texto.
1007
+ 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.
1008
+ 7. Si hay errores, revisa el log `video_generator_full.log` para más detalles.
1009
  """)
1010
  gr.Markdown("---")
1011
  gr.Markdown("Desarrollado por [Tu Nombre/Empresa/Alias - Opcional]")
 
1017
  try:
1018
  temp_clip = ColorClip((100,100), color=(255,0,0), duration=0.1)
1019
  temp_clip.close()
1020
+ logger.info("Clips base de MoviePy (como ColorClip) creados y cerrados exitosamente. FFmpeg parece accesible.")
1021
  except Exception as e:
1022
  logger.critical(f"Fallo al crear clip base de MoviePy. A menudo indica problemas con FFmpeg/ImageMagick. Error: {e}", exc_info=True)
1023