Cesar Mendivil c22767d3d4 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.
2025-10-25 00:00:02 -07:00

238 lines
8.5 KiB
Python

#!/usr/bin/env python3
"""CLI mínimo que expone el orquestador principal.
Este módulo proporciona la función `main()` que construye los adaptadores
por defecto e invoca `PipelineOrchestrator.run(...)`. Está diseñado para
reemplazar el antiguo `run_full_pipeline.py` como punto de entrada.
"""
from __future__ import annotations
import argparse
import glob
import os
import shutil
import sys
import tempfile
from whisper_project.logging_config import configure_logging
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():
p = argparse.ArgumentParser()
p.add_argument("--video", required=True)
p.add_argument("--srt", help="SRT de entrada (opcional)")
p.add_argument(
"--kokoro-endpoint",
required=False,
default=project_config.KOKORO_ENDPOINT or "https://kokoro.example/api/synthesize",
help=(
"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("--voice", default="em_alex")
p.add_argument("--kokoro-model", default="model")
p.add_argument("--whisper-model", default="base")
p.add_argument(
"--translate-method",
choices=[
"local",
"gemini",
"argos",
"none",
],
default="local",
)
p.add_argument(
"--gemini-key",
default=None,
help=(
"API key para Gemini (si eliges "
"--translate-method=gemini)"
),
)
p.add_argument("--mix", action="store_true")
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(
"--dry-run",
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",
help="Activar logging DEBUG",
)
args = p.parse_args()
# configurar logging según flag (crea carpeta logs/ y handlers por tipo)
configure_logging(args.verbose)
video = os.path.abspath(args.video)
if not os.path.exists(video):
logging.getLogger(__name__).error("Vídeo no encontrado: %s", video)
sys.exit(2)
workdir = tempfile.mkdtemp(prefix="full_pipeline_")
logging.info("Workdir creado: %s", workdir)
try:
# construir cliente Kokoro HTTP nativo e inyectarlo en el orquestador
kokoro_client = KokoroHttpClient(
args.kokoro_endpoint,
api_key=args.kokoro_key,
voice=args.voice,
model=args.kokoro_model,
)
orchestrator = PipelineOrchestrator(
kokoro_endpoint=args.kokoro_endpoint,
kokoro_key=args.kokoro_key,
voice=args.voice,
kokoro_model=args.kokoro_model,
tts_client=kokoro_client,
)
logging.info(
"Lanzando orquestador (dry_run=%s, translate_method=%s, whisper_model=%s)",
args.dry_run,
args.translate_method,
args.whisper_model,
)
result = orchestrator.run(
video=video,
srt=args.srt,
workdir=workdir,
translate_method=args.translate_method,
gemini_api_key=args.gemini_key,
whisper_model=args.whisper_model,
mix=args.mix,
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))
# Si no es dry-run, crear una subcarpeta por proyecto en output/
# (output/<basename-of-video>) y mover allí los artefactos generados.
final_path = None
if (
not args.dry_run
and result
and getattr(result, "burned_video", 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
# Mover el vídeo principal
src = result.burned_video
dest = os.path.join(project_out, os.path.basename(src))
try:
if os.path.abspath(src) != os.path.abspath(dest):
logging.info("Moviendo vídeo principal: %s -> %s", src, dest)
shutil.move(src, dest)
final_path = dest
except Exception:
final_path = src
# También mover otros artefactos que empiecen por el basename
try:
pattern = os.path.join(os.getcwd(), f"{base}*")
for p in glob.glob(pattern):
# no mover el archivo fuente ya movido
if os.path.abspath(p) == os.path.abspath(final_path):
continue
# mover sólo ficheros regulares
try:
if os.path.isfile(p):
logging.info("Moviendo artefacto: %s -> %s", p, os.path.join(project_out, os.path.basename(p)))
shutil.move(p, os.path.join(project_out, os.path.basename(p)))
except Exception:
pass
except Exception:
pass
else:
# 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/<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)
finally:
if not args.keep_temp:
try:
logging.info("Eliminando workdir: %s", workdir)
shutil.rmtree(workdir)
except Exception:
pass
# Limpiar posibles directorios temporales residuales creados
# por ejecuciones previas del pipeline: /tmp/full_pipeline_*
try:
tmp_pattern = "/tmp/full_pipeline_*"
for tmpd in glob.glob(tmp_pattern):
try:
# no intentar borrar si no es un directorio
if not os.path.isdir(tmpd):
continue
# evitar borrar el workdir actual por si acaso
try:
if os.path.abspath(tmpd) == os.path.abspath(workdir):
continue
except Exception:
pass
logging.info("Eliminando temporal residual: %s", tmpd)
shutil.rmtree(tmpd)
except Exception:
# no fallar la limpieza por errores puntuales
pass
except Exception:
pass
if __name__ == "__main__":
main()