JARVIS/nucleo/pruebas/prueba_cara.py
sito 8e4bc8ad94 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.
2026-08-16 15:34:37 +02:00

122 lines
5 KiB
Python
Executable file

#!/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())