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 9c9eee3..db20c37 100644 Binary files a/whisper_project/__pycache__/srt_to_kokoro.cpython-313.pyc and b/whisper_project/__pycache__/srt_to_kokoro.cpython-313.pyc differ 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, )