#!/usr/bin/env python3 """ Generador de video final CORREGIDO - 30fps Crea highlights con las muertes detectadas por OCR-GPU """ import json import os import subprocess from datetime import timedelta import logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s") logger = logging.getLogger(__name__) def format_time(seconds): return str(timedelta(seconds=int(seconds))) def extract_clip_correct(video_path, start_sec, end_sec, output_file): """Extrae clip manteniendo 30fps original""" duration = end_sec - start_sec cmd = [ "ffmpeg", "-y", "-ss", str(start_sec), "-t", str(duration), "-i", video_path, "-c:v", "libx264", # Re-encodear para asegurar consistencia "-preset", "fast", "-crf", "23", "-r", "30", # Forzar 30fps "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "128k", output_file, ] try: subprocess.run(cmd, capture_output=True, check=True, timeout=60) return True except Exception as e: logger.error(f"Error extrayendo clip: {e}") return False def group_nearby_deaths(deaths, min_gap=30): """Agrupa muertes que están cercanas para evitar clips repetidos""" if not deaths: return [] # Ordenar por timestamp sorted_deaths = sorted(deaths, key=lambda x: x.get("timestamp", 0)) groups = [] current_group = [sorted_deaths[0]] for death in sorted_deaths[1:]: if death.get("timestamp", 0) - current_group[-1].get("timestamp", 0) < min_gap: # Muerte cercana, agregar al grupo current_group.append(death) else: # Muerte lejana, cerrar grupo y empezar nuevo groups.append(current_group) current_group = [death] # Agregar último grupo if current_group: groups.append(current_group) return groups def create_final_video(video_path, deaths, output_file="HIGHLIGHTS_FINAL_30FPS.mp4"): """Crea video final concatenando clips de muertes""" logger.info("=" * 70) logger.info("GENERANDO VIDEO FINAL - 30 FPS") logger.info("=" * 70) os.makedirs("clips_final", exist_ok=True) # Agrupar muertes cercanas death_groups = group_nearby_deaths(deaths, min_gap=30) logger.info(f"Detectadas {len(deaths)} muertes en {len(death_groups)} grupos") # Extraer cada grupo como clip clip_files = [] for i, group in enumerate(death_groups[:10], 1): # Máximo 10 clips # Calcular rango del grupo timestamps = [d.get("timestamp", 0) for d in group] group_start = min(timestamps) group_end = max(timestamps) # Calcular timestamps del clip clip_start = max(0, group_start - 10) # 10s antes del primero clip_end = group_end + 15 # 15s después del último # Asegurar duración mínima de 20 segundos if clip_end - clip_start < 20: clip_end = clip_start + 20 clip_file = f"clips_final/group_{i:02d}_{int(group_start)}.mp4" death_nums = ", ".join([str(d.get("death_number", "?")) for d in group]) logger.info( f"[{i}/{len(death_groups)}] Extrayendo grupo {i} (muertes: {death_nums})" ) logger.info(f" Rango: {format_time(clip_start)} - {format_time(clip_end)}") if extract_clip_correct(video_path, clip_start, clip_end, clip_file): clip_files.append(clip_file) logger.info(f" ✓ Clip extraído: {clip_file}") if not clip_files: logger.error("No se pudieron extraer clips") return None # Crear archivo de concatenación concat_file = "/tmp/concat_final.txt" with open(concat_file, "w") as f: for clip in clip_files: f.write(f"file '{os.path.abspath(clip)}'\n") # Concatenar todo logger.info("\nConcatenando clips...") cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, "-c:v", "libx264", "-preset", "medium", "-crf", "20", "-r", "30", # Forzar 30fps en salida "-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "128k", output_file, ] try: subprocess.run(cmd, capture_output=True, check=True, timeout=120) logger.info(f"✓ Video final creado: {output_file}") # Verificar check = subprocess.run( [ "ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "default=noprint_wrappers=1:nokey=1", output_file, ], capture_output=True, text=True, ) logger.info(f" FPS del video: {check.stdout.strip()}") return output_file except Exception as e: logger.error(f"Error creando video final: {e}") return None def main(): # Usar video 1080p60 video_path = "/home/ren/proyectos/editor/twitch-highlight-detector/stream_2699641307_1080p60.mp4" # Cargar muertes detectadas (1080p60) deaths_file = ( "/home/ren/proyectos/editor/twitch-highlight-detector/deaths_1080p60_final.json" ) if not os.path.exists(deaths_file): logger.error(f"No existe: {deaths_file}") logger.info("Ejecuta primero: python3 detect_deaths_ocr_gpu.py") return with open(deaths_file, "r") as f: data = json.load(f) deaths = data.get("deaths", []) logger.info(f"Cargadas {len(deaths)} muertes detectadas") # Crear video final_video = create_final_video(video_path, deaths) if final_video: logger.info("\n" + "=" * 70) logger.info("✓ VIDEO FINAL GENERADO CORRECTAMENTE") logger.info(f" Archivo: {final_video}") logger.info(" FPS: 30") logger.info("=" * 70) if __name__ == "__main__": main()