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.
76 lines
2.7 KiB
Python
76 lines
2.7 KiB
Python
#!/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()
|