Compare commits

...

10 Commits

Author SHA1 Message Date
ren
fb8b390740 feat: Initial pipeline for Twitch highlight detection
- New 2-of-3 detection system (chat + audio + color)
- GPU support (PyTorch ROCm/CUDA ready)
- Draft mode (360p) for fast testing
- HD mode (1080p) for final render
- Auto download video + chat
- CLI pipeline script
- Documentation in Spanish
2026-02-18 20:41:58 -03:00
Vitalii Lebedynskyi
f9836a4265 Adjustments for chat peak detection 2022-08-23 15:03:27 +03:00
Vitalii Lebedynskyi
5cec84c26c Fixed clipper cut 2022-08-20 22:35:26 +03:00
Vitalii Lebedynskyi
924294c31e Logger for clipper module 2022-08-20 22:27:46 +03:00
Vitalii Lebedynskyi
a7b08ffa64 Some small cleanup 2022-08-20 22:21:42 +03:00
Vitalii Lebedynskyi
fdfd2c6135 Seems finished 2022-08-17 16:51:05 +03:00
Vitalii Lebedynskyi
173fcc098f pretty good status 2022-08-15 17:07:59 +03:00
Vitalii Lebedynskyi
efa6216e2a Added post processing of the chat 2022-08-15 14:36:17 +03:00
Vitalii Lebedynskyi
265bad0267 Added chat peak detector 2022-08-15 13:31:15 +03:00
Vitalii Lebedynskyi
0089dc2982 finished work with recorders 2022-08-14 23:03:22 +03:00
20 changed files with 1413 additions and 421 deletions

BIN
.DS_Store vendored

Binary file not shown.

185
.gitignore vendored
View File

@@ -1,162 +1,41 @@
# Byte-compiled / optimized / DLL files # Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
# C extensions
*.so *.so
# Distribution / packaging
.Python .Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/ venv/
ENV/ env/
env.bak/ .venv/
venv.bak/
# Spyder project settings # IDEs
.spyderproject .vscode/
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/ .idea/
*.swp
*.swo
recorded # Videos (no subir a git)
*.mp4
*.mkv
*.avi
*.mov
# Chat (puede ser grande)
*.json
*.txt
# Highlights
*highlights*.json
*_final.mp4
# Temp
temp_*
frames_temp/
*.wav
# OS
.DS_Store
Thumbs.db
# Env
.env

218
README.md
View File

@@ -1,5 +1,213 @@
## Known issues: # 🎬 Twitch Highlight Detector
- Configure logger with config file
- Support multiple streamer Pipeline automatizado para detectar y generar highlights de streams de Twitch y Kick.
- Post process with ffmpeg
- Avoid using streamer name. Need to use id instead ## ✨ Características
- **Descarga automática** de VODs y chat
- **Detección 2 de 3**: Chat saturado + Audio (gritos) + Colores brillantes
- **Modo Draft**: Procesa en 360p para prueba rápida
- **Modo HD**: Procesa en 1080p para calidad máxima
- **Soporte GPU**: Preparado para NVIDIA (CUDA) y AMD (ROCm)
- **CLI simple**: Un solo comando para todo el pipeline
## 🚀 Uso Rápido
```bash
# Modo Draft (360p) - Prueba rápida
./pipeline.sh <video_id> <nombre> --draft
# Modo HD (1080p) - Alta calidad
./pipeline.sh <video_id> <nombre> --hd
```
### Ejemplo
```bash
# Descargar y procesar en modo draft
./pipeline.sh 2701190361 elxokas --draft
# Si te gusta, procesar en HD
./pipeline.sh 2701190361 elxokas_hd --hd
```
## 📋 Requisitos
### Sistema
```bash
# Arch Linux
sudo pacman -S ffmpeg streamlink git
# Ubuntu/Debian
sudo apt install ffmpeg streamlink git
# macOS
brew install ffmpeg streamlink git
```
### Python
```bash
pip install moviepy opencv-python scipy numpy python-dotenv torch
```
### .NET (para TwitchDownloaderCLI)
```bash
# Descarga el binario desde releases o compila
# https://github.com/lay295/TwitchDownloader/releases
```
## 📖 Documentación
| Archivo | Descripción |
|---------|-------------|
| [README.md](README.md) | Este archivo |
| [CONtexto.md](contexto.md) | Historia y contexto del proyecto |
| [TODO.md](TODO.md) | Lista de tareas pendientes |
| [HIGHLIGHT.md](HIGHLIGHT.md) | Guía de uso del pipeline |
## 🔧 Instalación
### 1. Clonar el repo
```bash
git clone https://tu-gitea/twitch-highlight-detector.git
cd twitch-highlight-detector
```
### 2. Configurar credenciales
```bash
cp .env.example .env
# Edita .env con tus credenciales de Twitch
```
### 3. Instalar dependencias
```bash
pip install -r requirements.txt
```
### 4. Instalar TwitchDownloaderCLI
```bash
# Descargar desde releases
curl -L -o TwitchDownloaderCLI https://github.com/lay295/TwitchDownloader/releases/latest/download/TwitchDownloaderCLI
chmod +x TwitchDownloaderCLI
sudo mv TwitchDownloaderCLI /usr/local/bin/
```
## 🎯 Cómo Funciona
### Pipeline (2 de 3)
El sistema detecta highlights cuando se cumplen al menos 2 de estas 3 condiciones:
1. **Chat saturado**: Muchos mensajes en poco tiempo
2. **Audio intenso**: Picos de volumen (gritos, momentos épicos)
3. **Colores brillantes**: Efectos visuales, cambios de escena
### Flujo
```
1. streamlink → Descarga video (VOD)
2. TwitchDownloaderCLI → Descarga chat
3. detector_gpu.py → Analiza chat + audio + color
4. generate_video.py → Crea video resumen
```
## 📁 Estructura
```
├── .env # Credenciales (noCommit)
├── .gitignore
├── requirements.txt # Dependencias Python
├── main.py # Entry point
├── pipeline.sh # Pipeline completo
├── detector_gpu.py # Detector (chat + audio + color)
├── generate_video.py # Generador de video
├── lower # Script descarga streams
├── README.md # Este archivo
├── CONtexto.md # Contexto del proyecto
├── TODO.md # Tareas pendientes
└── HIGHLIGHT.md # Guía detallada
```
## ⚙️ Configuración
### Parámetros del Detector
Edita `detector_gpu.py` para ajustar:
```python
--threshold # Sensibilidad (default: 1.5)
--min-duration # Duración mínima highlight (default: 10s)
--device # GPU: auto/cuda/cpu
```
### Parámetros del Video
Edita `generate_video.py`:
```python
--padding # Segundos extra antes/después (default: 5)
```
## 🖥️ GPU
### NVIDIA (CUDA)
```bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121
```
### AMD (ROCm)
```bash
pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm7.1
```
**Nota**: El procesamiento actual es CPU-bound. GPU acceleration es future work.
## 🔨 Desarrollo
### Tests
```bash
# Test detector con video existente
python3 detector_gpu.py --video video.mp4 --chat chat.json --output highlights.json
```
### Pipeline Manual
```bash
# 1. Descargar video
streamlink "https://www.twitch.tv/videos/ID" best -o video.mp4
# 2. Descargar chat
TwitchDownloaderCLI chatdownload --id ID -o chat.json
# 3. Detectar highlights
python3 detector_gpu.py --video video.mp4 --chat chat.json --output highlights.json
# 4. Generar video
python3 generate_video.py --video video.mp4 --highlights highlights.json --output final.mp4
```
## 📊 Resultados
Con un stream de 5.3 horas (19GB):
- Chat: ~13,000 mensajes
- Picos detectados: ~139
- Highlights útiles (>5s): 4-10
- Video final: ~1-5 minutos
## 🤝 Contribuir
1. Fork el repo
2. Crea una branch (`git checkout -b feature/`)
3. Commit tus cambios (`git commit -m 'Add feature'`)
4. Push a la branch (`git push origin feature/`)
5. Abre un Pull Request
## 📝 Licencia
MIT License - Ver LICENSE para más detalles.
## 🙏 Créditos
- [TwitchDownloader](https://github.com/lay295/TwitchDownloader) - Chat downloading
- [streamlink](https://streamlink.github.io/) - Video downloading
- [MoviePy](https://zulko.github.io/moviepy/) - Video processing
- [PyTorch](https://pytorch.org/) - GPU support

View File

View File

45
bajar Executable file
View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Instalar dependencias si no existen
install_deps() {
echo "Verificando dependencias..."
if ! command -v streamlink &> /dev/null; then
echo "Instalando streamlink..."
sudo pacman -S streamlink --noconfirm
fi
if ! command -v ffmpeg &> /dev/null; then
echo "Instalando ffmpeg..."
sudo pacman -S ffmpeg --noconfirm
fi
echo "Dependencias listas!"
}
# Descargar video de Twitch
download() {
if [ -z "$1" ]; then
echo "Usage: bajar <url_twitch>"
echo "Ejemplo: bajar https://www.twitch.tv/videos/2699641307"
return 1
fi
install_deps
URL="$1"
OUTPUT_FILE="./$(date +%Y%m%d_%H%M%S)_twitch.mp4"
echo "Descargando: $URL"
echo "Guardando en: $OUTPUT_FILE"
streamlink "$URL" best -o "$OUTPUT_FILE"
if [ $? -eq 0 ]; then
echo "¡Descarga completada! Archivo: $OUTPUT_FILE"
else
echo "Error en la descarga"
fi
}
download "$@"

View File

View File

@@ -1,91 +0,0 @@
import enum
import logging
import socket
import time
from twitchAPI import Twitch, AuthScope
logger = logging.getLogger(__name__)
TW_CHAT_SERVER = 'irc.chat.twitch.tv'
TW_CHAT_PORT = 6667
class TwitchStreamStatus(enum.Enum):
ONLINE = 0
OFFLINE = 1
NOT_FOUND = 2
UNAUTHORIZED = 3
ERROR = 4
class TwitchApi:
_cached_token = None
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.twitch = Twitch(self.client_id, self.client_secret, target_app_auth_scope=[AuthScope.CHAT_READ])
self.twitch.authenticate_app([AuthScope.CHAT_READ])
def get_user_status(self, streamer):
try:
streams = self.twitch.get_streams(user_login=streamer)
if streams is None or len(streams["data"]) < 1:
return TwitchStreamStatus.OFFLINE
else:
return TwitchStreamStatus.ONLINE
except:
return TwitchStreamStatus.ERROR
def start_chat(self, streamer_name, on_message):
logger.info("Connecting to %s:%s", TW_CHAT_SERVER, TW_CHAT_PORT)
connection = ChatConnection(streamer_name, self, on_message)
self.twitch.get_app_token()
connection.run()
def get_user_chat_channel(self, streamer_name):
streams = self.twitch.get_streams(user_login=streamer_name)
if streams is None or len(streams["data"]) < 1:
return None
return streams["data"][0]["user_login"]
class ChatConnection:
logger = logging.getLogger(__name__)
connection = None
def __init__(self, streamer_name, api, on_message):
self.on_message = on_message
self.api = api
self.streamer_name = streamer_name
def run(self):
# Need to verify channel name.. case sensitive
channel = self.api.get_user_chat_channel(self.streamer_name)
if not channel:
logger.error("Cannot find streamer channel, Offline?")
return
self.connect_to_chat(f"#{channel}")
def connect_to_chat(self, channel):
self.connection = socket.socket()
self.connection.connect((TW_CHAT_SERVER, TW_CHAT_PORT))
# public data to join hat
self.connection.send(f"PASS couldBeRandomString\n".encode("utf-8"))
self.connection.send(f"NICK justinfan113\n".encode("utf-8"))
self.connection.send(f"JOIN {channel}\n".encode("utf-8"))
logger.info("Connected to %s", channel)
try:
while True:
msg = self.connection.recv(8192).decode('utf-8')
if self.on_message:
self.on_message(msg)
except BaseException as e:
logger.error(e)
logger.error("Error happened during reading chat")

View File

@@ -1,35 +0,0 @@
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
CHAT_DIVIDER = "<~|~>"
class TwitchChatRecorder:
is_running = False
def __init__(self, api, debug=False):
self.debug = debug
self.api = api
def run(self, streamer_name, output_file):
with open(output_file, "w") as stream:
def on_message(twitch_msg):
user, msg = self.parse_msg(twitch_msg)
if msg:
msg_line = f"{str(datetime.now())}{CHAT_DIVIDER}{user}{CHAT_DIVIDER}{msg}"
stream.write(msg_line)
stream.flush()
if self.debug:
logger.info("Chat: %s", msg_line)
self.is_running = True
self.api.start_chat(streamer_name, on_message)
def parse_msg(self, msg):
try:
return msg[1:].split('!')[0], msg.split(":", 2)[2]
except BaseException as e:
return None, None

View File

@@ -1,66 +0,0 @@
import logging
import os
import time
import sys
import threading
from datetime import datetime
from clipper.api import TwitchApi, TwitchStreamStatus
from clipper.chat import TwitchChatRecorder
from clipper.video import TwitchVideoRecorder
logger = logging.getLogger(__name__)
class RecorderConfig:
def __init__(self, tw_client, tw_secret, tw_streamer, tw_quality, output_folder):
self.output_folder = output_folder
self.tw_quality = tw_quality
self.tw_streamer = tw_streamer
self.tw_secret = tw_secret
self.tw_client = tw_client
class Recorder:
audio_thread = None
video_thread = None
def __init__(self, config):
self.config = config
self.api = TwitchApi(config.tw_client, config.tw_secret)
self.streamer_folder = os.path.join(self.config.output_folder, self.config.tw_streamer)
self.video_recorder = TwitchVideoRecorder()
self.chat_recorder = TwitchChatRecorder(self.api, debug=True)
def run(self):
while True:
logger.info("Start watching streamer %s", self.config.tw_streamer)
status = self.api.get_user_status(self.config.tw_streamer)
if status == TwitchStreamStatus.ONLINE:
logger.info("Streamer %s is online. Start recording", self.config.tw_streamer)
start_time = datetime.now()
record_folder_name = start_time.strftime("%d-%m-%Y_%H-%M-%S")
record_folder = os.path.join(self.streamer_folder, record_folder_name)
os.makedirs(record_folder)
chat_file = os.path.join(record_folder, "chat.txt")
video_file = os.path.join(record_folder, "video.mp4")
self.chat_recorder.run(self.config.tw_streamer, chat_file)
# self.video_recorder.run(file_template)
logger.info("Streamer %s has finished stream", self.config.tw_streamer)
time.sleep(15)
if status == TwitchStreamStatus.OFFLINE:
logger.info("Streamer %s is offline. Waiting for 300 sec", self.config.tw_streamer)
time.sleep(300)
if status == TwitchStreamStatus.ERROR:
logger.critical("Error occurred. Exit", self.config.tw_streamer)
sys.exit(1)
if status == TwitchStreamStatus.NOT_FOUND:
logger.critical(f"Streamer %s not found, invalid streamer_name or typo", self.config.tw_streamer)
sys.exit(1)

View File

@@ -1,30 +0,0 @@
import logging
import subprocess
logger = logging.getLogger(__name__)
class TwitchVideoRecorder:
is_running = False
refresh_timeout = 15
streamlink_process = None
def run(self, streamer_name, output_file, quality="480p"):
self._record_stream(streamer_name, output_file, quality)
def stop(self):
if self.streamlink_process:
self.streamlink_process.kill()
def _record_stream(self, streamer_name, output_file, quality):
# subprocess.call()
self.streamlink_process = subprocess.Popen([
"streamlink",
"--twitch-disable-ads",
"twitch.tv/" + streamer_name,
quality,
"-o",
output_file
])
self.is_running = True

221
contexto.md Normal file
View File

@@ -0,0 +1,221 @@
# Contexto del Proyecto
## Resumen Ejecutivo
Pipeline automatizado para detectar y generar highlights de streams de Twitch. El objetivo es:
1. Descargar un VOD completo de Twitch (varias horas)
2. Analizar el chat y el video para detectar momentos destacados
3. Generar un video resumen con los mejores momentos
El sistema original planeaba usar 3 métricas para detectar highlights (2 de 3 deben cumplirse):
- Chat saturado (muchos mensajes en poco tiempo)
- Picos de audio (gritos del streamer)
- Colores brillantes en pantalla (efectos visuales)
**Estado actual:** Solo chat implementado. Audio y color pendientes.
---
## Historia y Desarrollo
### Inicio
El proyecto comenzó con la carpeta `clipper/` que contenía código antiguo para descargar streams de Twitch en vivo. El usuario quería actualizar el enfoque para procesar VODs completos (streams de varias horas) y detectar automáticamente los mejores momentos.
### Primera Iteración (Código Viejo)
Existía código en `clipper/` y `analyser/` que:
- Descargaba streams en vivo
- Usaba `twitchAPI` para autenticación
- Tenía issues con versiones de dependencias (Python 3.14 incompatibilidades)
### Limpieza y Nuevo Pipeline
Se eliminó el código viejo y se creó una estructura nueva:
```
downloaders/ - Módulos para descargar video/chat
detector/ - Lógica de detección de highlights
generator/ - Creación del video resumen
```
### Problemas Encontrados
#### 1. Chat Downloader - Múltiples Intentos
Se probaron varios repositorios para descargar chat de VODs:
- **chat-downloader (xenova)**: No funcionó con VODs (KeyError 'data')
- **tcd (PetterKraabol)**: Mismo problema, API de Twitch devuelve 404
- **TwitchDownloader (lay295)**: Este sí funcionó. Es un proyecto C#/.NET con CLI.
**Solución:** Compilar TwitchDownloaderCLI desde código fuente usando .NET 10 SDK.
#### 2. Dependencias Python
Problemas de versiones:
- `requests` y `urllib3` entraron en conflicto al instalar `tcd`
- Streamlink dejó de funcionar
- **Solución:** Reinstalar versiones correctas de requests/urllib3
#### 3. Video de Prueba
- VOD: `https://www.twitch.tv/videos/2701190361` (elxokas)
- Duración: ~5.3 horas (19GB)
- Chat: 12,942 mensajes
- El chat estaba disponible (no había sido eliminado por Twitch)
#### 4. Detección de Highlights
Problemas con el detector:
- Formato de timestamp del chat no era reconocido
- **Solución:** Usar `content_offset_seconds` del JSON directamente
El detector actual solo usa chat saturado. Encuentra ~139 picos pero la mayoría son de 1-2 segundos (no útiles). Con filtro de duración >5s quedan solo 4 highlights.
#### 5. Generación de Video
- Usa moviepy
- Funciona correctamente
- Genera video de ~39MB (~1 minuto)
---
## Stack Tecnológico
### Herramientas de Descarga
| Herramienta | Uso | Estado |
|-------------|-----|--------|
| streamlink | Video streaming | ✅ Funciona |
| TwitchDownloaderCLI | Chat VODs | ✅ Compilado y funciona |
### Processing (Python)
| Paquete | Uso | GPU Support |
|---------|-----|-------------|
| opencv-python-headless | Análisis de video/color | CPU (sin ROCm) |
| librosa | Análisis de audio | CPU |
| scipy/numpy | Procesamiento numérico | CPU |
| moviepy | Generación de video | CPU |
### GPU
- **ROCm 7.1** instalado y funcionando
- **PyTorch 2.10.0** instalado con soporte ROCm
- GPU detectada: AMD Radeon Graphics (6800XT)
- **Pendiente:** hacer que OpenCV/librosa usen GPU
---
## Hardware
- **GPU Principal:** AMD Radeon 6800XT (16GB VRAM) con ROCm 7.1
- **GPU Alternativa:** NVIDIA RTX 3050 (8GB VRAM) - no configurada
- **CPU:** AMD Ryzen (12 cores)
- **RAM:** 32GB
- **Almacenamiento:** SSDNVMe
---
## Credenciales
- **Twitch Client ID:** `xk9gnw0wszfcwn3qq47a76wxvlz8oq`
- **Twitch Client Secret:** `51v7mkkd86u9urwadue8410hheu754`
---
## Pipeline Actual (Manual)
```bash
# 1. Descargar video
bajar "https://www.twitch.tv/videos/2701190361"
# 2. Descargar chat (después de compilar TwitchDownloaderCLI)
TwitchDownloaderCLI chatdownload --id 2701190361 -o chat.json
# 3. Convertir chat a texto
python3 -c "
import json
with open('chat.json') as f:
data = json.load(f)
with open('chat.txt', 'w') as f:
for c in data['comments']:
f.write(f\"[{c['created_at']}] {c['commenter']['name']}: {c['message']['body']}\n\")
"
# 4. Detectar highlights
python3 detector.py
# 5. Generar video
python3 generate_video.py
```
---
## Resultados Obtenidos
| Métrica | Valor |
|---------|-------|
| Video original | 19GB (5.3 horas) |
| Mensajes de chat | 12,942 |
| Picos detectados | 139 |
| Highlights útiles (>5s) | 4 |
| Video final | 39MB (~1 minuto) |
### Highlights Encontrados
1. ~4666s - ~4682s (16s)
2. ~4800s - ~4813s (13s)
3. ~8862s - ~8867s (5s)
4. ~11846s - ~11856s (10s)
---
## Pendientes (TODO)
### Alta Prioridad
1. **Sistema 2 de 3**: Implementar análisis de audio y color
2. **GPU**: Hacer que OpenCV/librosa usen la 6800XT
3. **Mejor detección**: Keywords, sentimiento, ranking
4. **Kick**: Soporte para chat (sin API pública)
### Media Prioridad
5. Paralelización
6. Interfaz web (Streamlit)
7. CLI mejorada
### Baja Prioridad
8. STT (reconocimiento de voz)
9. Detectar cuando streamer muestra algo en pantalla
10. Múltiples streamers
---
## Archivos del Proyecto
```
Twitch-Highlight-Detector/
├── .env # Credenciales Twitch
├── .git/ # Git repo
├── .gitignore
├── requirements.txt # Dependencias Python
├── lower # Script: descargar streams
├── pipeline.sh # Pipeline automatizado
├── detector.py # Detección de highlights (chat)
├── generate_video.py # Generación de video resumen
├── highlight.md # Docs: uso del pipeline
├── contexto.md # Este archivo
├── todo.md # Lista de tareas pendientes
├── chat.json # Chat descargado (TwitchDownloader)
├── chat.txt # Chat en formato texto
├── highlights.json # Timestamps de highlights
├── highlights.mp4 # Video final
└── 20260218_193846_twitch.mp4 # Video original de prueba
```
---
## Notas Importantes
1. **Twitch elimina el chat** de VODs después de un tiempo (no hay tiempo exacto definido)
2. **El threshold actual** es muy sensible - detecta muchos falsos positivos de 1-2 segundos
3. **El video de prueba** es de elxokas, un streamer español de League of Legends
4. **PyTorch con ROCm** está instalado pero no se está usando todavía en el código
---
## Links Relevantes
- TwitchDownloader: https://github.com/lay295/TwitchDownloader
- streamlink: https://streamlink.github.io/
- PyTorch ROCm: https://pytorch.org/
- ROCm: https://rocm.docs.amd.com/

95
detector.py Normal file
View File

@@ -0,0 +1,95 @@
import sys
import re
import json
import logging
import numpy as np
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def detect_highlights(chat_file, min_duration=10, threshold=2.0):
"""Detecta highlights por chat saturado"""
logger.info("Analizando picos de chat...")
# Leer mensajes
messages = []
with open(chat_file, 'r', encoding='utf-8') as f:
for line in f:
match = re.match(r'\[(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\]', line)
if match:
timestamp_str = match.group(1).replace('Z', '+00:00')
try:
timestamp = datetime.fromisoformat(timestamp_str)
messages.append((timestamp, line))
except:
pass
if not messages:
logger.error("No se encontraron mensajes")
return []
start_time = messages[0][0]
end_time = messages[-1][0]
duration = (end_time - start_time).total_seconds()
logger.info(f"Chat: {len(messages)} mensajes, duración: {duration:.1f}s")
# Agrupar por segundo
time_buckets = {}
for timestamp, _ in messages:
second = int((timestamp - start_time).total_seconds())
time_buckets[second] = time_buckets.get(second, 0) + 1
# Calcular estadísticas
counts = list(time_buckets.values())
mean_count = np.mean(counts)
std_count = np.std(counts)
logger.info(f"Stats: media={mean_count:.1f}, std={std_count:.1f}")
# Detectar picos
peak_seconds = []
for second, count in time_buckets.items():
if std_count > 0:
z_score = (count - mean_count) / std_count
if z_score > threshold:
peak_seconds.append(second)
logger.info(f"Picos encontrados: {len(peak_seconds)}")
# Unir segundos consecutivos
if not peak_seconds:
return []
intervals = []
start = peak_seconds[0]
prev = peak_seconds[0]
for second in peak_seconds[1:]:
if second - prev > 1:
if second - start >= min_duration:
intervals.append((start, prev))
start = second
prev = second
if prev - start >= min_duration:
intervals.append((start, prev))
return intervals
if __name__ == "__main__":
chat_file = "chat.txt"
highlights = detect_highlights(chat_file)
print(f"\nHighlights encontrados: {len(highlights)}")
for i, (start, end) in enumerate(highlights):
print(f" {i+1}. {start}s - {end}s (duración: {end-start}s)")
# Guardar JSON
with open("highlights.json", "w") as f:
json.dump(highlights, f)
print(f"\nGuardado en highlights.json")

283
detector_gpu.py Normal file
View File

@@ -0,0 +1,283 @@
import sys
import json
import logging
import subprocess
import torch
import numpy as np
from pathlib import Path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_device():
"""Obtiene el dispositivo (GPU o CPU)"""
if torch.cuda.is_available():
return torch.device("cuda")
return torch.device("cpu")
def extract_audio_gpu(video_file, output_wav="audio.wav"):
"""Extrae audio usando ffmpeg"""
logger.info(f"Extrayendo audio de {video_file}...")
subprocess.run([
"ffmpeg", "-i", video_file,
"-vn", "-acodec", "pcm_s16le",
"-ar", "16000", "-ac", "1", output_wav, "-y"
], capture_output=True)
return output_wav
def detect_audio_peaks_gpu(audio_file, threshold=1.5, window_seconds=5, device="cpu"):
"""
Detecta picos de audio usando PyTorch para procesamiento
"""
logger.info("Analizando picos de audio con GPU...")
# Cargar audio con scipy
import scipy.io.wavfile as wavfile
sr, waveform = wavfile.read(audio_file)
# Convertir a float
waveform = waveform.astype(np.float32) / 32768.0
# Calcular RMS por ventana usando numpy
frame_length = sr * window_seconds
hop_length = sr # 1 segundo entre ventanas
energies = []
for i in range(0, len(waveform) - frame_length, hop_length):
chunk = waveform[i:i + frame_length]
energy = np.sqrt(np.mean(chunk ** 2))
energies.append(energy)
energies = np.array(energies)
# Detectar picos
mean_e = np.mean(energies)
std_e = np.std(energies)
logger.info(f"Audio stats: media={mean_e:.4f}, std={std_e:.4f}")
audio_scores = {}
for i, energy in enumerate(energies):
if std_e > 0:
z_score = (energy - mean_e) / std_e
if z_score > threshold:
audio_scores[i] = z_score
logger.info(f"Picos de audio detectados: {len(audio_scores)}")
return audio_scores
def detect_video_peaks_fast(video_file, threshold=1.5, window_seconds=5):
"""
Detecta cambios de color/brillo (versión rápida, sin frames)
"""
logger.info("Analizando picos de color...")
# Usar ffmpeg para obtener información de brillo por segundo
# Esto es mucho más rápido que procesar frames
result = subprocess.run([
"ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=width,height,r_frame_rate,duration",
"-of", "csv=p=0", video_file
], capture_output=True, text=True)
# Extraer frames de referencia en baja resolución
video_360 = video_file.replace('.mp4', '_temp_360.mp4')
# Convertir a 360p para procesamiento rápido
logger.info("Convirtiendo a 360p para análisis...")
subprocess.run([
"ffmpeg", "-i", video_file,
"-vf", "scale=-2:360",
"-c:v", "libx264", "-preset", "fast",
"-crf", "28",
"-c:a", "copy",
video_360, "-y"
], capture_output=True)
# Extraer un frame cada N segundos
frames_dir = Path("frames_temp")
frames_dir.mkdir(exist_ok=True)
subprocess.run([
"ffmpeg", "-i", video_360,
"-vf", f"fps=1/{window_seconds}",
f"{frames_dir}/frame_%04d.png", "-y"
], capture_output=True)
# Procesar frames con PIL
from PIL import Image
import cv2
frame_files = sorted(frames_dir.glob("frame_*.png"))
if not frame_files:
logger.warning("No se pudieron extraer frames")
return {}
logger.info(f"Procesando {len(frame_files)} frames...")
brightness_scores = []
for frame_file in frame_files:
img = cv2.imread(str(frame_file))
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# Brillo = Value en HSV
brightness = hsv[:,:,2].mean()
# Saturación
saturation = hsv[:,:,1].mean()
# Score combinado
score = (brightness / 255) + (saturation / 255) * 0.5
brightness_scores.append(score)
brightness_scores = np.array(brightness_scores)
# Detectar picos
mean_b = np.mean(brightness_scores)
std_b = np.std(brightness_scores)
logger.info(f"Brillo stats: media={mean_b:.3f}, std={std_b:.3f}")
color_scores = {}
for i, score in enumerate(brightness_scores):
if std_b > 0:
z_score = (score - mean_b) / std_b
if z_score > threshold:
color_scores[i * window_seconds] = z_score
# Limpiar
subprocess.run(["rm", "-rf", str(frames_dir)])
subprocess.run(["rm", "-f", video_360], capture_output=True)
logger.info(f"Picos de color detectados: {len(color_scores)}")
return color_scores
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--video", required=True, help="Video file")
parser.add_argument("--chat", required=True, help="Chat JSON file")
parser.add_argument("--output", default="highlights.json", help="Output JSON")
parser.add_argument("--threshold", type=float, default=1.5, help="Threshold for peaks")
parser.add_argument("--min-duration", type=int, default=10, help="Min highlight duration")
parser.add_argument("--device", default="auto", help="Device: auto, cuda, cpu")
args = parser.parse_args()
# Determinar device
if args.device == "auto":
device = get_device()
else:
device = torch.device(args.device)
logger.info(f"Usando device: {device}")
# Cargar chat
logger.info("Cargando chat...")
with open(args.chat, 'r') as f:
chat_data = json.load(f)
# Extraer timestamps del chat
chat_times = {}
for comment in chat_data['comments']:
second = int(comment['content_offset_seconds'])
chat_times[second] = chat_times.get(second, 0) + 1
# Detectar picos de chat
chat_values = list(chat_times.values())
mean_c = np.mean(chat_values)
std_c = np.std(chat_values)
logger.info(f"Chat stats: media={mean_c:.1f}, std={std_c:.1f}")
chat_scores = {}
max_chat = max(chat_values) if chat_values else 1
for second, count in chat_times.items():
if std_c > 0:
z_score = (count - mean_c) / std_c
if z_score > args.threshold:
chat_scores[second] = z_score
logger.info(f"Picos de chat: {len(chat_scores)}")
# Extraer y analizar audio
audio_file = "temp_audio.wav"
extract_audio_gpu(args.video, audio_file)
audio_scores = detect_audio_peaks_gpu(audio_file, args.threshold, device=str(device))
# Limpiar audio temporal
Path(audio_file).unlink(missing_ok=True)
# Analizar video
video_scores = detect_video_peaks_fast(args.video, args.threshold)
# Combinar scores (2 de 3)
logger.info("Combinando scores (2 de 3)...")
# Obtener duración total
result = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrokey=1:nokey=1", args.video],
capture_output=True, text=True
)
duration = int(float(result.stdout.strip())) if result.stdout.strip() else 3600
# Normalizar scores
max_audio = max(audio_scores.values()) if audio_scores else 1
max_video = max(video_scores.values()) if video_scores else 1
max_chat_norm = max(chat_scores.values()) if chat_scores else 1
# Unir segundos consecutivos
highlights = []
for second in range(duration):
points = 0
# Chat
chat_point = chat_scores.get(second, 0) / max_chat_norm if max_chat_norm > 0 else 0
if chat_point > 0.5:
points += 1
# Audio
audio_point = audio_scores.get(second, 0) / max_audio if max_audio > 0 else 0
if audio_point > 0.5:
points += 1
# Color
video_point = video_scores.get(second, 0) / max_video if max_video > 0 else 0
if video_point > 0.5:
points += 1
if points >= 2:
highlights.append(second)
# Crear intervalos
intervals = []
if highlights:
start = highlights[0]
prev = highlights[0]
for second in highlights[1:]:
if second - prev > 1:
if second - start >= args.min_duration:
intervals.append((start, prev))
start = second
prev = second
if prev - start >= args.min_duration:
intervals.append((start, prev))
logger.info(f"Highlights encontrados: {len(intervals)}")
# Guardar
with open(args.output, 'w') as f:
json.dump(intervals, f)
logger.info(f"Guardado en {args.output}")
# Imprimir resumen
print(f"\nHighlights ({len(intervals)} total):")
for i, (s, e) in enumerate(intervals[:10]):
print(f" {i+1}. {s}s - {e}s (duración: {e-s}s)")
if __name__ == "__main__":
main()

63
generate_video.py Normal file
View File

@@ -0,0 +1,63 @@
import json
import argparse
from moviepy.editor import VideoFileClip, concatenate_videoclips
import logging
logging.basicConfig(level=logging.INFO)
def create_summary(video_file, highlights_file, output_file, padding=5):
"""Crea video resumen con los highlights"""
# Cargar highlights
with open(highlights_file, 'r') as f:
highlights = json.load(f)
if not highlights:
print("No hay highlights")
return
# Filtrar highlights con duración mínima
highlights = [(s, e) for s, e in highlights if e - s >= 5]
print(f"Creando video con {len(highlights)} highlights...")
clip = VideoFileClip(video_file)
duration = clip.duration
highlight_clips = []
for start, end in highlights:
start_pad = max(0, start - padding)
end_pad = min(duration, end + padding)
highlight_clip = clip.subclip(start_pad, end_pad)
highlight_clips.append(highlight_clip)
print(f" Clip: {start_pad:.1f}s - {end_pad:.1f}s (duración: {end_pad-start_pad:.1f}s)")
if not highlight_clips:
print("No se pudo crear ningún clip")
return
print(f"Exportando video ({len(highlight_clips)} clips, {sum(c.duration for c in highlight_clips):.1f}s total)...")
final_clip = concatenate_videoclips(highlight_clips, method="compose")
final_clip.write_videofile(
output_file,
codec='libx264',
audio_codec='aac',
fps=24,
verbose=False,
logger=None
)
print(f"¡Listo! Video guardado en: {output_file}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--video", required=True, help="Video file")
parser.add_argument("--highlights", required=True, help="Highlights JSON")
parser.add_argument("--output", required=True, help="Output video")
parser.add_argument("--padding", type=int, default=5, help="Padding seconds")
args = parser.parse_args()
create_summary(args.video, args.highlights, args.output, args.padding)

106
highlight.md Normal file
View File

@@ -0,0 +1,106 @@
# Highlight Detector Pipeline
Pipeline completo para detectar y generar highlights de streams de Twitch/Kick.
## Requisitos
```bash
# Instalar dependencias del sistema
sudo pacman -S ffmpeg streamlink dotnet-sdk --noconfirm
# Instalar dependencias de Python
pip install --break-system-packages moviepy opencv-python-headless scipy numpy python-dotenv
# Instalar TwitchDownloaderCLI (ya incluido en /usr/local/bin)
```
## Uso
### 1. Descargar Stream
```bash
# Usar streamlink (incluye video + audio)
bajar "https://www.twitch.tv/videos/2701190361"
# O manualmente con streamlink
streamlink "https://www.twitch.tv/videos/2701190361" best -o video.mp4
```
### 2. Descargar Chat
```bash
# Usar TwitchDownloaderCLI
TwitchDownloaderCLI chatdownload --id 2701190361 -o chat.json
```
### 3. Detectar Highlights
```bash
# Convertir chat a texto y detectar highlights
python3 detector.py
# Esto genera:
# - chat.txt (chat en formato texto)
# - highlights.json (timestamps de highlights)
```
### 4. Generar Video Resumen
```bash
python3 generate_video.py
# Esto genera:
# - highlights.mp4 (video con los mejores momentos)
```
## Automatizado (Un solo comando)
```bash
# Downloader + Chat + Detect + Generate
./pipeline.sh <video_id> <output_name>
```
## Parámetros Ajustables
En `detector.py`:
- `min_duration`: Duración mínima del highlight (default: 10s)
- `threshold`: Umbral de detección (default: 2.0 desviaciones estándar)
En `generate_video.py`:
- `padding`: Segundos adicionales antes/después del highlight (default: 5s)
## GPU vs CPU
**El pipeline actual es 100% CPU.**
Para mejor rendimiento:
- **MoviePy**: Usa CPU (puede usar GPU con ffmpeg)
- **Análisis de video**: CPU con OpenCV
- **Audio**: CPU con librosa
Para hacer GPU-dependiente:
- Usar `PyTorch`/`TensorFlow` para detección
- Usar GPU de la GPU para renderizado con ffmpeg
## Estructura de Archivos
```
Twitch-Highlight-Detector/
├── .env # Credenciales
├── main.py # Entry point
├── requirements.txt
├── bajar # Script para descargar streams
├── detector.py # Detección de highlights
├── generate_video.py # Generación de video
├── pipeline.sh # Pipeline automatizado
├── chat.json # Chat descargado
├── chat.txt # Chat en formato texto
├── highlights.json # Timestamps de highlights
└── highlights.mp4 # Video final
```
## Notas
- El chat de VODs antiguos puede no estar disponible (Twitch lo elimina)
- El threshold bajo detecta más highlights (puede ser ruido)
- Duraciones muy cortas pueden no ser highlights reales

37
main.py
View File

@@ -1,37 +0,0 @@
import argparse
import os
import sys
import logging
from clipper import recorder
def parse_arguments():
parser = argparse.ArgumentParser(description='Twitch highlighter')
parser.add_argument('--client', "-c", help='Twitch client id', required=True, dest="tw_client")
parser.add_argument('--secret', "-s", help='Twitch secret id', required=True, dest="tw_secret")
parser.add_argument('--streamer', "-t", help='Twitch streamer username', required=True, dest="tw_streamer")
parser.add_argument('--quality', "-q", help='Video downloading quality', dest="tw_quality", default="360p")
parser.add_argument('--output_path', "-o", help='Video download folder', dest="output_path",
default=os.path.join(os.getcwd(), "recorded"))
return parser.parse_args()
def on_video_recorded(streamer, filename):
pass
def on_chat_recorded(streamer, filename):
pass
if __name__ == "__main__":
# TODO configure logging
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
args = parse_arguments()
config = recorder.RecorderConfig(args.tw_client, args.tw_secret, args.tw_streamer, args.tw_quality,
args.output_path)
recorder = recorder.Recorder(config)
recorder.run()

109
pipeline.sh Executable file
View File

@@ -0,0 +1,109 @@
#!/bin/bash
# Highlight Detector Pipeline con Modo Draft
# Uso: ./pipeline.sh <video_id> [output_name] [--draft | --hd]
set -e
# Parsear argumentos
DRAFT_MODE=false
VIDEO_ID=""
OUTPUT_NAME="highlights"
while [[ $# -gt 0 ]]; do
case $1 in
--draft)
DRAFT_MODE=true
shift
;;
--hd)
DRAFT_MODE=false
shift
;;
*)
if [[ -z "$VIDEO_ID" ]]; then
VIDEO_ID="$1"
else
OUTPUT_NAME="$1"
fi
shift
;;
esac
done
if [ -z "$VIDEO_ID" ]; then
echo "Uso: $0 <video_id> [output_name] [--draft | --hd]"
echo ""
echo "Modos:"
echo " --draft Modo prueba rápida (360p, menos procesamiento)"
echo " --hd Modo alta calidad (1080p, por defecto)"
echo ""
echo "Ejemplo:"
echo " $0 2701190361 elxokas --draft # Prueba rápida"
echo " $0 2701190361 elxokas --hd # Alta calidad"
exit 1
fi
echo "============================================"
echo " HIGHLIGHT DETECTOR PIPELINE"
echo "============================================"
echo "Video ID: $VIDEO_ID"
echo "Output: $OUTPUT_NAME"
echo "Modo: $([ "$DRAFT_MODE" = true ] && echo "DRAFT (360p)" || echo "HD (1080p)")"
echo ""
# Determinar calidad
if [ "$DRAFT_MODE" = true ]; then
QUALITY="360p"
VIDEO_FILE="${OUTPUT_NAME}_draft.mp4"
else
QUALITY="best"
VIDEO_FILE="${OUTPUT_NAME}.mp4"
fi
# 1. Descargar video
echo "[1/5] Descargando video ($QUALITY)..."
if [ ! -f "$VIDEO_FILE" ]; then
streamlink "https://www.twitch.tv/videos/${VIDEO_ID}" "$QUALITY" -o "$VIDEO_FILE"
else
echo "Video ya existe: $VIDEO_FILE"
fi
# 2. Descargar chat
echo "[2/5] Descargando chat..."
if [ ! -f "${OUTPUT_NAME}_chat.json" ]; then
TwitchDownloaderCLI chatdownload --id "$VIDEO_ID" -o "${OUTPUT_NAME}_chat.json"
else
echo "Chat ya existe"
fi
# 3. Detectar highlights (usando GPU si está disponible)
echo "[3/5] Detectando highlights..."
python3 detector_gpu.py \
--video "$VIDEO_FILE" \
--chat "${OUTPUT_NAME}_chat.json" \
--output "${OUTPUT_NAME}_highlights.json" \
--threshold 1.5 \
--min-duration 10
# 4. Generar video
echo "[4/5] Generando video..."
python3 generate_video.py \
--video "$VIDEO_FILE" \
--highlights "${OUTPUT_NAME}_highlights.json" \
--output "${OUTPUT_NAME}_final.mp4"
# 5. Limpiar
echo "[5/5] Limpiando archivos temporales..."
if [ "$DRAFT_MODE" = true ]; then
rm -f "${OUTPUT_NAME}_draft_360p.mp4"
fi
echo ""
echo "============================================"
echo " COMPLETADO"
echo "============================================"
echo "Video final: ${OUTPUT_NAME}_final.mp4"
echo ""
echo "Para procesar en HD después:"
echo " $0 $VIDEO_ID ${OUTPUT_NAME}_hd --hd"

View File

@@ -1,4 +1,17 @@
requests==2.28.1 # Core
streamlink==4.2.0 requests
twitchAPI==2.5.7 python-dotenv
irc==20.1.0
# Video processing
moviepy
opencv-python-headless
# Audio processing
scipy
numpy
librosa
# Chat download
chat-downloader
# Chat analysis

229
todo.md Normal file
View File

@@ -0,0 +1,229 @@
# TODO - Mejoras Pendientes
## Estado Actual
### Working ✅
- Descarga de video (streamlink)
- Descarga de chat (TwitchDownloaderCLI)
- Detección por chat saturado
- Generación de video (moviepy)
- PyTorch con ROCm instalado
### Pendiente ❌
- Análisis de audio
- Análisis de color
- Uso de GPU en procesamiento
---
## PRIORIDAD 1: Sistema 2 de 3
### [ ] Audio - Picos de Sonido
Implementar detección de gritos/picos de volumen.
**Método actual (CPU):**
- Extraer audio con ffmpeg
- Usar librosa para RMS
- Detectar picos con scipy
**Método GPU (a implementar):**
```python
import torch
import torchaudio
# Usar GPU para análisis espectral
waveform, sr = torchaudio.load(audio_file)
spectrogram = torchaudio.transforms.Spectrogram()(waveform)
```
**Tareas:**
- [ ] Extraer audio del video con ffmpeg
- [ ] Calcular RMS/energía por ventana
- [ ] Detectar picos (threshold = media + 1.5*std)
- [ ] Devolver timestamps de picos
### [ ] Color - Momentos Brillantes
Detectar cambios de color/brillo en el video.
**Método GPU:**
```python
import cv2
# OpenCV con OpenCL
cv2.ocl::setUseOpenCL(True)
```
**Tareas:**
- [ ] Procesar frames con OpenCV GPU
- [ ] Calcular saturación y brillo HSV
- [ ] Detectar momentos con cambios significativos
- [ ] Devolver timestamps
### [ ] Combinar 2 de 3
Sistema de scoring:
```
highlight = (chat_score >= 2) + (audio_score >= 1.5) + (color_score >= 0.5)
if highlight >= 2: es highlight
```
---
## PRIORIDAD 2: GPU - Optimizar para 6800XT
### [ ] PyTorch con ROCm
✅ Ya instalado:
```
PyTorch: 2.10.0+rocm7.1
ROCm available: True
Device: AMD Radeon Graphics
```
### [ ] OpenCV con OpenCL
```bash
# Verificar soporte OpenCL
python -c "import cv2; print(cv2.ocl.haveOpenCL())"
```
**Si no tiene OpenCL:**
- [ ] Instalar opencv-python (no headless)
- [ ] Instalar ocl-runtime para AMD
### [ ] Reemplazar librerías CPU por GPU
| Componente | CPU | GPU |
|------------|-----|-----|
| Audio | librosa | torchaudio (ROCm) |
| Video frames | cv2 | cv2 + OpenCL |
| Procesamiento | scipy | torch |
| Concatenación | moviepy | torch + ffmpeg |
### [ ] MoviePy con GPU
MoviePy actualmente usa CPU. Opciones:
1. Usar ffmpeg directamente con flags GPU
2. Crear pipeline propio con torch
```bash
# ffmpeg con GPU
ffmpeg -hwaccel auto -i input.mp4 -c:v h264_amf output.mp4
```
---
## PRIORIDAD 3: Mejorar Detección
### [ ] Palabras Clave en Chat
Detectar momentos con keywords como:
- "LOL", "POG", "KEK", "RIP", "WTF"
- Emotes populares
- Mayúsculas (gritos en chat)
### [ ] Análisis de Sentimiento
- [ ] Usar modelo de sentiment (torch)
- [ ] Detectar momentos positivos/negativos intensos
### [ ] Ranking de Highlights
- [ ] Ordenar por intensidad (combinación de scores)
- [ ] Limitar a N mejores highlights
- [ ] Duration-aware scoring
---
## PRIORIDAD 4: Kick
### [ ] Descarga de Video
✅ Ya funciona con streamlink:
```bash
streamlink https://kick.com/streamer best -o video.mp4
```
### [ ] Chat
❌ Kick NO tiene API pública para chat.
**Opciones:**
1. Web scraping del chat
2. Usar herramientas de terceros
3. Omitir chat y usar solo audio/color
---
## PRIORIDAD 5: Optimizaciones
### [ ] Paralelización
- [ ] Procesar chunks del video en paralelo
- [ ] ThreadPool para I/O
### [ ] Cache
- [ ] Guardar resultados intermedios
- [ ] Reutilizar análisis si existe chat.txt
### [ ] Chunking
- [ ] Procesar video en segmentos
- [ ] Evitar cargar todo en memoria
---
## PRIORIDAD 6: UX/UI
### [ ] CLI Mejorada
```bash
python main.py --video-id 2701190361 --platform twitch \
--min-duration 10 --threshold 2.0 \
--output highlights.mp4 \
--use-gpu --gpu-device 0
```
### [ ] Interfaz Web
- [ ] Streamlit app
- [ ] Subir video/chat
- [ ] Ver timeline de highlights
- [ ] Preview de clips
### [ ] Progress Bars
- [ ] tqdm para descargas
- [ ] Progress para procesamiento
---
## RECETAS DE INSTALACIÓN
### GPU ROCm
```bash
# PyTorch con ROCm
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/rocm7.1
# Verificar
python -c "import torch; print(torch.cuda.is_available())"
```
### NVIDIA CUDA (alternativa)
```bash
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
```
### OpenCV con OpenCL
```bash
# Verificar
python -c "import cv2; print(cv2.ocl.haveOpenCL())"
# Si False, instalar con GPU support
pip uninstall opencv-python-headless
pip install opencv-python
```
---
## RENDIMIENTO ESPERADO
| Config | FPS Processing | Tiempo 5h Video |
|--------|----------------|------------------|
| CPU (12 cores) | ~5-10 FPS | ~1-2 horas |
| GPU NVIDIA 3050 | ~30-50 FPS | ~10-20 min |
| GPU AMD 6800XT | ~30-40 FPS | ~15-25 min |
---
## NOTAS
1. **ROCm 7.1** funcionando con PyTorch
2. **6800XT** detectada como "AMD Radeon Graphics"
3. **MoviePy** sigue usando CPU para renderizado
4. Para mejor rendimiento, considerar renderizado con ffmpeg GPU directamente