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
113
voz/prueba_arrastre.py
Normal file
113
voz/prueba_arrastre.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue