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.
122 lines
5 KiB
Python
Executable file
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())
|