Spaces:
Running
Running
Update app.py
Browse files
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)
|
| 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]):
|
| 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:
|
| 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
|
| 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 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
base_video = concatenate_videoclips(looped_clips)
|
| 427 |
-
|
| 428 |
-
|
| 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 =
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 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)
|
| 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,
|