- Implementación de detector híbrido (Whisper + Chat + Audio + VLM) - Sistema de detección de gameplay real vs hablando - Scene detection con FFmpeg - Soporte para RTX 3050 y RX 6800 XT - Guía completa en 6800xt.md para próxima IA - Scripts de filtrado visual y análisis de contexto - Pipeline automatizado de generación de videos
218 lines
6.9 KiB
Python
218 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Workflow de dos pasadas para highlights:
|
|
1. 360p (rápido) → Previsualización → Confirmación usuario
|
|
2. 1080p (calidad) → Video final
|
|
"""
|
|
import subprocess
|
|
import json
|
|
import sys
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def download_video(video_id, quality="360p", output="video.mp4"):
|
|
"""Descarga video con streamlink"""
|
|
logger.info(f"Descargando video en {quality}...")
|
|
|
|
# Mapeo de calidad
|
|
quality_map = {
|
|
"360p": "360p,480p,best",
|
|
"1080p": "1080p,720p,best"
|
|
}
|
|
|
|
cmd = [
|
|
"streamlink",
|
|
f"https://www.twitch.tv/videos/{video_id}",
|
|
quality_map[quality],
|
|
"-o", output
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True)
|
|
if result.returncode != 0:
|
|
logger.error(f"Error descargando video: {result.stderr.decode()}")
|
|
return False
|
|
return True
|
|
|
|
def download_chat(video_id, output="chat.json"):
|
|
"""Descarga chat con TwitchDownloaderCLI"""
|
|
logger.info(f"Descargando chat...")
|
|
|
|
cmd = ["dotnet", "/tmp/TDC_output/TwitchDownloaderCLI.dll",
|
|
"chatdownload", "--id", video_id, "-o", output]
|
|
|
|
result = subprocess.run(cmd, capture_output=True)
|
|
return result.returncode == 0
|
|
|
|
def detect_highlights(video, chat, output="highlights.json",
|
|
threshold=0.8, min_duration=5):
|
|
"""Detecta highlights con GPU"""
|
|
logger.info(f"Detectando highlights (threshold={threshold}, min_duration={min_duration})...")
|
|
|
|
cmd = [
|
|
"python3", "detector_gpu.py",
|
|
"--video", video,
|
|
"--chat", chat,
|
|
"--output", output,
|
|
"--threshold", str(threshold),
|
|
"--min-duration", str(min_duration),
|
|
"--device", "cuda"
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
print(result.stdout)
|
|
|
|
# Cargar resultados
|
|
with open(output, 'r') as f:
|
|
highlights = json.load(f)
|
|
|
|
return highlights
|
|
|
|
def generate_summary(video, highlights, output, padding=5):
|
|
"""Genera video resumen"""
|
|
logger.info(f"Generando video resumen ({len(highlights)} clips)...")
|
|
|
|
cmd = [
|
|
"python3", "generate_video.py",
|
|
"--video", video,
|
|
"--highlights", highlights,
|
|
"--output", output,
|
|
"--padding", str(padding)
|
|
]
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
print(result.stdout)
|
|
return result.returncode == 0
|
|
|
|
def format_timestamp(seconds):
|
|
"""Formatea segundos a HH:MM:SS"""
|
|
hours = seconds // 3600
|
|
minutes = (seconds % 3600) // 60
|
|
secs = seconds % 60
|
|
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
|
|
|
def show_highlights(highlights):
|
|
"""Muestra resumen de highlights"""
|
|
total_duration = sum(e - s for s, e in highlights)
|
|
|
|
print("\n" + "=" * 60)
|
|
print("HIGHLIGHTS DETECTADOS".center(60))
|
|
print("=" * 60)
|
|
print(f"Total: {len(highlights)} clips")
|
|
print(f"Duración total: {total_duration}s ({total_duration/60:.1f} minutos)")
|
|
print("-" * 60)
|
|
|
|
for i, (start, end) in enumerate(highlights[:20], 1):
|
|
duration = end - start
|
|
print(f"{i:2d}. {format_timestamp(start)} - {format_timestamp(end)} ({duration}s)")
|
|
|
|
if len(highlights) > 20:
|
|
print(f"... y {len(highlights) - 20} más")
|
|
|
|
print("=" * 60)
|
|
|
|
def confirm_action():
|
|
"""Pide confirmación al usuario"""
|
|
response = input("\n¿Generar versión en 1080p? (s/n): ").strip().lower()
|
|
return response in ['s', 'si', 'y', 'yes']
|
|
|
|
def main():
|
|
import argparse
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--video-id", required=True)
|
|
parser.add_argument("--threshold", type=float, default=0.8)
|
|
parser.add_argument("--min-duration", type=int, default=8)
|
|
parser.add_argument("--skip-360p", action="store_true")
|
|
parser.add_argument("--force-1080p", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
video_id = args.video_id
|
|
|
|
if args.force_1080p:
|
|
# Directo a 1080p
|
|
logger.info("Modo: Generar directamente en 1080p")
|
|
video_file = f"stream_{video_id}_1080p.mp4"
|
|
chat_file = f"chat_{video_id}.json"
|
|
highlights_file = f"highlights_{video_id}.json"
|
|
output_file = f"resumen_{video_id}_1080p.mp4"
|
|
|
|
if not download_video(video_id, "1080p", video_file):
|
|
return 1
|
|
|
|
if not download_chat(video_id, chat_file):
|
|
return 1
|
|
|
|
highlights = detect_highlights(video_file, chat_file, highlights_file,
|
|
args.threshold, args.min_duration)
|
|
show_highlights(highlights)
|
|
|
|
generate_summary(video_file, highlights_file, output_file)
|
|
print(f"\n✅ Video final: {output_file}")
|
|
|
|
elif not args.skip_360p:
|
|
# PASADA 1: 360p (previsualización rápida)
|
|
logger.info("PASADA 1: Procesando en 360p para previsualización")
|
|
|
|
video_360 = f"stream_{video_id}_360p.mp4"
|
|
chat_file = f"chat_{video_id}.json"
|
|
highlights_file = f"highlights_{video_id}.json"
|
|
output_360 = f"preview_{video_id}_360p.mp4"
|
|
|
|
# Descargar 360p
|
|
if not download_video(video_id, "360p", video_360):
|
|
return 1
|
|
|
|
# Descargar chat
|
|
if not download_chat(video_id, chat_file):
|
|
return 1
|
|
|
|
# Detectar highlights
|
|
highlights = detect_highlights(video_360, chat_file, highlights_file,
|
|
args.threshold, args.min_duration)
|
|
|
|
if not highlights:
|
|
print("❌ No se detectaron highlights. Intenta con threshold más bajo.")
|
|
return 1
|
|
|
|
# Mostrar resultados
|
|
show_highlights(highlights)
|
|
|
|
# Generar previsualización 360p
|
|
generate_summary(video_360, highlights_file, output_360)
|
|
print(f"\n📺 Previsualización: {output_360}")
|
|
|
|
# Confirmar para 1080p
|
|
if confirm_action():
|
|
# PASADA 2: 1080p (calidad final)
|
|
logger.info("PASADA 2: Procesando en 1080p para calidad final")
|
|
|
|
video_1080 = f"stream_{video_id}_1080p.mp4"
|
|
output_1080 = f"resumen_{video_id}_1080p.mp4"
|
|
|
|
print(f"\n⏳ Descargando video en 1080p...")
|
|
if not download_video(video_id, "1080p", video_1080):
|
|
print("❌ Error descargando en 1080p. Puedes usar el video 360p.")
|
|
return 1
|
|
|
|
# Reusar highlights ya detectados
|
|
generate_summary(video_1080, highlights_file, output_1080)
|
|
|
|
print(f"\n✅ Video final 1080p: {output_1080}")
|
|
print(f"📺 Previsualización 360p: {output_360}")
|
|
print(f"📊 Highlights: {highlights_file}")
|
|
else:
|
|
print("\n✅ Proceso cancelado. Puedes usar:")
|
|
print(f" - Video 360p: {video_360}")
|
|
print(f" - Previsualización: {output_360}")
|
|
print(f" - Highlights JSON: {highlights_file}")
|
|
|
|
else:
|
|
print("Usa --force-1080p para generar directamente en 1080p")
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|