diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..345bcb5 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/whisper_project/__pycache__/main.cpython-313.pyc b/whisper_project/__pycache__/main.cpython-313.pyc index 9c60d56..bd22353 100644 Binary files a/whisper_project/__pycache__/main.cpython-313.pyc and b/whisper_project/__pycache__/main.cpython-313.pyc differ diff --git a/whisper_project/cli/dub_and_burn.py b/whisper_project/cli/dub_and_burn.py index 8aaac8a..2a7d297 100644 --- a/whisper_project/cli/dub_and_burn.py +++ b/whisper_project/cli/dub_and_burn.py @@ -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() diff --git a/whisper_project/cli/orchestrator.py b/whisper_project/cli/orchestrator.py index 7bb3550..1b8c013 100644 --- a/whisper_project/cli/orchestrator.py +++ b/whisper_project/cli/orchestrator.py @@ -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__": diff --git a/whisper_project/cli/srt_to_kokoro.py b/whisper_project/cli/srt_to_kokoro.py index 96fd6d4..3bb6c9f 100644 --- a/whisper_project/cli/srt_to_kokoro.py +++ b/whisper_project/cli/srt_to_kokoro.py @@ -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() diff --git a/whisper_project/dub_and_burn.py b/whisper_project/dub_and_burn.py index 8013902..0f93739 100644 --- a/whisper_project/dub_and_burn.py +++ b/whisper_project/dub_and_burn.py @@ -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: diff --git a/whisper_project/infra/__pycache__/ffmpeg_adapter.cpython-313.pyc b/whisper_project/infra/__pycache__/ffmpeg_adapter.cpython-313.pyc index 30d90d8..99fddce 100644 Binary files a/whisper_project/infra/__pycache__/ffmpeg_adapter.cpython-313.pyc and b/whisper_project/infra/__pycache__/ffmpeg_adapter.cpython-313.pyc differ diff --git a/whisper_project/infra/__pycache__/kokoro_adapter.cpython-313.pyc b/whisper_project/infra/__pycache__/kokoro_adapter.cpython-313.pyc index 482b1f1..18e8340 100644 Binary files a/whisper_project/infra/__pycache__/kokoro_adapter.cpython-313.pyc and b/whisper_project/infra/__pycache__/kokoro_adapter.cpython-313.pyc differ diff --git a/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc b/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc index 2a7d3b8..2b6b06e 100644 Binary files a/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc and b/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc differ diff --git a/whisper_project/infra/ffmpeg_adapter.py b/whisper_project/infra/ffmpeg_adapter.py index 0259f23..cafcf04 100644 --- a/whisper_project/infra/ffmpeg_adapter.py +++ b/whisper_project/infra/ffmpeg_adapter.py @@ -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 diff --git a/whisper_project/infra/gemini_adapter.py b/whisper_project/infra/gemini_adapter.py index b8beed9..7183a53 100644 --- a/whisper_project/infra/gemini_adapter.py +++ b/whisper_project/infra/gemini_adapter.py @@ -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) diff --git a/whisper_project/infra/kokoro_adapter.py b/whisper_project/infra/kokoro_adapter.py index ffa7c2a..8c816f4 100644 --- a/whisper_project/infra/kokoro_adapter.py +++ b/whisper_project/infra/kokoro_adapter.py @@ -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) diff --git a/whisper_project/infra/transcribe_adapter.py b/whisper_project/infra/transcribe_adapter.py index 5f0ff93..ccfe3df 100644 --- a/whisper_project/infra/transcribe_adapter.py +++ b/whisper_project/infra/transcribe_adapter.py @@ -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: diff --git a/whisper_project/logging_config.py b/whisper_project/logging_config.py new file mode 100644 index 0000000..d6e873b --- /dev/null +++ b/whisper_project/logging_config.py @@ -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) diff --git a/whisper_project/main.py b/whisper_project/main.py index 0383c95..9590191 100644 --- a/whisper_project/main.py +++ b/whisper_project/main.py @@ -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/) 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__": diff --git a/whisper_project/run_xtts_clone.py b/whisper_project/run_xtts_clone.py index 8350949..a5dba7b 100644 --- a/whisper_project/run_xtts_clone.py +++ b/whisper_project/run_xtts_clone.py @@ -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 diff --git a/whisper_project/srt_to_kokoro.py b/whisper_project/srt_to_kokoro.py index 58df4ea..51f90b8 100644 --- a/whisper_project/srt_to_kokoro.py +++ b/whisper_project/srt_to_kokoro.py @@ -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) diff --git a/whisper_project/translate_srt_argos.py b/whisper_project/translate_srt_argos.py index 15a9067..182705f 100644 --- a/whisper_project/translate_srt_argos.py +++ b/whisper_project/translate_srt_argos.py @@ -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) diff --git a/whisper_project/translate_srt_local.py b/whisper_project/translate_srt_local.py index 56cd723..db6cade 100644 --- a/whisper_project/translate_srt_local.py +++ b/whisper_project/translate_srt_local.py @@ -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) diff --git a/whisper_project/translate_srt_with_gemini.py b/whisper_project/translate_srt_with_gemini.py index 5ddd4b4..e80ef5a 100644 --- a/whisper_project/translate_srt_with_gemini.py +++ b/whisper_project/translate_srt_with_gemini.py @@ -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) diff --git a/whisper_project/usecases/__pycache__/orchestrator.cpython-313.pyc b/whisper_project/usecases/__pycache__/orchestrator.cpython-313.pyc index 6c2172a..57755f7 100644 Binary files a/whisper_project/usecases/__pycache__/orchestrator.cpython-313.pyc and b/whisper_project/usecases/__pycache__/orchestrator.cpython-313.pyc differ diff --git a/whisper_project/usecases/orchestrator.py b/whisper_project/usecases/orchestrator.py index ed34c1f..10a866a 100644 --- a/whisper_project/usecases/orchestrator.py +++ b/whisper_project/usecases/orchestrator.py @@ -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)