# CLAUDE.md - TranscriptorIO ## ¿Qué es TranscriptorIO? TranscriptorIO es un sistema completo de generación automática de subtítulos para contenido multimedia usando IA (Whisper + modelos de traducción). Es un **hard fork** de [SubGen](https://github.com/McCloudS/subgen) con una arquitectura completamente rediseñada inspirada en Tdarr. ## Motivación SubGen es funcional pero tiene limitaciones fundamentales de diseño: ### Problemas de SubGen - **Procesamiento síncrono**: Bloquea threads mientras transcribe - **Sin cola persistente**: Los trabajos se pierden al reiniciar - **Sin WebUI**: Removida en marzo 2024, solo tiene Swagger docs - **Sin visibilidad**: No sabes progreso, ETA, o estado de trabajos - **Sin priorización**: No puedes reordenar trabajos - **Timeouts en Bazarr**: Si un episodio tarda >5min, throttle de 24 horas - **Configuración compleja**: 40+ variables ENV sin validación ### Visión de TranscriptorIO Un sistema tipo **Tdarr pero para subtítulos**, con: - ✅ Sistema de cola asíncrona persistente (SQLite) - ✅ Workers configurables (múltiples GPUs/CPUs) - ✅ WebUI moderna con progreso en tiempo real - ✅ Múltiples pipelines de calidad (Fast/Balanced/Best) - ✅ Integración asíncrona con Bazarr - ✅ Procesamiento batch (temporadas completas) - ✅ API REST completa - ✅ WebSocket para updates en vivo ## Casos de uso ### Caso principal: Anime japonés → Subtítulos español **Problema**: Anime sin fansubs en español, solo tiene audio japonés. **Pipeline**: ``` Audio japonés ↓ Whisper (task="translate") → Texto inglés ↓ Helsinki-NLP (en→es) → Texto español ↓ Generar .srt con timestamps ``` **Alternativas configurables**: - **Fast** (4GB VRAM): ja→en→es con Helsinki-NLP - **Balanced** (6GB VRAM): ja→ja→es con M2M100 - **Best** (10GB+ VRAM): ja→es directo con SeamlessM4T ### Integración con stack existente ``` Sonarr descarga episodio ↓ Bazarr detecta: faltan subtítulos español ↓ Bazarr → TranscriptorIO (provider asíncrono) ↓ TranscriptorIO encola trabajo ↓ Worker procesa cuando está libre ↓ Callback a Bazarr con .srt generado ↓ Jellyfin detecta nuevo subtítulo ``` ## Modos de Operación TranscriptorIO soporta dos modos de operación distintos que se configuran vía environment variables: ### Modo Standalone (Tdarr-like) **Descripción**: TranscriptorIO escanea automáticamente tu biblioteca de medios y genera subtítulos según reglas configurables. **Casos de uso**: - Procesamiento batch de biblioteca existente - Monitoreo automático de nuevos archivos - Control total sobre qué se transcribe sin depender de Bazarr **Funcionamiento**: ``` 1. Escaneo periódico con ffprobe └─> Detecta archivos que cumplen criterios (Ej: audio japonés + sin subs español) 2. Encolado automático └─> Añade a cola con prioridad configurada 3. Procesamiento batch └─> Workers procesan según disponibilidad 4. Escritura directa └─> Guarda .srt junto al archivo origen ``` **Configuración**: ```env # Habilitar modo standalone TRANSCRIPTARR_MODE=standalone # Carpetas a escanear (separadas por |) LIBRARY_PATHS=/media/anime|/media/movies # Reglas de filtrado REQUIRED_AUDIO_LANGUAGE=ja REQUIRED_MISSING_SUBTITLE=spa SKIP_IF_SUBTITLE_EXISTS=true # Escaneo automático AUTO_SCAN_ENABLED=true SCAN_INTERVAL_MINUTES=30 ``` **Ventajas**: - ✅ No depende de integraciones externas - ✅ Procesamiento batch eficiente - ✅ Monitoreo automático de nueva media - ✅ Control granular con reglas de filtrado ### Modo Provider (Bazarr-slave) **Descripción**: TranscriptorIO actúa como provider de subtítulos para Bazarr mediante una API asíncrona mejorada. **Casos de uso**: - Integración con stack *arr existente - Gestión centralizada de subtítulos en Bazarr - Fallback cuando no hay subtítulos pre-hechos **Funcionamiento**: ``` 1. Bazarr solicita subtítulo (API call) └─> POST /api/provider/request 2. TranscriptorIO encola trabajo └─> Retorna job_id inmediatamente └─> No bloquea thread de Bazarr 3. Procesamiento asíncrono └─> Worker transcribe cuando hay capacidad 4. Callback a Bazarr └─> POST {bazarr_callback_url} con .srt └─> O polling de Bazarr cada 30s ``` **Configuración**: ```env # Habilitar modo provider TRANSCRIPTARR_MODE=provider # API de Bazarr para callbacks BAZARR_URL=http://bazarr:6767 BAZARR_API_KEY=your_api_key_here # Configuración del provider PROVIDER_TIMEOUT_SECONDS=600 PROVIDER_CALLBACK_ENABLED=true PROVIDER_POLLING_INTERVAL=30 ``` **Ventajas vs SubGen original**: - ✅ **No bloquea**: Retorna inmediatamente con job_id - ✅ **Sin timeouts**: Bazarr no throttle por trabajos lentos - ✅ **Visibilidad**: Bazarr puede consultar progreso - ✅ **Reintentos**: Manejo automático de errores - ✅ **Priorización**: Trabajos manuales tienen mayor prioridad ### Modo Híbrido (Recomendado) Puedes habilitar ambos modos simultáneamente: ```env TRANSCRIPTARR_MODE=standalone,provider ``` **Beneficios**: - Bazarr maneja media nueva automáticamente - Standalone procesa biblioteca existente - Cola unificada con priorización inteligente - Mejor aprovechamiento de recursos ## Arquitectura técnica ### Stack tecnológico **Backend**: - FastAPI (API REST + WebSocket) - SQLAlchemy (ORM multi-backend) - SQLite / PostgreSQL / MariaDB (queue persistente) - faster-whisper (transcripción optimizada) - Helsinki-NLP/opus-mt-en-es (traducción ligera) - stable-ts (mejora de timestamps) **Frontend**: - Vue 3 + Vite - Tailwind CSS - Chart.js (estadísticas) - Socket.io-client (updates en tiempo real) **Infraestructura**: - Docker + Docker Compose - NVIDIA GPU support (opcional, también CPU) - Multi-container: backend + workers + frontend ### Componentes principales ``` transcriptorio/ ├── backend/ │ ├── core/ │ │ ├── pipelines/ │ │ │ ├── whisper_fast.py # ja→en→es (Helsinki) │ │ │ ├── whisper_balanced.py # ja→ja→es (M2M100) │ │ │ └── seamless.py # ja→es directo │ │ ├── queue_manager.py # Cola SQLite │ │ ├── worker_pool.py # Gestión de workers │ │ └── transcriber.py # Core Whisper │ ├── api/ │ │ ├── legacy.py # /asr (compat SubGen/Bazarr) │ │ ├── queue.py # /api/queue/* │ │ ├── jobs.py # /api/jobs/* │ │ └── websocket.py # /ws (real-time) │ └── main.py ├── frontend/ │ ├── src/ │ │ ├── components/ │ │ │ ├── Dashboard.vue # Stats + current job │ │ │ ├── QueueManager.vue # Lista de trabajos │ │ │ ├── JobDetails.vue # Detalles + logs │ │ │ └── Settings.vue # Configuración │ │ ├── App.vue │ │ └── main.js │ └── package.json ├── bazarr-integration/ │ └── transcriptorio_provider.py # Custom provider asíncrono └── docker-compose.yml ``` ### Base de datos (SQLite) ```sql CREATE TABLE jobs ( id TEXT PRIMARY KEY, file_path TEXT NOT NULL, file_name TEXT NOT NULL, status TEXT DEFAULT 'queued', -- queued, processing, completed, failed priority INTEGER DEFAULT 0, -- Config source_lang TEXT, target_lang TEXT, quality_preset TEXT DEFAULT 'fast', -- Progress progress REAL DEFAULT 0, current_stage TEXT, -- transcribing, translating, generating eta_seconds INTEGER, -- Timestamps created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, started_at TIMESTAMP, completed_at TIMESTAMP, -- Results output_path TEXT, srt_content TEXT, segments_count INTEGER, -- Error handling error TEXT, retry_count INTEGER DEFAULT 0, -- Metadata worker_id TEXT, vram_used_mb INTEGER, processing_time_seconds REAL ); CREATE INDEX idx_status ON jobs(status); CREATE INDEX idx_priority ON jobs(priority DESC, created_at ASC); CREATE INDEX idx_created ON jobs(created_at DESC); ``` ### API Endpoints #### Legacy (compatibilidad SubGen/Bazarr) ```http POST /asr?task=translate&language=ja&output=srt Content-Type: multipart/form-data → Respuesta síncrona con .srt ``` #### Modernos (TranscriptorIO) ```http # Añadir trabajo a cola POST /api/queue/add { "files": ["/media/anime/episode.mkv"], "source_lang": "ja", "target_lang": "es", "quality_preset": "fast", "priority": 0 } → { "job_ids": ["uuid-1234"], "queued": 1 } # Estado de la cola GET /api/queue/status → { "pending": 3, "processing": 1, "completed_today": 12, "failed_today": 0, "vram_available": "1.5GB/4GB" } # Detalles de trabajo GET /api/jobs/{job_id} → { "id": "uuid-1234", "status": "processing", "progress": 45.2, "current_stage": "translating", "eta_seconds": 120, "file_name": "anime_ep01.mkv" } # Historial GET /api/jobs/history?limit=50 → [ { job }, { job }, ... ] # WebSocket updates WS /ws → Stream continuo de updates ``` ### WebUI #### Dashboard ``` ┌─────────────────────────────────────────────────────────┐ │ TranscriptorIO 🟢 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📊 Stats │ │ ┌─────────┬──────────┬──────────┬─────────────────┐ │ │ │ Queue: 3│Processing│Completed │ VRAM: 2.8/4.0GB │ │ │ │ │ 1 │ Today │ │ │ │ │ │ │ 12 │ │ │ │ └─────────┴──────────┴──────────┴─────────────────┘ │ │ │ │ 🎬 Current Job │ │ ┌─────────────────────────────────────────────────┐ │ │ │ File: Anime_S01E05.mkv │ │ │ │ Stage: Translating segments │ │ │ │ Progress: ████████████░░░░░░ 65% │ │ │ │ ETA: 2m 15s │ │ │ │ Model: whisper-medium + helsinki-nlp │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📋 Queue (3 pending) │ │ ┌──┬─────────────────────┬────────┬──────────────┐ │ │ │#1│Anime_S01E06.mkv │ Fast │ Priority: 0 │ │ │ │#2│Movie_2024.mkv │ Best │ Priority: 0 │ │ │ │#3│Show_S02E01.mkv │ Fast │ Priority: -1 │ │ │ └──┴─────────────────────┴────────┴──────────────┘ │ │ │ │ [+ Add Files] [⚙️ Settings] [📊 Stats] [📖 Logs] │ └─────────────────────────────────────────────────────────┘ ``` #### Settings ``` ┌─────────────────────────────────────────────────────────┐ │ Settings │ ├─────────────────────────────────────────────────────────┤ │ │ │ 🎯 Default Quality Preset │ │ ○ Fast (4GB VRAM, ~3min/episode) │ │ Whisper medium + Helsinki-NLP │ │ Best for: GTX 1650, RTX 3050 │ │ │ │ ● Balanced (6GB VRAM, ~5min/episode) │ │ Whisper medium + M2M100 │ │ Best for: RTX 3060, RTX 4060 │ │ │ │ ○ Best (10GB+ VRAM, ~10min/episode) │ │ SeamlessM4T direct translation │ │ Best for: RTX 4070+, professional GPUs │ │ │ │ ⚡ Workers Configuration │ │ GPU Workers: [2] ▾ │ │ CPU Workers: [1] ▾ │ │ Concurrent jobs per worker: [1] ▾ │ │ │ │ 🌐 Default Languages │ │ Source: [Japanese ▾] Target: [Spanish ▾] │ │ │ │ 📁 Paths │ │ Watch folders: /media/anime │ │ /media/movies │ │ Output format: {filename}.{lang}.srt │ │ │ │ 🔔 Notifications │ │ ☑ Discord webhook on completion │ │ ☑ Email on failure │ │ │ │ [Save Changes] [Reset Defaults] │ └─────────────────────────────────────────────────────────┘ ``` ## Pipeline de transcripción detallado ### Flujo Fast Preset (ja→en→es) ```python # 1. Extracción de audio (si es video) ffprobe detecta pistas de audio → Selecciona pista japonesa → Extrae con ffmpeg (opcional, Whisper acepta video directo) # 2. Whisper transcripción WhisperModel("medium", compute_type="int8") → transcribe(audio, language="ja", task="translate") → Output: Segmentos con timestamps en INGLÉS Ejemplo: [0.00s -> 3.50s] "Hello, welcome to today's episode" [3.50s -> 7.80s] "We're going to see something interesting" # 3. Traducción en→es (batch) Helsinki-NLP/opus-mt-en-es → Batch de 32 segmentos a la vez → Mantiene timestamps originales Ejemplo: [0.00s -> 3.50s] "Hola, bienvenido al episodio de hoy" [3.50s -> 7.80s] "Vamos a ver algo interesante" # 4. Generación SRT Formato timestamps + texto → Guarda archivo.es.srt # 5. Post-processing (opcional) - Aeneas re-sync (ajuste fino de timestamps) - Subtitle styling (ASS format) - Quality check (detección de errores) ``` ### Uso de VRAM esperado **GTX 1650 (4GB VRAM)**: ``` Fast preset: - Whisper medium INT8: ~2.5GB - Helsinki-NLP: ~1GB - Overhead sistema: ~0.5GB Total: ~4GB ✅ Cabe perfecto Tiempo: ~3-5 min por episodio 24min ``` **RTX 3060 (12GB VRAM)**: ``` Balanced preset: - Whisper large-v3 INT8: ~5GB - M2M100: ~2GB - Overhead: ~1GB Total: ~8GB ✅ Sobra espacio Tiempo: ~4-7 min por episodio 24min ``` ## Integración con Bazarr ### Custom Provider (asíncrono) ```python # bazarr/libs/subliminal_patch/providers/transcriptorio.py class TranscriptorIOProvider(Provider): """ Provider asíncrono para TranscriptorIO A diferencia del provider Whisper original, NO bloquea """ provider_name = 'transcriptorio' def download_subtitle(self, subtitle): # Si es búsqueda automática → async (no bloquea) if not subtitle.manual_search: job_id = self._queue_job(subtitle) raise SubtitlePending( job_id=job_id, eta=self._estimate_time(subtitle) ) # Si es búsqueda manual → sync con long polling return self._process_sync(subtitle, timeout=600) def _queue_job(self, subtitle): """Encola trabajo sin esperar""" response = requests.post( f"{self.endpoint}/api/queue/add", json={ "file": subtitle.video.name, "source_lang": "ja", "target_lang": "es", "quality_preset": self.quality_preset, "callback_url": self._get_callback_url(subtitle.id) }, headers={"X-API-Key": self.api_key} ) return response.json()["job_ids"][0] # Background task en Bazarr (cada 30s) @scheduler.scheduled_job('interval', seconds=30) def poll_transcriptorio_jobs(): """Revisar trabajos completados""" pending = db.get_pending_transcriptorio_jobs() for job in pending: status = get_job_status(job.provider_job_id) if status['status'] == 'completed': save_subtitle(job.subtitle_id, status['srt_content']) db.mark_completed(job.id) ``` ### Ventajas vs provider Whisper original | Feature | Whisper (original) | TranscriptorIO | |---------|-------------------|----------------| | Bloquea thread Bazarr | ✅ Sí (3-10min) | ❌ No (async) | | Timeout 24h si tarda | ✅ Sí | ❌ No | | Cola visible | ❌ No | ✅ Sí (WebUI) | | Retry automático | ❌ No | ✅ Sí | | Priorización | ❌ No | ✅ Sí | | Múltiples GPUs | ❌ No | ✅ Sí | | WebUI | ❌ No | ✅ Sí | ## Roadmap de desarrollo ### Fase 1: MVP Backend (2-3 semanas) **Objetivos**: - [ ] Queue manager con SQLite - [ ] Worker pool básico - [ ] Pipeline Fast (Whisper + Helsinki-NLP) - [ ] API REST completa - [ ] Endpoint legacy `/asr` compatible **Entregables**: - Backend funcional headless - Docker Compose para testing - Documentación API ### Fase 2: WebUI (2-3 semanas) **Objetivos**: - [ ] Dashboard con stats - [ ] Queue viewer con drag&drop - [ ] Job details con logs - [ ] Settings page - [ ] WebSocket integration **Entregables**: - WebUI completa y funcional - Mobile responsive - Tema dark/light ### Fase 3: Bazarr Integration (1-2 semanas) **Objetivos**: - [ ] Custom provider asíncrono - [ ] Background polling task - [ ] Callback webhook support - [ ] Testing con Bazarr real **Entregables**: - Provider plugin para Bazarr - Documentación integración - PR al repo de Bazarr (si aceptan) ### Fase 4: Features Avanzados (3-4 semanas) **Objetivos**: - [ ] Pipeline Balanced (M2M100) - [ ] Pipeline Best (SeamlessM4T) - [ ] Batch operations (temporadas) - [ ] Scanner automático (inotify) - [ ] Post-processing (Aeneas sync) - [ ] Notificaciones (Discord, email) **Entregables**: - Sistema completo production-ready - Docs completas - Tests automatizados ### Fase 5: Release & Community (ongoing) **Objetivos**: - [ ] Docker Hub releases - [ ] GitHub Actions CI/CD - [ ] Documentación completa - [ ] Video tutoriales - [ ] Anuncio en comunidades **Canales**: - /r/selfhosted - /r/homelab - Discord de Bazarr - LinuxServer.io ## Métricas de éxito **Técnicas**: - ✅ Procesa episodio 24min en <5min (GTX 1650) - ✅ Uso VRAM <4GB total - ✅ Queue persiste entre reinicios - ✅ API response time <100ms - ✅ WebUI load time <2s **UX**: - ✅ Setup en <15min para usuario promedio - ✅ Zero-config con defaults razonables - ✅ WebUI intuitiva (no necesita docs) **Comunidad**: - 🎯 100 stars en primer mes - 🎯 500 stars en 6 meses - 🎯 10+ contributors - 🎯 Featured en LinuxServer.io ## Diferenciadores clave ### vs SubGen - ✅ WebUI moderna vs ❌ Sin UI - ✅ Cola asíncrona vs ❌ Queue simple - ✅ Múltiples presets vs ❌ Config manual - ✅ Worker pool vs ❌ Single process ### vs Tdarr - ✅ Específico para subtítulos vs 🔧 General transcoding - ✅ Integración Bazarr nativa vs ⚠️ Solo webhooks - ✅ Traducción multilingüe vs ❌ No traduce ### vs Whisper-ASR-Webservice - ✅ Cola persistente vs ❌ Stateless - ✅ WebUI vs ❌ Solo API - ✅ Múltiples pipelines vs ⚠️ Solo Whisper ## Consideraciones técnicas ### Limitaciones conocidas **Whisper**: - Solo traduce a inglés (limitación del modelo) - Necesita audio limpio (música de fondo degrada calidad) - Nombres propios se traducen mal - Honoríficos japoneses se pierden **Traducción**: - Helsinki-NLP a veces muy literal - Expresiones idiomáticas se pierden - Sin contexto entre segmentos **Hardware**: - GPU mínima: GTX 1050 Ti (4GB VRAM) - Recomendada: RTX 3060 (12GB VRAM) - CPU funciona pero 10x más lento ### Mitigaciones **Mejorar calidad**: - Usar Balanced/Best presets si hay VRAM - Post-processing con Aeneas para mejor sync - Manual review de nombres propios - Context prompting en Whisper **Optimizar velocidad**: - Batch translation (32 segments) - Cache de modelos en VRAM - Pipeline paralelo (transcribe + traduce simultáneo) ## Stack de desarrollo ### Backend ``` Python 3.11+ FastAPI 0.100+ SQLite 3.40+ faster-whisper 1.0+ transformers 4.35+ torch 2.1+ (CUDA 12.x) ``` ### Frontend ``` Node 20+ Vue 3.4+ Vite 5+ Tailwind CSS 3.4+ Socket.io-client 4.7+ Chart.js 4.4+ ``` ### DevOps ``` Docker 24+ Docker Compose 2.20+ GitHub Actions Docker Hub ``` ## Licencia **Apache 2.0** (misma que SubGen) Permite: - ✅ Uso comercial - ✅ Modificación - ✅ Distribución - ✅ Uso privado Requiere: - ⚠️ Incluir licencia y copyright - ⚠️ Documentar cambios ## Contacto - **GitHub**: `github.com/[tu-usuario]/transcriptorio` - **Discord**: [crear servidor] - **Email**: [configurar] ## Referencias - SubGen original: https://github.com/McCloudS/subgen - Bazarr: https://github.com/morpheus65535/bazarr - Whisper: https://github.com/openai/whisper - faster-whisper: https://github.com/guillaumekln/faster-whisper - stable-ts: https://github.com/jianfch/stable-ts - Tdarr: https://github.com/HaveAGitGat/Tdarr --- **Última actualización**: 2026-01-11 **Versión**: 0.1.0-planning **Estado**: En diseño