JARVIS/nucleo/manos/medidas.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

115 lines
4.2 KiB
Python

"""Las medidas de eficiencia de JARVIS, sacadas del diario.
El diario ya apunta por cada frase: cuanto tardo (`seg`), por donde fue (`via`)
y cuando (`t`). De ahi salen las metricas de rendimiento sin instrumentar nada
del camino caliente: esto solo LEE, y se calcula fuera del hilo que contesta.
## Que se mide y por que importa cada una
latencia p50/p95/max del tiempo de respuesta. La mediana dice como va de
normal; el p95 dice como va cuando va mal, que es lo que se nota.
reparto que fraccion va al CEREBRO (lento, ~2-3 s) y cual al CATALOGO
(instantaneo, salta el modelo). Es la palanca de eficiencia mas
grande: cuanto mas catalogo, mas rapido el conjunto.
ruido que fraccion de lo que oye es Whisper oyendose a si mismo. No es
velocidad, es calidad de la escucha, y envenena todo lo demas.
volumen frases en las ultimas 24 h. Contexto para lo anterior: un p95 malo
con 4 frases no dice nada.
rag hit@5/hit@1 del ultimo benchmark del RAG (informe_rag.txt). Es la
unica que no sale del diario: es una medida de test, periodica.
Todo sobre una VENTANA reciente (las ultimas MUESTRA frases), no sobre el diario
entero: interesa como va AHORA, no la media historica que nunca cambia.
"""
import os
import re
from datetime import datetime, timedelta
from manos.diario import lee
AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INFORME_RAG = os.path.join(AQUI, "datos", "informe_rag.txt")
MUESTRA = 600 # las ultimas N frases: "como va ahora"
CEREBRO = "cerebro"
def _percentil(valores, q):
if not valores:
return 0.0
v = sorted(valores)
k = (len(v) - 1) * q
lo, hi = int(k), min(int(k) + 1, len(v) - 1)
return v[lo] + (v[hi] - v[lo]) * (k - lo)
def _rag():
"""hit@5 y hit@1 del ultimo informe del RAG, si existe."""
try:
txt = open(INFORME_RAG, encoding="utf-8").read()
except OSError:
return None
# el bloque CANDIDATO es el que se promociono; si no, el ultimo que haya
def pct(clave):
# " hit@5 43/45 (95%)" -> 95
ms = re.findall(clave + r"\s+\d+/\d+\s+\((\d+)%\)", txt)
return int(ms[-1]) if ms else None
h5, h1 = pct("hit@5"), pct("hit@1")
if h5 is None:
return None
return {"hit5": h5, "hit1": h1 or 0}
def lee_medidas():
"""El dict de medidas, con las unidades ya listas para el HUD."""
diario = lee()[-MUESTRA:]
if not diario:
return {"n": 0}
segs = [d["seg"] for d in diario if isinstance(d.get("seg"), (int, float)) and d["seg"] > 0]
seg_cerebro = [d["seg"] for d in diario
if d.get("via") == CEREBRO and isinstance(d.get("seg"), (int, float))]
seg_catalogo = [d["seg"] for d in diario
if d.get("via") == "catalogo" and isinstance(d.get("seg"), (int, float))]
vias = {}
for d in diario:
vias[d.get("via", "?")] = vias.get(d.get("via", "?"), 0) + 1
total = len(diario)
def pct_via(v):
return round(100 * vias.get(v, 0) / total) if total else 0
# volumen: cuantas en las ultimas 24 h, y por hora las ultimas 12 para la traza
ahora = datetime.now()
hace24 = ahora - timedelta(hours=24)
vol24 = 0
porhora = [0] * 12
for d in diario:
try:
t = datetime.fromisoformat(d.get("t", ""))
except (ValueError, TypeError):
continue
if t >= hace24:
vol24 += 1
h = int((ahora - t).total_seconds() // 3600)
if 0 <= h < 12:
porhora[11 - h] += 1
medidas = {
"n": total,
"lat_p50": round(_percentil(segs, 0.50), 2),
"lat_p95": round(_percentil(segs, 0.95), 2),
"lat_max": round(max(segs), 2) if segs else 0.0,
"lat_cerebro": round(sum(seg_cerebro) / len(seg_cerebro), 2) if seg_cerebro else 0.0,
"lat_catalogo": round(sum(seg_catalogo) / len(seg_catalogo), 2) if seg_catalogo else 0.0,
"pct_cerebro": pct_via("cerebro"),
"pct_catalogo": pct_via("catalogo"),
"pct_ruido": pct_via("ruido"),
"vol24": vol24,
"porhora": porhora,
}
rag = _rag()
if rag:
medidas["rag"] = rag
return medidas