gnosticdev commited on
Commit
986adda
·
verified ·
1 Parent(s): cc81e3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -123
app.py CHANGED
@@ -14,7 +14,8 @@ from moviepy.editor import (
14
  AudioClip,
15
  TextClip,
16
  CompositeVideoClip,
17
- VideoClip
 
18
  )
19
  import numpy as np
20
  import json
@@ -31,19 +32,15 @@ import time
31
  from datetime import datetime, timedelta
32
 
33
  # ------------------- FIX PARA PILLOW -------------------
34
- # Solución para el error de ANTIALIAS en versiones nuevas de Pillow
35
  try:
36
  from PIL import Image
37
  if not hasattr(Image, 'ANTIALIAS'):
38
- # Para versiones nuevas de Pillow
39
  Image.ANTIALIAS = Image.Resampling.LANCZOS
40
  except ImportError:
41
  pass
42
 
43
- # ------------------- Configuración de Timeout -------------------
44
- os.environ["GRADIO_SERVER_TIMEOUT"] = "3800" # 30 minutos en segundos
45
-
46
  # ------------------- Configuración & Globals -------------------
 
47
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
48
  logger = logging.getLogger(__name__)
49
  PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
@@ -62,7 +59,6 @@ class EdgeTTSEngine:
62
  logger.info(f"Inicializando Edge TTS con voz: {voice}")
63
 
64
  async def _synthesize_async(self, text, output_path):
65
- """Sintetiza texto a voz usando Edge TTS de forma asíncrona"""
66
  try:
67
  communicate = edge_tts.Communicate(text, self.voice)
68
  await communicate.save(output_path)
@@ -72,15 +68,12 @@ class EdgeTTSEngine:
72
  return False
73
 
74
  def synthesize(self, text, output_path):
75
- """Sintetiza texto a voz (wrapper síncrono)"""
76
  try:
77
- # Ejecutar la función async en un nuevo loop
78
  return asyncio.run(self._synthesize_async(text, output_path))
79
  except Exception as e:
80
  logger.error(f"Error al sintetizar con Edge TTS: {e}")
81
  return False
82
 
83
- # Instancia global del motor TTS
84
  tts_engine = EdgeTTSEngine()
85
 
86
  # ------------------- Carga Perezosa de Modelos -------------------
@@ -114,7 +107,6 @@ def update_task_progress(task_id, message):
114
  logger.info(f"[{task_id}] {message}")
115
 
116
  def gpt2_script(prompt: str) -> str:
117
- """Genera un guión usando GPT-2"""
118
  try:
119
  local_tokenizer = get_tokenizer()
120
  local_gpt2_model = get_gpt2_model()
@@ -143,7 +135,6 @@ def gpt2_script(prompt: str) -> str:
143
  return f"Hoy hablaremos sobre {prompt}. Este es un tema fascinante que merece nuestra atención."
144
 
145
  def generate_tts_audio(text: str, output_path: str) -> bool:
146
- """Genera audio usando Edge TTS"""
147
  try:
148
  logger.info("Generando audio con Edge TTS...")
149
  success = tts_engine.synthesize(text, output_path)
@@ -158,7 +149,6 @@ def generate_tts_audio(text: str, output_path: str) -> bool:
158
  return False
159
 
160
  def extract_keywords(text: str) -> list[str]:
161
- """Extrae palabras clave del texto para búsqueda de videos"""
162
  try:
163
  local_kw_model = get_kw_model()
164
  clean_text = re.sub(r"[^\w\sáéíóúñÁÉÍÓÚÑ]", "", text.lower())
@@ -184,7 +174,6 @@ def extract_keywords(text: str) -> list[str]:
184
  "symbolism", "occult", "eerie", "haunting", "unexplained", "forbidden knowledge", "redacted", "conspiracy theorist"]
185
 
186
  def search_pexels_videos(query: str, count: int = 3) -> list[dict]:
187
- """Busca videos en Pexels"""
188
  if not PEXELS_API_KEY:
189
  return []
190
 
@@ -202,7 +191,6 @@ def search_pexels_videos(query: str, count: int = 3) -> list[dict]:
202
  return []
203
 
204
  def download_video(url: str, folder: str) -> str | None:
205
- """Descarga un video desde URL"""
206
  try:
207
  filename = f"{uuid.uuid4().hex}.mp4"
208
  filepath = os.path.join(folder, filename)
@@ -224,7 +212,6 @@ def download_video(url: str, folder: str) -> str | None:
224
  return None
225
 
226
  def create_subtitle_clips(script: str, video_width: int, video_height: int, duration: float):
227
- """Crea clips de subtítulos"""
228
  try:
229
  sentences = [s.strip() for s in re.split(r"[.!?¿¡]", script) if s.strip()]
230
  if not sentences:
@@ -273,7 +260,6 @@ def create_subtitle_clips(script: str, video_width: int, video_height: int, dura
273
  return []
274
 
275
  def loop_audio_to_duration(audio_clip: AudioFileClip, target_duration: float) -> AudioFileClip:
276
- """Hace loop del audio hasta alcanzar la duración objetivo"""
277
  if audio_clip is None:
278
  return None
279
  try:
@@ -289,12 +275,10 @@ def loop_audio_to_duration(audio_clip: AudioFileClip, target_duration: float) ->
289
 
290
  def create_video(script_text: str, generate_script: bool, music_path: str | None, task_id: str) -> str:
291
  temp_dir = tempfile.mkdtemp()
292
- # Constantes para normalización
293
  TARGET_FPS = 24
294
- TARGET_RESOLUTION = (1280, 720) # (ancho, alto)
295
 
296
  def normalize_clip(clip):
297
- """Normaliza un clip de video a resolución y FPS estándar"""
298
  if clip is None:
299
  return None
300
  try:
@@ -345,17 +329,16 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
345
  video_paths = []
346
  keywords = extract_keywords(script)
347
 
348
- for i, keyword in enumerate(keywords[:3]): # Límite de 3 keywords
349
  update_task_progress(task_id, f"Paso 3/7: Buscando videos para '{keyword}' ({i+1}/{len(keywords[:3])})")
350
 
351
  videos = search_pexels_videos(keyword, 2)
352
  for video_data in videos:
353
- if len(video_paths) >= 6: # Límite de 6 videos
354
  break
355
 
356
  video_files = video_data.get("video_files", [])
357
  if video_files:
358
- # Tomar el video de mejor calidad
359
  best_file = max(video_files, key=lambda f: f.get("width", 0))
360
  video_url = best_file.get("link")
361
 
@@ -374,19 +357,39 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
374
  for path in video_paths:
375
  clip = None
376
  try:
 
377
  clip = VideoFileClip(path)
378
- if clip is None:
 
 
 
379
  continue
380
-
 
 
 
 
 
 
 
 
381
  # Tomar máximo 8 segundos de cada clip
382
  duration = min(8, clip.duration)
 
 
383
  processed_clip = clip.subclip(0, duration)
 
 
 
 
384
 
385
- # Normalizar el clip (resolución y FPS)
386
  processed_clip = normalize_clip(processed_clip)
387
-
388
  if processed_clip is not None:
389
  video_clips.append(processed_clip)
 
 
 
390
 
391
  except Exception as e:
392
  logger.error(f"Error procesando video {path}: {e}")
@@ -394,52 +397,35 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
394
  if clip is not None:
395
  clip.close()
396
 
 
397
  if not video_clips:
398
- raise RuntimeError("No se pudieron procesar los videos")
399
-
400
- # Concatenar videos
401
- base_video = None
402
- try:
 
 
 
 
403
  base_video = concatenate_videoclips(video_clips, method="chain")
404
- if base_video is None:
405
- raise RuntimeError("No se pudo concatenar los videos")
406
- except Exception as e:
407
- if base_video is not None:
408
- base_video.close()
409
- raise e
410
-
411
- # Extender video si es más corto que el audio con transiciones suaves
412
- if base_video.duration < video_duration:
413
- fade_duration = 0.5 # segundos de fundido
414
- loops_needed = math.ceil(video_duration / base_video.duration)
415
-
416
- # Crear una lista de clips para el loop
417
- looped_clips = [base_video]
418
- for _ in range(loops_needed - 1):
419
- # Crear un clip con fundido de entrada para la transición
420
- fade_in_clip = base_video.crossfadein(fade_duration)
421
- if fade_in_clip is not None:
422
- looped_clips.append(fade_in_clip)
423
- looped_clips.append(base_video)
424
 
425
- try:
 
 
 
 
 
 
 
 
 
 
 
426
  base_video = concatenate_videoclips(looped_clips)
427
- if base_video is None:
428
- raise RuntimeError("No se pudo extender el video")
429
- except Exception as e:
430
- if base_video is not None:
431
- base_video.close()
432
- raise e
433
-
434
- # Asegurar que el video tenga la duración exacta del audio
435
- try:
436
  base_video = base_video.subclip(0, video_duration)
437
- if base_video is None:
438
- raise RuntimeError("No se pudo recortar el video")
439
- except Exception as e:
440
- if base_video is not None:
441
- base_video.close()
442
- raise e
443
 
444
  # Paso 5: Componer audio final
445
  update_task_progress(task_id, "Paso 5/7: Componiendo audio...")
@@ -466,60 +452,46 @@ def create_video(script_text: str, generate_script: bool, music_path: str | None
466
  if subtitle_clips:
467
  try:
468
  base_video = CompositeVideoClip([base_video] + subtitle_clips)
469
- if base_video is None:
470
- raise RuntimeError("No se pudo agregar subtítulos")
471
  except Exception as e:
472
  logger.error(f"Error creando video con subtítulos: {e}")
473
- # Continuar sin subtítulos si falla
474
 
475
  # Paso 7: Renderizar video final
476
  update_task_progress(task_id, "Paso 7/7: Renderizando video final...")
477
- final_video = None
478
- try:
479
- final_video = base_video.set_audio(final_audio)
480
- if final_video is None:
481
- raise RuntimeError("No se pudo combinar video y audio")
482
-
483
- output_path = os.path.join(RESULTS_DIR, f"video_{task_id}.mp4")
484
- final_video.write_videofile(
485
- output_path,
486
- fps=TARGET_FPS,
487
- codec="libx264",
488
- audio_codec="aac", # Mejor calidad de audio
489
- bitrate="8000k", # Controlar calidad de video
490
- threads=4, # Mejor uso de CPU
491
- preset="slow", # Mejor compresión
492
- logger=None,
493
- verbose=False
494
- )
495
-
496
- return output_path
497
- except Exception as e:
498
- raise e
499
- finally:
500
- # Limpiar clips
501
- if voice_clip is not None:
502
- voice_clip.close()
503
- if base_video is not None:
504
- base_video.close()
505
- if final_video is not None:
506
- final_video.close()
507
- for clip in video_clips:
508
- if clip is not None:
509
- clip.close()
510
 
511
  except Exception as e:
512
  logger.error(f"Error creando video: {e}")
513
  raise
514
  finally:
515
- # Limpiar directorio temporal
516
  try:
517
  shutil.rmtree(temp_dir)
518
  except:
519
  pass
520
 
521
  def worker_thread(task_id: str, mode: str, topic: str, user_script: str, music_path: str | None):
522
- """Hilo worker para procesamiento de video"""
523
  try:
524
  generate_script = (mode == "Generar Guion con IA")
525
  content = topic if generate_script else user_script
@@ -541,14 +513,11 @@ def worker_thread(task_id: str, mode: str, topic: str, user_script: str, music_p
541
  })
542
 
543
  def generate_video_with_progress(mode, topic, user_script, music):
544
- """Función principal que maneja la generación con progreso en tiempo real"""
545
- # Validar entrada
546
  content = topic if mode == "Generar Guion con IA" else user_script
547
  if not content or not content.strip():
548
  yield "❌ Error: Por favor, ingresa un tema o guion.", None, None
549
  return
550
 
551
- # Crear tarea
552
  task_id = uuid.uuid4().hex[:8]
553
  TASKS[task_id] = {
554
  "status": "processing",
@@ -556,7 +525,6 @@ def generate_video_with_progress(mode, topic, user_script, music):
556
  "timestamp": datetime.utcnow()
557
  }
558
 
559
- # Iniciar worker
560
  worker = threading.Thread(
561
  target=worker_thread,
562
  args=(task_id, mode, topic, user_script, music),
@@ -564,12 +532,10 @@ def generate_video_with_progress(mode, topic, user_script, music):
564
  )
565
  worker.start()
566
 
567
- # Monitorear progreso
568
  while TASKS[task_id]["status"] == "processing":
569
  yield TASKS[task_id]['progress_log'], None, None
570
  time.sleep(1)
571
 
572
- # Retornar resultado final
573
  if TASKS[task_id]["status"] == "error":
574
  yield TASKS[task_id]['progress_log'], None, None
575
  elif TASKS[task_id]["status"] == "done":
@@ -578,10 +544,9 @@ def generate_video_with_progress(mode, topic, user_script, music):
578
 
579
  # ------------------- Limpieza automática -------------------
580
  def cleanup_old_files():
581
- """Limpia archivos antiguos cada hora"""
582
  while True:
583
  try:
584
- time.sleep(6600) # 1 hora
585
  now = datetime.utcnow()
586
  logger.info("Ejecutando limpieza de archivos antiguos...")
587
 
@@ -598,18 +563,15 @@ def cleanup_old_files():
598
  except Exception as e:
599
  logger.error(f"Error en cleanup: {e}")
600
 
601
- # Iniciar hilo de limpieza
602
  threading.Thread(target=cleanup_old_files, daemon=True).start()
603
 
604
  # ------------------- Interfaz Gradio -------------------
605
  def toggle_input_fields(mode):
606
- """Alterna los campos de entrada según el modo seleccionado"""
607
  return (
608
  gr.update(visible=mode == "Generar Guion con IA"),
609
  gr.update(visible=mode != "Generar Guion con IA")
610
  )
611
 
612
- # Crear interfaz
613
  with gr.Blocks(title="🎬 Generador de Videos IA", theme=gr.themes.Soft()) as demo:
614
  gr.Markdown("""
615
  # 🎬 Generador de Videos con IA
@@ -676,7 +638,6 @@ with gr.Blocks(title="🎬 Generador de Videos IA", theme=gr.themes.Soft()) as d
676
  label="📥 Descargar archivo"
677
  )
678
 
679
- # Event handlers
680
  mode_radio.change(
681
  fn=toggle_input_fields,
682
  inputs=[mode_radio],
@@ -699,14 +660,9 @@ with gr.Blocks(title="🎬 Generador de Videos IA", theme=gr.themes.Soft()) as d
699
  ⏱️ **Tiempo estimado**: 2-5 minutos dependiendo de la duración del contenido.
700
  """)
701
 
702
- # Ejecutar aplicación
703
  if __name__ == "__main__":
704
  logger.info("🚀 Iniciando aplicación Generador de Videos IA...")
705
-
706
- # Configurar la cola (versión compatible)
707
  demo.queue(max_size=10)
708
-
709
- # Lanzar aplicación (parámetros básicos compatibles)
710
  demo.launch(
711
  server_name="0.0.0.0",
712
  server_port=7860,
 
14
  AudioClip,
15
  TextClip,
16
  CompositeVideoClip,
17
+ VideoClip,
18
+ ColorClip # Importar para video de respaldo
19
  )
20
  import numpy as np
21
  import json
 
32
  from datetime import datetime, timedelta
33
 
34
  # ------------------- FIX PARA PILLOW -------------------
 
35
  try:
36
  from PIL import Image
37
  if not hasattr(Image, 'ANTIALIAS'):
 
38
  Image.ANTIALIAS = Image.Resampling.LANCZOS
39
  except ImportError:
40
  pass
41
 
 
 
 
42
  # ------------------- Configuración & Globals -------------------
43
+ os.environ["GRADIO_SERVER_TIMEOUT"] = "3800"
44
  logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
45
  logger = logging.getLogger(__name__)
46
  PEXELS_API_KEY = os.getenv("PEXELS_API_KEY")
 
59
  logger.info(f"Inicializando Edge TTS con voz: {voice}")
60
 
61
  async def _synthesize_async(self, text, output_path):
 
62
  try:
63
  communicate = edge_tts.Communicate(text, self.voice)
64
  await communicate.save(output_path)
 
68
  return False
69
 
70
  def synthesize(self, text, output_path):
 
71
  try:
 
72
  return asyncio.run(self._synthesize_async(text, output_path))
73
  except Exception as e:
74
  logger.error(f"Error al sintetizar con Edge TTS: {e}")
75
  return False
76
 
 
77
  tts_engine = EdgeTTSEngine()
78
 
79
  # ------------------- Carga Perezosa de Modelos -------------------
 
107
  logger.info(f"[{task_id}] {message}")
108
 
109
  def gpt2_script(prompt: str) -> str:
 
110
  try:
111
  local_tokenizer = get_tokenizer()
112
  local_gpt2_model = get_gpt2_model()
 
135
  return f"Hoy hablaremos sobre {prompt}. Este es un tema fascinante que merece nuestra atención."
136
 
137
  def generate_tts_audio(text: str, output_path: str) -> bool:
 
138
  try:
139
  logger.info("Generando audio con Edge TTS...")
140
  success = tts_engine.synthesize(text, output_path)
 
149
  return False
150
 
151
  def extract_keywords(text: str) -> list[str]:
 
152
  try:
153
  local_kw_model = get_kw_model()
154
  clean_text = re.sub(r"[^\w\sáéíóúñÁÉÍÓÚÑ]", "", text.lower())
 
174
  "symbolism", "occult", "eerie", "haunting", "unexplained", "forbidden knowledge", "redacted", "conspiracy theorist"]
175
 
176
  def search_pexels_videos(query: str, count: int = 3) -> list[dict]:
 
177
  if not PEXELS_API_KEY:
178
  return []
179
 
 
191
  return []
192
 
193
  def download_video(url: str, folder: str) -> str | None:
 
194
  try:
195
  filename = f"{uuid.uuid4().hex}.mp4"
196
  filepath = os.path.join(folder, filename)
 
212
  return None
213
 
214
  def create_subtitle_clips(script: str, video_width: int, video_height: int, duration: float):
 
215
  try:
216
  sentences = [s.strip() for s in re.split(r"[.!?¿¡]", script) if s.strip()]
217
  if not sentences:
 
260
  return []
261
 
262
  def loop_audio_to_duration(audio_clip: AudioFileClip, target_duration: float) -> AudioFileClip:
 
263
  if audio_clip is None:
264
  return None
265
  try:
 
275
 
276
  def create_video(script_text: str, generate_script: bool, music_path: str | None, task_id: str) -> str:
277
  temp_dir = tempfile.mkdtemp()
 
278
  TARGET_FPS = 24
279
+ TARGET_RESOLUTION = (1280, 720)
280
 
281
  def normalize_clip(clip):
 
282
  if clip is None:
283
  return None
284
  try:
 
329
  video_paths = []
330
  keywords = extract_keywords(script)
331
 
332
+ for i, keyword in enumerate(keywords[:3]):
333
  update_task_progress(task_id, f"Paso 3/7: Buscando videos para '{keyword}' ({i+1}/{len(keywords[:3])})")
334
 
335
  videos = search_pexels_videos(keyword, 2)
336
  for video_data in videos:
337
+ if len(video_paths) >= 6:
338
  break
339
 
340
  video_files = video_data.get("video_files", [])
341
  if video_files:
 
342
  best_file = max(video_files, key=lambda f: f.get("width", 0))
343
  video_url = best_file.get("link")
344
 
 
357
  for path in video_paths:
358
  clip = None
359
  try:
360
+ # 1. Validar inmediatamente después de cargar cada video
361
  clip = VideoFileClip(path)
362
+ if clip is None or clip.duration <= 0:
363
+ logger.error(f"Video inválido: {path}")
364
+ if clip is not None:
365
+ clip.close()
366
  continue
367
+
368
+ # 2. Probar cada clip antes de usarlo
369
+ try:
370
+ test_frame = clip.get_frame(0)
371
+ except Exception as e:
372
+ logger.error(f"Video corrupto: {path} - {e}")
373
+ clip.close()
374
+ continue
375
+
376
  # Tomar máximo 8 segundos de cada clip
377
  duration = min(8, clip.duration)
378
+
379
+ # 4. Verificar cada operación crítica
380
  processed_clip = clip.subclip(0, duration)
381
+ if processed_clip is None:
382
+ logger.error("Error al recortar video")
383
+ clip.close()
384
+ continue
385
 
386
+ # Normalizar el clip
387
  processed_clip = normalize_clip(processed_clip)
 
388
  if processed_clip is not None:
389
  video_clips.append(processed_clip)
390
+ else:
391
+ logger.error(f"Error normalizando video: {path}")
392
+ processed_clip.close()
393
 
394
  except Exception as e:
395
  logger.error(f"Error procesando video {path}: {e}")
 
397
  if clip is not None:
398
  clip.close()
399
 
400
+ # 3. Manejar explícitamente los casos donde no hay videos válidos
401
  if not video_clips:
402
+ logger.warning("No se procesaron videos válidos, creando video de respaldo...")
403
+ base_video = ColorClip(
404
+ size=TARGET_RESOLUTION,
405
+ color=(0, 0, 0),
406
+ duration=video_duration
407
+ )
408
+ base_video.fps = TARGET_FPS
409
+ else:
410
+ # Concatenar videos
411
  base_video = concatenate_videoclips(video_clips, method="chain")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
+ # Extender video si es más corto que el audio
414
+ if base_video.duration < video_duration:
415
+ fade_duration = 0.5
416
+ loops_needed = math.ceil(video_duration / base_video.duration)
417
+
418
+ looped_clips = [base_video]
419
+ for _ in range(loops_needed - 1):
420
+ fade_in_clip = base_video.crossfadein(fade_duration)
421
+ if fade_in_clip is not None:
422
+ looped_clips.append(fade_in_clip)
423
+ looped_clips.append(base_video)
424
+
425
  base_video = concatenate_videoclips(looped_clips)
426
+
427
+ # Asegurar duración exacta
 
 
 
 
 
 
 
428
  base_video = base_video.subclip(0, video_duration)
 
 
 
 
 
 
429
 
430
  # Paso 5: Componer audio final
431
  update_task_progress(task_id, "Paso 5/7: Componiendo audio...")
 
452
  if subtitle_clips:
453
  try:
454
  base_video = CompositeVideoClip([base_video] + subtitle_clips)
 
 
455
  except Exception as e:
456
  logger.error(f"Error creando video con subtítulos: {e}")
 
457
 
458
  # Paso 7: Renderizar video final
459
  update_task_progress(task_id, "Paso 7/7: Renderizando video final...")
460
+ final_video = base_video.set_audio(final_audio)
461
+
462
+ output_path = os.path.join(RESULTS_DIR, f"video_{task_id}.mp4")
463
+ final_video.write_videofile(
464
+ output_path,
465
+ fps=TARGET_FPS,
466
+ codec="libx264",
467
+ audio_codec="aac",
468
+ bitrate="8000k",
469
+ threads=4,
470
+ preset="slow",
471
+ logger=None,
472
+ verbose=False
473
+ )
474
+
475
+ # Limpiar clips
476
+ voice_clip.close()
477
+ base_video.close()
478
+ final_video.close()
479
+ for clip in video_clips:
480
+ if clip is not None:
481
+ clip.close()
482
+
483
+ return output_path
 
 
 
 
 
 
 
 
 
484
 
485
  except Exception as e:
486
  logger.error(f"Error creando video: {e}")
487
  raise
488
  finally:
 
489
  try:
490
  shutil.rmtree(temp_dir)
491
  except:
492
  pass
493
 
494
  def worker_thread(task_id: str, mode: str, topic: str, user_script: str, music_path: str | None):
 
495
  try:
496
  generate_script = (mode == "Generar Guion con IA")
497
  content = topic if generate_script else user_script
 
513
  })
514
 
515
  def generate_video_with_progress(mode, topic, user_script, music):
 
 
516
  content = topic if mode == "Generar Guion con IA" else user_script
517
  if not content or not content.strip():
518
  yield "❌ Error: Por favor, ingresa un tema o guion.", None, None
519
  return
520
 
 
521
  task_id = uuid.uuid4().hex[:8]
522
  TASKS[task_id] = {
523
  "status": "processing",
 
525
  "timestamp": datetime.utcnow()
526
  }
527
 
 
528
  worker = threading.Thread(
529
  target=worker_thread,
530
  args=(task_id, mode, topic, user_script, music),
 
532
  )
533
  worker.start()
534
 
 
535
  while TASKS[task_id]["status"] == "processing":
536
  yield TASKS[task_id]['progress_log'], None, None
537
  time.sleep(1)
538
 
 
539
  if TASKS[task_id]["status"] == "error":
540
  yield TASKS[task_id]['progress_log'], None, None
541
  elif TASKS[task_id]["status"] == "done":
 
544
 
545
  # ------------------- Limpieza automática -------------------
546
  def cleanup_old_files():
 
547
  while True:
548
  try:
549
+ time.sleep(6600)
550
  now = datetime.utcnow()
551
  logger.info("Ejecutando limpieza de archivos antiguos...")
552
 
 
563
  except Exception as e:
564
  logger.error(f"Error en cleanup: {e}")
565
 
 
566
  threading.Thread(target=cleanup_old_files, daemon=True).start()
567
 
568
  # ------------------- Interfaz Gradio -------------------
569
  def toggle_input_fields(mode):
 
570
  return (
571
  gr.update(visible=mode == "Generar Guion con IA"),
572
  gr.update(visible=mode != "Generar Guion con IA")
573
  )
574
 
 
575
  with gr.Blocks(title="🎬 Generador de Videos IA", theme=gr.themes.Soft()) as demo:
576
  gr.Markdown("""
577
  # 🎬 Generador de Videos con IA
 
638
  label="📥 Descargar archivo"
639
  )
640
 
 
641
  mode_radio.change(
642
  fn=toggle_input_fields,
643
  inputs=[mode_radio],
 
660
  ⏱️ **Tiempo estimado**: 2-5 minutos dependiendo de la duración del contenido.
661
  """)
662
 
 
663
  if __name__ == "__main__":
664
  logger.info("🚀 Iniciando aplicación Generador de Videos IA...")
 
 
665
  demo.queue(max_size=10)
 
 
666
  demo.launch(
667
  server_name="0.0.0.0",
668
  server_port=7860,