Files
feadulta/scripts/tts_xtts.py
T
rafa 69e849d38e Sync: guardar en el historial el trabajo de las ultimas semanas que solo vivia en el disco
Este repo local tenia origin apuntando al Gitea local (localhost:3000), que Rafa
declaro archivado el 2026-06-28 (commit 962f33a, desarrollo movido a
gitea.feadulta.com). Ese memo nunca llego a este checkout: main quedo congelado
y todo el trabajo real de las ultimas 3+ semanas se fue commiteando solo en la
rama fix/multiidioma-portada-132 (ya fusionada a main sin perdida, commit
2504666), mientras que ademas se acumulaban 78 cambios sin commitear en el
working tree que nunca llegaron a NINGUN historial de git.

Este commit consolida esos cambios sueltos: TTS multi-voz (tts_*.py), scripts
de traduccion (translate_haiku.py, pretranslate_en_haiku.py, sync_translations_to_prod.py),
mu-plugins nuevos desplegados a prod (fea-beta-feedback, fea-cloudflare-realip,
fea-legacy-redirect, fea-gsc-verification, fea-support-campaign, fea-ui, etc.),
scripts de mantenimiento de enlaces/cartas, capturas E2E (tools/e2e/shot_*.cjs)
y documentacion de sesiones recientes.

Excluido deliberadamente (no es codigo versionable): tts-voices/ (3.3GB de
muestras de audio para clonacion de voz, anadido a .gitignore), logs/ (logs de
ejecucion, anadido a .gitignore), y 2 ficheros vacios accidentales + 2 copias
duplicadas sueltas en la raiz que ya existen en su ubicacion correcta.
2026-07-15 20:03:21 -04:00

73 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""Clona una voz con XTTS-v2 (local) y locuta la muestra de feadulta. Issue #76.
Uso:
tts_xtts.py <muestra_voz.wav|mp3> [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 <muestra_voz.wav|mp3> [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()