Files
twitch-highlight-detector/detector_segunda_pasada.py
renato97 00180d0b1c Sistema completo de detección de highlights con VLM y análisis de gameplay
- 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
2026-02-19 17:38:14 +00:00

181 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
Segunda pasada: Filtra los mejores clips para reducir a máximo 15 minutos.
Prioriza: Rage/insultos > Muertes de aliados > Jugadas épicas
"""
import json
import logging
import re
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def score_highlights(transcripcion_json, highlights_json, max_duration=900):
"""
Analiza los clips existentes y les da un puntaje.
Devuelve los mejores clips hasta max_duration segundos.
"""
logger.info("=== SEGUNDA PASADA - Filtrando mejores momentos ===")
with open(transcripcion_json, 'r', encoding='utf-8') as f:
trans_data = json.load(f)
with open(highlights_json, 'r') as f:
highlights = json.load(f)
segments = trans_data.get("segments", [])
# Patrones de alta prioridad para puntuar clips
priority_patterns = {
"rage_extreme": [ # 100 puntos - MUY buenos clips
r'\bputa (madre|mikdre)\b',
r'\bretrasados?\b.*\bmentales?\b',
r'\bbinguno de (ustedes|vosotros)\b',
r'\babsol[úu]to (inter|in[úu]til|retrasado)\b',
r'\binfumable\b',
r'\basqueroso\w*\b',
r'\bbasura\b',
],
"ally_death": [ # 80 puntos - Tilt triggers
r'\b(ha muerto|murio|muri[óo]|falleci[óo]) (un aliado|un teammate|el compa)\b',
r'\b(se ha muerto|mur[ií]o) el (top|jungla|support|adc)\b',
r'\b(perdemos|perdi|perdiste) al (top|jg|support)\b',
],
"epic_plays": [ # 70 puntos - Jugadas épicas
r'\b(triple|quadra|penta)( kill)?\b',
r'\b(pentakill|ace)\b',
r'\bbaron\b.*\b(steal|rob[ao])\b',
r'\bdrag[oó]n\b.*\b(steal|rob[ao])\b',
r'\bnashor\b.*\b(steal|rob[ao])\b',
],
"insultos": [ # 60 puntos - Insultos varios
r'\b(retrasado|imbecil|est[úu]pido|idiota|burro|tonto|mongolo)\b',
r'\bcaraj[oó]\b',
r'\bhostia\w*\b',
r'\bc[áa]gatear\b',
],
"skills": [ # 40 puntos - Habilidades
r'\b(ulti|ultimate|h)\b',
r'\bflash\b',
r'\bsmite|ignite|exhaust|teleport|heal\b',
],
}
# Analizar cada clip y asignar puntaje
scored_clips = []
for start, end in highlights:
clip_duration = end - start
# Buscar segmentos dentro del clip
text_in_clip = []
for seg in segments:
seg_start = seg["start"]
seg_end = seg["end"]
# Si el segmento está dentro o solapa con el clip
if not (seg_end < start or seg_start > end):
text_in_clip.append(seg["text"])
# Unir texto del clip
clip_text = " ".join(text_in_clip).lower()
# Calcular puntaje
score = 0
matched_types = []
for event_type, patterns in priority_patterns.items():
for pattern in patterns:
if re.search(pattern, clip_text, re.IGNORECASE):
if event_type == "rage_extreme":
score += 100
elif event_type == "ally_death":
score += 80
elif event_type == "epic_plays":
score += 70
elif event_type == "insultos":
score += 60
elif event_type == "skills":
score += 40
if event_type not in matched_types:
matched_types.append(event_type)
break
# Bonus por duración (clips más largos tienen más contexto)
if clip_duration > 60:
score += 20
elif clip_duration > 45:
score += 10
scored_clips.append({
"start": start,
"end": end,
"duration": clip_duration,
"score": score,
"types": matched_types
})
# Ordenar por puntaje descendente
scored_clips.sort(key=lambda x: (-x["score"], x["start"]))
# Seleccionar clips hasta max_duration
selected = []
total_duration = 0
logger.info("\n=== TOP CLIPS SELECCIONADOS ===")
logger.info(f"Puntaje | Duración | Tipo | Timestamp")
logger.info("-" * 60)
for clip in scored_clips:
if total_duration + clip["duration"] > max_duration:
# Si este clip excede el límite, intentar incluirlo si hay espacio
remaining = max_duration - total_duration
if remaining >= 30: # Solo si hay espacio para al menos 30s
# Recortar el clip
selected.append((clip["start"], clip["start"] + remaining))
total_duration += remaining
logger.info(f"{clip['score']:3d}* | {remaining:3d}s | {clip['types']} | {clip['start']}")
break
selected.append((clip["start"], clip["end"]))
total_duration += clip["duration"]
types_str = ", ".join(clip['types'])
logger.info(f"{clip['score']:3d} | {int(clip['duration']):3d}s | {types_str} | {clip['start']}")
# Ordenar selected por timestamp
selected.sort()
logger.info("-" * 60)
logger.info(f"Total: {len(selected)} clips, {int(total_duration)}s ({total_duration/60:.1f} min)")
return selected
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--transcripcion", required=True)
parser.add_argument("--highlights", required=True)
parser.add_argument("--output", default="highlights_finales.json")
parser.add_argument("--max-duration", type=int, default=900, help="Duración máxima en segundos (default: 900s = 15min)")
args = parser.parse_args()
selected = score_highlights(
args.transcripcion,
args.highlights,
args.max_duration
)
# Guardar
with open(args.output, 'w') as f:
json.dump(selected, f)
logger.info(f"Guardado en {args.output}")
if __name__ == "__main__":
main()