#!/usr/bin/env python3 """Genera una locución con XTTS-v2 usando la voz de referencia de GRP (o una voz de referencia propia de la estación, si reemplazó el archivo). Uso: generar.py "texto de la locución" [nombre_salida] Salida: ruta del MP3 generado, impresa en stdout (para que scripts la capturen). Variables de entorno: GR_LOCUTOR_VOZ ruta al WAV de referencia (default: ~/.gradio/data/locutor-ia/referencia/voz_referencia.wav) GR_LOCUTOR_DIR carpeta de datos (default: ~/.gradio/data/locutor-ia) """ import os import socket import subprocess import sys import time # Forzar resolución IPv4: en la prueba real en RPi5 una conexión IPv6 mal # enrutada dejó el proceso colgado ~53 minutos esperando una respuesta que # nunca llegó. Esto evita ese modo de falla en cualquier red similar. _orig_getaddrinfo = socket.getaddrinfo def _ipv4_only_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): return _orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags) socket.getaddrinfo = _ipv4_only_getaddrinfo DATA_DIR = os.environ.get("GR_LOCUTOR_DIR", os.path.expanduser("~/.gradio/data/locutor-ia")) SALIDA_DIR = os.path.join(DATA_DIR, "salida") def log(msg): print(f"[gr-locutor] {msg}", file=sys.stderr, flush=True) def voz_referencia(): """Prioridad: variable de entorno GR_LOCUTOR_VOZ > locutor.config (voz_modo=propia + voz_propia_path, escrito por gr-locutor-config) > voz genérica de GRP por defecto.""" env = os.environ.get("GR_LOCUTOR_VOZ") if env: return env config_path = os.path.join(DATA_DIR, "locutor.config") modo = "generica" ruta_propia = "" try: with open(config_path) as f: for line in f: if "=" not in line or line.strip().startswith("#"): continue clave, valor = line.split("=", 1) clave, valor = clave.strip(), valor.strip() if clave == "voz_modo": modo = valor elif clave == "voz_propia_path": ruta_propia = valor except FileNotFoundError: pass if modo == "propia" and ruta_propia: return ruta_propia return os.path.join(DATA_DIR, "referencia", "voz_referencia.wav") REF_VOICE = voz_referencia() def has_gpu(): try: import torch return torch.cuda.is_available() except Exception: return False def main(): if len(sys.argv) < 2: print(f"Uso: {sys.argv[0]} \"texto de la locución\" [nombre_salida]", file=sys.stderr) sys.exit(1) texto = sys.argv[1] nombre = sys.argv[2] if len(sys.argv) > 2 else time.strftime("locucion_%Y%m%d_%H%M%S") if not os.path.isfile(REF_VOICE): log(f"ERROR: no existe la voz de referencia en {REF_VOICE}") log("Ejecuta el instalador o coloca un WAV de 15-30s en esa ruta.") sys.exit(1) os.makedirs(SALIDA_DIR, exist_ok=True) wav_path = f"/tmp/{nombre}.wav" mp3_path = os.path.join(SALIDA_DIR, f"{nombre}.mp3") device = "cuda" if has_gpu() else "cpu" log(f"Dispositivo: {device} | Voz de referencia: {REF_VOICE}") t0 = time.time() from TTS.api import TTS tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(device) t_load = time.time() tts.tts_to_file(text=texto, speaker_wav=REF_VOICE, language="es", file_path=wav_path) t_gen = time.time() log(f"Cargar modelo: {t_load - t0:.1f}s | Generar: {t_gen - t_load:.1f}s") subprocess.run( ["ffmpeg", "-y", "-loglevel", "error", "-i", wav_path, "-ar", "44100", "-b:a", "320k", mp3_path], check=True, ) os.remove(wav_path) print(mp3_path) if __name__ == "__main__": main()