From c22767d3d47d8955e44eb4477c42cdc22edfcc1f Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Sat, 25 Oct 2025 00:00:02 -0700 Subject: [PATCH] Refactor SRT to Kokoro synthesis script for improved CLI functionality and compatibility - Updated `srt_to_kokoro.py` to provide a CLI entrypoint with argument parsing. - Enhanced error handling and logging for better user feedback. - Introduced a compatibility layer for legacy scripts. - Added configuration handling via `config.toml` for endpoint and API key. - Improved documentation and comments for clarity. Enhance PipelineOrchestrator with in-process transcriber fallback - Implemented `InProcessTranscriber` to handle transcription using multiple strategies. - Added support for `srt_only` flag to return translated SRT without TTS synthesis. - Improved error handling and logging for transcriber initialization. Add installation and usage documentation - Created `INSTALLATION.md` for detailed setup instructions for CPU and GPU environments. - Added `USAGE.md` with practical examples for common use cases and command-line options. - Included a script for automated installation and environment setup. Implement SRT burning utility - Added `burn_srt.py` to facilitate embedding SRT subtitles into video files using ffmpeg. - Provided command-line options for style and codec customization. Update project configuration management - Introduced `config.py` to centralize configuration loading from `config.toml`. - Ensured that environment variables are not read to avoid implicit overrides. Enhance package management with `pyproject.toml` - Added `pyproject.toml` for modern packaging and dependency management. - Defined optional dependencies for CPU and TTS support. Add smoke test fixture for SRT - Created `smoke_test.srt` as a sample subtitle file for testing purposes. Update requirements and setup configurations - Revised `requirements.txt` and `setup.cfg` for better dependency management and clarity. - Included installation instructions for editable mode and local TTS support. --- .gitignore | 5 +- DOCS/INSTALLATION.md | 214 ++++++++++++++++++ DOCS/USAGE.md | 160 +++++++++++++ README.md | 97 ++++++++ config.toml.example | 10 + pyproject.toml | 42 ++++ requirements.txt | 61 +++++ scripts/auto_install.sh | 119 ++++++++++ setup.cfg | 11 + tests/fixtures/smoke_test.srt | 7 + .../__pycache__/srt_to_kokoro.cpython-313.pyc | Bin 5235 -> 8779 bytes whisper_project/burn_srt.py | 77 +++++++ whisper_project/config.py | 80 +++++++ whisper_project/core/models.py | 14 +- whisper_project/main.py | 44 +++- whisper_project/requirements.txt | 71 +++++- whisper_project/srt_to_kokoro.py | 158 +++++++------ whisper_project/usecases/orchestrator.py | 73 +++++- 18 files changed, 1146 insertions(+), 97 deletions(-) create mode 100644 DOCS/INSTALLATION.md create mode 100644 DOCS/USAGE.md create mode 100644 config.toml.example create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 scripts/auto_install.sh create mode 100644 setup.cfg create mode 100644 tests/fixtures/smoke_test.srt create mode 100644 whisper_project/burn_srt.py create mode 100644 whisper_project/config.py diff --git a/.gitignore b/.gitignore index 345bcb5..70cc067 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,8 @@ venv/ ENV/ env.bak/ venv.bak/ +.venv311/ +config.toml # Spyder project settings .spyderproject @@ -168,4 +170,5 @@ cython_debug/ .Spotlight-V100 .Trashes ehthumbs.db -Thumbs.db \ No newline at end of file +Thumbs.db +output/ diff --git a/DOCS/INSTALLATION.md b/DOCS/INSTALLATION.md new file mode 100644 index 0000000..c5311b9 --- /dev/null +++ b/DOCS/INSTALLATION.md @@ -0,0 +1,214 @@ +# Instalación y Quickstart + +Este documento explica las opciones de instalación y comandos recomendados para entornos CPU-only y entornos con GPU. Incluye instrucciones para Coqui TTS (local) y el uso del `config.toml` para Kokoro. + +Requisitos mínimos +- Python 3.11 (recomendado para compatibilidad con Coqui TTS y dependencias). +- `ffmpeg` y `ffprobe` en PATH. + +Instalación recomendada (CPU-only) + +1) Crear virtualenv y activarlo: + +```bash +python3.11 -m venv .venv311 +source .venv311/bin/activate +python -m pip install --upgrade pip +``` + +2) Instalar PyTorch CPU explícito (recomendado): + +```bash +python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +``` + +3) Instalar el resto de dependencias (pinned): + +```bash +python -m pip install -r requirements.txt +``` + +4) (Opcional) instalar dependencias del paquete editables: + +```bash +python -m pip install -e . +``` + +Instalación para GPU + +- Si dispones de GPU y quieres aprovecharla, instala la rueda de PyTorch adecuada para tu versión de CUDA siguiendo las instrucciones oficiales en https://pytorch.org/ y luego instala el resto de dependencias. + +Coqui TTS (local) + +- Coqui TTS permite ejecutar TTS localmente sin depender de un servicio externo. +- Ten en cuenta: + - Algunos modelos son pesados (centenas de MB). Hay variantes optimizadas para CPU (p.ej. ~326 MB fp32). + - La primera ejecución puede descargar modelos; para uso en CI descárgalos y cachea con anticipación. + +Instalación: + +```bash +# dentro del entorno virtual +python -m pip install -r whisper_project/requirements.txt +``` + +Uso del `config.toml` + +- Coloca `config.toml` en la raíz con la sección `[kokoro]`. Usa `config.toml.example` como plantilla. +- Precedencia por defecto: CLI > `config.toml` > ENV (modo `override-env`). + +Comandos de ejemplo + +- Ejecutar pipeline real con traducción local: + +```bash +.venv311/bin/python -m whisper_project.main --video output/dailyrutines/dailyrutines.mp4 --translate-method local +``` + +- Ejecutar smoke test (fixture corto): + +```bash +.venv311/bin/python -m whisper_project.main --video tests/fixtures/smoke_fixture.mp4 --translate-method none --dry-run +``` + +- Forzar instalación CPU-only de PyTorch y dependencies en una sola línea: + +```bash +python -m pip install --index-url https://download.pytorch.org/whl/cpu -r requirements.txt +``` + +Pinning y reproducibilidad + +- `requirements.txt` (raíz) y `whisper_project/requirements.txt` contienen pins orientados a CPU. Revisa y actualiza si tu entorno requiere otras variantes. +- Para CI reproducible, genera un lockfile con `pip-compile` o fija versiones a través de `pip freeze` en tu pipeline. + +Notas y recomendaciones + +- Para entornos con recursos limitados usa modelos `small` o `base`. +- Si necesitas ayuda para crear un Dockerfile o un job de CI (GitHub Actions) para pre-cachear modelos, lo puedo generar. + +## Instalación editable (pip install -e .) usando `pyproject.toml` + +Este repositorio incluye un `pyproject.toml` para soportar instalaciones en modo editable (`pip install -e .`). +La instalación editable facilita el desarrollo (cambios en el código se reflejan sin reinstalar). + +Recomendaciones y ejemplos (CPU-first) + +1) Activa tu entorno virtual (ejemplo `.venv311`) y actualiza pip: + +```bash +source .venv311/bin/activate +python -m pip install --upgrade pip +``` + +2) Instala primero la rueda CPU de PyTorch (recomendado): + +```bash +python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +``` + +3) Instala el paquete en modo editable con el extra `cpu` (y opcionalmente `tts`): + +```bash +python -m pip install -e .[cpu] +# o para incluir soporte TTS local: +python -m pip install -e .[cpu,tts] +``` + +Notas importantes +- `pip install -e .[cpu]` usará el `pyproject.toml` para leer metadatos y extras definidos allí. +- Si prefieres instalar PyTorch y otras dependencias con un único comando que fuerce las ruedas CPU, puedes usar: + +```bash +python -m pip install --index-url https://download.pytorch.org/whl/cpu -e .[cpu,tts] +``` + +- Asegúrate de tener `setuptools` y `wheel` actualizados (el `pyproject.toml` ya declara `setuptools` como build-system). Si tienes problemas al instalar en modo editable, ejecuta: + +```bash +python -m pip install --upgrade setuptools wheel +``` + +- El extra `tts` instala dependencias opcionales para Coqui TTS y reproducción (puede descargar modelos a la primera ejecución). + +- Recuerda no subir `config.toml` con credenciales reales; usa `config.toml.example`. + + +## Script de instalación automática: `scripts/auto_install.sh` + +Se incluye un script helper en `scripts/auto_install.sh` para automatizar la creación del virtualenv e instalación de dependencias con opciones CPU/GPU y soporte para TTS local o uso de Kokoro remoto. + +Propósito +- Simplificar la puesta en marcha del proyecto en entornos de desarrollo. +- Permitir elegir instalación CPU-only (recomendado) o GPU, y decidir si instalar dependencias para Coqui TTS local. + +Ubicación +- `scripts/auto_install.sh` (hacer ejecutable con `chmod +x scripts/auto_install.sh`). + +Uso (resumen) + +```bash +# instalar en venv por defecto (.venv311), CPU y soporte TTS local: +./scripts/auto_install.sh --cpu --local-tts + +# instalar en venv personalizado, GPU y usar Kokoro remoto (no instala TTS extra): +./scripts/auto_install.sh --venv .venv_gpu --gpu --kokoro +``` + +Opciones principales +- `--venv PATH` : ruta del virtualenv a crear/usar (default: `.venv311`). +- `--cpu` : instalar la build CPU de PyTorch (usa índice CPU de PyTorch). +- `--gpu` : intentar instalar PyTorch (selección de rueda GPU queda a cargo de pip/usuario si requiere CUDA específica). +- `--torch-version V` : opción para forzar una versión concreta de `torch`. +- `--local-tts` : instala extras para Coqui TTS (`tts` extra) y soporte de reproducción. +- `--kokoro` : indica que usarás Kokoro remoto (no fuerza instalación de TTS local). +- `--no-editable` : instala dependencias sin modo editable (usa `requirements.txt`). + +Qué hace internamente +- Crea (si no existe) y activa el virtualenv. +- Actualiza `pip`, `setuptools` y `wheel`. +- Instala `torch` según la opción `--cpu`/`--gpu` y la `--torch-version` si se proporciona. +- Instala el paquete en modo editable (`pip install -e .`) y añade extras `cpu` y/o `tts` según flags. +- Si existe `config.toml.example` y no hay `config.toml`, copia el ejemplo a `config.toml` para que lo rellenes con tus credenciales. + +Ejemplos + +- Instalación mínima CPU y editable (sin TTS): + +```bash +./scripts/auto_install.sh --cpu +``` + +- Instalación CPU y extras para Coqui TTS local: + +```bash +./scripts/auto_install.sh --cpu --local-tts +``` + +- Instalación editable en un venv personalizado e instalar PyTorch GPU (nota: el script no elige la rueda CUDA exacta por ti): + +```bash +./scripts/auto_install.sh --venv .venv_gpu --gpu +``` + +Notas y recomendaciones +- Recomendado: para evitar problemas con ruedas de PyTorch, instala primero la rueda CPU/GPU apropiada siguiendo https://pytorch.org/ y luego ejecuta `--cpu`/`--gpu` sin forzar versión en el script. +- Si necesitas reproducibilidad en CI, considera usar el comando combinado que fuerza el índice CPU para todo: + +```bash +python -m pip install --index-url https://download.pytorch.org/whl/cpu -e .[cpu,tts] +``` + +- Si se detectan errores de compilación de dependencias nativas (p.ej. `av`, `soundfile`), instala las dependencias de sistema necesarias (headers de libav, libsndfile, etc.) antes de repetir la instalación. + +- El script no elimina ni borra artefactos; solo prepara el entorno. Para ejecutar la pipeline usa la CLI: + +```bash +.venv311/bin/python -m whisper_project.main --video path/to/video.mp4 --translate-method local +``` + +Soporte y troubleshooting +- Si la instalación falla en una dependencia binaria, pega la salida de error y te indico el paquete del sistema que falta (por ejemplo `libsndfile1-dev`, `libavcodec-dev`, etc.). +- Si el script no encuentra `python3.11`, intenta `python3` o instala Python 3.11 en el sistema. + +Fin de la documentación del script. diff --git a/DOCS/USAGE.md b/DOCS/USAGE.md new file mode 100644 index 0000000..53c3ef1 --- /dev/null +++ b/DOCS/USAGE.md @@ -0,0 +1,160 @@ +## Casos de uso y ejemplos de ejecución + +Este documento muestra ejemplos de uso reales del proyecto y variantes de +ejecución para cubrir los flujos más comunes: extraer sólo el SRT traducido, +ejecutar la pipeline completa, modo dry-run, TTS local vs Kokoro, y cómo quemar +el `.srt` en un vídeo por separado. + +Prerequisitos +- Tener Python 3.11 y el virtualenv activado (ejemplo: `.venv311`). +- `ffmpeg` disponible en PATH. +- Paquetes instalados en el venv (usar `requirements.txt` o `pip install -e .[cpu,tts]`). + +Rutas/convención +- El CLI principal es: `python -m whisper_project.main`. +- El script para quemar SRT es: `python -m whisper_project.burn_srt`. +- En los ejemplos uso `.venv311/bin/python` para indicar el venv; adáptalo a tu + entorno si usas otro nombre de venv. + +1) Extraer y traducir sólo el SRT (modo rápido, sin TTS) + +Descripción: ejecuta la transcripción + traducción y deja como salida el +archivo SRT traducido. Útil cuando sólo necesitas subtítulos. + +Comando: + +```bash +.venv311/bin/python -m whisper_project.main \ + --video output/dailyrutines/dailyrutines.mp4 \ + --translate-method local \ + --srt-only +``` + +Salida esperada: +- `output/dailyrutines/dailyrutines.translated.srt` (o un SRT en el workdir + que se moverá a `output//`). + +Notas: +- No se ejecuta síntesis (TTS) ni se modifica el vídeo. +- Si quieres mantener temporales (chunks) añade `--keep-chunks --keep-temp`. + +2) Pipeline completa (SRT -> TTS -> reemplazar audio -> quemar subtítulos) + +Descripción: flujo completo por defecto (si no usas `--srt-only`). + +Comando: + +```bash +.venv311/bin/python -m whisper_project.main \ + --video input.mp4 \ + --translate-method local \ + --kokoro-endpoint https://kokoro.example/api/synthesize \ + --kokoro-key $KOKORO_KEY \ + --voice em_alex +``` + +Salida esperada: +- `input.replaced_audio.mp4` (vídeo con audio reemplazado) +- `input.replaced_audio.subs.mp4` (vídeo final con subtítulos quemados) +- `output//` cuando la CLI mueve artefactos al terminar. + +Opciones útiles: +- `--mix` si quieres mezclar la pista sintetizada con la original. +- `--mix-background-volume 0.1` para controlar volumen de fondo. +- `--keep-chunks`/`--keep-temp` para conservar archivos intermedios. + +3) Dry-run (simular pasos sin ejecutar) + +Descripción: imprimirá los pasos planeados sin hacer trabajo pesado. + +Comando: + +```bash +.venv311/bin/python -m whisper_project.main --video input.mp4 --dry-run +``` + +4) Usar TTS local en lugar de Kokoro + +Si tienes Coqui TTS instalado y quieres sintetizar localmente: + +```bash +.venv311/bin/python -m whisper_project.main \ + --video input.mp4 \ + --translate-method local \ + --local-tts +``` + +Notas: +- Asegúrate de que en tu `pyproject`/requirements está la dependencia `TTS` y + que tu Python es 3.11 (Coqui TTS no siempre es compatible con 3.12+). +- Local TTS reduce dependencia de red y suele ser más rápido si el HW es + suficiente. + +5) Traducir con Gemini/Argos (servicios externos) + +Ejemplo con Gemini (requiere clave): + +```bash +.venv311/bin/python -m whisper_project.main \ + --video input.mp4 \ + --translate-method gemini \ + --gemini-key $GEMINI_KEY +``` + +Si falla el adaptador local, el orquestador cae a un wrapper que puede lanzar +scripts auxiliares (ver `whisper_project/translate_*`). + +6) Quemar un `.srt` en un vídeo por separado + +Descripción: si ya tienes un `.srt` (p. ej. generado por `--srt-only`) y +quieres incrustarlo en el vídeo sin ejecutar todo el pipeline. + +Comando: + +```bash +python -m whisper_project.burn_srt \ + --video input.mp4 \ + --srt translated.srt \ + --out input.subbed.mp4 \ + --style "FontName=Arial,FontSize=24" \ + --codec libx264 +``` + +Notas: +- El filtro `subtitles` de `ffmpeg` normalmente requiere reencodear el vídeo. +- Si tu SRT contiene caracteres especiales, el script hace uso de rutas + absolutas para evitar problemas. + +7) Encadenar extracción SRT y quemado (ejemplo rápido) + +Extraer SRT traducido y quemarlo en un solo pipeline usando tuberías de shell +o pasos secuenciales: + +```bash +.venv311/bin/python -m whisper_project.main \ + --video input.mp4 --translate-method local --srt-only && \ +python -m whisper_project.burn_srt \ + --video input.mp4 --srt output/input/input.translated.srt --out input.subbed.mp4 +``` + +8) Flags de diagnóstico y tiempo de ejecución +- `--verbose` para más logging. +- Revisar `logs/` (si está configurado) para detalles de errores. + +Problemas comunes y soluciones rápidas +- Error: "This tokenizer cannot be instantiated — sentencepiece missing" → + instalar `sentencepiece` en el venv: `.venv311/bin/python -m pip install sentencepiece`. +- Error: problemas con import paths cuando se ejecutan scripts auxiliares → + usar `python -m whisper_project.main` desde la raíz del repo para garantizar + que el paquete se importa correctamente. +- Si la síntesis via Kokoro tarda o da timeouts, considera `--local-tts` o + aumentar timeouts en la configuración del adaptador Kokoro. + +¿Qué artefactos debo buscar? +- SRT transcrito: `*.srt` (original y traducido) +- WAV sintetizado: `*.dub.wav` o `dub_final.wav` en workdir +- Vídeos finales: `*.replaced_audio.mp4`, `*.replaced_audio.subs.mp4` + +¿Quieres que ejecute alguno de estos ejemplos ahora en tu venv `.venv311` y con +tu fichero `output/dailyrutines/dailyrutines.mp4`? Puedo ejecutar sólo la +extracción SRT traducida (`--srt-only`) y traerte el log + ruta del SRT. diff --git a/README.md b/README.md index 4729552..08c15c8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,100 @@ + +# Whisper Project (pipeline multimedia) + +Resumen rápido +---------------- +Canalización para: extraer audio de vídeo -> transcripción -> (opcional) traducción -> generar SRT -> sintetizar por segmentos (Kokoro o TTS local) -> reemplazar pista de audio -> quemar subtítulos. + +Este repo prioriza instalaciones CPU-first por defecto. En `requirements.txt` y `whisper_project/requirements.txt` encontrarás pins y recomendaciones para instalaciones CPU-only. + +Quick start (recomendado para CPU) +--------------------------------- +1. Crear y activar virtualenv (Python 3.11 recomendado): + +```bash +python3.11 -m venv .venv311 +source .venv311/bin/activate +python -m pip install --upgrade pip +``` + +2. Instalar PyTorch (CPU wheel) explícitamente: + +```bash +python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +``` + +3. Instalar las demás dependencias pinned: + +```bash +python -m pip install -r requirements.txt +``` + +4. (Opcional) instalar deps internas del paquete editable: + +```bash +python -m pip install -e . +``` + +Nota: con `pyproject.toml` en la raíz ahora puedes instalar el paquete en modo editable usando: + +```bash +python -m pip install -e . +``` +esto usará el `pyproject.toml` (TOML) como fuente de metadatos y extras. + +Ejecutar la pipeline (ejemplo) +------------------------------ +Usando `config.toml` en la raíz (si tienes Kokoro): + +```bash +.venv311/bin/python -m whisper_project.main \ + --video output/dailyrutines/dailyrutines.mp4 \ + --translate-method local +``` + +Ejemplo sin traducción (solo SRT->TTS): + +```bash +.venv311/bin/python -m whisper_project.main --video input.mp4 --translate-method none +``` + +Uso de `config.toml` +-------------------- +Coloca un `config.toml` en la raíz con: + +```toml +[kokoro] +endpoint = "https://kokoro.example/synthesize" +api_key = "sk-..." +voice = "em_anna" +model = "tacotron2" +``` + +El CLI usa la precedencia: CLI > `config.toml` > ENV (modo `override-env` por defecto). + +Coqui TTS (local) vs Kokoro (remoto) +----------------------------------- +- Kokoro: cliente HTTP que sintetiza por segmento. Bueno si tienes endpoint estable y quieres offload. +- Local Coqui `TTS`: útil si no quieres dependencia de red y tienes CPU suficiente; requiere modelos locales y más espacio. + +Para usar Coqui TTS local, instala `TTS` y dependencias (ya listadas en `whisper_project/requirements.txt`) y ejecuta con la flag `--local-tts`. + +Recomendaciones y troubleshooting +-------------------------------- +- Si ves problemas de memoria/tiempo, reduce el modelo de `faster-whisper` a `small` o `base`. +- Para problemas con `torch`, instala explicitamente desde el índice CPU (ver arriba). +- Si `ffmpeg` falla, revisa que `ffmpeg` y `ffprobe` estén en `PATH`. + +Más documentación +------------------ +Consulta `DOCS/architecture.md` para la documentación técnica completa, diagramas y guías de pruebas. +Para instrucciones de instalación y quickstart (CPU/GPU) revisa `DOCS/INSTALLATION.md`. + +Licencia y contribución +----------------------- +Revisa `CONTRIBUTING.md` (si existe) y respeta la política de no subir credenciales en `config.toml` — utiliza `config.toml.example` como plantilla. + +Fin del README. # Whisper Pipeline — Documentación Bienvenido: esta carpeta contiene la documentación generada para la canalización multimedia (Whisper + Kokoro). diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..1aa7a4d --- /dev/null +++ b/config.toml.example @@ -0,0 +1,10 @@ + # Example configuration file for whisper_project + # Copy to `config.toml` and fill with your real values. Do NOT commit `config.toml` to VCS. + +[kokoro] +# The HTTP endpoint for Kokoro-compatible TTS services +endpoint = "https://kokoro.example/api/v1/audio/speech" + +# API key used for Authorization: Bearer +# Replace the value below with your real key before running the CLI. +api_key = "PASTE_YOUR_API_KEY_HERE" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..914b894 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,42 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "whisper_project" +version = "0.1.0" +description = "Canalización multimedia: extracción, transcripción, traducción y TTS (Kokoro/Coqui)." +readme = "README.md" +requires-python = ">=3.11" +authors = [ { name = "Nextream" } ] +license = { text = "MIT" } + +dependencies = [ + "numpy==1.26.4", + "ffmpeg-python==0.4.0", + "faster-whisper==1.2.0", + "transformers==4.34.0", + "tokenizers==0.13.3", + "sentencepiece==0.1.99", + "huggingface-hub==0.16.4", + "sacremoses==0.0.53", + "ctranslate2==3.18.0", + "onnxruntime==1.15.1", + "requests==2.31.0", + "tqdm==4.66.1", + "coloredlogs==15.0.1", + "humanfriendly==10.0", + "flatbuffers==23.5.26", + "av==10.0.0", +] + +[project.optional-dependencies] +cpu = [ "torch==2.2.2", "onnxruntime==1.15.1" ] +tts = [ "TTS==0.13.0", "soundfile==0.12.1", "librosa==0.10.0.post2", "pyttsx3==2.90" ] +dev = [ "pytest", "pre-commit", "black", "ruff" ] + +[project.scripts] +whisper_project = "whisper_project.main:main" +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..87f6d85 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,61 @@ +# Dependencias de alto nivel para instalar todo lo necesario en el proyecto +# Instalar con: +# python -m pip install -r requirements.txt + +# IMPORTANT: This requirements file targets CPU-only installations. +# For packages that provide GPU vs CPU wheels (notably `torch`), we +# recommend installing the CPU builds explicitly. Example recommended +# command to install CPU-only PyTorch: +# +# python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +# +# If you prefer to keep everything in one step, run: +# +# python -m pip install --index-url https://download.pytorch.org/whl/cpu -r requirements.txt + +# Core +python-dateutil +numpy + +# Audio / multimedia +ffmpeg-python +av +soundfile +librosa + +# Transcripción / modelos +# Recomendado: instalar la build CPU de torch usando el índice oficial +# de PyTorch (ver comentario arriba). No forzamos la URL aquí para +# mantener compatibilidad, pero si instalas desde este fichero y no +# quieres GPU, usa el comando recomendado. +torch==2.2.2 +# faster-whisper: versión probada en este entorno +faster-whisper==1.2.0 +transformers==4.34.0 +tokenizers==0.13.3 +sentencepiece==0.1.99 +huggingface-hub==0.16.4 + +# Traducción +sacremoses==0.0.53 +ctranslate2==3.18.0 +# ONNX Runtime (CPU). Si necesitas GPU, instala la variante apropiada. +onnxruntime==1.15.1 + +# TTS (Coqui TTS and fallbacks) +TTS==0.13.0 +pyttsx3==2.90 + +# Networking / utils +requests==2.31.0 +tqdm==4.66.1 +coloredlogs==15.0.1 +humanfriendly==10.0 +flatbuffers==23.5.26 + +# Notas: +# - He fijado versiones compatibles con Python 3.11 y enfoque CPU. Si prefieres +# otras versiones o quieres soporte GPU, dime y actualizo los pins. +# - Para `torch` en CPU, usa el índice oficial de PyTorch como se muestra arriba. +# - `TTS` (Coqui) puede descargar modelos en la primera ejecución; consulta la +# documentación de Coqui para descargar modelos offline si lo deseas. \ No newline at end of file diff --git a/scripts/auto_install.sh b/scripts/auto_install.sh new file mode 100644 index 0000000..37060fe --- /dev/null +++ b/scripts/auto_install.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Auto-install helper for the project. +# Usage: scripts/auto_install.sh [--venv PATH] [--cpu|--gpu] [--local-tts] [--kokoro] [--no-editable] +# Examples: +# ./scripts/auto_install.sh --cpu --local-tts +# ./scripts/auto_install.sh --venv .venv311 --gpu --kokoro + +VENV=".venv311" +MODE="cpu" +USE_LOCAL_TTS="false" +USE_KOKORO="false" +EDITABLE="true" +TORCH_VERSION="" + +print_help(){ + cat <<'EOF' +Usage: auto_install.sh [options] + +Options: + --venv PATH Path to virtualenv (default: .venv311) + --cpu Install CPU-only PyTorch (default) + --gpu Install PyTorch (GPU). You may need to pick the right CUDA wheel manually. + --torch-version V Optional torch version to install (e.g. 2.2.2) + --local-tts Install Coqui TTS and extras for local TTS + --kokoro Assume you'll use Kokoro endpoint (no local TTS extras) + --no-editable Do not install in editable mode; install packages normally + -h, --help Show this help + +Examples: + ./scripts/auto_install.sh --cpu --local-tts + ./scripts/auto_install.sh --venv .venv311 --gpu --kokoro +EOF +} + +while [[ ${#} -gt 0 ]]; do + case "$1" in + --venv) VENV="$2"; shift 2;; + --cpu) MODE="cpu"; shift;; + --gpu) MODE="gpu"; shift;; + --torch-version) TORCH_VERSION="$2"; shift 2;; + --local-tts) USE_LOCAL_TTS="false"; shift;; + --kokoro) USE_KOKORO="true"; shift;; + --no-editable) EDITABLE="false"; shift;; + -h|--help) print_help; exit 0;; + *) echo "Unknown arg: $1"; print_help; exit 2;; + esac +done + +echo "Installing into venv: ${VENV}" +if [[ ! -d "${VENV}" ]]; then + echo "Creating virtualenv ${VENV} (Python 3.11 recommended)..." + python3.11 -m venv "${VENV}" || python3 -m venv "${VENV}" +fi + +echo "Activating virtualenv..." +source "${VENV}/bin/activate" + +echo "Upgrading pip, setuptools and wheel..." +python -m pip install --upgrade pip setuptools wheel + +install_torch_cpu(){ + if [[ -n "${TORCH_VERSION}" ]]; then + echo "Installing CPU torch ${TORCH_VERSION} from PyTorch CPU index..." + python -m pip install "torch==${TORCH_VERSION}" --index-url https://download.pytorch.org/whl/cpu + else + echo "Installing latest CPU torch from PyTorch CPU index..." + python -m pip install torch --index-url https://download.pytorch.org/whl/cpu + fi +} + +install_torch_gpu(){ + if [[ -n "${TORCH_VERSION}" ]]; then + echo "Installing torch ${TORCH_VERSION} (GPU) - pip will try to pick a matching wheel" + python -m pip install "torch==${TORCH_VERSION}" + else + echo "Installing torch (GPU) - pip will try to pick a matching wheel. If you need a specific CUDA wheel, install it manually following https://pytorch.org/" + python -m pip install torch + fi +} + +if [[ "${MODE}" == "cpu" ]]; then + install_torch_cpu +else + install_torch_gpu +fi + +EXTRAS=() +if [[ "${USE_LOCAL_TTS}" == "true" ]]; then + EXTRAS+=(tts) +fi +if [[ "${MODE}" == "cpu" ]]; then + EXTRAS+=(cpu) +fi + +if [[ "${EDITABLE}" == "true" ]]; then + if [[ ${#EXTRAS[@]} -gt 0 ]]; then + IFS=, extras_str="${EXTRAS[*]}" + echo "Installing package editable with extras: ${extras_str// /,}" + python -m pip install -e .[${extras_str// /,}] + else + echo "Installing package editable without extras" + python -m pip install -e . + fi +else + echo "Installing dependencies from requirements.txt" + python -m pip install -r requirements.txt +fi + +echo "Optional: if you plan to use Kokoro remote endpoint, copy and edit config.toml.example -> config.toml and fill kokoro values." +if [[ -f config.toml.example && ! -f config.toml ]]; then + echo "Copying config.toml.example -> config.toml (edit before running)" + cp config.toml.example config.toml +fi + +echo "Setup complete. Next steps:\n source ${VENV}/bin/activate\n ./${VENV}/bin/python -m whisper_project.main --help" + +echo "Done." diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dd36eb5 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,11 @@ +[metadata] +name = whisper-project +version = 0.0.0 +description = Editable packaging for Whisper project + +[options] +packages = find: +include-package-data = True + +[options.packages.find] +where = . diff --git a/tests/fixtures/smoke_test.srt b/tests/fixtures/smoke_test.srt new file mode 100644 index 0000000..fa5167c --- /dev/null +++ b/tests/fixtures/smoke_test.srt @@ -0,0 +1,7 @@ +1 +00:00:00,000 --> 00:00:02,500 +Hola mundo. + +2 +00:00:03,000 --> 00:00:07,000 +Esto es una prueba rápida de síntesis local usando Kokoro. diff --git a/whisper_project/__pycache__/srt_to_kokoro.cpython-313.pyc b/whisper_project/__pycache__/srt_to_kokoro.cpython-313.pyc index 9c9eee3eb195489f24b255956688768cc04cff3b..db20c3745d45bb7609aa82fbf10bccc1f268b424 100644 GIT binary patch literal 8779 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 delta 1452 zcmb7EOKcle6n$@IJRbkX-#E^M+T*xM>@-gLp(*(aQ@8mTs^XDRQ6fcYGIpINljMy9 zB~sNaKq9aR)mv3Usv?2fuBhrI8!91M1PeC-g^VILhy@ZthKP@-gm`Zf*X<6TH0Rv8 z@1A?_`^ z4WYaI2b+Yjv3%IF17QUJ!*{`RxYH`*`_3TqHCNtoK7i1Mj|$Ben1fk6_WBOBMrf{a z=YCcIvSQ7ubJmMD#DI7b?z=RGh<*P}&`V zRVv!ckml-MsWxBd7NE4$a+BCYwV4Ys(q$NX8yfI&uivS}dO3{?gB+KQnNB9)=SV~w zeSvWkJ51~}v5O*pJK&#H4)WvgY6pSCQ&04`!KLDELsVr?d z3spR|ICIIb{I?~y*DFPULNcc;g#*sRJQ!pnAP*Dq@^WwkHqt#=aV3+NRdq2reQ8;{ ztS2C9!O75Ra!r;(jrfPAD7Hz%LIN6GzBs4i^HKoHSe0_H8GkA@Gs|t*klJm1@=x>< zBqS^}_g_P9VjIkYZdg_AjUrmm47Q+_P$P9)7Ew_v8La-6Zt!|(Mn%Zr)GNgjqKAm+ z6+kkD5CIZ~NbJIL2_>kngTfICofMJ;4BMG2)9PZWxS%C?11=g|{}d|dW)J%n`RTc$ zN=iai=ZQcz2cka!gPWf!YUm|mX33+o49+}Pecj<)A6OfBe`qyb<$))#es%5YwmbC^ zbIZPAza?&nmF^d}AAafce*0> zeeQoC-Iq3nmv;rny6~Rxfp^`p=6GDSkift7CerU09jOx|Vel|)AC0jO+7W0P;{s&bc~n@0nzJ<%sKk?>|6A>_APosrj=;KAIt6dkvz?r%X8`wdX4z#ozPbZR9Oh&uM7{l p-$ejpKY}3MjGl}9#<#*i^*jWe^WBv#wrPhG-Z@)k0VkO${{=~rDAxc0 diff --git a/whisper_project/burn_srt.py b/whisper_project/burn_srt.py new file mode 100644 index 0000000..660276d --- /dev/null +++ b/whisper_project/burn_srt.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Herramienta simple para quemar un archivo .srt en un vídeo usando ffmpeg. + +Uso: + python -m whisper_project.burn_srt --video input.mp4 --srt subs.srt --out output.mp4 + +Opciones: + --style: opcional, cadena de estilos para `subtitles` filter (por ejemplo "FontName=Arial,FontSize=24"). + --codec: codec de vídeo de salida (por defecto reencode con libx264). +""" +from __future__ import annotations + +import argparse +import logging +import os +import shlex +import subprocess +import sys + + +def burn_subtitles_ffmpeg(video: str, srt: str, out: str, style: str | None = None, codec: str | None = None) -> None: + # Asegurar rutas absolutas + video_p = os.path.abspath(video) + srt_p = os.path.abspath(srt) + out_p = os.path.abspath(out) + + # Construir filtro subtitles. Escapar correctamente la ruta con shlex.quote + # para evitar problemas con espacios y caracteres especiales. + quoted_srt = shlex.quote(srt_p) + vf = f"subtitles={quoted_srt}" + if style: + # force_style debe ir separado por ':' en ffmpeg + vf = vf + f":force_style='{style}'" + + cmd = [ + "ffmpeg", + "-y", + "-i", + video_p, + "-vf", + vf, + ] + + # Si se especifica codec, usarlo; si no, reencode por defecto con libx264 + if codec: + cmd += ["-c:v", codec, "-c:a", "copy", out_p] + else: + cmd += ["-c:v", "libx264", "-crf", "23", "-preset", "medium", out_p] + + logging.info("Ejecutando ffmpeg para quemar SRT: %s", " ".join(shlex.quote(c) for c in cmd)) + subprocess.run(cmd, check=True) + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description="Quemar un .srt en un vídeo con ffmpeg") + p.add_argument("--video", required=True, help="Vídeo de entrada") + p.add_argument("--srt", required=True, help="Archivo SRT a quemar") + p.add_argument("--out", required=True, help="Vídeo de salida") + p.add_argument("--style", required=False, help="Force style para el filtro subtitles (ej: FontName=Arial,FontSize=24)") + p.add_argument("--codec", required=False, help="Codec de vídeo para salida (ej: libx264)") + p.add_argument("--verbose", action="store_true") + args = p.parse_args(argv) + + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO) + + try: + burn_subtitles_ffmpeg(args.video, args.srt, args.out, style=args.style, codec=args.codec) + except subprocess.CalledProcessError as e: + logging.exception("ffmpeg falló al quemar subtítulos: %s", e) + return 2 + + logging.info("Vídeo con subtítulos guardado en: %s", args.out) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/whisper_project/config.py b/whisper_project/config.py new file mode 100644 index 0000000..835ebc1 --- /dev/null +++ b/whisper_project/config.py @@ -0,0 +1,80 @@ +"""Central configuration loader. + +This loader reads configuration exclusively from `config.toml` located in the +current working directory. It intentionally does NOT read environment +variables (per project configuration choice) to avoid implicit overrides. + +Expected TOML structure: + +[kokoro] +endpoint = "https://..." +api_key = "..." + +""" +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +# Defaults (used when keys are missing in TOML) +_DEFAULTS: dict[str, Any] = { + "KOKORO_ENDPOINT": None, + "KOKORO_API_KEY": None, +} + + +def _load_toml(path: Path) -> dict[str, Any]: + try: + import tomllib + + with path.open("rb") as fh: + data = tomllib.load(fh) + return data if isinstance(data, dict) else {} + except FileNotFoundError: + return {} + except Exception: + logger.exception("Error leyendo config TOML: %s", path) + return {} + + +# Look for ./config.toml only (no environment-based path resolution) +_config_path = Path.cwd() / "config.toml" +_TOML: dict[str, Any] +if _config_path.exists(): + _TOML = _load_toml(_config_path) +else: + _TOML = {} + + +def _toml_get(*keys: str, default: Any = None) -> Any: + node = _TOML + for k in keys: + if not isinstance(node, dict): + return default + node = node.get(k, {}) + return node or default + + +# Public config values read exclusively from TOML (TOML > defaults) +KOKORO_ENDPOINT = ( + _toml_get( + "kokoro", + "endpoint", + default=_DEFAULTS["KOKORO_ENDPOINT"], + ) + or None +) +KOKORO_API_KEY = ( + _toml_get( + "kokoro", + "api_key", + default=_DEFAULTS["KOKORO_API_KEY"], + ) + or None +) + + +__all__ = ["KOKORO_ENDPOINT", "KOKORO_API_KEY"] diff --git a/whisper_project/core/models.py b/whisper_project/core/models.py index a56c905..a7a1d01 100644 --- a/whisper_project/core/models.py +++ b/whisper_project/core/models.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from typing import Optional @dataclass @@ -10,7 +11,14 @@ class Segment: @dataclass class PipelineResult: + """Resultado de la pipeline. + + Campos opcionales porque en ciertos modos (por ejemplo `--srt-only`) no se + generan artefactos como WAVs o vídeos. + """ workdir: str - dub_wav: str - replaced_video: str - burned_video: str + dub_wav: Optional[str] = None + replaced_video: Optional[str] = None + burned_video: Optional[str] = None + srt_translated: Optional[str] = None + srt_original: Optional[str] = None diff --git a/whisper_project/main.py b/whisper_project/main.py index 9590191..f1fcdac 100644 --- a/whisper_project/main.py +++ b/whisper_project/main.py @@ -19,6 +19,7 @@ import logging from whisper_project.usecases.orchestrator import PipelineOrchestrator from whisper_project.infra.kokoro_adapter import KokoroHttpClient +from whisper_project import config as project_config def main(): @@ -28,13 +29,21 @@ def main(): p.add_argument( "--kokoro-endpoint", required=False, - default="https://kokoro.example/api/synthesize", + default=project_config.KOKORO_ENDPOINT or "https://kokoro.example/api/synthesize", help=( - "Endpoint HTTP de Kokoro (por defecto: " - "https://kokoro.example/api/synthesize)" + "Endpoint HTTP de Kokoro. Si existe en ./config.toml se usará por defecto. " + "(override con esta opción para cambiar)" + ), + ) + p.add_argument( + "--kokoro-key", + required=False, + default=project_config.KOKORO_API_KEY, + help=( + "API key para Kokoro. Si existe en ./config.toml se usará por defecto. " + "(override con esta opción para cambiar)" ), ) - p.add_argument("--kokoro-key", required=False) p.add_argument("--voice", default="em_alex") p.add_argument("--kokoro-model", default="model") p.add_argument("--whisper-model", default="base") @@ -57,7 +66,7 @@ def main(): ), ) p.add_argument("--mix", action="store_true") - p.add_argument("--mix-background-volume", type=float, default=0.2) + p.add_argument("--mix-background-volume", type=float, default=0.1) p.add_argument("--keep-chunks", action="store_true") p.add_argument("--keep-temp", action="store_true") p.add_argument( @@ -65,6 +74,11 @@ def main(): action="store_true", help="Simular pasos sin ejecutar", ) + p.add_argument( + "--srt-only", + action="store_true", + help="Solo extraer y traducir el SRT, devolver la ruta del SRT traducido y no ejecutar TTS ni quemado", + ) p.add_argument( "--verbose", action="store_true", @@ -117,6 +131,7 @@ def main(): mix_background_volume=args.mix_background_volume, keep_chunks=args.keep_chunks, dry_run=args.dry_run, + srt_only=args.srt_only, ) logging.info("Orquestador finalizado; resultado (burned_video): %s", getattr(result, "burned_video", None)) @@ -167,6 +182,25 @@ def main(): # En dry-run o sin resultado, no movemos nada final_path = getattr(result, "burned_video", None) + # Si el usuario pidió sólo el SRT traducido, mover el srt al output// + if args.srt_only and not args.dry_run and result and getattr(result, "srt_translated", None): + base = os.path.splitext(os.path.basename(video))[0] + project_out = os.path.join(os.getcwd(), "output", base) + try: + os.makedirs(project_out, exist_ok=True) + except Exception: + pass + + src = result.srt_translated + dest = os.path.join(project_out, os.path.basename(src)) if src else None + try: + if src and dest and os.path.abspath(src) != os.path.abspath(dest): + logging.info("Moviendo SRT traducido: %s -> %s", src, dest) + shutil.move(src, dest) + final_path = dest or src + except Exception: + final_path = src + logging.info("Flujo completado. Vídeo final: %s", final_path) finally: if not args.keep_temp: diff --git a/whisper_project/requirements.txt b/whisper_project/requirements.txt index c841123..5222cdc 100644 --- a/whisper_project/requirements.txt +++ b/whisper_project/requirements.txt @@ -1,12 +1,59 @@ -# Dependencias básicas para ejecutar Whisper en CPU -torch>=1.12.0 -ffmpeg-python -numpy -# Optional backends (comment/uncomment as needed) -openai-whisper -transformers -faster-whisper -# TTS (opcional) -TTS -pyttsx3 -huggingface-hub +# Dependencias comunes del paquete `whisper_project`. +# Este fichero está orientado a instalaciones en CPU. Algunas librerías +# publican ruedas separadas para GPU y CPU (p.ej. `torch`). Para forzar +# la instalación CPU de PyTorch ejecuta: +# +# python -m pip install torch --index-url https://download.pytorch.org/whl/cpu +# +# O bien instala todo de una vez usando el índice CPU: +# +# python -m pip install --index-url https://download.pytorch.org/whl/cpu -r whisper_project/requirements.txt +# +# Luego instala el resto de dependencias con: +# python -m pip install -r whisper_project/requirements.txt +# +# Si deseas GPU en el futuro, instala la rueda apropiada de PyTorch +# para tu versión de CUDA y, si procede, elimina el uso del índice CPU. +# +# Este archivo lista las librerías que el código del proyecto puede usar. +# Instalar en el virtualenv del proyecto con: +# python -m pip install -r whisper_project/requirements.txt + +# Core / audio +torch==2.2.2 +numpy==1.26.4 +ffmpeg-python==0.4.0 + +# Transcripción y modelos +faster-whisper==1.2.0 +transformers==4.34.0 +tokenizers==0.13.3 +sentencepiece==0.1.99 +huggingface-hub==0.16.4 + +# Traducción (Marian / transformers) +sacremoses==0.0.53 + +# TTS (opcional: Coqui TTS o fallbacks) +TTS==0.13.0 +soundfile==0.12.1 +librosa==0.10.0.post2 +pyttsx3==2.90 + +# HTTP / utilidades +requests==2.31.0 +tqdm==4.66.1 + +# Dependencias instaladas por `faster-whisper` en algunos entornos +onnxruntime==1.15.1 +ctranslate2==3.18.0 +av==10.0.0 +coloredlogs==15.0.1 +humanfriendly==10.0 +flatbuffers==23.5.26 + +# Nota: estos pins se eligieron para compatibilidad con Python 3.11 +# y uso en CPU. Si prefieres versiones distintas (p.ej. ruedas GPU de +# `torch`), indícamelo y actualizo los pins o añado instrucciones +# para instalar desde índices alternativos. + diff --git a/whisper_project/srt_to_kokoro.py b/whisper_project/srt_to_kokoro.py index 51f90b8..80619b8 100644 --- a/whisper_project/srt_to_kokoro.py +++ b/whisper_project/srt_to_kokoro.py @@ -1,110 +1,122 @@ -"""Funciones helper para sintetizar desde SRT. +"""Small CLI shim for SRT -> Kokoro synthesis. -Este módulo mantiene compatibilidad con la antigua utilidad `srt_to_kokoro.py`. -Contiene `parse_srt_file` y `synth_chunk` delegando a infra.kokoro_utils. -Se incluye una función `synthesize_from_srt` que documenta la compatibilidad -con `KokoroHttpClient` (nombre esperado por otros módulos). +This file provides: parse_srt_file, synth_chunk (thin wrappers) and a +CLI entrypoint that uses `whisper_project.config` (config.toml) and CLI +flags. It intentionally does NOT read environment variables. """ + from __future__ import annotations from typing import Any +import argparse import logging +import sys -from whisper_project.infra.kokoro_utils import parse_srt_file as _parse_srt_file, synth_chunk as _synth_chunk +from whisper_project.infra.kokoro_utils import ( + parse_srt_file as _parse_srt_file, + synth_chunk as _synth_chunk, +) +from whisper_project.infra.kokoro_adapter import KokoroHttpClient +from whisper_project import config def parse_srt_file(path: str): - """Parsea un .srt y devuelve la lista de subtítulos. + """Parse a .srt and return the list of subtitles. - Delegado a `whisper_project.infra.kokoro_utils.parse_srt_file`. + Delegates to `whisper_project.infra.kokoro_utils.parse_srt_file`. """ return _parse_srt_file(path) -def synth_chunk(endpoint: str, text: str, headers: dict, payload_template: Any, timeout: int = 60) -> bytes: - """Envía texto al endpoint y devuelve bytes de audio. +def synth_chunk( + endpoint: str, + text: str, + headers: dict, + payload_template: Any, + timeout: int = 60, +) -> bytes: + """Send text to the endpoint and return audio bytes. - Delegado a `whisper_project.infra.kokoro_utils.synth_chunk`. + Delegates to `whisper_project.infra.kokoro_utils.synth_chunk`. """ return _synth_chunk(endpoint, text, headers, payload_template, timeout=timeout) def synthesize_from_srt(srt_path: str, out_wav: str, endpoint: str = "", api_key: str = ""): - """Compat layer: función con el nombre esperado por scripts legacy. + """Compatibility layer name used historically by scripts. - Nota: la implementación completa se encuentra ahora en `KokoroHttpClient`. - Esta función delega a `parse_srt_file` y `synth_chunk` si se necesita. + The canonical implementation lives in `KokoroHttpClient`. Call that class + method instead when integrating programmatically. """ - raise NotImplementedError("Use KokoroHttpClient.synthesize_from_srt or the infra adapter instead") + raise NotImplementedError( + "Use KokoroHttpClient.synthesize_from_srt or the infra adapter" + ) -__all__ = ["parse_srt_file", "synth_chunk", "synthesize_from_srt"] -#!/usr/bin/env python3 -""" -srt_to_kokoro.py - -Leer un archivo .srt y sintetizar cada subtítulo usando una API OpenAPI-compatible (p. ej. Kokoro). -- Intenta autodetectar un endpoint de síntesis en `--openapi` (URL JSON) buscando paths que contengan 'synth'|'tts'|'text' y que acepten POST. -- Alternativamente usa `--endpoint` y un `--payload-template` con {text} como placeholder. -- Guarda fragmentos temporales y los concatena con ffmpeg en un único WAV de salida. - -Dependencias: requests, srt (pip install requests srt) -Requiere ffmpeg en PATH. - -Ejemplos: - python srt_to_kokoro.py --srt subs.srt --openapi "https://kokoro.../openapi.json" --voice "alloy" --out out.wav --api-key "TOKEN" - python srt_to_kokoro.py --srt subs.srt --endpoint "https://kokoro.../v1/synthesize" --payload-template '{"text": "{text}", "voice": "alloy"}' --out out.wav - -""" - -import argparse -import os -import shutil -import subprocess -import sys -import tempfile -from typing import Optional - -""" -Thin wrapper CLI que delega en `KokoroHttpClient.synthesize_from_srt`. - -Conserva la interfaz CLI previa para compatibilidad, pero internamente usa -el cliente HTTP nativo definido en `whisper_project.infra.kokoro_adapter`. -""" - -import argparse -import os -import sys -import tempfile - -from whisper_project.infra.kokoro_adapter import KokoroHttpClient -import logging - - -def main(): +def _build_arg_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser() - p.add_argument("--srt", required=True, help="Ruta al archivo .srt traducido") - p.add_argument("--endpoint", required=False, help="URL directa del endpoint de síntesis (opcional)") - p.add_argument("--api-key", required=False, help="Valor para autorización (se envía como header Authorization: Bearer )") + p.add_argument("--srt", required=True, help="Path to input .srt file") + p.add_argument("--endpoint", required=False, help="Direct synthesis endpoint (optional)") + p.add_argument( + "--api-key", + required=False, + help=( + "API key for Authorization header; if omitted the value from" + " config.toml is used" + ), + ) p.add_argument("--voice", default="em_alex") p.add_argument("--model", default="model") - p.add_argument("--out", required=True, help="Ruta de salida WAV final") - p.add_argument("--video", required=False, help="Ruta al vídeo original (opcional)") - p.add_argument("--align", action="store_true", help="Alinear segmentos con timestamps del SRT") + p.add_argument("--out", required=True, help="Output WAV path") + p.add_argument("--video", required=False, help="Optional original video path to mix or align with") + p.add_argument("--align", action="store_true", help="Align segments using SRT timestamps") p.add_argument("--keep-chunks", action="store_true") p.add_argument("--mix-with-original", action="store_true") p.add_argument("--mix-background-volume", type=float, default=0.2) p.add_argument("--replace-original", action="store_true") + p.add_argument( + "--config-mode", + choices=["defaults", "override-env", "force"], + default="override-env", + help=( + "Configuration precedence: 'defaults' = CLI > TOML; " + "'override-env' = CLI > TOML; 'force' = TOML > CLI" + ), + ) + return p + + +def main() -> None: + p = _build_arg_parser() args = p.parse_args() - # Construir cliente Kokoro HTTP y delegar la síntesis completa - endpoint = args.endpoint or os.environ.get("KOKORO_ENDPOINT") - api_key = args.api_key or os.environ.get("KOKORO_API_KEY") + # Resolve configuration: only CLI flags and config.toml are used. + kokoro_ep = getattr(args, "endpoint", None) + kokoro_key = getattr(args, "api_key", None) + + mode = getattr(args, "config_mode", "defaults") + if mode in ("defaults", "override-env"): + # CLI > TOML + endpoint = kokoro_ep or args.endpoint or config.KOKORO_ENDPOINT + api_key = kokoro_key or args.api_key or config.KOKORO_API_KEY + else: + # force: TOML > CLI + endpoint = config.KOKORO_ENDPOINT or kokoro_ep or args.endpoint + api_key = config.KOKORO_API_KEY or kokoro_key or args.api_key + if not endpoint: - logging.getLogger(__name__).error("Debe proporcionar --endpoint o la variable de entorno KOKORO_ENDPOINT") + logging.getLogger(__name__).error( + "Please provide --endpoint or set kokoro.endpoint in config.toml" + ) sys.exit(2) - client = KokoroHttpClient(endpoint, api_key=api_key, voice=args.voice, model=args.model) + client = KokoroHttpClient( + endpoint, + api_key=api_key, + voice=args.voice, + model=args.model, + ) + try: client.synthesize_from_srt( srt_path=args.srt, @@ -115,11 +127,11 @@ def main(): mix_with_original=args.mix_with_original, mix_background_volume=args.mix_background_volume, ) - logging.getLogger(__name__).info("Archivo final generado en: %s", args.out) + logging.getLogger(__name__).info("Output written to: %s", args.out) except Exception: - logging.getLogger(__name__).exception("Error durante la síntesis desde SRT") + logging.getLogger(__name__).exception("Error synthesizing from SRT") sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/whisper_project/usecases/orchestrator.py b/whisper_project/usecases/orchestrator.py index 10a866a..114b123 100644 --- a/whisper_project/usecases/orchestrator.py +++ b/whisper_project/usecases/orchestrator.py @@ -155,12 +155,65 @@ class PipelineOrchestrator: # Si no se inyectan adaptadores, crear implementaciones por defecto # Sólo importar adaptadores pesados si no se inyectan implementaciones. if transcriber is None: + # Definir un transcriptor en-proceso de respaldo que pruebe varias + # estrategias antes de fallar (faster-whisper -> openai -> segmentado). try: - from ..infra.faster_whisper_adapter import FasterWhisperTranscriber + from ..infra.transcribe_adapter import TranscribeService - self.transcriber = FasterWhisperTranscriber() + class InProcessTranscriber: + def __init__(self, model: str = "base") -> None: + self._svc = TranscribeService(model=model) + + def transcribe(self, file: str, *args, **kwargs): + # Compatibilidad con llamadas posicionales: si se pasa + # un segundo argumento posicional lo tratamos como srt_out + srt_out = None + if args: + srt_out = args[0] + srt_flag = kwargs.get("srt", bool(srt_out)) + srt_file = kwargs.get("srt_file", srt_out) + + try: + return self._svc.transcribe_faster(file) + except Exception: + try: + return self._svc.transcribe_openai(file) + except Exception: + try: + duration = self._svc.get_audio_duration(file) or 0 + segs = self._svc.make_uniform_segments(duration, seg_seconds=max(30, int(duration) or 30)) + results = self._svc.transcribe_segmented_with_tempfiles(file, segs, backend="openai-whisper", model=self._svc.model) + if srt_flag and srt_file: + self._svc.write_srt(results, srt_file) + return results + except Exception: + return [] + + # Intentar usar el adaptador rápido si existe, pero envolver su + # llamada para detectar errores en tiempo de ejecución y caer + # al InProcessTranscriber. + try: + from ..infra.faster_whisper_adapter import FasterWhisperTranscriber + + fw = FasterWhisperTranscriber() + + class TranscriberProxy: + def __init__(self, fast_impl, fallback): + self._fast = fast_impl + self._fallback = fallback + + def transcribe(self, *args, **kwargs): + try: + return self._fast.transcribe(*args, **kwargs) + except Exception: + return self._fallback.transcribe(*args, **kwargs) + + self.transcriber = TranscriberProxy(fw, InProcessTranscriber()) + except Exception: + # Si no existe el adaptador rápido, usar directamente el in-process + self.transcriber = InProcessTranscriber() except Exception: - # dejar como None para permitir fallback a subprocess en tiempo de ejecución + # último recurso: no hay transcriptor en memoria self.transcriber = None else: self.transcriber = transcriber @@ -207,6 +260,7 @@ class PipelineOrchestrator: mix_background_volume: float = 0.2, keep_chunks: bool = False, dry_run: bool = False, + srt_only: bool = False, ) -> PipelineResult: """Run the pipeline. @@ -325,6 +379,17 @@ class PipelineOrchestrator: else: raise ValueError("translate_method not supported in this orchestrator") + # Si el usuario solicitó sólo el SRT traducido, devolvemos inmediatamente + # tras la traducción y NO ejecutamos síntesis TTS / reemplazo / quemado. + if srt_only: + if dry_run: + logger.info("[dry-run] srt-only mode: devolver %s", srt_translated) + return PipelineResult( + workdir=workdir, + srt_translated=srt_translated, + srt_original=srt_in, + ) + # 4) sintetizar por segmento dub_wav = os.path.join(workdir, "dub_final.wav") if dry_run: @@ -360,4 +425,6 @@ class PipelineOrchestrator: dub_wav=dub_wav, replaced_video=replaced, burned_video=burned, + srt_translated=srt_translated, + srt_original=srt_in, )