Commit inicial: gr-locutor (Locutor IA - GRP)
Locuciones de radio con voz IA local (XTTS-v2) integradas a G Radio Player: interfaz Rust/GTK4 de configuración, motor de generación y enriquecimiento de texto en Python, daemon de monitoreo que dispara locuciones sin pausar playlist-refill. Requiere radio-player >= 0.4.11.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user