From a091d33fdaff695377045584f4cf73ad71a87756 Mon Sep 17 00:00:00 2001 From: Cesar Mendivil Date: Fri, 24 Oct 2025 15:36:41 -0700 Subject: [PATCH] =?UTF-8?q?Implementar=20configuraci=C3=B3n=20de=20logging?= =?UTF-8?q?=20centralizada=20y=20actualizar=20el=20manejo=20de=20logs=20en?= =?UTF-8?q?=20varios=20m=C3=B3dulos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 171 ++++++++++++++++++ .../__pycache__/main.cpython-313.pyc | Bin 6653 -> 9588 bytes whisper_project/cli/dub_and_burn.py | 3 + whisper_project/cli/orchestrator.py | 6 +- whisper_project/cli/srt_to_kokoro.py | 3 + whisper_project/dub_and_burn.py | 37 ++-- .../ffmpeg_adapter.cpython-313.pyc | Bin 13975 -> 15121 bytes .../kokoro_adapter.cpython-313.pyc | Bin 7905 -> 8262 bytes .../transcribe_adapter.cpython-313.pyc | Bin 13481 -> 13835 bytes whisper_project/infra/ffmpeg_adapter.py | 14 +- whisper_project/infra/gemini_adapter.py | 13 +- whisper_project/infra/kokoro_adapter.py | 15 +- whisper_project/infra/transcribe_adapter.py | 26 +-- whisper_project/logging_config.py | 81 +++++++++ whisper_project/main.py | 49 ++++- whisper_project/run_xtts_clone.py | 6 +- whisper_project/srt_to_kokoro.py | 10 +- whisper_project/translate_srt_argos.py | 6 +- whisper_project/translate_srt_local.py | 6 +- whisper_project/translate_srt_with_gemini.py | 6 +- .../__pycache__/orchestrator.cpython-313.pyc | Bin 14007 -> 14120 bytes whisper_project/usecases/orchestrator.py | 21 ++- 22 files changed, 407 insertions(+), 66 deletions(-) create mode 100644 .gitignore create mode 100644 whisper_project/logging_config.py 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 9c60d5618ab6272fcdb5a761e7e4ce1f85b7f5ea..bd223532979f688c91c9edc726af8faa833813f0 100644 GIT binary patch literal 9588 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 delta 2519 zcma)7YfMvT82-L<>E+PVg1u6DJJ^d*E|ZH4WK~qef#A||In@3k6@{P~Ljbk1+u9ldh?$k(h+m{)7DWr&6jYcafp*#hVit6mb=e|2l;qgtP)K3d ztlDg?iPW}3iFSdQpg2Srt|nzcZ(jO|4D3+6g*g&zXCx*TR4k?9+DgQUOr)-zl~|Dt z*DJAwS)_2D1~j9#qMNE(8irf)OH`>bqB?b*7dtx(WCca)+c`-U0g{^d(WKsfriU7e z0wAiC0(z*#0A9mAN{Fb6G(9PTP$1#OBjD4zTr-jDoRUw0#m zHA%CHyph|zLCq^g5eLzEt+VPSQKOVDY4PS23O+QeHtiF&5h}8?o$}xX2p`^@PvVLq zN}bDYbd@)6ad!8kPdNvYIDQMB0rQ+&a#lEj#YZ|efIcpBp+jZHR|-9qtZ;KzMLm&K za+pMo9z>d&sa9&8Ea_rlbVKXjsZ%b55gfXKx1@LhOMT3M=5-EFQU21TsLtoZySHIK zN-;&8&c_Za-BycRegFS$$6nso|EyCF=*NBae+NJrex8o-FFN;&}dT5g>qAui{aI1VbJ}H|U`TsA2F5?FPf}KMWON zh@7OxEI5#U0Sn)gd>+{UG5{~&nDNymq=A#g1Qe|h;CkWsPM0iPHJmRS{bcmP7PM-- zZBXMDvg)DaaAJCFO4gtSlOJl)W7ALtk3BV1R(*VQB6U2OmbGZid_<+iR!sZe+=pqr z<<+1uZ@^-jywTB{Uk)WFJko?GDIK4XrsTxXL`MzGGQ+8{B>LRqMvE3BEJF`0U1-vJ z8hLC1HVbQ~-DuWUP1V#RZRPHkEH#nNGLwm^(X2Z0R+<3Wjgx0p$&;z{R65H}$|-3o zOQ%nzv#RveP*RrBXO)$d%%X2AZLN4KK$e!LCE0_KTuoGNBsrdvQt`Kwr(`d&`iSro z5g=j*;_UBGH9Y#z-VJqV!R}?#WCf7E>h&`Gnga5llm3FaK3UZQIURql^MlThLhl_w zcN{?jzo7TcPk+_?b;q@iUo;`)bUNr9^&0AP9;0&QXvrC)axT>Gx&m4B*yZ15zt}h1 zH%~8{YVSCO^IVSOQOF&HE9itfXkpCGdsWW4wo7eup-Z95!mm|zi&b^#Pj~H^6~`NY z)4);xkO%gejs}cfeiM`b1@t4lA^-pY 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 30d90d865b3da87494bfe8ae3e889070cea45ccd..99fddce05fe33241f46081c36a4a1781852d6b89 100644 GIT binary patch delta 5038 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 Dmas?* diff --git a/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc b/whisper_project/infra/__pycache__/transcribe_adapter.cpython-313.pyc index 2a7d3b809d6e2e321cb46a3a66ac6fd3c49b88e1..2b6b06e54f36af5b991089b9f653a3d4aa5f5c79 100644 GIT binary patch delta 2565 zcmZuyZEO_B8J^j_{kq#b+uWDE=a2Q-4n7=zfNdJ{5eqv3f*q&1CGNKu=p^X~bK z9X!%L`_4P_zWeUXGtVsMUYqfM?e}{TYl-jwJN=t;{<;&>tn&sk5J3huxDkAaBb*L4 zKf)8f5iHM$=a4`I9UbM9DgK4qA(Ry8-DrqQ zA$mU&_OH5il;V;KZIZ(DK_u92@P=oAv-tH<8!LvMA<&rGqKJmX=6X^ddNLaBmJL*? z+7ePi`)XLG2ceV@LQLMz%36UOe>hBl4L zqJ(dZz@N=`3};9-V@>Aue8IFbxz1lq=j?(>^x6ERIg{VpW>a1`SSvfy>VPB8)Nhmd5qSGG2g0p>$2QVd&S3hM$W6})CYM1w49lBN5y{z`o(_$m1bM$?2ud>D{wwfW$ z@T5?(npN?G*3Z$UEltgvj-@0+FvJ1gl2bB0`&gJxO931NKHzG4T58@WrR0>7jQk)a z>oRZcqh3QclmWr=j&CT9GraVH6!XJ6xh>@zucsH=>c$^K=;734Q)T?&JLO9@KkQX3 z!)K_EvL?GB7aa{YdKe}A^jGp;F~~ya6ZAv5L2a{j)6$23oa*Z8qW_d<4ushlfnOvV z092W%afD)?OiVj!5l-Dyp3IuWCXAPebO1cgz>%hA@|j}q4${DS^mmFb_QF^nT~Z=F zaWJ9ygu0GF{ovi0hPVZEQS8+{t!$Wn6^hb5-r;`ukbRc{@GA6o*|;eicWWXG;_XQ6 z{Oq~e1>YLi2A4wDwQJh4JW48a|HeHC!7*ViDV5*r z(ctMHe34d0RaZ>c4EbH|lSvQb9RWTra^;;@m`x&dpFh&za?j@k${EwE z;zWjFMgz_JTc30(3e-v{>saMZ*u%VzYMa?v(<<8OtZ9)(2&@BW1JLMS{bQX6m}y^T za1wnkcdt?C`5VTO730`Vu?RHgk zq3YXz38;3b4W#&URd|>y{Us2^xP{)U8pB4Zw|bcmLD^rh$88R8T9YJ3W1)jsqm!Y* zo;Xcu|1eo9xY7H_8q zVlTwq?A7Jvg6xjRV&|E>04ATGE0Q*PE%pvBl+5}Mc>LQ^gZ@6ob@Zd=V|bA!;;-Vm z(tGh);TY@=u^G$15!57 zJ$)U!?=X|e6|!N$@ADYr2b>3c{^Lcs{xcM~?-5uhfN&jCL2u29qJ5%h7oNDKw4M~{ I>Aw2^15aZPcK`qY delta 2284 zcmZuzU2IfE6rQ<%`+xhhKizIiyKI-1ZE2~LA_Xg2D6P~Mx;K}Ar7YX+?RLrbZk@Y? zxDmk@QzMC#kpLcUyJ~O!j^=bMBcl zXU=!d?0qdclW_d%a99CpU);JkUOMfleCi1@IRPYqfCMF`4{l=+;|Dc##`dw>IK=sZ z7X*OB`GCZ?GjuTkh?n@)5D9T%`uOGSvoHu@Iewn`7dml@UAJWqG>k$C^Z}I_V)Kgt zkQf<+xy4zb7lFjKvF&6PZGIp#VH3V$a9i|hzRx@_1&b#cGLB&eLs zg>Cp7&V3jx`l|9WzcOzbMC$>Nc_-+t0YDWB24x{^TTm(LB&%dwv{Klx-w%L|I>`q? zkp;4E(NeImFQbTqV;3zg%>Yv$cbC$ z%stob&Ut&y6?@I8^gEf;nb{RNdri*1ehS_o>f-V_a{yl!R<*c~#ZJbjDF zu9Y_4Wr5fKqqAyi`%Qb{*G`D$uqH2+Ol9WVMwuD`H(CmgD zLc6E5gIBFX1y5}Fyt&Fzx#)vz8}i|=>nr7I018^NElk7uK$Gl}<6-vWOV$;#f>0{r zh^2F-iW6jMW*Zh7h>~^`*?(crC0|b5GH33_ zXKZ68aUp9&ySL$?#JD4XQrB-RaFidbKiYr2#ED9YN&GtN@!!84m_D!#s^q#E;>kOs zNvsU8-|ON?u5kFQ-QzKmhA#wKW&$9u!$<6xCY)T>GCs zVwJtsUf7BcR__WJ)=Tptq}Fv#CFO>8(IJ~UkVZj#xB6X}%$`_wk%h;z{mVaq(1-5@ zhT(g7cikcA%g)zL@WZtDkVYUQrY03NnrL36iZYGd1hyhC9U4Bh(-FPp(JBh7DYQ~} zjKb3tC_88ig>HO46h2HZ8B|NbPk~k@GOE$A^*~9kqi-Llu%5#G5=R|$x`9Fu1tVd4 z>4*lx@GTU~N8=0?7y+j#f-q_fz#u-+*wAHo5+FjICI2J&13l_qXr}8nQlQ@nsFT7W z3SIcWMi1=6?xs_4Hv3_dmxWc?&ssJ?Lb<7R55dvaarkHUtJXgtv}QkD_hC)$2qC g$)nLY-(ewycNq?Hzgq#UzQgfkBm!88e{Qe-AJ;A2)c^nh 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 6c2172a9aaa23f658a36808088efb1dc24a3f20e..57755f7c5e0a2c2a52f284d424784b3693307350 100644 GIT binary patch delta 2790 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 vXaVdFO07RY=9Ti|1o| 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)