Implementar configuración de logging centralizada y actualizar el manejo de logs en varios módulos
This commit is contained in:
parent
e7f1ac2173
commit
a091d33fda
171
.gitignore
vendored
Normal file
171
.gitignore
vendored
Normal 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
|
||||||
Binary file not shown.
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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__":
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
81
whisper_project/logging_config.py
Normal file
81
whisper_project/logging_config.py
Normal 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)
|
||||||
@ -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__":
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -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)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user