Sesión 19 Feb: OCR intentos, MCP op.gg, timestamps manuales, video final muertes
- 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
This commit is contained in:
292
detectar_primera_muerte_inteligente.py
Normal file
292
detectar_primera_muerte_inteligente.py
Normal file
@@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DETECTOR INTELIGENTE DE PRIMERA MUERTE
|
||||
======================================
|
||||
|
||||
METODOLOGÍA: Búsqueda binaria automatizada con OCR
|
||||
1. Comienza desde un punto conocido (donde hay 0/1)
|
||||
2. Retrocede en pasos de 30s analizando con Tesseract OCR
|
||||
3. Cuando encuentra 0/0, hace búsqueda fina cada 2s
|
||||
4. Encuentra el momento EXACTO del cambio
|
||||
|
||||
TECNOLOGÍA: Tesseract OCR + OpenCV (GPU para extracción de frames)
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pytesseract
|
||||
import subprocess
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuración
|
||||
VIDEO_PATH = (
|
||||
"/home/ren/proyectos/editor/twitch-highlight-detector/stream_2699641307_1080p60.mp4"
|
||||
)
|
||||
OUTPUT_DIR = "/home/ren/proyectos/editor/twitch-highlight-detector/muertes"
|
||||
|
||||
# Coordenadas exactas del KDA (1080p)
|
||||
KDA_CROP = {"x": 0, "y": 0, "w": 280, "h": 120} # Esquina superior izquierda
|
||||
|
||||
|
||||
def format_time(seconds):
|
||||
"""Convierte segundos a HH:MM:SS"""
|
||||
return str(timedelta(seconds=int(seconds)))
|
||||
|
||||
|
||||
def extract_frame(video_path, timestamp):
|
||||
"""Extrae un frame específico del video"""
|
||||
temp_file = f"/tmp/frame_{int(timestamp * 100)}.png"
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-ss",
|
||||
str(timestamp),
|
||||
"-i",
|
||||
video_path,
|
||||
"-vframes",
|
||||
"1",
|
||||
"-vf",
|
||||
f"crop={KDA_CROP['w']}:{KDA_CROP['h']}:{KDA_CROP['x']}:{KDA_CROP['y']},scale=560:240",
|
||||
"-pix_fmt",
|
||||
"rgb24",
|
||||
temp_file,
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, check=True, timeout=10)
|
||||
return temp_file if os.path.exists(temp_file) else None
|
||||
except:
|
||||
return None
|
||||
|
||||
|
||||
def read_kda_tesseract(image_path):
|
||||
"""
|
||||
Lee el KDA usando Tesseract OCR
|
||||
Busca el formato X/Y/Z donde Y es el deaths
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
return None
|
||||
|
||||
# Cargar imagen
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
return None
|
||||
|
||||
# Preprocesamiento para mejorar OCR
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Aumentar contraste
|
||||
_, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# OCR con Tesseract
|
||||
custom_config = r"--oem 3 --psm 6 -c tessedit_char_whitelist=0123456789/"
|
||||
text = pytesseract.image_to_string(thresh, config=custom_config)
|
||||
|
||||
# Limpiar y buscar formato KDA
|
||||
text = text.strip().replace(" ", "").replace("\n", "")
|
||||
|
||||
# Buscar formato X/Y/Z
|
||||
import re
|
||||
|
||||
matches = re.findall(r"(\d+)/(\d+)/(\d+)", text)
|
||||
|
||||
if matches:
|
||||
kills, deaths, assists = matches[0]
|
||||
return int(kills), int(deaths), int(assists)
|
||||
|
||||
# Si no encuentra formato completo, buscar números sueltos
|
||||
numbers = re.findall(r"\d+", text)
|
||||
if len(numbers) >= 3:
|
||||
return int(numbers[0]), int(numbers[1]), int(numbers[2])
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_first_death_smart(start_timestamp=4475, step_back=30):
|
||||
"""
|
||||
Búsqueda inteligente hacia atrás
|
||||
|
||||
Estrategia:
|
||||
1. Retrocede en pasos grandes (30s) hasta encontrar 0/0
|
||||
2. Luego busca fina cada 2s entre el último 0/0 y primer 0/1
|
||||
"""
|
||||
logger.info("=" * 70)
|
||||
logger.info("DETECTOR INTELIGENTE - Búsqueda hacia atrás")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"Punto de inicio: {format_time(start_timestamp)} (0/1 confirmado)")
|
||||
logger.info(f"Buscando cambio a 0/0 retrocediendo...")
|
||||
logger.info("")
|
||||
|
||||
current_ts = start_timestamp
|
||||
last_01_ts = start_timestamp
|
||||
found_00 = False
|
||||
|
||||
# FASE 1: Retroceder en pasos grandes hasta encontrar 0/0
|
||||
logger.info("FASE 1: Retroceso grueso (pasos de 30s)")
|
||||
logger.info("-" * 70)
|
||||
|
||||
max_attempts = 20 # Máximo 10 minutos hacia atrás
|
||||
attempt = 0
|
||||
|
||||
while attempt < max_attempts:
|
||||
frame_path = extract_frame(VIDEO_PATH, current_ts)
|
||||
|
||||
if not frame_path:
|
||||
logger.warning(f" [{format_time(current_ts)}] No se pudo extraer frame")
|
||||
current_ts -= step_back
|
||||
attempt += 1
|
||||
continue
|
||||
|
||||
kda = read_kda_tesseract(frame_path)
|
||||
|
||||
if kda:
|
||||
kills, deaths, assists = kda
|
||||
logger.info(
|
||||
f" [{format_time(current_ts)}] KDA: {kills}/{deaths}/{assists}"
|
||||
)
|
||||
|
||||
if deaths == 0:
|
||||
logger.info(f" ✓ Encontrado 0/0 en {format_time(current_ts)}")
|
||||
found_00 = True
|
||||
break
|
||||
else:
|
||||
last_01_ts = current_ts
|
||||
else:
|
||||
logger.warning(f" [{format_time(current_ts)}] No se pudo leer KDA")
|
||||
|
||||
# Limpiar temporal
|
||||
if os.path.exists(frame_path):
|
||||
os.remove(frame_path)
|
||||
|
||||
current_ts -= step_back
|
||||
attempt += 1
|
||||
|
||||
if not found_00:
|
||||
logger.error("No se encontró momento con 0/0")
|
||||
return None
|
||||
|
||||
# FASE 2: Búsqueda fina entre el último 0/0 y el primer 0/1
|
||||
logger.info("")
|
||||
logger.info("FASE 2: Búsqueda fina (pasos de 2s)")
|
||||
logger.info("-" * 70)
|
||||
logger.info(f"Buscando entre {format_time(current_ts)} y {format_time(last_01_ts)}")
|
||||
|
||||
# Retroceder 30s más para asegurar, luego avanzar fino
|
||||
fine_start = current_ts - 30
|
||||
fine_end = last_01_ts + 5
|
||||
|
||||
death_timestamp = None
|
||||
|
||||
for ts in range(int(fine_start), int(fine_end), 2): # Cada 2 segundos
|
||||
frame_path = extract_frame(VIDEO_PATH, ts)
|
||||
|
||||
if not frame_path:
|
||||
continue
|
||||
|
||||
kda = read_kda_tesseract(frame_path)
|
||||
|
||||
if kda:
|
||||
kills, deaths, assists = kda
|
||||
logger.info(f" [{format_time(ts)}] KDA: {kills}/{deaths}/{assists}")
|
||||
|
||||
# Detectar cambio de 0 a 1
|
||||
if deaths >= 1 and death_timestamp is None:
|
||||
death_timestamp = ts
|
||||
logger.info(f" 💀 PRIMERA MUERTE DETECTADA: {format_time(ts)}")
|
||||
break
|
||||
|
||||
if os.path.exists(frame_path):
|
||||
os.remove(frame_path)
|
||||
|
||||
return death_timestamp
|
||||
|
||||
|
||||
def extract_death_clip(timestamp, output_file):
|
||||
"""Extrae clip de la muerte con contexto"""
|
||||
start = max(0, timestamp - 10)
|
||||
duration = 25 # 10s antes + 15s después
|
||||
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-ss",
|
||||
str(start),
|
||||
"-t",
|
||||
str(duration),
|
||||
"-i",
|
||||
VIDEO_PATH,
|
||||
"-c:v",
|
||||
"h264_nvenc",
|
||||
"-preset",
|
||||
"fast",
|
||||
"-rc",
|
||||
"vbr",
|
||||
"-cq",
|
||||
"23",
|
||||
"-r",
|
||||
"60",
|
||||
"-c:a",
|
||||
"copy",
|
||||
output_file,
|
||||
]
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, check=True, timeout=120)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
logger.info("\n" + "=" * 70)
|
||||
logger.info("BUSCADOR INTELIGENTE DE PRIMERA MUERTE")
|
||||
logger.info("Tecnología: Tesseract OCR + Retroceso automatizado")
|
||||
logger.info("=" * 70)
|
||||
logger.info("")
|
||||
|
||||
# Encontrar primera muerte
|
||||
death_ts = find_first_death_smart(start_timestamp=4475)
|
||||
|
||||
if death_ts:
|
||||
logger.info("")
|
||||
logger.info("=" * 70)
|
||||
logger.info("RESULTADO FINAL")
|
||||
logger.info("=" * 70)
|
||||
logger.info(f"✓ Primera muerte detectada en: {format_time(death_ts)}")
|
||||
logger.info(f" Timestamp exacto: {death_ts} segundos")
|
||||
logger.info("")
|
||||
logger.info("Extrayendo clip final...")
|
||||
|
||||
# Extraer clip
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
output_file = f"{OUTPUT_DIR}/PRIMERA_MUERTE_{int(death_ts)}s.mp4"
|
||||
|
||||
if extract_death_clip(death_ts, output_file):
|
||||
size_mb = os.path.getsize(output_file) / (1024 * 1024)
|
||||
logger.info(f"✓ Clip guardado: {output_file}")
|
||||
logger.info(f" Tamaño: {size_mb:.1f}MB")
|
||||
logger.info(f" Duración: 25 segundos (contexto completo)")
|
||||
else:
|
||||
logger.error("Error extrayendo clip final")
|
||||
|
||||
logger.info("")
|
||||
logger.info("=" * 70)
|
||||
logger.info("METODOLOGÍA UTILIZADA:")
|
||||
logger.info("=" * 70)
|
||||
logger.info("1. Tesseract OCR para lectura de KDA")
|
||||
logger.info("2. Retroceso automatizado en pasos de 30s")
|
||||
logger.info("3. Búsqueda fina cada 2s en zona crítica")
|
||||
logger.info("4. Detección de cambio: 0/0 → 0/1")
|
||||
logger.info("=" * 70)
|
||||
else:
|
||||
logger.error("No se pudo determinar la primera muerte")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user