- Agregado intentos.md con registro de todos los fallos - Actualizado contexto.md con sesión de noche - MCP op.gg instalado (no funcionó - 0 matches) - OCR con Tesseract y EasyOCR (falló - texto muy pequeño) - Video final generado: HIGHLIGHTS_MUERTES_COMPLETO.mp4 - Juegos separados: JUEGO_1/2/3_COMPLETO.mp4 - 10 muertes secuenciales: 0/1→0/10 - Scripts de extracción automática con timestamps
227 lines
6.1 KiB
Python
227 lines
6.1 KiB
Python
#!/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()
|