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.
This commit is contained in:
Cesar Mendivil 2025-10-25 00:00:02 -07:00
parent 1264ae8587
commit c22767d3d4
18 changed files with 1146 additions and 97 deletions

3
.gitignore vendored
View File

@ -127,6 +127,8 @@ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
.venv311/
config.toml
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@ -169,3 +171,4 @@ cython_debug/
.Trashes .Trashes
ehthumbs.db ehthumbs.db
Thumbs.db Thumbs.db
output/

214
DOCS/INSTALLATION.md Normal file
View File

@ -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.

160
DOCS/USAGE.md Normal file
View File

@ -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/<basename>/`).
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/<basename>/` 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.

View File

@ -1,3 +1,100 @@
<!-- README: Quickstart y decisiones CPU/GPU, configuración y ejemplos -->
# 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 # Whisper Pipeline — Documentación
Bienvenido: esta carpeta contiene la documentación generada para la canalización multimedia (Whisper + Kokoro). Bienvenido: esta carpeta contiene la documentación generada para la canalización multimedia (Whisper + Kokoro).

10
config.toml.example Normal file
View File

@ -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 <api_key>
# Replace the value below with your real key before running the CLI.
api_key = "PASTE_YOUR_API_KEY_HERE"

42
pyproject.toml Normal file
View File

@ -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"

61
requirements.txt Normal file
View File

@ -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.

119
scripts/auto_install.sh Normal file
View File

@ -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."

11
setup.cfg Normal file
View File

@ -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 = .

7
tests/fixtures/smoke_test.srt vendored Normal file
View File

@ -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.

View File

@ -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())

80
whisper_project/config.py Normal file
View File

@ -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"]

View File

@ -1,4 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional
@dataclass @dataclass
@ -10,7 +11,14 @@ class Segment:
@dataclass @dataclass
class PipelineResult: 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 workdir: str
dub_wav: str dub_wav: Optional[str] = None
replaced_video: str replaced_video: Optional[str] = None
burned_video: str burned_video: Optional[str] = None
srt_translated: Optional[str] = None
srt_original: Optional[str] = None

View File

@ -19,6 +19,7 @@ import logging
from whisper_project.usecases.orchestrator import PipelineOrchestrator from whisper_project.usecases.orchestrator import PipelineOrchestrator
from whisper_project.infra.kokoro_adapter import KokoroHttpClient from whisper_project.infra.kokoro_adapter import KokoroHttpClient
from whisper_project import config as project_config
def main(): def main():
@ -28,13 +29,21 @@ def main():
p.add_argument( p.add_argument(
"--kokoro-endpoint", "--kokoro-endpoint",
required=False, required=False,
default="https://kokoro.example/api/synthesize", default=project_config.KOKORO_ENDPOINT or "https://kokoro.example/api/synthesize",
help=( help=(
"Endpoint HTTP de Kokoro (por defecto: " "Endpoint HTTP de Kokoro. Si existe en ./config.toml se usará por defecto. "
"https://kokoro.example/api/synthesize)" "(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("--voice", default="em_alex")
p.add_argument("--kokoro-model", default="model") p.add_argument("--kokoro-model", default="model")
p.add_argument("--whisper-model", default="base") p.add_argument("--whisper-model", default="base")
@ -57,7 +66,7 @@ def main():
), ),
) )
p.add_argument("--mix", action="store_true") 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-chunks", action="store_true")
p.add_argument("--keep-temp", action="store_true") p.add_argument("--keep-temp", action="store_true")
p.add_argument( p.add_argument(
@ -65,6 +74,11 @@ def main():
action="store_true", action="store_true",
help="Simular pasos sin ejecutar", 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( p.add_argument(
"--verbose", "--verbose",
action="store_true", action="store_true",
@ -117,6 +131,7 @@ def main():
mix_background_volume=args.mix_background_volume, mix_background_volume=args.mix_background_volume,
keep_chunks=args.keep_chunks, keep_chunks=args.keep_chunks,
dry_run=args.dry_run, dry_run=args.dry_run,
srt_only=args.srt_only,
) )
logging.info("Orquestador finalizado; resultado (burned_video): %s", getattr(result, "burned_video", None)) 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 # En dry-run o sin resultado, no movemos nada
final_path = getattr(result, "burned_video", None) final_path = getattr(result, "burned_video", None)
# Si el usuario pidió sólo el SRT traducido, mover el srt al output/<basename>/
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) logging.info("Flujo completado. Vídeo final: %s", final_path)
finally: finally:
if not args.keep_temp: if not args.keep_temp:

View File

@ -1,12 +1,59 @@
# Dependencias básicas para ejecutar Whisper en CPU # Dependencias comunes del paquete `whisper_project`.
torch>=1.12.0 # Este fichero está orientado a instalaciones en CPU. Algunas librerías
ffmpeg-python # publican ruedas separadas para GPU y CPU (p.ej. `torch`). Para forzar
numpy # la instalación CPU de PyTorch ejecuta:
# Optional backends (comment/uncomment as needed) #
openai-whisper # python -m pip install torch --index-url https://download.pytorch.org/whl/cpu
transformers #
faster-whisper # O bien instala todo de una vez usando el índice CPU:
# TTS (opcional) #
TTS # python -m pip install --index-url https://download.pytorch.org/whl/cpu -r whisper_project/requirements.txt
pyttsx3 #
huggingface-hub # 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.

View File

@ -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`. This file provides: parse_srt_file, synth_chunk (thin wrappers) and a
Contiene `parse_srt_file` y `synth_chunk` delegando a infra.kokoro_utils. CLI entrypoint that uses `whisper_project.config` (config.toml) and CLI
Se incluye una función `synthesize_from_srt` que documenta la compatibilidad flags. It intentionally does NOT read environment variables.
con `KokoroHttpClient` (nombre esperado por otros módulos).
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
import argparse
import logging 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): 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) return _parse_srt_file(path)
def synth_chunk(endpoint: str, text: str, headers: dict, payload_template: Any, timeout: int = 60) -> bytes: def synth_chunk(
"""Envía texto al endpoint y devuelve bytes de audio. 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) 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 = ""): 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`. The canonical implementation lives in `KokoroHttpClient`. Call that class
Esta función delega a `parse_srt_file` y `synth_chunk` si se necesita. 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"] def _build_arg_parser() -> argparse.ArgumentParser:
#!/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():
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument("--srt", required=True, help="Ruta al archivo .srt traducido") p.add_argument("--srt", required=True, help="Path to input .srt file")
p.add_argument("--endpoint", required=False, help="URL directa del endpoint de síntesis (opcional)") p.add_argument("--endpoint", required=False, help="Direct synthesis endpoint (optional)")
p.add_argument("--api-key", required=False, help="Valor para autorización (se envía como header Authorization: Bearer <key>)") 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("--voice", default="em_alex")
p.add_argument("--model", default="model") p.add_argument("--model", default="model")
p.add_argument("--out", required=True, help="Ruta de salida WAV final") p.add_argument("--out", required=True, help="Output WAV path")
p.add_argument("--video", required=False, help="Ruta al vídeo original (opcional)") p.add_argument("--video", required=False, help="Optional original video path to mix or align with")
p.add_argument("--align", action="store_true", help="Alinear segmentos con timestamps del SRT") p.add_argument("--align", action="store_true", help="Align segments using SRT timestamps")
p.add_argument("--keep-chunks", action="store_true") p.add_argument("--keep-chunks", action="store_true")
p.add_argument("--mix-with-original", 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("--mix-background-volume", type=float, default=0.2)
p.add_argument("--replace-original", action="store_true") 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() args = p.parse_args()
# Construir cliente Kokoro HTTP y delegar la síntesis completa # Resolve configuration: only CLI flags and config.toml are used.
endpoint = args.endpoint or os.environ.get("KOKORO_ENDPOINT") kokoro_ep = getattr(args, "endpoint", None)
api_key = args.api_key or os.environ.get("KOKORO_API_KEY") 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: 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) 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: try:
client.synthesize_from_srt( client.synthesize_from_srt(
srt_path=args.srt, srt_path=args.srt,
@ -115,11 +127,11 @@ def main():
mix_with_original=args.mix_with_original, mix_with_original=args.mix_with_original,
mix_background_volume=args.mix_background_volume, 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: except Exception:
logging.getLogger(__name__).exception("Error durante la síntesis desde SRT") logging.getLogger(__name__).exception("Error synthesizing from SRT")
sys.exit(1) sys.exit(1)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -155,12 +155,65 @@ class PipelineOrchestrator:
# Si no se inyectan adaptadores, crear implementaciones por defecto # Si no se inyectan adaptadores, crear implementaciones por defecto
# Sólo importar adaptadores pesados si no se inyectan implementaciones. # Sólo importar adaptadores pesados si no se inyectan implementaciones.
if transcriber is None: 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.transcribe_adapter import TranscribeService
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: try:
from ..infra.faster_whisper_adapter import FasterWhisperTranscriber from ..infra.faster_whisper_adapter import FasterWhisperTranscriber
self.transcriber = 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: except Exception:
# dejar como None para permitir fallback a subprocess en tiempo de ejecución 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:
# último recurso: no hay transcriptor en memoria
self.transcriber = None self.transcriber = None
else: else:
self.transcriber = transcriber self.transcriber = transcriber
@ -207,6 +260,7 @@ class PipelineOrchestrator:
mix_background_volume: float = 0.2, mix_background_volume: float = 0.2,
keep_chunks: bool = False, keep_chunks: bool = False,
dry_run: bool = False, dry_run: bool = False,
srt_only: bool = False,
) -> PipelineResult: ) -> PipelineResult:
"""Run the pipeline. """Run the pipeline.
@ -325,6 +379,17 @@ class PipelineOrchestrator:
else: else:
raise ValueError("translate_method not supported in this orchestrator") 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 # 4) sintetizar por segmento
dub_wav = os.path.join(workdir, "dub_final.wav") dub_wav = os.path.join(workdir, "dub_final.wav")
if dry_run: if dry_run:
@ -360,4 +425,6 @@ class PipelineOrchestrator:
dub_wav=dub_wav, dub_wav=dub_wav,
replaced_video=replaced, replaced_video=replaced,
burned_video=burned, burned_video=burned,
srt_translated=srt_translated,
srt_original=srt_in,
) )