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
85
voz/prueba_octava.py
Normal file
85
voz/prueba_octava.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""¿El clon no se parece porque esta una octava por debajo?
|
||||
|
||||
Medido: los originales de fish.audio rondan los 210 Hz de tono medio con un
|
||||
recorrido de 105 a 349; los clones salen a 105 Hz y con diez hercios de
|
||||
recorrido. O sea una octava mas grave y ademas planos.
|
||||
|
||||
Esto lo comprueba de la unica forma que vale: subiendo el clon de tono y
|
||||
midiendo si el parecido sube. Si sube mucho, el problema es el tono y se
|
||||
arregla; si no se mueve, el tono es un sintoma y el problema esta en otro sitio.
|
||||
|
||||
python prueba_octava.py [cuda|cpu]
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
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", "jarvis_lento_b.wav")
|
||||
os.environ.setdefault("COQUI_TOS_AGREED", "1")
|
||||
LARGO = dict(gpt_cond_len=30, gpt_cond_chunk_len=6, max_ref_length=30)
|
||||
|
||||
SEMITONOS = [0, 3, 5, 7, 9, 12]
|
||||
|
||||
|
||||
def desplaza(origen, destino, semitonos):
|
||||
if semitonos == 0:
|
||||
subprocess.run(["cp", origen, destino], check=True)
|
||||
return destino
|
||||
subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", origen,
|
||||
"-af", f"rubberband=pitch={semitonos}", destino], check=True)
|
||||
return destino
|
||||
|
||||
|
||||
def tono(ruta):
|
||||
import librosa
|
||||
y, sr = librosa.load(ruta, sr=22050, mono=True)
|
||||
f, _, _ = librosa.pyin(y, sr=sr, fmin=60, fmax=400, frame_length=2048)
|
||||
v = f[~np.isnan(f)]
|
||||
return float(np.median(v)) if len(v) else 0.0
|
||||
|
||||
|
||||
def main():
|
||||
from TTS.api import TTS
|
||||
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(REF)
|
||||
|
||||
def parecido(ruta):
|
||||
return float(torch.nn.functional.cosine_similarity(
|
||||
patron.squeeze().float(), emb(ruta).squeeze().float(), dim=0))
|
||||
|
||||
# los clones mas largos de la cache, que son los que mejor se miden
|
||||
clones = sorted(glob.glob(os.path.join(BASE, "cache_voz", "*.wav")),
|
||||
key=os.path.getsize, reverse=True)[:4]
|
||||
|
||||
print(f" referencia: {tono(REF):.0f} Hz\n")
|
||||
print(f" {'semitonos':>10s} {'tono clon':>10s} {'parecido':>9s}")
|
||||
print(" " + "-" * 33)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
for st in SEMITONOS:
|
||||
puntos, tonos = [], []
|
||||
for i, c in enumerate(clones):
|
||||
d = os.path.join(tmp, f"c{i}_{st}.wav")
|
||||
desplaza(c, d, st)
|
||||
puntos.append(parecido(d))
|
||||
tonos.append(tono(d))
|
||||
print(f" {st:>+10d} {np.mean(tonos):9.0f}Hz {np.mean(puntos):9.4f}",
|
||||
flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue