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
83
voz/calienta_cache.py
Normal file
83
voz/calienta_cache.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Calienta la cache de voz: coge las frases que cayeron a Piper (misses.log,
|
||||
que escribe jarvis-voz.sh) y las regenera con la voz CLONADA (XTTS) en
|
||||
cache_voz/. La proxima vez que JARVIS diga esa frase, sonara con tu voz al
|
||||
instante. Cuanto mas usas JARVIS, mas habla con tu voz.
|
||||
|
||||
python calienta_cache.py [cuda|cpu] [--min N]
|
||||
|
||||
--min N: solo frases vistas al menos N veces (por defecto 1 = todas las nuevas).
|
||||
No reproduce nada. Pensado para correr de noche (timer de systemd).
|
||||
"""
|
||||
import os, sys, re, json, time, hashlib, collections
|
||||
os.environ.setdefault("COQUI_TOS_AGREED", "1")
|
||||
|
||||
DISP = "cuda"
|
||||
MIN = 1
|
||||
for i, a in enumerate(sys.argv[1:]):
|
||||
if a in ("cuda", "cpu"):
|
||||
DISP = a
|
||||
elif a == "--min" and i + 2 <= len(sys.argv) - 1:
|
||||
MIN = int(sys.argv[i + 2])
|
||||
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
MISSES = os.path.join(BASE, "misses.log")
|
||||
CACHE = os.path.join(BASE, "cache_voz")
|
||||
sys.path.insert(0, BASE)
|
||||
from clonador import Clonador, clave # la voz, definida en un solo sitio
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
|
||||
|
||||
def cacheable(f):
|
||||
# ni muy corta ni muy larga; nada con rutas (frases unicas que no repiten);
|
||||
# ni mayoria de digitos (numeros sueltos de telemetria, casi unicos)
|
||||
if not (3 <= len(f) <= 180):
|
||||
return False
|
||||
if "/" in f or "\\" in f:
|
||||
return False
|
||||
letras = sum(c.isalpha() for c in f)
|
||||
return letras >= max(3, len(f) * 0.5)
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(MISSES):
|
||||
print("no hay misses.log todavia; nada que calentar."); return 0
|
||||
with open(MISSES, encoding="utf-8") as f:
|
||||
frases = [ln.rstrip("\n") for ln in f if ln.strip()]
|
||||
cuenta = collections.Counter(frases)
|
||||
|
||||
pendientes = []
|
||||
for frase, n in cuenta.items():
|
||||
if n < MIN or not cacheable(frase):
|
||||
continue
|
||||
dst = os.path.join(CACHE, clave(frase) + ".wav")
|
||||
if not os.path.exists(dst):
|
||||
pendientes.append((frase, n))
|
||||
pendientes.sort(key=lambda x: -x[1]) # las mas repetidas primero
|
||||
print(f"{len(cuenta)} frases distintas en el log · "
|
||||
f"{len(pendientes)} nuevas a generar (min={MIN})", flush=True)
|
||||
if not pendientes:
|
||||
return 0
|
||||
|
||||
t0 = time.time()
|
||||
voz = Clonador(DISP)
|
||||
print(f"modelo cargado en {time.time()-t0:.1f}s", flush=True)
|
||||
|
||||
hechas = 0
|
||||
for i, (frase, n) in enumerate(pendientes, 1):
|
||||
dst = os.path.join(CACHE, clave(frase) + ".wav")
|
||||
try:
|
||||
t0 = time.time()
|
||||
voz.genera(frase, dst)
|
||||
hechas += 1
|
||||
print(f" [{i}/{len(pendientes)}] x{n} {time.time()-t0:.1f}s "
|
||||
f"{frase[:50]}", flush=True)
|
||||
except Exception as e:
|
||||
print(f" fallo '{frase[:40]}': {e}", flush=True)
|
||||
print(f"\nlisto: {hechas} frases nuevas en la voz clonada. "
|
||||
f"cache -> {CACHE}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue