gnosticdev commited on
Commit
f206a0f
·
verified ·
1 Parent(s): fef178b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +61 -54
app.py CHANGED
@@ -20,19 +20,21 @@ except ImportError:
20
  logger = logging.getLogger(__name__)
21
  logger.info("Intentando instalar moviepy e imageio-ffmpeg...")
22
  try:
23
- subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", "--force-reinstall", "moviepy>=1.0.3", "imageio-ffmpeg>=0.5.1"])
24
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
25
  logger.info("MoviePy instalado tras reintento")
26
  except Exception as e:
27
  logger.critical(f"Fallo al instalar moviepy: {str(e)}")
28
- logger.info("Continuando con placeholder. Video no se generará, solo archivo vacío.")
29
- moviepy = None
30
  import re
31
  import math
32
  import shutil
33
  import json
34
  from collections import Counter
 
35
 
 
36
  logging.basicConfig(
37
  level=logging.DEBUG,
38
  format='%(asctime)s - %(levelname)s - %(message)s',
@@ -45,6 +47,7 @@ logger.info("="*80)
45
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
46
  logger.info("="*80)
47
 
 
48
  VOCES_DISPONIBLES = {
49
  "Español (España)": {
50
  "es-ES-JuanNeural": "Juan (España) - Masculino",
@@ -107,6 +110,7 @@ VOCES_DISPONIBLES = {
107
  }
108
  }
109
 
 
110
  def get_voice_choices():
111
  choices = []
112
  for region, voices in VOCES_DISPONIBLES.items():
@@ -114,8 +118,9 @@ def get_voice_choices():
114
  choices.append((f"{voice_name} ({region})", voice_id))
115
  return choices
116
 
 
117
  AVAILABLE_VOICES = get_voice_choices()
118
- DEFAULT_VOICE_ID = "es-MX-DaliaNeural"
119
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
120
  for text, voice_id in AVAILABLE_VOICES:
121
  if voice_id == DEFAULT_VOICE_ID:
@@ -126,10 +131,12 @@ if DEFAULT_VOICE_ID not in [v[1] for v in AVAILABLE_VOICES]:
126
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Dalia (México) - Femenino"
127
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
128
 
 
129
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
130
  if not PEXELS_API_KEY:
131
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
132
 
 
133
  MODEL_NAME = "datificate/gpt2-small-spanish"
134
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
135
  tokenizer = None
@@ -157,6 +164,7 @@ def buscar_videos_pexels(query, api_key, per_page=5):
157
  if not api_key:
158
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
159
  return []
 
160
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
161
  headers = {"Authorization": api_key}
162
  try:
@@ -192,8 +200,10 @@ def generate_script(prompt, max_length=150):
192
  if not tokenizer or not model:
193
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
194
  return prompt.strip()
 
195
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
196
  ai_prompt = f"{instruction_phrase_start} {prompt}"
 
197
  try:
198
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
199
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -254,6 +264,7 @@ async def text_to_speech(text, output_path, voice):
254
  logger.warning(f"edge_tts falló, intentando gTTS...")
255
  except Exception as e:
256
  logger.error(f"Error en edge_tts con voz '{voice}': {str(e)}")
 
257
  try:
258
  tts = gTTS(text=text, lang='es')
259
  tts.save(output_path)
@@ -372,14 +383,15 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
372
  video_final = None
373
  source_clips = []
374
  clips_to_concatenate = []
375
- output_path = None
376
 
377
  try:
 
378
  guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
379
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
380
  if not guion.strip():
381
  raise ValueError("El guion está vacío.")
382
 
 
383
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
384
  tts_voices_to_try = [selected_voice, "es-MX-DaliaNeural"]
385
  tts_success = False
@@ -433,16 +445,18 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
433
  audio_duration = audio_tts_original.duration
434
  else:
435
  logger.warning("MoviePy no disponible, asumiendo duración mínima para audio")
436
- audio_duration = 1.0
437
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
438
  if audio_duration < 1.0:
439
  raise ValueError("Audio de voz demasiado corto.")
440
 
 
441
  keywords = extract_visual_keywords_from_script(guion)
442
  if not keywords:
443
  keywords = ["video", "background"]
444
  logger.info(f"Palabras clave: {keywords}")
445
 
 
446
  videos_data = []
447
  total_desired_videos = 10
448
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
@@ -477,13 +491,15 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
477
  if not video_paths:
478
  raise ValueError("No se descargaron videos utilizables.")
479
 
 
480
  if not moviepy:
481
  logger.warning("MoviePy no disponible, retornando placeholder...")
482
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
483
- output_path = os.path.join(temp_dir_intermediate, output_filename)
484
- open(output_path, 'a').close()
485
- download_url = f"https://gnosticdev-invideo-basic.hf.space/file={output_path}"
486
- return output_path, download_url
 
487
 
488
  current_duration = 0
489
  min_clip_duration = 0.5
@@ -512,6 +528,7 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
512
  if video_base.duration is None or video_base.duration <= 0:
513
  raise ValueError("Video base inválido.")
514
 
 
515
  if video_base.duration < audio_duration:
516
  num_full_repeats = int(audio_duration // video_base.duration)
517
  remaining_duration = audio_duration % video_base.duration
@@ -523,6 +540,7 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
523
  elif video_base.duration > audio_duration:
524
  video_base = video_base.subclip(0, audio_duration)
525
 
 
526
  final_audio = audio_tts
527
  if musica_file:
528
  try:
@@ -543,9 +561,13 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
543
  if abs(final_audio.duration - video_base.duration) > 0.2:
544
  final_audio = final_audio.subclip(0, video_base.duration)
545
 
 
546
  video_final = video_base.set_audio(final_audio)
547
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
548
  output_path = os.path.join(temp_dir_intermediate, output_filename)
 
 
 
549
 
550
  video_final.write_videofile(
551
  output_path,
@@ -558,12 +580,13 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
558
  logger='bar'
559
  )
560
 
561
- download_url = f"https://gnosticdev-invideo-basic.hf.space/file={output_path}"
562
- logger.info(f"Video guardado en: {output_path}")
 
563
  logger.info(f"URL de descarga: {download_url}")
564
  total_time = (datetime.now() - start_time).total_seconds()
565
  logger.info(f"Video generado en {total_time:.2f}s")
566
- return output_path, download_url
567
 
568
  except ValueError as ve:
569
  logger.error(f"Error controlado: {str(ve)}")
@@ -603,13 +626,13 @@ async def crear_video_async(prompt_type, input_text, selected_voice, musica_file
603
  except:
604
  pass
605
  for path in temp_intermediate_files:
606
- if os.path.isfile(path) and (output_path is None or path != output_path):
607
  try:
608
  os.remove(path)
609
  except:
610
  logger.warning(f"No se pudo eliminar {path}")
611
  try:
612
- if os.path.exists(temp_dir_intermediate) and (output_path is None or not output_path.startswith(temp_dir_intermediate)):
613
  shutil.rmtree(temp_dir_intermediate)
614
  except:
615
  logger.warning(f"No se pudo eliminar directorio temporal {temp_dir_intermediate}")
@@ -620,7 +643,7 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
620
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
621
  output_video = None
622
  output_file = None
623
- status_msg = gr.update(value="⏳ Iniciando generación de video...")
624
 
625
  if not input_text or not input_text.strip():
626
  logger.warning("Texto de entrada vacío.")
@@ -632,53 +655,34 @@ async def run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, sele
632
  selected_voice = DEFAULT_VOICE_ID
633
 
634
  try:
635
- task = asyncio.create_task(crear_video_async(prompt_type, input_text, selected_voice, musica_file))
636
- timeout = 600
637
- interval = 5
638
- elapsed = 0
639
- while elapsed < timeout:
640
- if task.done():
641
- video_path, download_url = await task
642
- if video_path and os.path.exists(video_path):
643
- output_video = video_path
644
- output_file = video_path
645
- if not moviepy and os.path.getsize(video_path) <= 100:
646
- status_msg = gr.update(value=f"⚠️ Video no generado (MoviePy no disponible). Archivo vacío: {download_url} (descárgalo pronto, /tmp es temporal). Revisa video_generator_full.log.")
647
- else:
648
- status_msg = gr.update(value=f"✅ Video generado exitosamente. Descarga: {download_url} (Nota: Archivos en /tmp son temporales, descárgalo pronto)")
649
- logger.info(f"Retornando video_path: {video_path}, URL: {download_url}")
650
- return output_video, gr.File(value=output_file, label="Descargar Video"), status_msg
651
- else:
652
- status_msg = gr.update(value="❌ Error: Falló la generación del video.")
653
- logger.error("No se generó video_path válido.")
654
- return None, None, status_msg
655
- await asyncio.sleep(interval)
656
- elapsed += interval
657
- status_msg = gr.update(value=f"⏳ Procesando... Tiempo transcurrido: {elapsed}s")
658
- logger.debug(f"Esperando video, tiempo transcurrido: {elapsed}s")
659
- logger.error("Tiempo de espera excedido para la generación del video.")
660
- status_msg = gr.update(value="❌ Error: Tiempo de espera excedido (10 minutos).")
661
- task.cancel()
662
- try:
663
- await task
664
- except asyncio.CancelledError:
665
- pass
666
- return None, None, status_msg
667
  except ValueError as ve:
668
  logger.warning(f"Error de validación: {str(ve)}")
669
  status_msg = gr.update(value=f"⚠️ Error: {str(ve)}")
670
- return None, None, status_msg
671
  except Exception as e:
672
  logger.critical(f"Error crítico: {str(e)}")
673
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}")
674
- return None, None, status_msg
 
 
675
 
676
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
677
  return asyncio.run(run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice))
678
 
 
679
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as app:
680
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
681
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
 
682
  with gr.Row():
683
  with gr.Column():
684
  prompt_type = gr.Radio(
@@ -729,13 +733,15 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as ap
729
  placeholder="Esperando acción...",
730
  value="Esperando entrada..."
731
  )
 
732
  prompt_type.change(
733
  fn=lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")),
734
  inputs=prompt_type,
735
  outputs=[ia_guion_column, manual_guion_column]
736
  )
 
737
  generate_btn.click(
738
- fn=lambda: (None, None, gr.update(value="⏳ Iniciando generación de video...")),
739
  outputs=[video_output, file_output, status_output]
740
  ).then(
741
  fn=run_app,
@@ -743,10 +749,11 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as ap
743
  outputs=[video_output, file_output, status_output],
744
  queue=True
745
  ).then(
746
- fn=lambda video_path, file_output, status_msg: gr.update(visible=bool(file_output)),
747
  inputs=[video_output, file_output, status_output],
748
  outputs=[file_output]
749
  )
 
750
  gr.Markdown("### Instrucciones:")
751
  gr.Markdown("""
752
  1. Configura la variable de entorno `PEXELS_API_KEY`.
@@ -754,8 +761,8 @@ with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as ap
754
  3. Sube música (opcional).
755
  4. Selecciona la voz.
756
  5. Haz clic en "✨ Generar Video".
757
- 6. Revisa el estado. Si el video se genera, estará disponible en /tmp (descárgalo pronto, es temporal).
758
- 7. Si el video es 0 KB, revisa video_generator_full.log (probable fallo de MoviePy).
759
  """)
760
 
761
  if __name__ == "__main__":
 
20
  logger = logging.getLogger(__name__)
21
  logger.info("Intentando instalar moviepy e imageio-ffmpeg...")
22
  try:
23
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", "moviepy>=1.0.3", "imageio-ffmpeg>=0.5.1"])
24
  from moviepy.editor import VideoFileClip, concatenate_videoclips, AudioFileClip, CompositeAudioClip, concatenate_audioclips, AudioClip
25
  logger.info("MoviePy instalado tras reintento")
26
  except Exception as e:
27
  logger.critical(f"Fallo al instalar moviepy: {str(e)}")
28
+ logger.info("Continuando con placeholder para pruebas...")
29
+ moviepy = None # Placeholder para evitar errores
30
  import re
31
  import math
32
  import shutil
33
  import json
34
  from collections import Counter
35
+ import time
36
 
37
+ # Configuración de logging
38
  logging.basicConfig(
39
  level=logging.DEBUG,
40
  format='%(asctime)s - %(levelname)s - %(message)s',
 
47
  logger.info("INICIO DE EJECUCIÓN - GENERADOR DE VIDEOS")
48
  logger.info("="*80)
49
 
50
+ # Diccionario de voces TTS disponibles organizadas por idioma
51
  VOCES_DISPONIBLES = {
52
  "Español (España)": {
53
  "es-ES-JuanNeural": "Juan (España) - Masculino",
 
110
  }
111
  }
112
 
113
+ # Función para obtener lista plana de voces para el dropdown
114
  def get_voice_choices():
115
  choices = []
116
  for region, voices in VOCES_DISPONIBLES.items():
 
118
  choices.append((f"{voice_name} ({region})", voice_id))
119
  return choices
120
 
121
+ # Obtener las voces al inicio del script
122
  AVAILABLE_VOICES = get_voice_choices()
123
+ DEFAULT_VOICE_ID = "es-MX-DaliaNeural" # Voz más estable
124
  DEFAULT_VOICE_NAME = DEFAULT_VOICE_ID
125
  for text, voice_id in AVAILABLE_VOICES:
126
  if voice_id == DEFAULT_VOICE_ID:
 
131
  DEFAULT_VOICE_NAME = AVAILABLE_VOICES[0][0] if AVAILABLE_VOICES else "Dalia (México) - Femenino"
132
  logger.info(f"Voz por defecto seleccionada (ID): {DEFAULT_VOICE_ID}")
133
 
134
+ # Clave API de Pexels
135
  PEXELS_API_KEY = os.environ.get("PEXELS_API_KEY")
136
  if not PEXELS_API_KEY:
137
  logger.critical("NO SE ENCONTRÓ PEXELS_API_KEY EN VARIABLES DE ENTORNO")
138
 
139
+ # Inicialización de modelos
140
  MODEL_NAME = "datificate/gpt2-small-spanish"
141
  logger.info(f"Inicializando modelo GPT-2: {MODEL_NAME}")
142
  tokenizer = None
 
164
  if not api_key:
165
  logger.warning("No se puede buscar en Pexels: API Key no configurada.")
166
  return []
167
+
168
  logger.debug(f"Buscando en Pexels: '{query}' | Resultados: {per_page}")
169
  headers = {"Authorization": api_key}
170
  try:
 
200
  if not tokenizer or not model:
201
  logger.warning("Modelos GPT-2 no disponibles - Usando prompt original como guion.")
202
  return prompt.strip()
203
+
204
  instruction_phrase_start = "Escribe un guion corto, interesante y coherente sobre:"
205
  ai_prompt = f"{instruction_phrase_start} {prompt}"
206
+
207
  try:
208
  inputs = tokenizer(ai_prompt, return_tensors="pt", truncation=True, max_length=512)
209
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
 
264
  logger.warning(f"edge_tts falló, intentando gTTS...")
265
  except Exception as e:
266
  logger.error(f"Error en edge_tts con voz '{voice}': {str(e)}")
267
+
268
  try:
269
  tts = gTTS(text=text, lang='es')
270
  tts.save(output_path)
 
383
  video_final = None
384
  source_clips = []
385
  clips_to_concatenate = []
 
386
 
387
  try:
388
+ # 1. Generar o usar guion
389
  guion = generate_script(input_text) if prompt_type == "Generar Guion con IA" else input_text.strip()
390
  logger.info(f"Guion final ({len(guion)} chars): '{guion[:100]}...'")
391
  if not guion.strip():
392
  raise ValueError("El guion está vacío.")
393
 
394
+ # 2. Generar audio de voz
395
  voz_path = os.path.join(temp_dir_intermediate, "voz.mp3")
396
  tts_voices_to_try = [selected_voice, "es-MX-DaliaNeural"]
397
  tts_success = False
 
445
  audio_duration = audio_tts_original.duration
446
  else:
447
  logger.warning("MoviePy no disponible, asumiendo duración mínima para audio")
448
+ audio_duration = 1.0 # Valor placeholder
449
  logger.info(f"Duración audio voz: {audio_duration:.2f} segundos")
450
  if audio_duration < 1.0:
451
  raise ValueError("Audio de voz demasiado corto.")
452
 
453
+ # 3. Extraer palabras clave
454
  keywords = extract_visual_keywords_from_script(guion)
455
  if not keywords:
456
  keywords = ["video", "background"]
457
  logger.info(f"Palabras clave: {keywords}")
458
 
459
+ # 4. Buscar y descargar videos
460
  videos_data = []
461
  total_desired_videos = 10
462
  per_page_per_keyword = max(1, total_desired_videos // len(keywords))
 
491
  if not video_paths:
492
  raise ValueError("No se descargaron videos utilizables.")
493
 
494
+ # 5. Procesar y concatenar clips de video
495
  if not moviepy:
496
  logger.warning("MoviePy no disponible, retornando placeholder...")
497
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
498
+ persistent_path = os.path.join("/data", output_filename)
499
+ os.makedirs("/data", exist_ok=True)
500
+ open(persistent_path, 'a').close() # Crea archivo vacío como placeholder
501
+ download_url = f"https://gnosticdev-invideo-basic.hf.space/file={persistent_path}"
502
+ return persistent_path, download_url
503
 
504
  current_duration = 0
505
  min_clip_duration = 0.5
 
528
  if video_base.duration is None or video_base.duration <= 0:
529
  raise ValueError("Video base inválido.")
530
 
531
+ # Ajustar duración del video
532
  if video_base.duration < audio_duration:
533
  num_full_repeats = int(audio_duration // video_base.duration)
534
  remaining_duration = audio_duration % video_base.duration
 
540
  elif video_base.duration > audio_duration:
541
  video_base = video_base.subclip(0, audio_duration)
542
 
543
+ # 6. Manejar música de fondo
544
  final_audio = audio_tts
545
  if musica_file:
546
  try:
 
561
  if abs(final_audio.duration - video_base.duration) > 0.2:
562
  final_audio = final_audio.subclip(0, video_base.duration)
563
 
564
+ # 7. Combinar audio y video
565
  video_final = video_base.set_audio(final_audio)
566
  output_filename = f"video_{int(datetime.now().timestamp())}.mp4"
567
  output_path = os.path.join(temp_dir_intermediate, output_filename)
568
+ persistent_dir = "/data"
569
+ os.makedirs(persistent_dir, exist_ok=True)
570
+ persistent_path = os.path.join(persistent_dir, output_filename)
571
 
572
  video_final.write_videofile(
573
  output_path,
 
580
  logger='bar'
581
  )
582
 
583
+ shutil.move(output_path, persistent_path)
584
+ download_url = f"https://gnosticdev-invideo-basic.hf.space/file={persistent_path}"
585
+ logger.info(f"Video guardado en: {persistent_path}")
586
  logger.info(f"URL de descarga: {download_url}")
587
  total_time = (datetime.now() - start_time).total_seconds()
588
  logger.info(f"Video generado en {total_time:.2f}s")
589
+ return persistent_path, download_url
590
 
591
  except ValueError as ve:
592
  logger.error(f"Error controlado: {str(ve)}")
 
626
  except:
627
  pass
628
  for path in temp_intermediate_files:
629
+ if os.path.isfile(path) and path != persistent_path:
630
  try:
631
  os.remove(path)
632
  except:
633
  logger.warning(f"No se pudo eliminar {path}")
634
  try:
635
+ if os.path.exists(temp_dir_intermediate):
636
  shutil.rmtree(temp_dir_intermediate)
637
  except:
638
  logger.warning(f"No se pudo eliminar directorio temporal {temp_dir_intermediate}")
 
643
  input_text = prompt_ia if prompt_type == "Generar Guion con IA" else prompt_manual
644
  output_video = None
645
  output_file = None
646
+ status_msg = gr.update(value="⏳ Procesando... Esto puede tomar hasta 1 hora.")
647
 
648
  if not input_text or not input_text.strip():
649
  logger.warning("Texto de entrada vacío.")
 
655
  selected_voice = DEFAULT_VOICE_ID
656
 
657
  try:
658
+ logger.info("Iniciando generación de video...")
659
+ video_path, download_url = await crear_video_async(prompt_type, input_text, selected_voice, musica_file)
660
+ if video_path and os.path.exists(video_path):
661
+ output_video = video_path
662
+ output_file = video_path
663
+ status_msg = gr.update(value=f"✅ Video generado exitosamente. Descarga: {download_url}")
664
+ logger.info(f"Retornando video_path: {video_path}, URL: {download_url}")
665
+ else:
666
+ status_msg = gr.update(value="❌ Error: Falló la generación del video.")
667
+ logger.error("No se generó video_path válido.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
668
  except ValueError as ve:
669
  logger.warning(f"Error de validación: {str(ve)}")
670
  status_msg = gr.update(value=f"⚠️ Error: {str(ve)}")
 
671
  except Exception as e:
672
  logger.critical(f"Error crítico: {str(e)}")
673
  status_msg = gr.update(value=f"❌ Error inesperado: {str(e)}")
674
+ finally:
675
+ logger.info("Finalizando run_app_async")
676
+ return output_video, gr.File(value=output_file, label="Descargar Video"), status_msg
677
 
678
  def run_app(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice):
679
  return asyncio.run(run_app_async(prompt_type, prompt_ia, prompt_manual, musica_file, selected_voice))
680
 
681
+ # Interfaz de Gradio
682
  with gr.Blocks(title="Generador de Videos con IA", theme=gr.themes.Soft()) as app:
683
  gr.Markdown("# 🎬 Generador Automático de Videos con IA")
684
  gr.Markdown("Genera videos cortos a partir de un tema o guion, usando imágenes de archivo de Pexels y voz generada.")
685
+
686
  with gr.Row():
687
  with gr.Column():
688
  prompt_type = gr.Radio(
 
733
  placeholder="Esperando acción...",
734
  value="Esperando entrada..."
735
  )
736
+
737
  prompt_type.change(
738
  fn=lambda x: (gr.update(visible=x == "Generar Guion con IA"), gr.update(visible=x == "Usar Mi Guion")),
739
  inputs=prompt_type,
740
  outputs=[ia_guion_column, manual_guion_column]
741
  )
742
+
743
  generate_btn.click(
744
+ fn=lambda: (None, None, gr.update(value="⏳ Procesando... Esto puede tomar hasta 1 hora.")),
745
  outputs=[video_output, file_output, status_output]
746
  ).then(
747
  fn=run_app,
 
749
  outputs=[video_output, file_output, status_output],
750
  queue=True
751
  ).then(
752
+ fn=lambda video_path, file_output, status_msg: gr.update(visible=file_output.value is not None),
753
  inputs=[video_output, file_output, status_output],
754
  outputs=[file_output]
755
  )
756
+
757
  gr.Markdown("### Instrucciones:")
758
  gr.Markdown("""
759
  1. Configura la variable de entorno `PEXELS_API_KEY`.
 
761
  3. Sube música (opcional).
762
  4. Selecciona la voz.
763
  5. Haz clic en "✨ Generar Video".
764
+ 6. Revisa el estado. Si el video se genera, estará disponible en /data.
765
+ 7. Consulta `video_generator_full.log` para detalles.
766
  """)
767
 
768
  if __name__ == "__main__":