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.
This commit is contained in:
commit
8e4bc8ad94
125 changed files with 25033 additions and 0 deletions
122
voz/antes_despues.py
Normal file
122
voz/antes_despues.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue