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
122
nucleo/pruebas/prueba_cara.py
Executable file
122
nucleo/pruebas/prueba_cara.py
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
#!/usr/bin/env python3
|
||||
"""El panel: que lo que manda el nucleo sea lo que el HUD sabe leer.
|
||||
|
||||
./venv/bin/python pruebas/prueba_cara.py
|
||||
|
||||
## Por que existe esta prueba
|
||||
|
||||
Montando la telemetria, `ps` devolvia el uso de CPU como cadena y el nucleo la
|
||||
mandaba tal cual. El HUD hace `p.cpu.toFixed(0)`, y una cadena no tiene
|
||||
`toFixed`: la excepcion abortaba el manejador ENTERO y se llevaba por delante
|
||||
todo lo que se pinta despues. En pantalla se veia que CPU, RAM y grafica
|
||||
salian en "--", y el fallo no apuntaba a los procesos por ningun lado.
|
||||
|
||||
Ese es el problema de un puente sin contrato: un campo con el tipo equivocado
|
||||
apaga media pantalla y no dice donde. Aqui se fija el contrato —que campo, de
|
||||
que tipo y en que unidad— comprobando ademas que `hud.js` lo sigue usando asi,
|
||||
para que enterarse no dependa de mirar la pantalla.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, AQUI)
|
||||
|
||||
from cara.panel import Telemetria # noqa: E402
|
||||
|
||||
HUD = os.path.join(AQUI, "cara", "hud")
|
||||
|
||||
fallos = []
|
||||
|
||||
|
||||
def afirma(condicion, queja):
|
||||
print(f" {'OK ' if condicion else 'MAL'} {queja}")
|
||||
if not condicion:
|
||||
fallos.append(queja)
|
||||
|
||||
|
||||
def numero(v):
|
||||
return isinstance(v, (int, float)) and not isinstance(v, bool)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("\n el puente esta puesto")
|
||||
with open(os.path.join(HUD, "index.html"), encoding="utf-8") as f:
|
||||
html = f.read()
|
||||
afirma("puente.js" in html, "index.html carga puente.js")
|
||||
# Se comparan las etiquetas <script>, no el texto suelto: el comentario que
|
||||
# hay encima menciona hud.js y una busqueda ingenua lo encuentra antes.
|
||||
scripts = re.findall(r'<script\s+src="([^"]+)"', html)
|
||||
afirma("puente.js" in scripts and "hud.js" in scripts
|
||||
and scripts.index("puente.js") < scripts.index("hud.js"),
|
||||
"y lo carga ANTES que hud.js, que pregunta por window.webkit al arrancar")
|
||||
with open(os.path.join(HUD, "puente.js"), encoding="utf-8") as f:
|
||||
puente = f.read()
|
||||
afirma("WebSocket" in puente, "el puente habla por WebSocket")
|
||||
afirma("messageHandlers" in puente,
|
||||
"suplanta window.webkit, asi hud.js no se toca")
|
||||
afirma("cola" in puente, "guarda lo pulsado mientras reconecta")
|
||||
|
||||
print("\n la telemetria: campos, tipos y unidades")
|
||||
d = Telemetria().lee()
|
||||
Telemetria().lee() # el segundo tiene ya las diferencias de cpu
|
||||
|
||||
afirma(numero(d.get("cpu")), "cpu es un numero")
|
||||
afirma(isinstance(d.get("nucleos"), list) and all(numero(x) for x in d["nucleos"]),
|
||||
f"nucleos es una lista de numeros ({len(d.get('nucleos', []))})")
|
||||
for clave in ("ram", "swap"):
|
||||
v = d.get(clave, {})
|
||||
afirma(numero(v.get("usada")) and numero(v.get("total")),
|
||||
f"{clave} lleva usada y total")
|
||||
afirma(0 < v.get("total", 0) < 1024,
|
||||
f"{clave} viene en GB, no en MB ({v.get('total', 0):.1f})")
|
||||
disco = d.get("disco", {})
|
||||
afirma(0 < disco.get("total", 0) < 200,
|
||||
f"disco viene en TB, no en GB ({disco.get('total', 0):.2f})")
|
||||
afirma(isinstance(d.get("carga"), list),
|
||||
"carga es una lista: el HUD hace carga.map()")
|
||||
afirma(d.get("uptime", 0) > 60,
|
||||
"uptime va en segundos: el HUD decide solo si pone minutos o dias")
|
||||
|
||||
if "gpu" in d:
|
||||
g = d["gpu"]
|
||||
afirma(all(numero(g.get(k)) for k in ("uso", "vram_usada", "vram_total")),
|
||||
"la grafica manda uso, vram_usada y vram_total")
|
||||
afirma(0 < g.get("vram_total", 0) < 128,
|
||||
f"la vram viene en GB ({g.get('vram_total', 0):.1f})")
|
||||
|
||||
print("\n los procesos: el campo que apago media pantalla")
|
||||
procs = d.get("procesos", [])
|
||||
afirma(bool(procs), f"llegan procesos ({len(procs)})")
|
||||
for p in procs[:3]:
|
||||
afirma(numero(p.get("cpu")) and numero(p.get("mem")),
|
||||
f"{p.get('nombre','?')}: cpu y mem son numeros, no cadenas")
|
||||
afirma(all("pid" in p for p in procs), "todos traen pid, que el HUD pone en el title")
|
||||
|
||||
print("\n y el HUD sigue esperando eso mismo")
|
||||
with open(os.path.join(HUD, "hud.js"), encoding="utf-8") as f:
|
||||
js = f.read()
|
||||
# Si alguien cambia hud.js para leer otro campo, esta prueba no lo sabria
|
||||
# mirando solo al nucleo: se comprueba en el propio JavaScript.
|
||||
for campo in ("datos.cpu", "datos.ram", "datos.swap", "datos.disco",
|
||||
"datos.nucleos", "datos.procesos"):
|
||||
afirma(campo in js, f"hud.js lee {campo}")
|
||||
afirma("p.cpu.toFixed" in js,
|
||||
"hud.js llama a toFixed sobre el cpu de cada proceso (por eso tiene que ser numero)")
|
||||
|
||||
# Todo lo que se manda tiene que poder viajar por el socket.
|
||||
print("\n serializable")
|
||||
try:
|
||||
json.dumps(d)
|
||||
afirma(True, "la telemetria entera cabe en JSON")
|
||||
except (TypeError, ValueError) as e:
|
||||
afirma(False, f"la telemetria no es JSON: {e}")
|
||||
|
||||
print(f"\n {'todo en orden' if not fallos else str(len(fallos)) + ' fallan'}\n")
|
||||
return 1 if fallos else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue