JARVIS/voz/prueba_arrastre.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

113 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Frases cortas generadas con carrerilla, y recortadas despues.
Medido en prueba_longitud.py: un acuse de segundo y medio puntua 0,51 de
parecido, y el mismo estilo dicho en catorce segundos puntua 0,70. XTTS
arranca cada generacion en frio y no le da tiempo a asentar la voz antes de
que la frase termine. Como la cache de JARVIS son casi todo acuses de un
segundo, lo que mas se oye es lo que peor suena.
La idea: no generar "Voy, señor" a secas, sino una frase de arrastre delante,
y quedarse solo con el final. La voz llega al trozo util ya asentada.
Se prueban dos formas de cortar:
- por silencio: XTTS deja pausa entre oraciones, asi que el ultimo tramo
sonoro es la frase que se queria
- repitiendo la frase tres veces y quedandose con la ultima
python prueba_arrastre.py [cuda|cpu] [repeticiones]
"""
import os
import statistics
import sys
import tempfile
import numpy as np
import torch
DISPOSITIVO = sys.argv[1] if len(sys.argv) > 1 else "cuda"
REPES = int(sys.argv[2]) if len(sys.argv) > 2 else 4
BASE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, BASE)
from clonador import CONDICIONADO, GENERACION, FRECUENCIA, referencias
os.environ.setdefault("COQUI_TOS_AGREED", "1")
PATRON = os.path.join(BASE, "ref", "jarvis_lento_b.wav")
MEDIR = dict(gpt_cond_len=30, gpt_cond_chunk_len=6, max_ref_length=30)
# Frase de arrastre: larga, del mismo registro, y que acaba en punto para que
# XTTS deje pausa antes de lo que viene.
ARRASTRE = ("Buenas noches, señor. He revisado el sistema y todo está en orden, "
"sin ninguna anomalía digna de mención.")
CORTAS = ["Voy, señor.", "Enseguida, señor.", "Hecho, señor.", "A la orden, señor."]
def ultimo_tramo(wav, sr, minimo=0.35):
"""El ultimo trozo sonoro, separado por silencios."""
import librosa
tramos = librosa.effects.split(wav, top_db=32, frame_length=1024, hop_length=256)
tramos = [(a, b) for a, b in tramos if (b - a) / sr >= minimo]
if not tramos:
return wav
a, b = tramos[-1]
margen = int(0.05 * sr)
return wav[max(0, a - margen):min(len(wav), b + margen)]
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], **MEDIR)
return e
patron = emb(PATRON)
def parecido(ruta):
return float(torch.nn.functional.cosine_similarity(
patron.squeeze().float(), emb(ruta).squeeze().float(), dim=0))
gpt, hablante = modelo.get_conditioning_latents(
audio_path=referencias(), **CONDICIONADO)
def genera(texto):
out = modelo.inference(text=texto, language="es", gpt_cond_latent=gpt,
speaker_embedding=hablante,
enable_text_splitting=False, **GENERACION)
return np.asarray(out["wav"], dtype=np.float32)
metodos = {
"directo (como ahora)": lambda f: genera(f),
"con arrastre delante": lambda f: ultimo_tramo(
genera(f"{ARRASTRE} {f}"), FRECUENCIA),
"repetida x3, la ultima": lambda f: ultimo_tramo(
genera(" ".join([f] * 3)), FRECUENCIA),
}
print(f" {len(CORTAS)} frases cortas x {REPES} repeticiones\n")
print(f" {'metodo':24s} {'dur':>6s} {'parecido':>9s} {'desv':>7s}")
print(" " + "-" * 50)
with tempfile.TemporaryDirectory() as tmp:
for nombre, hacer in metodos.items():
puntos, duraciones = [], []
for frase in CORTAS:
for r in range(REPES):
wav = hacer(frase)
d = os.path.join(tmp, f"{abs(hash(nombre))}_{len(puntos)}.wav")
sf.write(d, wav, FRECUENCIA)
duraciones.append(len(wav) / FRECUENCIA)
puntos.append(parecido(d))
print(f" {nombre:24s} {statistics.mean(duraciones):5.1f}s "
f"{statistics.mean(puntos):9.4f} {statistics.pstdev(puntos):7.4f}",
flush=True)
if __name__ == "__main__":
main()