JARVIS/voz/antes_despues.py
sito 8e4bc8ad94 JARVIS: asistente de voz local para Linux
Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla
con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin
cuentas ni claves.

Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper
comparten tarjeta y solo caben porque estan dimensionados para ello. El
RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve
de una cache de frases.

Incluye instalador (install.sh), requisitos, y documentacion del stack,
del manejo de root y de las acciones. Los apuntes indexados y el diario NO
se incluyen: son privados y el .gitignore los bloquea.
2026-08-16 15:34:37 +02:00

122 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Mide el ajuste ACTUAL contra el propuesto, con las mismas frases.
Las dos vueltas anteriores compararon cosas distintas entre si; esto compara lo
que hay hoy en la cache con lo que se quiere poner, para poder decir una cifra
honesta de cuanto mejora.
De paso deja creada la referencia buena: ref/jarvis_sesion.wav, las tomas de
una misma sesion pegadas. Mezclar sesiones distintas puntuaba peor —el
promediado de XTTS diluye el timbre cuando los clips no suenan igual—, que es
justo lo contrario de lo que se supuso al montarlo.
python antes_despues.py [cuda|cpu]
"""
import glob
import os
import subprocess
import sys
import tempfile
import torch
DISPOSITIVO = sys.argv[1] if len(sys.argv) > 1 else "cuda"
BASE = os.path.dirname(os.path.abspath(__file__))
REF = os.path.join(BASE, "ref")
SALIDA = os.path.join(BASE, "comparativa")
DESCARGAS = os.path.expanduser("~/Downloads")
SESION = os.path.join(REF, "jarvis_sesion.wav")
os.environ.setdefault("COQUI_TOS_AGREED", "1")
FRASES = [
"Buenas noches, señor. Soy su asistente personal.",
"Un momento, lo miro.",
"He encontrado catorce ficheros, señor. Dígame cuál abro.",
]
# lo que hay hoy en generar_cache_voz.py
ANTES = dict(
refs=[os.path.join(REF, n) for n in
("jarvis_lento_b.wav", "jarvis_lento_a.wav", "jarvis_ref.wav")],
cond=dict(gpt_cond_len=6, gpt_cond_chunk_len=6, max_ref_length=30),
gen=dict(temperature=0.65, repetition_penalty=2.0, speed=1.0),
)
# lo propuesto
DESPUES = dict(
refs=[SESION],
cond=dict(gpt_cond_len=30, gpt_cond_chunk_len=6, max_ref_length=30),
gen=dict(temperature=0.65, repetition_penalty=2.0, speed=1.0),
)
def parecido(a, b):
return float(torch.nn.functional.cosine_similarity(
a.squeeze().float(), b.squeeze().float(), dim=0))
def construye_sesion():
"""Pega las tomas de la misma sesion en un unico wav de referencia."""
if os.path.exists(SESION):
return SESION
mp3s = sorted(glob.glob(os.path.join(DESCARGAS, "Jarvis-*Buenas-noches*.mp3")))
if not mp3s:
raise SystemExit("no encuentro los mp3 de la sesion en ~/Downloads")
with tempfile.TemporaryDirectory() as tmp:
trozos = []
for i, m in enumerate(mp3s):
d = os.path.join(tmp, f"{i}.wav")
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", m,
"-ar", "24000", "-ac", "1", d], check=True)
trozos.append(d)
lista = os.path.join(tmp, "lista.txt")
with open(lista, "w") as f:
for t in trozos:
f.write(f"file '{t}'\n")
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-f", "concat",
"-safe", "0", "-i", lista, "-ar", "24000", "-ac", "1",
SESION], check=True)
print(f" referencia nueva: {SESION}")
return SESION
def main():
os.makedirs(SALIDA, exist_ok=True)
construye_sesion()
from TTS.api import TTS
import soundfile as sf
print(f"cargando XTTS en {DISPOSITIVO}...", flush=True)
api = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(DISPOSITIVO)
modelo = api.synthesizer.tts_model
largo = dict(gpt_cond_len=30, gpt_cond_chunk_len=6, max_ref_length=30)
def emb(rutas):
_, e = modelo.get_conditioning_latents(audio_path=list(rutas), **largo)
return e
patron = emb([os.path.join(REF, "jarvis_lento_b.wav")])
print()
for etiqueta, cfg in (("ANTES (lo que suena hoy)", ANTES),
("DESPUES(lo propuesto) ", DESPUES)):
gpt, hablante = modelo.get_conditioning_latents(
audio_path=cfg["refs"], **cfg["cond"])
puntos = []
for j, frase in enumerate(FRASES):
out = modelo.inference(text=frase, language="es",
gpt_cond_latent=gpt, speaker_embedding=hablante,
enable_text_splitting=True, **cfg["gen"])
nombre = ("antes" if cfg is ANTES else "despues") + f"_{j}.wav"
sf.write(os.path.join(SALIDA, nombre), out["wav"], 24000)
puntos.append(parecido(patron, emb([os.path.join(SALIDA, nombre)])))
media = sum(puntos) / len(puntos)
detalle = " ".join(f"{p:.3f}" for p in puntos)
print(f" {etiqueta} {media:.4f} [{detalle}]", flush=True)
print(f"\n wavs para escuchar en {SALIDA} (antes_*.wav / despues_*.wav)")
if __name__ == "__main__":
main()