#!/usr/bin/env python3 """Genera la misma frase en castellano con las voces masculinas disponibles. flatpak run --command=python3 io.github.qwersyk.Newelle//master \ ~/COFRE/CODERS/JARVIS/config/comparar_voces_es.py Kokoro solo trae DOS voces masculinas en español: em_alex y em_santa. De los motores que Newelle soporta, los unicos locales son kokoro y espeak; el resto —gtts, edge, elevenlabs, openai, groq— son de nube y quedan descartados. Como ninguna de las dos es especialmente grave, cada una se genera tambien en version grave: se baja el tono con ffmpeg sin cambiar la velocidad. El truco es reproducir a menos muestras por segundo (baja tono y alarga) y devolver la duracion con atempo. A 0.90 baja unos dos semitonos, que se nota sin sonar a disco rayado; por debajo de 0.85 empieza a sonar artificial. Se escuchan las cuatro y se fija la elegida con set_voz_es.py. """ import os import subprocess import sys sys.path.insert(0, "/app/share/newelle") sys.path.insert(0, os.path.expanduser( "~/.var/app/io.github.qwersyk.Newelle/config/pip")) from gi.repository import Gio # noqa: E402 from newelle.handlers.tts.kokoro_handler import KokoroTTSHandler # noqa: E402 from newelle.handlers.tts.tts import jarvis_audio_filter # noqa: E402 VOCES = ["em_alex", "em_santa"] GRAVEDAD = 0.90 # 1.0 = sin tocar; menos = mas grave MUESTREO = 24000 # el que saca kokoro FRASE = ("Buenas tardes. Todos los sistemas funcionan en local. " "La tarjeta grafica esta libre y quedan ochocientos cuarenta y seis " "gigabytes en el disco. Quiere que ejecute el diagnostico?") SALIDA = os.path.expanduser("~/COFRE/CODERS/JARVIS/muestras/voces_es") CONFIG = os.path.expanduser("~/.var/app/io.github.qwersyk.Newelle/config") def cadena(grave: bool) -> str: """El tratamiento JARVIS, con o sin bajada de tono delante.""" base = jarvis_audio_filter() if not grave: return base tono = (f"asetrate={int(MUESTREO * GRAVEDAD)}," f"aresample={MUESTREO},atempo={1 / GRAVEDAD:.4f}") return f"{tono},{base}" if base else tono def nivel(path): """mean/max en dB, para confirmar que ninguna sale floja o recortada.""" r = subprocess.run( ["ffmpeg", "-hide_banner", "-i", path, "-af", "volumedetect", "-f", "null", "/dev/null"], capture_output=True, text=True, ) vals = [l.split(":")[-1].strip() for l in r.stderr.splitlines() if "mean_volume:" in l or "max_volume:" in l] return " / ".join(vals) if vals else "?" def main(): os.makedirs(SALIDA, exist_ok=True) settings = Gio.Settings.new("io.github.qwersyk.Newelle") handler = KokoroTTSHandler(settings, os.path.join(CONFIG, "models")) if not handler.is_installed(): print("instalando kokoro...") handler.install() previa = handler.get_current_voice() if hasattr(handler, "get_current_voice") else None print(f"{'fichero':<28} nivel (mean / max)") for voz in VOCES: handler.set_voice(voz) crudo = os.path.join(SALIDA, f"_{voz}.wav") try: handler.save_audio(FRASE, crudo) except Exception as e: print(f"{voz:<28} FALLO al generar: {e}") continue for etiqueta, grave in (("", False), ("_grave", True)): final = os.path.join(SALIDA, f"{voz}{etiqueta}.wav") try: subprocess.run( ["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", crudo, "-af", cadena(grave), "-y", final], check=True, ) print(f"{os.path.basename(final):<28} {nivel(final)}") except Exception as e: print(f"{voz}{etiqueta:<20} FALLO: {e}") os.remove(crudo) if previa: handler.set_voice(previa) print(f"\nescuchalas en: {SALIDA}") print("la elegida se fija con set_voz_es.py [--grave]") return 0 if __name__ == "__main__": sys.exit(main())