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
115
nucleo/manos/medidas.py
Normal file
115
nucleo/manos/medidas.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue