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:
327
moment_finder.py
Normal file
327
moment_finder.py
Normal file
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MOMENT FINDER - Busca momentos específicos en transcripción guardada
|
||||
Uso: python3 moment_finder.py --transcription transcripcion_rage.json --type rage
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class MomentFinder:
|
||||
"""Busca momentos específicos en una transcripción guardada."""
|
||||
|
||||
def __init__(self, transcription_file):
|
||||
with open(transcription_file, "r") as f:
|
||||
self.trans = json.load(f)
|
||||
print(f"Transcripción cargada: {len(self.trans['segments'])} segmentos")
|
||||
|
||||
def find_rage_moments(self, skip_intro=455, min_score=5):
|
||||
"""Busca momentos de rage, muertes y fails."""
|
||||
|
||||
patterns = {
|
||||
"EXTREME_RAGE": [
|
||||
r"\bputa\w*",
|
||||
r"\bmadre\b",
|
||||
r"\bretrasad\w*",
|
||||
r"\bimbecil\w*",
|
||||
r"\bestupid\w*",
|
||||
r"\bidiota\w*",
|
||||
r"\bmierda\b",
|
||||
r"\bbasura\w*",
|
||||
r"\binutil\w*",
|
||||
r"\bmongol\w*",
|
||||
r"\bmaricon\w*",
|
||||
r"\bcallate\b",
|
||||
],
|
||||
"DEATH": [
|
||||
r"\bme mataron\b",
|
||||
r"\bme mori\b",
|
||||
r"\bme muero\b",
|
||||
r"\bmatenme\b",
|
||||
r"\bfeed\w*",
|
||||
r"\bme destrozaron\b",
|
||||
r"\bme comieron\b",
|
||||
r"\bme cargaron\b",
|
||||
r"\bme jodieron\b",
|
||||
],
|
||||
"FAIL": [
|
||||
r"\bla cague\b",
|
||||
r"\bla lie\b",
|
||||
r"\berror\b",
|
||||
r"\bfail\b",
|
||||
r"\bperdon\b",
|
||||
r"\bperd[oó]n\b",
|
||||
r"\blo siento\b",
|
||||
r"\bmala mia\b",
|
||||
r"\bfall[eé]\b",
|
||||
r"\bno puede ser\b",
|
||||
],
|
||||
"TEAM_RAGE": [
|
||||
r"\bequipo\b.*\b(mierda|basura|malos|peor)\b",
|
||||
r"\bteam\b.*\b(trash|bad|mierda)\b",
|
||||
r"\breport\w*",
|
||||
r"\btroll\w*",
|
||||
r"\binting\b",
|
||||
],
|
||||
"FRUSTRATION": [
|
||||
r"\b(nooo+|no no)\b",
|
||||
r"\bpor que\b",
|
||||
r"\bporque\b",
|
||||
r"\ben serio\b",
|
||||
r"\bno me jodas\b",
|
||||
r"\bque (haces|hace)\b",
|
||||
r"\bhostia\b",
|
||||
r"\bjoder\b",
|
||||
r"\bdios\b",
|
||||
],
|
||||
}
|
||||
|
||||
return self._find_moments(patterns, skip_intro, min_score)
|
||||
|
||||
def find_epic_moments(self, skip_intro=455, min_score=5):
|
||||
"""Busca jugadas épicas y celebraciones."""
|
||||
|
||||
patterns = {
|
||||
"EPIC_PLAY": [
|
||||
r"\bpentakill\b",
|
||||
r"\bbaron\b",
|
||||
r"\bdrag[oó]n\b",
|
||||
r"\btriple\b",
|
||||
r"\bquadra\b",
|
||||
r"\bace\b",
|
||||
r"\bepico\b",
|
||||
r"\bgod\b",
|
||||
r"\binsane\b",
|
||||
r"\bclutch\b",
|
||||
],
|
||||
"CELEBRATION": [
|
||||
r"\bnice\b",
|
||||
r"\bgg\b",
|
||||
r"\bgood\b",
|
||||
r"\bwell\b",
|
||||
r"\bperfecto\b",
|
||||
r"\bexcelente\b",
|
||||
r"\bgenial\b",
|
||||
],
|
||||
"LAUGHTER": [
|
||||
r"\bjajaj\w*",
|
||||
r"\bjejej\w*",
|
||||
r"\brisas?\b",
|
||||
r"\bcarcajada\b",
|
||||
],
|
||||
"SKILLS": [
|
||||
r"\bulti\b",
|
||||
r"\bflash\b",
|
||||
r"\bignite\b",
|
||||
r"\bexhaust\b",
|
||||
],
|
||||
}
|
||||
|
||||
return self._find_moments(patterns, skip_intro, min_score)
|
||||
|
||||
def find_reaction_moments(self, skip_intro=455, min_score=3):
|
||||
"""Busca reacciones y momentos emotivos."""
|
||||
|
||||
patterns = {
|
||||
"SURPRISE": [
|
||||
r"\bwo+w*\b",
|
||||
r"\bwhat\b",
|
||||
r"\bcomo\?\b",
|
||||
r"\ben serio\?\b",
|
||||
r"\bno puede ser\b",
|
||||
r"\bimpresionante\b",
|
||||
],
|
||||
"HYPE": [
|
||||
r"\bvamos\b",
|
||||
r"\bvamoo+s\b",
|
||||
r"\blet.s go\b",
|
||||
r"\bvamo+s\b",
|
||||
r"\bgg\b",
|
||||
r"\bnice\b",
|
||||
r"\bway\b",
|
||||
],
|
||||
"EMOTION": [
|
||||
r"\bomg\b",
|
||||
r"\boh dios\b",
|
||||
r"\bno lo creo\b",
|
||||
r"\bes increible\b",
|
||||
r"\bque locura\b",
|
||||
],
|
||||
}
|
||||
|
||||
return self._find_moments(patterns, skip_intro, min_score)
|
||||
|
||||
def _find_moments(self, patterns, skip_intro, min_score):
|
||||
"""Busca momentos basados en patrones."""
|
||||
moments = []
|
||||
|
||||
for seg in self.trans.get("segments", []):
|
||||
if seg["start"] < skip_intro:
|
||||
continue
|
||||
|
||||
text = seg["text"].lower()
|
||||
score = 0
|
||||
reasons = []
|
||||
|
||||
for category, pattern_list in patterns.items():
|
||||
for pattern in pattern_list:
|
||||
if re.search(pattern, text, re.IGNORECASE):
|
||||
# Puntuación por categoría
|
||||
if category in ["EXTREME_RAGE", "EPIC_PLAY"]:
|
||||
score += 10
|
||||
elif category in ["DEATH", "TEAM_RAGE"]:
|
||||
score += 8
|
||||
elif category in ["FAIL", "CELEBRATION"]:
|
||||
score += 6
|
||||
else:
|
||||
score += 4
|
||||
|
||||
if category not in reasons:
|
||||
reasons.append(category)
|
||||
break
|
||||
|
||||
if score >= min_score:
|
||||
moments.append(
|
||||
{
|
||||
"start": seg["start"],
|
||||
"end": seg["end"],
|
||||
"score": score,
|
||||
"text": seg["text"][:80],
|
||||
"reasons": reasons,
|
||||
}
|
||||
)
|
||||
|
||||
return moments
|
||||
|
||||
def create_clips(self, moments, max_clips=15, extend_before=10, extend_after=20):
|
||||
"""Crea clips a partir de momentos."""
|
||||
|
||||
# Ordenar por score
|
||||
moments.sort(key=lambda x: -x["score"])
|
||||
|
||||
# Crear clips extendidos
|
||||
clips = []
|
||||
for m in moments[: max_clips * 2]: # Más candidatos
|
||||
start = max(455, int(m["start"]) - extend_before)
|
||||
end = min(8237, int(m["end"]) + extend_after)
|
||||
|
||||
if end - start >= 12:
|
||||
clips.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"score": m["score"],
|
||||
"reasons": m["reasons"],
|
||||
"text": m["text"],
|
||||
}
|
||||
)
|
||||
|
||||
# Eliminar solapamientos
|
||||
clips.sort(key=lambda x: x["start"])
|
||||
filtered = []
|
||||
|
||||
for clip in clips:
|
||||
if not filtered:
|
||||
filtered.append(clip)
|
||||
else:
|
||||
last = filtered[-1]
|
||||
if clip["start"] <= last["end"] + 3:
|
||||
# Fusionar
|
||||
last["end"] = max(last["end"], clip["end"])
|
||||
last["score"] = max(last["score"], clip["score"])
|
||||
last["reasons"] = list(set(last["reasons"] + clip["reasons"]))
|
||||
else:
|
||||
filtered.append(clip)
|
||||
|
||||
# Tomar top clips
|
||||
filtered.sort(key=lambda x: -x["score"])
|
||||
final = filtered[:max_clips]
|
||||
final.sort(key=lambda x: x["start"])
|
||||
|
||||
return final
|
||||
|
||||
def save_clips(self, clips, output_file):
|
||||
"""Guarda clips en formato JSON."""
|
||||
highlights = [[c["start"], c["end"]] for c in clips]
|
||||
with open(output_file, "w") as f:
|
||||
json.dump(highlights, f)
|
||||
|
||||
print(f"\nGuardado: {output_file}")
|
||||
print(f"Total: {len(clips)} clips")
|
||||
total_dur = sum(c["end"] - c["start"] for c in clips)
|
||||
print(f"Duración: {total_dur}s ({total_dur // 60}m {total_dur % 60}s)")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Find moments in saved transcription")
|
||||
parser.add_argument(
|
||||
"--transcription", required=True, help="Transcription JSON file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--type",
|
||||
choices=["rage", "epic", "reaction", "all"],
|
||||
default="rage",
|
||||
help="Type of moments to find",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", default="highlights_moments.json", help="Output file"
|
||||
)
|
||||
parser.add_argument("--max-clips", type=int, default=12, help="Max clips")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
finder = MomentFinder(args.transcription)
|
||||
|
||||
print(f"\nBuscando momentos tipo: {args.type.upper()}")
|
||||
print("=" * 60)
|
||||
|
||||
if args.type == "rage":
|
||||
moments = finder.find_rage_moments()
|
||||
elif args.type == "epic":
|
||||
moments = finder.find_epic_moments()
|
||||
elif args.type == "reaction":
|
||||
moments = finder.find_reaction_moments()
|
||||
else: # all
|
||||
rage = finder.find_rage_moments(min_score=4)
|
||||
epic = finder.find_epic_moments(min_score=4)
|
||||
reaction = finder.find_reaction_moments(min_score=3)
|
||||
moments = rage + epic + reaction
|
||||
# Eliminar duplicados
|
||||
seen = set()
|
||||
unique = []
|
||||
for m in moments:
|
||||
key = int(m["start"])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(m)
|
||||
moments = unique
|
||||
|
||||
print(f"Momentos encontrados: {len(moments)}")
|
||||
|
||||
# Mostrar top 10
|
||||
moments.sort(key=lambda x: -x["score"])
|
||||
print("\nTop momentos:")
|
||||
for i, m in enumerate(moments[:10], 1):
|
||||
mins = int(m["start"]) // 60
|
||||
secs = int(m["start"]) % 60
|
||||
print(
|
||||
f"{i:2d}. {mins:02d}:{secs:02d} [Score: {m['score']:2d}] "
|
||||
f"{'/'.join(m['reasons'][:2])} - {m['text'][:50]}..."
|
||||
)
|
||||
|
||||
# Crear y guardar clips
|
||||
clips = finder.create_clips(moments, max_clips=args.max_clips)
|
||||
finder.save_clips(clips, args.output)
|
||||
|
||||
print("\nTimeline final:")
|
||||
for i, c in enumerate(clips, 1):
|
||||
mins, secs = divmod(c["start"], 60)
|
||||
dur = c["end"] - c["start"]
|
||||
print(f"{i:2d}. {mins:02d}:{secs:02d} - {dur}s [{', '.join(c['reasons'][:2])}]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user