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.dub_and_burn import main as _legacy_main
from whisper_project.logging_config import configure_logging
def main(): def main():
# configurar logging con nivel INFO por defecto
configure_logging(False)
return _legacy_main() return _legacy_main()

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
from whisper_project.logging_config import configure_logging
import logging import logging
from whisper_project.usecases.orchestrator import Orchestrator from whisper_project.usecases.orchestrator import Orchestrator
@ -16,10 +17,13 @@ def main():
p.add_argument("--verbose", action="store_true", help="Mostrar logs detallados") p.add_argument("--verbose", action="store_true", help="Mostrar logs detallados")
args = p.parse_args() 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) 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) res = orb.run(args.src_video, args.out_dir, translate=args.translate)
if args.verbose: if args.verbose:
print(res) logging.getLogger(__name__).debug(res)
if __name__ == "__main__": 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.srt_to_kokoro import main as _legacy_main
from whisper_project.logging_config import configure_logging
def main(): def main():
# configurar logging con nivel INFO por defecto
configure_logging(False)
return _legacy_main() return _legacy_main()

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import os import os
import subprocess import subprocess
import shutil import shutil
import logging
from typing import Optional from typing import Optional
# Importar funciones pesadas (parsing/synth) de forma perezosa dentro de # Importar funciones pesadas (parsing/synth) de forma perezosa dentro de
@ -9,6 +10,8 @@ from typing import Optional
from .ffmpeg_adapter import FFmpegAudioProcessor from .ffmpeg_adapter import FFmpegAudioProcessor
logger = logging.getLogger(__name__)
class KokoroHttpClient: class KokoroHttpClient:
"""Cliente HTTP para sintetizar segmentos desde un .srt usando un endpoint compatible. """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) raw = synth_chunk(self.endpoint, text, headers, payload_template)
except Exception as e: except Exception as e:
# saltar segmento con log y continuar # 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 prev_end = end_sec
continue continue
@ -87,12 +90,12 @@ class KokoroHttpClient:
try: try:
os.remove(target) os.remove(target)
except Exception: except Exception:
pass logger.debug("No se pudo eliminar chunk intermedio %s", target)
else: else:
chunk_files.append(target) chunk_files.append(target)
prev_end = end_sec 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: if not chunk_files:
raise RuntimeError("No se generaron fragmentos de audio desde el SRT") 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" out_video = os.path.splitext(video)[0] + ".replaced_audio.mp4"
try: try:
self._processor.replace_audio_in_video(video, out_wav, out_video) self._processor.replace_audio_in_video(video, out_wav, out_video)
except Exception as e: except Exception:
print(f"Error al reemplazar audio en el vídeo: {e}") logger.exception("Error al reemplazar audio en el vídeo")
# limpieza: opcional conservar tmpdir si keep_chunks # limpieza: opcional conservar tmpdir si keep_chunks
if not keep_chunks: if not keep_chunks:
@ -149,5 +152,5 @@ class KokoroHttpClient:
_sh.rmtree(tmpdir, ignore_errors=True) _sh.rmtree(tmpdir, ignore_errors=True)
except Exception: 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. so callers can depend on an object instead of free functions.
""" """
from typing import Optional from typing import Optional
import logging
"""Transcribe service with inlined implementation. """Transcribe service with inlined implementation.
@ -15,6 +16,9 @@ and makes it easier to unit-test.
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__)
class TranscribeService: class TranscribeService:
def __init__(self, model: str = "base", compute_type: str = "int8") -> None: def __init__(self, model: str = "base", compute_type: str = "int8") -> None:
self.model = model self.model = model
@ -23,17 +27,17 @@ class TranscribeService:
def transcribe_openai(self, file: str): def transcribe_openai(self, file: str):
import whisper 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") m = whisper.load_model(self.model, device="cpu")
print("Transcribiendo...") logger.info("Transcribiendo...")
result = m.transcribe(file, fp16=False) result = m.transcribe(file, fp16=False)
segments = result.get("segments", None) segments = result.get("segments", None)
if segments: if segments:
for seg in segments: for seg in segments:
print(seg.get("text", "")) logger.debug(seg.get("text", ""))
return segments return segments
else: else:
print(result.get("text", "")) logger.debug(result.get("text", ""))
return None return None
def transcribe_transformers(self, file: str): def transcribe_transformers(self, file: str):
@ -43,7 +47,7 @@ class TranscribeService:
device = "cpu" device = "cpu"
torch_dtype = torch.float32 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 = AutoModelForSpeechSeq2Seq.from_pretrained(self.model, torch_dtype=torch_dtype, low_cpu_mem_usage=True)
model_obj.to(device) model_obj.to(device)
processor = AutoProcessor.from_pretrained(self.model) processor = AutoProcessor.from_pretrained(self.model)
@ -56,24 +60,24 @@ class TranscribeService:
device=-1, device=-1,
) )
print("Transcribiendo...") logger.info("Transcribiendo...")
result = pipe(file) result = pipe(file)
if isinstance(result, dict): if isinstance(result, dict):
print(result.get("text", "")) logger.debug(result.get("text", ""))
else: else:
print(result) logger.debug(result)
return None return None
def transcribe_faster(self, file: str): def transcribe_faster(self, file: str):
from faster_whisper import WhisperModel 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) 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_gen, info = model_obj.transcribe(file, beam_size=5)
segments = list(segments_gen) segments = list(segments_gen)
text = "".join([seg.text for seg in segments]) text = "".join([seg.text for seg in segments])
print(text) logger.debug(text)
return segments return segments
def _format_timestamp(self, seconds: float) -> str: 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 shutil
import sys import sys
import tempfile import tempfile
from whisper_project.logging_config import configure_logging
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
@ -63,14 +65,23 @@ def main():
action="store_true", action="store_true",
help="Simular pasos sin ejecutar", help="Simular pasos sin ejecutar",
) )
p.add_argument(
"--verbose",
action="store_true",
help="Activar logging DEBUG",
)
args = p.parse_args() 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) video = os.path.abspath(args.video)
if not os.path.exists(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) sys.exit(2)
workdir = tempfile.mkdtemp(prefix="full_pipeline_") workdir = tempfile.mkdtemp(prefix="full_pipeline_")
logging.info("Workdir creado: %s", workdir)
try: try:
# construir cliente Kokoro HTTP nativo e inyectarlo en el orquestador # construir cliente Kokoro HTTP nativo e inyectarlo en el orquestador
kokoro_client = KokoroHttpClient( kokoro_client = KokoroHttpClient(
@ -88,6 +99,13 @@ def main():
tts_client=kokoro_client, 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( result = orchestrator.run(
video=video, video=video,
srt=args.srt, srt=args.srt,
@ -101,6 +119,8 @@ def main():
dry_run=args.dry_run, 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/ # Si no es dry-run, crear una subcarpeta por proyecto en output/
# (output/<basename-of-video>) y mover allí los artefactos generados. # (output/<basename-of-video>) y mover allí los artefactos generados.
final_path = None final_path = None
@ -121,6 +141,7 @@ def main():
dest = os.path.join(project_out, os.path.basename(src)) dest = os.path.join(project_out, os.path.basename(src))
try: try:
if os.path.abspath(src) != os.path.abspath(dest): if os.path.abspath(src) != os.path.abspath(dest):
logging.info("Moviendo vídeo principal: %s -> %s", src, dest)
shutil.move(src, dest) shutil.move(src, dest)
final_path = dest final_path = dest
except Exception: except Exception:
@ -136,6 +157,7 @@ def main():
# mover sólo ficheros regulares # mover sólo ficheros regulares
try: try:
if os.path.isfile(p): 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))) shutil.move(p, os.path.join(project_out, os.path.basename(p)))
except Exception: except Exception:
pass pass
@ -145,13 +167,36 @@ 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)
print("Flujo completado. Vídeo final:", final_path) logging.info("Flujo completado. Vídeo final: %s", final_path)
finally: finally:
if not args.keep_temp: if not args.keep_temp:
try: try:
logging.info("Eliminando workdir: %s", workdir)
shutil.rmtree(workdir) shutil.rmtree(workdir)
except Exception: except Exception:
pass 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__": if __name__ == "__main__":

View File

@ -15,8 +15,10 @@ def main():
try: try:
subprocess.run([sys.executable, script], check=True) subprocess.run([sys.executable, script], check=True)
except Exception as e: except Exception as e:
print("Error ejecutando run_xtts_clone ejemplo:", e, file=sys.stderr) import logging
print("Ejecuta 'python examples/run_xtts_clone.py' para la demo.") 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 1
return 0 return 0

View File

@ -8,6 +8,7 @@ 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 logging
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
@ -77,6 +78,7 @@ import sys
import tempfile import tempfile
from whisper_project.infra.kokoro_adapter import KokoroHttpClient from whisper_project.infra.kokoro_adapter import KokoroHttpClient
import logging
def main(): def main():
@ -99,7 +101,7 @@ def main():
endpoint = args.endpoint or os.environ.get("KOKORO_ENDPOINT") endpoint = args.endpoint or os.environ.get("KOKORO_ENDPOINT")
api_key = args.api_key or os.environ.get("KOKORO_API_KEY") api_key = args.api_key or os.environ.get("KOKORO_API_KEY")
if not endpoint: 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) 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)
@ -113,9 +115,9 @@ 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,
) )
print(f"Archivo final generado en: {args.out}") logging.getLogger(__name__).info("Archivo final generado en: %s", args.out)
except Exception as e: except Exception:
print(f"Error durante la síntesis desde SRT: {e}", file=sys.stderr) logging.getLogger(__name__).exception("Error durante la síntesis desde SRT")
sys.exit(1) sys.exit(1)

View File

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

View File

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

View File

@ -32,8 +32,10 @@ def main():
cmd += ["--gemini-api-key", args.gemini_api_key] cmd += ["--gemini-api-key", args.gemini_api_key]
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
return return
except Exception as e: except Exception:
print("Error: no se pudo ejecutar la traducción con Gemini:", e, file=sys.stderr) import logging
logger = logging.getLogger(__name__)
logger.exception("Error al ejecutar traducción con Gemini")
sys.exit(1) 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): def __init__(self, dry_run: bool = False, tts_model: str = "kokoro", verbose: bool = False):
self.dry_run = dry_run self.dry_run = dry_run
self.tts_model = tts_model self.tts_model = tts_model
# No configurar basicConfig aquí: usar configure_logging desde el CLI/main
if verbose: if verbose:
logging.basicConfig(level=logging.DEBUG) logger.setLevel(logging.DEBUG)
def run(self, src_video: str, out_dir: str, translate: bool = False) -> dict: def run(self, src_video: str, out_dir: str, translate: bool = False) -> dict:
"""Ejecuta el pipeline. """Ejecuta el pipeline.
@ -214,12 +215,12 @@ class PipelineOrchestrator:
""" """
# 0) prepare paths # 0) prepare paths
if dry_run: if dry_run:
print("[dry-run] workdir:", workdir) logger.info("[dry-run] workdir: %s", workdir)
# 1) extraer audio # 1) extraer audio
audio_tmp = os.path.join(workdir, "extracted_audio.wav") audio_tmp = os.path.join(workdir, "extracted_audio.wav")
if dry_run: if dry_run:
print(f"[dry-run] ffmpeg extract audio -> {audio_tmp}") logger.info("[dry-run] ffmpeg extract audio -> %s", audio_tmp)
else: else:
self.audio_processor.extract_audio(video, audio_tmp, sr=16000) self.audio_processor.extract_audio(video, audio_tmp, sr=16000)
@ -242,7 +243,7 @@ class PipelineOrchestrator:
srt_in, srt_in,
] ]
if dry_run: if dry_run:
print("[dry-run] ", " ".join(cmd_trans)) logger.info("[dry-run] %s", " ".join(cmd_trans))
else: else:
# Use injected transcriber when possible # Use injected transcriber when possible
try: try:
@ -263,7 +264,7 @@ class PipelineOrchestrator:
srt_translated, srt_translated,
] ]
if dry_run: if dry_run:
print("[dry-run] ", " ".join(cmd_translate)) logger.info("[dry-run] %s", " ".join(cmd_translate))
else: else:
try: try:
self.translator.translate_srt(srt_in, srt_translated) self.translator.translate_srt(srt_in, srt_translated)
@ -283,7 +284,7 @@ class PipelineOrchestrator:
cmd_translate += ["--gemini-api-key", gemini_api_key] cmd_translate += ["--gemini-api-key", gemini_api_key]
if dry_run: if dry_run:
print("[dry-run] ", " ".join(cmd_translate)) logger.info("[dry-run] %s", " ".join(cmd_translate))
else: else:
try: try:
# intentar usar adaptador Gemini si está disponible # intentar usar adaptador Gemini si está disponible
@ -307,7 +308,7 @@ class PipelineOrchestrator:
srt_translated, srt_translated,
] ]
if dry_run: if dry_run:
print("[dry-run] ", " ".join(cmd_translate)) logger.info("[dry-run] %s", " ".join(cmd_translate))
else: else:
try: try:
if self.translator and getattr(self.translator, "__class__", None).__name__ == "ArgosTranslator": if self.translator and getattr(self.translator, "__class__", None).__name__ == "ArgosTranslator":
@ -327,7 +328,7 @@ class PipelineOrchestrator:
# 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:
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: else:
# Use injected tts_client # Use injected tts_client
self.tts_client.synthesize_from_srt( self.tts_client.synthesize_from_srt(
@ -343,14 +344,14 @@ class PipelineOrchestrator:
# 5) reemplazar audio en vídeo # 5) reemplazar audio en vídeo
replaced = os.path.splitext(video)[0] + ".replaced_audio.mp4" replaced = os.path.splitext(video)[0] + ".replaced_audio.mp4"
if dry_run: if dry_run:
print(f"[dry-run] replace audio in video -> {replaced}") logger.info("[dry-run] replace audio in video -> %s", replaced)
else: else:
self.audio_processor.replace_audio_in_video(video, dub_wav, replaced) self.audio_processor.replace_audio_in_video(video, dub_wav, replaced)
# 6) quemar subtítulos # 6) quemar subtítulos
burned = os.path.splitext(video)[0] + ".replaced_audio.subs.mp4" burned = os.path.splitext(video)[0] + ".replaced_audio.subs.mp4"
if dry_run: 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: else:
self.audio_processor.burn_subtitles(replaced, srt_translated, burned) self.audio_processor.burn_subtitles(replaced, srt_translated, burned)