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.logging_config import configure_logging
|
||||
|
||||
|
||||
def main():
|
||||
# configurar logging con nivel INFO por defecto
|
||||
configure_logging(False)
|
||||
return _legacy_main()
|
||||
|
||||
|
||||
|
||||
@ -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__":
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
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 sys
|
||||
import tempfile
|
||||
from whisper_project.logging_config import configure_logging
|
||||
import logging
|
||||
|
||||
from whisper_project.usecases.orchestrator import PipelineOrchestrator
|
||||
from whisper_project.infra.kokoro_adapter import KokoroHttpClient
|
||||
@ -63,14 +65,23 @@ def main():
|
||||
action="store_true",
|
||||
help="Simular pasos sin ejecutar",
|
||||
)
|
||||
p.add_argument(
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Activar logging DEBUG",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
# configurar logging según flag (crea carpeta logs/ y handlers por tipo)
|
||||
configure_logging(args.verbose)
|
||||
|
||||
video = os.path.abspath(args.video)
|
||||
if not os.path.exists(video):
|
||||
print("Vídeo no encontrado:", video, file=sys.stderr)
|
||||
logging.getLogger(__name__).error("Vídeo no encontrado: %s", video)
|
||||
sys.exit(2)
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="full_pipeline_")
|
||||
logging.info("Workdir creado: %s", workdir)
|
||||
try:
|
||||
# construir cliente Kokoro HTTP nativo e inyectarlo en el orquestador
|
||||
kokoro_client = KokoroHttpClient(
|
||||
@ -88,6 +99,13 @@ def main():
|
||||
tts_client=kokoro_client,
|
||||
)
|
||||
|
||||
logging.info(
|
||||
"Lanzando orquestador (dry_run=%s, translate_method=%s, whisper_model=%s)",
|
||||
args.dry_run,
|
||||
args.translate_method,
|
||||
args.whisper_model,
|
||||
)
|
||||
|
||||
result = orchestrator.run(
|
||||
video=video,
|
||||
srt=args.srt,
|
||||
@ -101,6 +119,8 @@ def main():
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
|
||||
logging.info("Orquestador finalizado; resultado (burned_video): %s", getattr(result, "burned_video", None))
|
||||
|
||||
# Si no es dry-run, crear una subcarpeta por proyecto en output/
|
||||
# (output/<basename-of-video>) y mover allí los artefactos generados.
|
||||
final_path = None
|
||||
@ -121,6 +141,7 @@ def main():
|
||||
dest = os.path.join(project_out, os.path.basename(src))
|
||||
try:
|
||||
if os.path.abspath(src) != os.path.abspath(dest):
|
||||
logging.info("Moviendo vídeo principal: %s -> %s", src, dest)
|
||||
shutil.move(src, dest)
|
||||
final_path = dest
|
||||
except Exception:
|
||||
@ -136,6 +157,7 @@ def main():
|
||||
# mover sólo ficheros regulares
|
||||
try:
|
||||
if os.path.isfile(p):
|
||||
logging.info("Moviendo artefacto: %s -> %s", p, os.path.join(project_out, os.path.basename(p)))
|
||||
shutil.move(p, os.path.join(project_out, os.path.basename(p)))
|
||||
except Exception:
|
||||
pass
|
||||
@ -145,13 +167,36 @@ def main():
|
||||
# En dry-run o sin resultado, no movemos nada
|
||||
final_path = getattr(result, "burned_video", None)
|
||||
|
||||
print("Flujo completado. Vídeo final:", final_path)
|
||||
logging.info("Flujo completado. Vídeo final: %s", final_path)
|
||||
finally:
|
||||
if not args.keep_temp:
|
||||
try:
|
||||
logging.info("Eliminando workdir: %s", workdir)
|
||||
shutil.rmtree(workdir)
|
||||
except Exception:
|
||||
pass
|
||||
# Limpiar posibles directorios temporales residuales creados
|
||||
# por ejecuciones previas del pipeline: /tmp/full_pipeline_*
|
||||
try:
|
||||
tmp_pattern = "/tmp/full_pipeline_*"
|
||||
for tmpd in glob.glob(tmp_pattern):
|
||||
try:
|
||||
# no intentar borrar si no es un directorio
|
||||
if not os.path.isdir(tmpd):
|
||||
continue
|
||||
# evitar borrar el workdir actual por si acaso
|
||||
try:
|
||||
if os.path.abspath(tmpd) == os.path.abspath(workdir):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
logging.info("Eliminando temporal residual: %s", tmpd)
|
||||
shutil.rmtree(tmpd)
|
||||
except Exception:
|
||||
# no fallar la limpieza por errores puntuales
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@ -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)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user