#!/usr/bin/env python3 """Clona una voz con XTTS-v2 (local) y locuta la muestra de feadulta. Issue #76. Uso: tts_xtts.py [nombre_salida] La muestra: 6-20s de voz limpia en español. Salida en uploads/tts-samples/. NOTA: XTTS-v2 tiene licencia no comercial (CPML). En CPU tarda ~1-2 min por muestra; con GPU sería casi instantáneo. """ import os import subprocess import sys from pathlib import Path os.environ.setdefault("COQUI_TOS_AGREED", "1") # acepta licencia CPML no-interactivo import torch # noqa: E402 from TTS.api import TTS # noqa: E402 DEVICE = "cuda" if torch.cuda.is_available() and not os.environ.get("FEA_CPU") else "cpu" SAMPLE = ( "Bienvenido a Fe Adulta. La humanidad abriga una esperanza: verse liberada de la " "esclavitud y alcanzar la libertad de los hijos de Dios. Una fe adulta es una fe " "personal, valiente, sin miedos infantiles. Detente un instante y respira." ) OUT = Path(__file__).resolve().parent.parent / "wordpress/wp-content/uploads/tts-samples" def main(): if len(sys.argv) < 2: sys.exit("uso: tts_xtts.py [nombre_salida]") spk = sys.argv[1] name = sys.argv[2] if len(sys.argv) > 2 else "xtts-clon" OUT.mkdir(parents=True, exist_ok=True) print(f"Cargando XTTS-v2 en {DEVICE}…", flush=True) tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(DEVICE) raw = OUT / f"{name}.raw.wav" print(f"Clonando voz de {spk} y locutando…", flush=True) tts.tts_to_file( text=SAMPLE, speaker_wav=spk, language="es", file_path=str(raw), temperature=0.65, # menos aleatoriedad → más estable length_penalty=1.0, repetition_penalty=5.0, # reduce artefactos/balbuceos en español top_k=50, top_p=0.85, enable_text_splitting=True, # parte por frases → mejor prosodia ) # Comfort noise: ruido marrón suave y constante que rellena los silencios de # comas/puntos para que no contrasten con el suelo de ruido del habla clonada. wav = OUT / f"{name}.wav" if os.environ.get("FEA_NO_COMFORT"): subprocess.run(["ffmpeg", "-y", "-i", str(raw), str(wav)], capture_output=True) else: subprocess.run([ "ffmpeg", "-y", "-i", str(raw), "-filter_complex", "anoisesrc=color=brown:amplitude=0.004:sample_rate=24000[n];" "[n]highpass=f=120,lowpass=f=3800[nf];" "[0:a][nf]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[a]", "-map", "[a]", "-ar", "24000", str(wav), ], capture_output=True) raw.unlink(missing_ok=True) mp3 = OUT / f"{name}.mp3" subprocess.run(["ffmpeg", "-y", "-i", str(wav), "-b:a", "96k", str(mp3)], capture_output=True) print(f"OK -> {mp3}") if __name__ == "__main__": main()