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
This commit is contained in:
200
detector_eventos.py
Normal file
200
detector_eventos.py
Normal file
@@ -0,0 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Detector de EVENTOS DE JUEGO:
|
||||
Busca momentos específicos: muertes de aliados, habilidades, objetivos
|
||||
EXTIENDE mucho los clips para no cortar la acción.
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def detect_game_events(transcripcion_json, intro_skip=120, clip_duration=45, padding_after=15, top=40):
|
||||
"""
|
||||
Detecta eventos específicos del juego y extiende los clips.
|
||||
"""
|
||||
logger.info("=== Buscando EVENTOS DE JUEGO ===")
|
||||
|
||||
with open(transcripcion_json, 'r', encoding='utf-8') as f:
|
||||
trans_data = json.load(f)
|
||||
|
||||
segments = trans_data.get("segments", [])
|
||||
|
||||
# Eventos de ALIADO MUERTO / TILT MOMENTS
|
||||
ally_death_events = [
|
||||
r'\b(ha muerto|murio|muri[óo]|falleci[óo]) (un aliado|un teammate|el compa|el compa[ñn]ero|mi equipo|mis aliados|el team)\b',
|
||||
r'\b(teammate|compa|compa[ñn]ero) (ha muerto|murio|muri[óo])\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',
|
||||
r'\breport|reporteo\b',
|
||||
r'\b(thank|gracias) (for|por) the (gank|help|kill)\b', # sarcasmo tras muerte
|
||||
]
|
||||
|
||||
# Eventos de HABILIDAD / JUGADAS
|
||||
skill_events = [
|
||||
r'\b(ulti|ultimate|h)|habilidad ultimate\b',
|
||||
r'\bflash\b.*\b(in|out|en)\b',
|
||||
r'\b(smite|ignite|exhaust|teleport|heal)\b',
|
||||
r'\btriple|quadra|penta\b',
|
||||
r'\b(ace|pentakill)\b',
|
||||
r'\bbaron\b.*\b(bait|steal|take)\b',
|
||||
r'\bdrag[oó]n\b.*\b(bait|steal|take)\b',
|
||||
r'\binhib\b.*\b(bait|steal|take)\b',
|
||||
r'\b(nashor|elder)\b',
|
||||
r'\b(base|nexus)\b.*\b(destroy|se cae|ca[íi]go)\b',
|
||||
]
|
||||
|
||||
# Eventos de INSULTO / RAGE (buenos clips)
|
||||
rage_events = [
|
||||
r'\b(retrasado|imbecil|est[úu]pido|idiota|burro|tonto|mongolo)\b',
|
||||
r'\bputa (madre|mikdre)\b',
|
||||
r'\bcaraj[oó]\b',
|
||||
r'\bhostia\b',
|
||||
r'\bmierda\b',
|
||||
r'\bme la suda\b',
|
||||
r'\bc[áa]gatear\b',
|
||||
r'\b(inteles|bots|afk)\b',
|
||||
]
|
||||
|
||||
# Combinar todos los patrones
|
||||
all_patterns = {
|
||||
"ally_death": ally_death_events,
|
||||
"skill": skill_events,
|
||||
"rage": rage_events
|
||||
}
|
||||
|
||||
# Analizar segmentos
|
||||
events = []
|
||||
|
||||
for seg in segments:
|
||||
start = seg["start"]
|
||||
end = seg["end"]
|
||||
text = seg["text"]
|
||||
text_lower = text.lower()
|
||||
|
||||
# Saltar intro
|
||||
if start < intro_skip:
|
||||
continue
|
||||
|
||||
for event_type, patterns in all_patterns.items():
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, text_lower, re.IGNORECASE):
|
||||
events.append({
|
||||
"start": start,
|
||||
"end": end,
|
||||
"type": event_type,
|
||||
"text": text.strip()[:100],
|
||||
"pattern": pattern
|
||||
})
|
||||
break # Solo un tipo de evento por segmento
|
||||
|
||||
if not events:
|
||||
logger.warning("No se encontraron eventos")
|
||||
return []
|
||||
|
||||
# Ordenar por timestamp (para mantener orden cronológico)
|
||||
events.sort(key=lambda x: x["start"])
|
||||
|
||||
logger.info(f"Eventos encontrados: {len(events)}")
|
||||
for e in events[:10]:
|
||||
logger.info(f" {e['type']}: {e['text'][:50]}...")
|
||||
|
||||
# Convertir a intervalos EXTENDIDOS
|
||||
intervals = []
|
||||
for event in events:
|
||||
start = int(event["start"])
|
||||
|
||||
# Duración base + padding DESPUÉS para no cortar la acción
|
||||
end = int(event["end"]) + clip_duration
|
||||
|
||||
# Verificar solapamiento con intervalos existentes
|
||||
overlaps = False
|
||||
for s, e in intervals:
|
||||
if not (end < s or start > e):
|
||||
# Si hay solapamiento, extender el existente
|
||||
if e < end:
|
||||
# Extender el intervalo existente
|
||||
idx = intervals.index((s, e))
|
||||
intervals[idx] = (s, int(end))
|
||||
overlaps = True
|
||||
break
|
||||
|
||||
if not overlaps:
|
||||
intervals.append((start, int(end)))
|
||||
|
||||
# Ordenar
|
||||
intervals.sort()
|
||||
|
||||
# Limitar al top solicitado
|
||||
intervals = intervals[:top]
|
||||
|
||||
logger.info(f"Intervalos finales: {len(intervals)}")
|
||||
|
||||
return intervals, events
|
||||
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--transcripcion", required=True)
|
||||
parser.add_argument("--output", default="highlights_eventos.json")
|
||||
parser.add_argument("--top", type=int, default=40)
|
||||
parser.add_argument("--intro-skip", type=int, default=120)
|
||||
parser.add_argument("--clip-duration", type=int, default=45, help="Duración base del clip")
|
||||
parser.add_argument("--padding-after", type=int, default=15, help="Padding después del evento")
|
||||
args = parser.parse_args()
|
||||
|
||||
intervals, events = detect_game_events(
|
||||
args.transcripcion,
|
||||
args.intro_skip,
|
||||
args.clip_duration,
|
||||
args.padding_after,
|
||||
args.top
|
||||
)
|
||||
|
||||
# Guardar
|
||||
with open(args.output, 'w') as f:
|
||||
json.dump(intervals, f)
|
||||
|
||||
logger.info(f"Guardado en {args.output}")
|
||||
|
||||
# Imprimir resumen
|
||||
type_emoji = {
|
||||
"ally_death": "💀",
|
||||
"skill": "⚡",
|
||||
"rage": "🤬"
|
||||
}
|
||||
|
||||
print(f"\n{'='*70}")
|
||||
print(f"EVENTOS DE JUEGO - CLIPS EXTENDIDOS".center(70))
|
||||
print(f"{'='*70}")
|
||||
print(f"Total: {len(intervals)} clips")
|
||||
print(f"Duración total: {sum(e-s for s,e in intervals)}s ({sum(e-s for s,e in intervals)/60:.1f} min)")
|
||||
print(f"Duración clips: ~{args.clip_duration + args.padding_after}s")
|
||||
print(f"Intro excluida: {args.intro_skip}s")
|
||||
print(f"{'-'*70}")
|
||||
|
||||
for i, (start, end) in enumerate(intervals, 1):
|
||||
duration = end - start
|
||||
h = start // 3600
|
||||
m = (start % 3600) // 60
|
||||
sec = start % 60
|
||||
|
||||
# Buscar el evento correspondiente
|
||||
for event in events:
|
||||
if abs(event["start"] - start) < 10:
|
||||
emoji = type_emoji.get(event["type"], "🎮")
|
||||
text_preview = event["text"][:60].replace('\n', ' ')
|
||||
print(f"{i:2d}. {h:02d}:{m:02d}:{sec:02d} - {duration}s {emoji} - {text_preview}...")
|
||||
break
|
||||
else:
|
||||
print(f"{i:2d}. {h:02d}:{m:02d}:{sec:02d} - {duration}s 🎮")
|
||||
|
||||
print(f"{'='*70}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user