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
76
voz/generar_cache_voz.py
Normal file
76
voz/generar_cache_voz.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env python3
|
||||
# Genera, con la voz CLONADA (XTTS-v2), las frases fijas que JARVIS repite:
|
||||
# los acuses de las acciones y las respuestas de plantilla del panel. Cada WAV
|
||||
# se guarda en cache_voz/ con nombre = sha1(texto normalizado), que es la misma
|
||||
# clave que usa el wrapper jarvis-voz.sh para servirlas al instante.
|
||||
#
|
||||
# Corre en el host (venv clonador-venv), en GPU, sin prisa: es trabajo de una
|
||||
# sola vez. En uso, el wrapper solo copia el WAV -> latencia cero, GPU libre.
|
||||
#
|
||||
# python generar_cache_voz.py [cuda|cpu]
|
||||
import os, sys, re, json, glob, hashlib, time
|
||||
|
||||
DISPOSITIVO = sys.argv[1] if len(sys.argv) > 1 else "cuda"
|
||||
BASE = os.path.dirname(os.path.abspath(__file__))
|
||||
# La voz (referencia, condicionado y parametros) vive en clonador.py: tenerla
|
||||
# aqui y en calienta_cache.py significaba dos copias que se desincronizan.
|
||||
sys.path.insert(0, BASE)
|
||||
from clonador import Clonador, clave
|
||||
CACHE = os.path.join(BASE, "cache_voz")
|
||||
CATALOGO = os.path.expanduser(
|
||||
"~/COFRE/CODERS/JARVIS/newelle/data/acciones/acciones.json")
|
||||
os.makedirs(CACHE, exist_ok=True)
|
||||
|
||||
os.environ.setdefault("COQUI_TOS_AGREED", "1")
|
||||
|
||||
|
||||
def frases_fijas():
|
||||
"""Acuses del catalogo + respuestas de plantilla del panel."""
|
||||
fijas = set()
|
||||
try:
|
||||
with open(CATALOGO, encoding="utf-8") as f:
|
||||
cat = json.load(f)
|
||||
for a in cat.get("acciones", cat if isinstance(cat, list) else []):
|
||||
ac = a.get("acuse")
|
||||
if ac:
|
||||
fijas.add(ac)
|
||||
except Exception as e:
|
||||
print(f"aviso: no pude leer el catalogo ({e}); sigo con las fijas")
|
||||
# las de plantilla del panel que no dependen de argumento
|
||||
fijas.update({
|
||||
"Buenas noches, señor. Todo está listo.",
|
||||
"Lo busco dentro, señor.",
|
||||
"A la orden, señor.",
|
||||
"Hecho, señor.",
|
||||
"No he encontrado nada, señor.",
|
||||
"¿Cuál de estos, señor?",
|
||||
})
|
||||
return sorted(fijas)
|
||||
|
||||
|
||||
def main():
|
||||
frases = frases_fijas()
|
||||
print(f"{len(frases)} frases fijas a generar en {DISPOSITIVO}", flush=True)
|
||||
|
||||
t0 = time.time()
|
||||
voz = Clonador(DISPOSITIVO)
|
||||
print(f"modelo cargado en {time.time()-t0:.1f} s "
|
||||
f"(referencia: {os.path.basename(voz.referencia)})", flush=True)
|
||||
|
||||
hechas = saltadas = 0
|
||||
for i, frase in enumerate(frases, 1):
|
||||
dst = os.path.join(CACHE, clave(frase) + ".wav")
|
||||
if os.path.exists(dst):
|
||||
saltadas += 1
|
||||
continue
|
||||
t0 = time.time()
|
||||
voz.genera(frase, dst)
|
||||
print(f" [{i}/{len(frases)}] {time.time()-t0:4.1f}s {frase[:50]}",
|
||||
flush=True)
|
||||
hechas += 1
|
||||
print(f"listo: {hechas} generadas, {saltadas} ya estaban. cache -> {CACHE}",
|
||||
flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue