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
110
voz/duelo_ajustes.py
Normal file
110
voz/duelo_ajustes.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Duelo honesto entre dos configuraciones de voz.
|
||||
|
||||
Por que existe esto: la primera comparacion uso UNA frase, y la segunda tres.
|
||||
Con esos tamaños se concluyo que el ajuste nuevo mejoraba... y al regenerar la
|
||||
cache entera salio peor. XTTS muestrea en cada generacion (do_sample, temperatura
|
||||
0,65), asi que dos pasadas de la MISMA frase con la MISMA configuracion ya dan
|
||||
numeros distintos. Comparar pocas muestras es leer ruido.
|
||||
|
||||
Aqui cada configuracion genera las mismas N frases R veces, en el mismo proceso
|
||||
y el mismo dispositivo, y se comparan las medias. Ademas se mide la dispersion
|
||||
DENTRO de cada configuracion: si la diferencia entre las dos es menor que lo que
|
||||
varia una consigo misma, no hay diferencia que defender.
|
||||
|
||||
python duelo_ajustes.py [cuda|cpu] [repeticiones]
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import torch
|
||||
|
||||
DISPOSITIVO = sys.argv[1] if len(sys.argv) > 1 else "cuda"
|
||||
REPES = int(sys.argv[2]) if len(sys.argv) > 2 else 3
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
REF = os.path.join(BASE, "ref")
|
||||
CATALOGO = os.path.join(BASE, "..", "newelle", "data", "acciones", "acciones.json")
|
||||
|
||||
os.environ.setdefault("COQUI_TOS_AGREED", "1")
|
||||
LARGO = dict(gpt_cond_len=30, gpt_cond_chunk_len=6, max_ref_length=30)
|
||||
|
||||
VIEJA = dict(
|
||||
nombre="vieja (3 refs, cond 6s)",
|
||||
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),
|
||||
)
|
||||
NUEVA = dict(
|
||||
nombre="nueva (1 ref sesion, cond 30s)",
|
||||
refs=[os.path.join(REF, "jarvis_sesion.wav")],
|
||||
cond=LARGO,
|
||||
)
|
||||
GEN = dict(temperature=0.65, repetition_penalty=2.0, speed=1.0)
|
||||
|
||||
|
||||
def frases(n=8):
|
||||
with open(CATALOGO, encoding="utf-8") as f:
|
||||
d = json.load(f)
|
||||
acc = d.get("acciones", d if isinstance(d, list) else [])
|
||||
return sorted({a["acuse"] for a in acc if a.get("acuse")})[:n]
|
||||
|
||||
|
||||
def main():
|
||||
from TTS.api import TTS
|
||||
import soundfile as sf
|
||||
|
||||
api = TTS("tts_models/multilingual/multi-dataset/xtts_v2").to(DISPOSITIVO)
|
||||
modelo = api.synthesizer.tts_model
|
||||
|
||||
def emb(ruta):
|
||||
_, e = modelo.get_conditioning_latents(audio_path=[ruta], **LARGO)
|
||||
return e
|
||||
|
||||
patron = emb(os.path.join(REF, "jarvis_lento_b.wav"))
|
||||
|
||||
def parecido(ruta):
|
||||
return float(torch.nn.functional.cosine_similarity(
|
||||
patron.squeeze().float(), emb(ruta).squeeze().float(), dim=0))
|
||||
|
||||
textos = frases()
|
||||
print(f" {len(textos)} frases x {REPES} repeticiones por configuracion\n")
|
||||
|
||||
resumen = {}
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for cfg in (VIEJA, NUEVA):
|
||||
refs = [r for r in cfg["refs"] if os.path.exists(r)]
|
||||
gpt, hablante = modelo.get_conditioning_latents(
|
||||
audio_path=refs, **cfg["cond"])
|
||||
puntos = []
|
||||
for i, texto in enumerate(textos):
|
||||
for r in range(REPES):
|
||||
out = modelo.inference(
|
||||
text=texto, language="es", gpt_cond_latent=gpt,
|
||||
speaker_embedding=hablante, enable_text_splitting=True,
|
||||
**GEN)
|
||||
d = os.path.join(tmp, f"{id(cfg)}_{i}_{r}.wav")
|
||||
sf.write(d, out["wav"], 24000)
|
||||
puntos.append(parecido(d))
|
||||
resumen[cfg["nombre"]] = puntos
|
||||
print(f" {cfg['nombre']:32s} media {statistics.mean(puntos):.4f}"
|
||||
f" desv {statistics.pstdev(puntos):.4f}"
|
||||
f" ({len(puntos)} muestras)", flush=True)
|
||||
|
||||
a, b = list(resumen.values())
|
||||
ma, mb = statistics.mean(a), statistics.mean(b)
|
||||
ruido = (statistics.pstdev(a) + statistics.pstdev(b)) / 2
|
||||
print(f"\n diferencia entre configuraciones: {abs(mb-ma):.4f}")
|
||||
print(f" variacion dentro de cada una: {ruido:.4f}")
|
||||
if abs(mb - ma) < ruido:
|
||||
print("\n => la diferencia es MENOR que el ruido: no hay mejora que defender")
|
||||
else:
|
||||
gana = list(resumen)[1] if mb > ma else list(resumen)[0]
|
||||
print(f"\n => gana: {gana}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue