Implementar configuración de logging centralizada y actualizar el manejo de logs en varios módulos

This commit is contained in:
Cesar Mendivil 2025-10-24 15:36:41 -07:00
parent e7f1ac2173
commit a091d33fda
22 changed files with 407 additions and 66 deletions

171
.gitignore vendored Normal file
View File

@ -0,0 +1,171 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
# project, it is recommended to include the following JetBrains specific files:
# .idea/
# VS Code
.vscode/
# Operating System Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@ -6,9 +6,12 @@ This keeps the original behaviour but exposes the CLI under
"""
from whisper_project.dub_and_burn import main as _legacy_main
from whisper_project.logging_config import configure_logging
def main():
# configurar logging con nivel INFO por defecto
configure_logging(False)
return _legacy_main()

View File

@ -2,6 +2,7 @@
from __future__ import annotations
import argparse
from whisper_project.logging_config import configure_logging
import logging
from whisper_project.usecases.orchestrator import Orchestrator
@ -16,10 +17,13 @@ def main():
p.add_argument("--verbose", action="store_true", help="Mostrar logs detallados")
args = p.parse_args()
# configurar logging (archivos y consola)
configure_logging(args.verbose)
orb = Orchestrator(dry_run=args.dry_run, tts_model=args.tts_model, verbose=args.verbose)
res = orb.run(args.src_video, args.out_dir, translate=args.translate)
if args.verbose:
print(res)
logging.getLogger(__name__).debug(res)
if __name__ == "__main__":

View File

@ -6,9 +6,12 @@ Thin wrapper that delegates to the legacy
"""
from whisper_project.srt_to_kokoro import main as _legacy_main
from whisper_project.logging_config import configure_logging
def main():
# configurar logging con nivel INFO por defecto
configure_logging(False)
return _legacy_main()

View File

@ -63,6 +63,7 @@ from pathlib import Path
import requests
import shutil
import subprocess
import logging
from typing import List, Dict
from whisper_project.infra.kokoro_adapter import KokoroHttpClient
@ -152,7 +153,7 @@ def translate_with_gemini(text: str, target_lang: str, api_key: str, model: str
if isinstance(j, str):
return j
except Exception as e:
print(f"Warning: Gemini translation failed: {e}")
logging.getLogger(__name__).warning("Warning: Gemini translation failed: %s", e)
return text
@ -247,7 +248,7 @@ def main():
video = Path(args.video)
if not video.exists():
print("Vídeo no encontrado", file=sys.stderr)
logging.getLogger(__name__).error("Vídeo no encontrado")
sys.exit(2)
out_video = args.out if args.out else str(video.with_name(video.stem + "_dubbed.mp4"))
@ -255,16 +256,16 @@ def main():
try:
audio_wav = os.path.join(tmpdir, "extracted_audio.wav")
print("Extrayendo audio...")
logging.getLogger(__name__).info("Extrayendo audio...")
process_video.extract_audio(str(video), audio_wav)
print("Transcribiendo y traduciendo...")
logging.getLogger(__name__).info("Transcribiendo y traduciendo...")
if args.use_gemini:
# permitir pasar la key por variable de entorno GEMINI_API_KEY
if not args.gemini_api_key:
args.gemini_api_key = os.environ.get("GEMINI_API_KEY")
if not args.gemini_api_key:
print("--use-gemini requiere --gemini-api-key o la var de entorno GEMINI_API_KEY", file=sys.stderr)
logging.getLogger(__name__).error("--use-gemini requiere --gemini-api-key o la var de entorno GEMINI_API_KEY")
sys.exit(4)
# transcribir sin traducir (luego traduciremos por segmento)
from faster_whisper import WhisperModel
@ -278,22 +279,24 @@ def main():
segments = process_video.transcribe_and_translate_openai(audio_wav, args.whisper_model, "es")
if not segments:
print("No se obtuvieron segmentos; abortando", file=sys.stderr)
logging.getLogger(__name__).error("No se obtuvieron segmentos; abortando")
sys.exit(3)
segs = normalize_segments(segments)
# si usamos gemini, traducir por segmento ahora (mantener la función existente)
if args.use_gemini:
print(f"Traduciendo {len(segs)} segmentos con Gemini (model={args.gemini_model})...")
logging.getLogger(__name__).info(
"Traduciendo %s segmentos con Gemini (model=%s)...", len(segs), args.gemini_model
)
for s in segs:
try:
src = s.get("text", "")
if src:
tgt = translate_with_gemini(src, "es", args.gemini_api_key, model=args.gemini_model)
s["text"] = tgt
except Exception as e:
print(f"Warning: Gemini fallo en segmento: {e}")
except Exception:
logging.getLogger(__name__).warning("Gemini fallo en segmento")
# generar SRT traducido
srt_out = os.path.join(tmpdir, "translated.srt")
@ -301,36 +304,36 @@ def main():
for i, s in enumerate(segs, start=1):
srt_segments.append(s)
write_srt(srt_segments, srt_out)
print(f"SRT traducido guardado en: {srt_out}")
logging.getLogger(__name__).info("SRT traducido guardado en: %s", srt_out)
# sintetizar todo el SRT usando KokoroHttpClient (delegar en el adapter)
kokoro_endpoint = args.kokoro_endpoint or os.environ.get("KOKORO_ENDPOINT")
kokoro_key = args.api_key or os.environ.get("KOKORO_API_KEY")
if not kokoro_endpoint:
print("--kokoro-endpoint es requerido para sintetizar (o establecer KOKORO_ENDPOINT)", file=sys.stderr)
logging.getLogger(__name__).error("--kokoro-endpoint es requerido para sintetizar (o establecer KOKORO_ENDPOINT)")
sys.exit(5)
client = KokoroHttpClient(kokoro_endpoint, api_key=kokoro_key, voice=args.voice, model=args.model)
dub_wav = args.temp_dub if args.temp_dub else os.path.join(tmpdir, "dub_final.wav")
try:
client.synthesize_from_srt(srt_out, dub_wav, video=None, align=True, keep_chunks=False)
except Exception as e:
print(f"Error sintetizando desde SRT con Kokoro: {e}", file=sys.stderr)
except Exception:
logging.getLogger(__name__).exception("Error sintetizando desde SRT con Kokoro")
sys.exit(6)
print(f"Archivo dub generado en: {dub_wav}")
logging.getLogger(__name__).info("Archivo dub generado en: %s", dub_wav)
# reemplazar audio en el vídeo
replaced = os.path.join(tmpdir, "video_replaced.mp4")
print("Reemplazando pista de audio en el vídeo...")
logging.getLogger(__name__).info("Reemplazando pista de audio en el vídeo...")
ff = FFmpegAudioProcessor()
ff.replace_audio_in_video(str(video), dub_wav, replaced)
# quemar SRT traducido
print("Quemando SRT traducido en el vídeo...")
logging.getLogger(__name__).info("Quemando SRT traducido en el vídeo...")
ff.burn_subtitles(replaced, srt_out, out_video)
print(f"Vídeo final generado: {out_video}")
logging.getLogger(__name__).info("Vídeo final generado: %s", out_video)
finally:
try:

View File

@ -8,9 +8,13 @@ import subprocess
import os
import shutil
import tempfile
import logging
from typing import Iterable, List, Optional
logger = logging.getLogger(__name__)
def ensure_ffmpeg_available() -> bool:
"""Simple check to ensure ffmpeg/ffprobe are present in PATH.
@ -38,6 +42,7 @@ def ensure_ffmpeg_available() -> None:
def _run(cmd: List[str], hide_output: bool = False) -> None:
logger.debug("Ejecutando comando: %s", " ".join(cmd))
if hide_output:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else:
@ -61,6 +66,7 @@ def extract_audio(video_path: str, out_wav: str, sr: int = 16000) -> None:
"1",
out_wav,
]
logger.info("Extraer audio: %s -> %s (sr=%s)", video_path, out_wav, sr)
_run(cmd)
@ -86,6 +92,7 @@ def replace_audio_in_video(video_path: str, audio_path: str, out_video: str) ->
"192k",
out_video,
]
logger.info("Replace audio en video: %s + %s -> %s", video_path, audio_path, out_video)
_run(cmd)
@ -110,6 +117,7 @@ def burn_subtitles(video_path: str, srt_path: str, out_video: str, font: Optiona
"copy",
out_video,
]
logger.info("Burn subtitles: %s + %s -> %s", video_path, srt_path, out_video)
_run(cmd)
@ -124,6 +132,7 @@ def save_bytes_as_wav(raw_bytes: bytes, target_path: str, sr: int = 22050) -> No
tmp.flush()
tmp_path = tmp.name
logger.debug("Convertir bytes a wav -> %s (sr=%s)", target_path, sr)
try:
cmd = [
"ffmpeg",
@ -141,6 +150,7 @@ def save_bytes_as_wav(raw_bytes: bytes, target_path: str, sr: int = 22050) -> No
_run(cmd, hide_output=True)
except subprocess.CalledProcessError:
# fallback: escribir bytes crudos
logger.exception("ffmpeg falló al convertir bytes a wav; escribiendo crudo")
with open(target_path, "wb") as out:
out.write(raw_bytes)
finally:
@ -169,7 +179,7 @@ def create_silence(duration: float, out_path: str, sr: int = 22050) -> None:
try:
_run(cmd, hide_output=True)
except subprocess.CalledProcessError:
# fallback: crear archivo pequeño de ceros
logger.exception("No se pudo crear silencio con ffmpeg; creando archivo de ceros")
with open(out_path, "wb") as fh:
fh.write(b"\x00" * 1024)
@ -199,10 +209,12 @@ def pad_or_trim_wav(in_path: str, out_path: str, target_duration: float, sr: int
cur = 0.0
if cur == 0.0:
logger.debug("Duración desconocida; copiando %s -> %s", in_path, out_path)
shutil.copy(in_path, out_path)
return
if abs(cur - target_duration) < 0.02:
logger.debug("Duración ya cercana al target; copiando")
shutil.copy(in_path, out_path)
return

View File

@ -5,6 +5,7 @@ import time
from typing import Optional
import requests
import logging
try:
import srt # type: ignore
@ -34,8 +35,8 @@ def translate_text_google_gl(text: str, api_key: str, model: str = "gemini-2.5-f
parts = [p.text for p in c.content.parts if getattr(p, "text", None)]
if parts:
return "\n".join(parts).strip()
except Exception as e:
print(f"Warning: genai library translate failed: {e}")
except Exception:
logging.getLogger(__name__).warning("genai library translate failed")
for prefix in ("v1", "v1beta2"):
endpoint = f"https://generativelanguage.googleapis.com/{prefix}/models/{model}:generateContent?key={api_key}"
@ -66,8 +67,8 @@ def translate_text_google_gl(text: str, api_key: str, model: str = "gemini-2.5-f
for key in ("output_text", "text", "response", "translated_text"):
if key in j and isinstance(j[key], str):
return j[key].strip()
except Exception as e:
print(f"Warning: GL translate failed ({prefix}): {e}")
except Exception:
logging.getLogger(__name__).warning("GL translate failed for prefix %s", prefix)
return text
@ -85,8 +86,8 @@ def translate_srt_file(in_path: str, out_path: str, api_key: str, model: str):
continue
try:
translated = translate_text_google_gl(text, api_key, model=model)
except Exception as e:
print(f"Warning: translate failed for index {sub.index}: {e}")
except Exception:
logging.getLogger(__name__).warning("translate failed for index %s", sub.index)
translated = text
sub.content = translated
time.sleep(0.15)

View File

@ -1,6 +1,7 @@
import os
import subprocess
import shutil
import logging
from typing import Optional
# Importar funciones pesadas (parsing/synth) de forma perezosa dentro de
@ -9,6 +10,8 @@ from typing import Optional
from .ffmpeg_adapter import FFmpegAudioProcessor
logger = logging.getLogger(__name__)
class KokoroHttpClient:
"""Cliente HTTP para sintetizar segmentos desde un .srt usando un endpoint compatible.
@ -71,7 +74,7 @@ class KokoroHttpClient:
raw = synth_chunk(self.endpoint, text, headers, payload_template)
except Exception as e:
# saltar segmento con log y continuar
print(f"Error al sintetizar segmento {i}: {e}")
logger.exception("Error al sintetizar segmento %s", i)
prev_end = end_sec
continue
@ -87,12 +90,12 @@ class KokoroHttpClient:
try:
os.remove(target)
except Exception:
pass
logger.debug("No se pudo eliminar chunk intermedio %s", target)
else:
chunk_files.append(target)
prev_end = end_sec
print(f" - Segmento {i}/{len(subs)} -> {os.path.basename(chunk_files[-1])}")
logger.info(" - Segmento %s/%s -> %s", i, len(subs), os.path.basename(chunk_files[-1]))
if not chunk_files:
raise RuntimeError("No se generaron fragmentos de audio desde el SRT")
@ -139,8 +142,8 @@ class KokoroHttpClient:
out_video = os.path.splitext(video)[0] + ".replaced_audio.mp4"
try:
self._processor.replace_audio_in_video(video, out_wav, out_video)
except Exception as e:
print(f"Error al reemplazar audio en el vídeo: {e}")
except Exception:
logger.exception("Error al reemplazar audio en el vídeo")
# limpieza: opcional conservar tmpdir si keep_chunks
if not keep_chunks:
@ -149,5 +152,5 @@ class KokoroHttpClient:
_sh.rmtree(tmpdir, ignore_errors=True)
except Exception:
pass
logger.debug("No se pudo eliminar tmpdir %s", tmpdir)

View File

@ -4,6 +4,7 @@ Provides a small class that wraps transcription and SRT helper functions
so callers can depend on an object instead of free functions.
"""
from typing import Optional
import logging
"""Transcribe service with inlined implementation.
@ -15,6 +16,9 @@ and makes it easier to unit-test.
from pathlib import Path
logger = logging.getLogger(__name__)
class TranscribeService:
def __init__(self, model: str = "base", compute_type: str = "int8") -> None:
self.model = model
@ -23,17 +27,17 @@ class TranscribeService:
def transcribe_openai(self, file: str):
import whisper
print(f"Cargando openai-whisper modelo={self.model} en CPU...")
logger.info("Cargando openai-whisper modelo=%s en CPU...", self.model)
m = whisper.load_model(self.model, device="cpu")
print("Transcribiendo...")
logger.info("Transcribiendo...")
result = m.transcribe(file, fp16=False)
segments = result.get("segments", None)
if segments:
for seg in segments:
print(seg.get("text", ""))
logger.debug(seg.get("text", ""))
return segments
else:
print(result.get("text", ""))
logger.debug(result.get("text", ""))
return None
def transcribe_transformers(self, file: str):
@ -43,7 +47,7 @@ class TranscribeService:
device = "cpu"
torch_dtype = torch.float32
print(f"Cargando transformers modelo={self.model} en CPU...")
logger.info("Cargando transformers modelo=%s en CPU...", self.model)
model_obj = AutoModelForSpeechSeq2Seq.from_pretrained(self.model, torch_dtype=torch_dtype, low_cpu_mem_usage=True)
model_obj.to(device)
processor = AutoProcessor.from_pretrained(self.model)
@ -56,24 +60,24 @@ class TranscribeService:
device=-1,
)
print("Transcribiendo...")
logger.info("Transcribiendo...")
result = pipe(file)
if isinstance(result, dict):
print(result.get("text", ""))
logger.debug(result.get("text", ""))
else:
print(result)
logger.debug(result)
return None
def transcribe_faster(self, file: str):
from faster_whisper import WhisperModel
print(f"Cargando faster-whisper modelo={self.model} en CPU compute_type={self.compute_type}...")
logger.info("Cargando faster-whisper modelo=%s en CPU compute_type=%s...", self.model, self.compute_type)
model_obj = WhisperModel(self.model, device="cpu", compute_type=self.compute_type)
print("Transcribiendo...")
logger.info("Transcribiendo...")
segments_gen, info = model_obj.transcribe(file, beam_size=5)
segments = list(segments_gen)
text = "".join([seg.text for seg in segments])
print(text)
logger.debug(text)
return segments
def _format_timestamp(self, seconds: float) -> str:

View File

@ -0,0 +1,81 @@
"""Configuración centralizada de logging para el proyecto.
Provee `configure_logging(verbose: bool = False)` que crea una carpeta
`logs/` y subcarpetas por tipo de módulo (infra, usecases, cli) y añade
handlers rotativos para cada tipo. También deja un handler de consola.
"""
from __future__ import annotations
import logging
import logging.handlers
import os
from typing import Optional
def _ensure_dir(path: str) -> None:
try:
os.makedirs(path, exist_ok=True)
except Exception:
# si no podemos crear logs no rompemos la ejecución
pass
def configure_logging(verbose: bool = False, base_logs_dir: Optional[str] = None) -> None:
"""Configura logging del proyecto.
Args:
verbose: si True activa nivel DEBUG, sino INFO.
base_logs_dir: directorio raíz para logs (por defecto ./logs).
"""
level = logging.DEBUG if verbose else logging.INFO
if base_logs_dir is None:
base_logs_dir = os.path.join(os.getcwd(), "logs")
infra_dir = os.path.join(base_logs_dir, "infra")
usecases_dir = os.path.join(base_logs_dir, "usecases")
cli_dir = os.path.join(base_logs_dir, "cli")
_ensure_dir(infra_dir)
_ensure_dir(usecases_dir)
_ensure_dir(cli_dir)
# Formato simple
fmt = logging.Formatter("%(asctime)s %(levelname)s [%(name)s] %(message)s")
# Console handler (root)
console = logging.StreamHandler()
console.setLevel(level)
console.setFormatter(fmt)
# File handlers rotativos por tipo
infra_fh = logging.handlers.RotatingFileHandler(
os.path.join(infra_dir, "infra.log"), maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
infra_fh.setLevel(logging.DEBUG)
infra_fh.setFormatter(fmt)
usecases_fh = logging.handlers.RotatingFileHandler(
os.path.join(usecases_dir, "usecases.log"), maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
usecases_fh.setLevel(logging.DEBUG)
usecases_fh.setFormatter(fmt)
cli_fh = logging.handlers.RotatingFileHandler(
os.path.join(cli_dir, "cli.log"), maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"
)
cli_fh.setLevel(logging.DEBUG)
cli_fh.setFormatter(fmt)
root = logging.getLogger()
# limpiar handlers previos para evitar duplicados si se llama varias veces
for h in list(root.handlers):
root.removeHandler(h)
root.setLevel(level)
root.addHandler(console)
# Asignar handlers a sub-loggers por prefijo
logging.getLogger("whisper_project.infra").addHandler(infra_fh)
logging.getLogger("whisper_project.usecases").addHandler(usecases_fh)
logging.getLogger("whisper_project.cli").addHandler(cli_fh)

View File

@ -14,6 +14,8 @@ 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
@ -63,14 +65,23 @@ def main():
action="store_true",
help="Simular pasos sin ejecutar",
)
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):
print("Vídeo no encontrado:", video, file=sys.stderr)
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(
@ -88,6 +99,13 @@ def main():
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,
@ -101,6 +119,8 @@ def main():
dry_run=args.dry_run,
)
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
@ -121,6 +141,7 @@ def main():
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:
@ -136,6 +157,7 @@ def main():
# 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
@ -145,13 +167,36 @@ def main():
# En dry-run o sin resultado, no movemos nada
final_path = getattr(result, "burned_video", None)
print("Flujo completado. Vídeo final:", final_path)
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__":

View File

@ -15,8 +15,10 @@ def main():
try:
subprocess.run([sys.executable, script], check=True)
except Exception as e:
print("Error ejecutando run_xtts_clone ejemplo:", e, file=sys.stderr)
print("Ejecuta 'python examples/run_xtts_clone.py' para la demo.")
import logging
logger = logging.getLogger(__name__)
logger.exception("Error ejecutando run_xtts_clone ejemplo: %s", e)
logger.info("Ejecuta examples/run_xtts_clone.py para la demo.")
return 1
return 0

View File

@ -8,6 +8,7 @@ con `KokoroHttpClient` (nombre esperado por otros módulos).
from __future__ import annotations
from typing import Any
import logging
from whisper_project.infra.kokoro_utils import parse_srt_file as _parse_srt_file, synth_chunk as _synth_chunk
@ -77,6 +78,7 @@ import sys
import tempfile
from whisper_project.infra.kokoro_adapter import KokoroHttpClient
import logging
def main():
@ -99,7 +101,7 @@ def main():
endpoint = args.endpoint or os.environ.get("KOKORO_ENDPOINT")
api_key = args.api_key or os.environ.get("KOKORO_API_KEY")
if not endpoint:
print("Debe proporcionar --endpoint o la variable de entorno KOKORO_ENDPOINT", file=sys.stderr)
logging.getLogger(__name__).error("Debe proporcionar --endpoint o la variable de entorno KOKORO_ENDPOINT")
sys.exit(2)
client = KokoroHttpClient(endpoint, api_key=api_key, voice=args.voice, model=args.model)
@ -113,9 +115,9 @@ def main():
mix_with_original=args.mix_with_original,
mix_background_volume=args.mix_background_volume,
)
print(f"Archivo final generado en: {args.out}")
except Exception as e:
print(f"Error durante la síntesis desde SRT: {e}", file=sys.stderr)
logging.getLogger(__name__).info("Archivo final generado en: %s", args.out)
except Exception:
logging.getLogger(__name__).exception("Error durante la síntesis desde SRT")
sys.exit(1)

View File

@ -29,8 +29,10 @@ def main():
cmd = [sys.executable, script, "--in", args.in_srt, "--out", args.out_srt]
subprocess.run(cmd, check=True)
return
except Exception as e:
print("Error: no se pudo ejecutar Argos Translate:", e, file=sys.stderr)
except Exception:
import logging
logger = logging.getLogger(__name__)
logger.exception("Error al ejecutar Argos Translate")
sys.exit(1)

View File

@ -31,8 +31,10 @@ def main():
cmd = [sys.executable, script, "--in", args.in_srt, "--out", args.out_srt]
subprocess.run(cmd, check=True)
return
except Exception as e:
print("Error: no se pudo ejecutar la traducción local:", e, file=sys.stderr)
except Exception:
import logging
logger = logging.getLogger(__name__)
logger.exception("Error al ejecutar la traducción local")
sys.exit(1)

View File

@ -32,8 +32,10 @@ def main():
cmd += ["--gemini-api-key", args.gemini_api_key]
subprocess.run(cmd, check=True)
return
except Exception as e:
print("Error: no se pudo ejecutar la traducción con Gemini:", e, file=sys.stderr)
except Exception:
import logging
logger = logging.getLogger(__name__)
logger.exception("Error al ejecutar traducción con Gemini")
sys.exit(1)

View File

@ -23,8 +23,9 @@ class Orchestrator:
def __init__(self, dry_run: bool = False, tts_model: str = "kokoro", verbose: bool = False):
self.dry_run = dry_run
self.tts_model = tts_model
# No configurar basicConfig aquí: usar configure_logging desde el CLI/main
if verbose:
logging.basicConfig(level=logging.DEBUG)
logger.setLevel(logging.DEBUG)
def run(self, src_video: str, out_dir: str, translate: bool = False) -> dict:
"""Ejecuta el pipeline.
@ -214,12 +215,12 @@ class PipelineOrchestrator:
"""
# 0) prepare paths
if dry_run:
print("[dry-run] workdir:", workdir)
logger.info("[dry-run] workdir: %s", workdir)
# 1) extraer audio
audio_tmp = os.path.join(workdir, "extracted_audio.wav")
if dry_run:
print(f"[dry-run] ffmpeg extract audio -> {audio_tmp}")
logger.info("[dry-run] ffmpeg extract audio -> %s", audio_tmp)
else:
self.audio_processor.extract_audio(video, audio_tmp, sr=16000)
@ -242,7 +243,7 @@ class PipelineOrchestrator:
srt_in,
]
if dry_run:
print("[dry-run] ", " ".join(cmd_trans))
logger.info("[dry-run] %s", " ".join(cmd_trans))
else:
# Use injected transcriber when possible
try:
@ -263,7 +264,7 @@ class PipelineOrchestrator:
srt_translated,
]
if dry_run:
print("[dry-run] ", " ".join(cmd_translate))
logger.info("[dry-run] %s", " ".join(cmd_translate))
else:
try:
self.translator.translate_srt(srt_in, srt_translated)
@ -283,7 +284,7 @@ class PipelineOrchestrator:
cmd_translate += ["--gemini-api-key", gemini_api_key]
if dry_run:
print("[dry-run] ", " ".join(cmd_translate))
logger.info("[dry-run] %s", " ".join(cmd_translate))
else:
try:
# intentar usar adaptador Gemini si está disponible
@ -307,7 +308,7 @@ class PipelineOrchestrator:
srt_translated,
]
if dry_run:
print("[dry-run] ", " ".join(cmd_translate))
logger.info("[dry-run] %s", " ".join(cmd_translate))
else:
try:
if self.translator and getattr(self.translator, "__class__", None).__name__ == "ArgosTranslator":
@ -327,7 +328,7 @@ class PipelineOrchestrator:
# 4) sintetizar por segmento
dub_wav = os.path.join(workdir, "dub_final.wav")
if dry_run:
print(f"[dry-run] synthesize from srt {srt_translated} -> {dub_wav} (align={True} mix={mix})")
logger.info("[dry-run] synthesize from srt %s -> %s", srt_translated, dub_wav)
else:
# Use injected tts_client
self.tts_client.synthesize_from_srt(
@ -343,14 +344,14 @@ class PipelineOrchestrator:
# 5) reemplazar audio en vídeo
replaced = os.path.splitext(video)[0] + ".replaced_audio.mp4"
if dry_run:
print(f"[dry-run] replace audio in video -> {replaced}")
logger.info("[dry-run] replace audio in video -> %s", replaced)
else:
self.audio_processor.replace_audio_in_video(video, dub_wav, replaced)
# 6) quemar subtítulos
burned = os.path.splitext(video)[0] + ".replaced_audio.subs.mp4"
if dry_run:
print(f"[dry-run] burn subtitles {srt_translated} into -> {burned}")
logger.info("[dry-run] burn subtitles %s -> %s", srt_translated, burned)
else:
self.audio_processor.burn_subtitles(replaced, srt_translated, burned)