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

182 lines
6.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""Que ha aprendido JARVIS del uso, y que conviene cambiar.
./mejora.py que hay en el diario
./mejora.py --propone que acciones o frases faltan
./mejora.py --mide cuanto acierta ahora mismo
## En que se diferencia de la mejora nocturna que se retiro
La de antes corria sola de madrugada, se modificaba el codigo y se commiteaba.
Nueve horas, 39 rondas, cero commits utiles. Fallo por tres cosas, y las tres
estan corregidas aqui:
1. **Minaba ruido.** Su fuente era un registro donde el 60 % era JARVIS
oyendose a si mismo y alucinaciones de Whisper. Ahora la guarda del eco y
el filtro de ruido limpian antes, y el diario apunta la VIA de cada frase,
asi que el ruido esta separado y etiquetado en vez de mezclado.
2. **Su medida tenia mas ruido que su mejora.** Generaba audio nuevo cada
ronda: ±1,9 puntos de ruido contra +1,45 de mejora acumulada, o sea que
"¿esto mejora o empeora?" se decidia a cara o cruz. Ahora se mide contra
`datos/banco.json`, que es fijo y determinista.
3. **Actuaba sola.** Un bucle que se reescribe y se aprueba a si mismo acaba
pasando siempre. Esto PROPONE y para: lo que se aplica lo decide una
persona mirandolo.
Y una cuarta, de fondo: aquella tocaba el codigo. Esta solo mira el catalogo,
que es lo unico que se puede validar de verdad con el banco.
"""
import argparse
import collections
import os
import sys
AQUI = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, AQUI)
from manos import Manos # noqa: E402
from manos.acciones import normaliza, parecido # noqa: E402
from manos.diario import lee # noqa: E402
def color(t, c):
return f"\033[{c}m{t}\033[0m" if sys.stdout.isatty() else t
# ------------------------------------------------------------------ mirar
def resumen(lineas):
if not lineas:
print(" el diario esta vacio: usa JARVIS un rato y vuelve")
return 1
vias = collections.Counter(l.get("via", "?") for l in lineas)
tot = sum(vias.values())
print(f"\n {tot} frases desde {lineas[0]['t'][:10]}\n")
print(f" {'via':14s} {'n':>5s} {'%':>7s}")
print(" " + "-" * 32)
ORDEN = ["catalogo", "cerebro", "respuesta", "ruido", "sin-nombre",
"bloqueado", "sin-cerebro"]
for v in ORDEN + [k for k in vias if k not in ORDEN]:
if v in vias:
print(f" {v:14s} {vias[v]:5d} {100*vias[v]/tot:6.1f} %")
atendidas = vias["catalogo"] + vias["cerebro"]
if atendidas:
print(f"\n de lo ATENDIDO ({atendidas}), el catalogo cogio "
f"{100*vias['catalogo']/atendidas:.0f} %")
segs = [l.get("seg", 0) for l in lineas if l.get("via") == "catalogo"]
segc = [l.get("seg", 0) for l in lineas if l.get("via") == "cerebro"]
if segs:
print(f" catalogo: {sum(segs)/len(segs):.2f} s de media")
if segc:
print(f" cerebro: {sum(segc)/len(segc):.1f} s de media")
ahorro = (sum(segc)/len(segc) - (sum(segs)/len(segs) if segs else 0))
print(f"\n cada accion que se añada al catalogo ahorra ~{ahorro:.0f} s")
top = collections.Counter(l["accion"] for l in lineas
if l.get("via") == "catalogo" and l.get("accion"))
if top:
print("\n lo que mas usas:")
for a, n in top.most_common(8):
print(f" {n:4d} {a}")
return 0
# --------------------------------------------------------------- proponer
def propone(lineas, manos):
"""Lo que fue al cerebro y podria ser una accion del catalogo."""
# "sin-cerebro" cuenta igual: es lo mismo —una frase que el catalogo no
# cubrio— y solo se diferencia en que no habia modelo conectado para
# atenderla. El hueco del catalogo es el mismo.
alCerebro = [l for l in lineas if l.get("via") in ("cerebro", "sin-cerebro")]
if not alCerebro:
print("\n nada ha ido al cerebro todavia: no hay hueco que rellenar")
return 0
# Agrupar lo parecido: si has pedido lo mismo de tres formas, es UNA accion
# que falta, no tres. Y que lo hayas repetido es justo la señal de que
# importa.
grupos = []
for l in alCerebro:
t = normaliza(l["oido"])
if len(t.split()) < 2:
continue
for g in grupos:
if parecido(t, g["patron"]) > 0.62:
g["frases"].append(l["oido"])
break
else:
grupos.append({"patron": t, "frases": [l["oido"]],
"dicho": l.get("dicho", "")})
grupos.sort(key=lambda g: -len(g["frases"]))
print(f"\n {len(alCerebro)} frases fueron al cerebro, en {len(grupos)} grupos.")
print(" Las que mas se repiten son las acciones que faltan:\n")
for g in grupos[:12]:
n = len(g["frases"])
marca = color(f"{n:3d} veces", "33" if n > 1 else "90")
print(f" {marca} {g['frases'][0][:64]}")
for f in g["frases"][1:4]:
print(f" {color('tambien: ' + f[:56], '90')}")
if g["dicho"]:
print(f" {color('contesto: ' + g['dicho'][:56], '90')}")
print()
repetidas = [g for g in grupos if len(g["frases"]) > 1]
print(f" {len(repetidas)} se han pedido mas de una vez: por ahi empezaria.\n")
print(color(" Esto NO cambia nada. Para añadir una accion se edita\n"
" datos/_generar.py —que es la FUENTE— y se regenera.\n", "90"))
return 0
# ------------------------------------------------------------------ medir
def mide(manos):
"""El acierto del emparejador contra el banco fijo."""
import json
banco = os.path.join(AQUI, "datos", "banco.json")
try:
with open(banco, encoding="utf-8") as f:
casos = json.load(f)["casos"]
except OSError:
print(" no hay banco: pruebas/banco.py --crear")
return 1
bien = otra = 0
for c in casos:
a, _ = manos.busca(c["oido"])
if a is None:
continue
if a.id == c["id"]:
bien += 1
else:
otra += 1
print(f"\n acierto {100*bien/len(casos):.1f} % sobre {len(casos)} frases")
print(f" ordenes que acaban en la accion equivocada: {otra}")
print(color("\n Este numero es comparable entre dias: el banco esta grabado\n"
" una vez y no cambia. Era lo que le faltaba a la mejora\n"
" nocturna, cuyo ruido de medida era mayor que su mejora.\n", "90"))
return 0
def main() -> int:
p = argparse.ArgumentParser(description="Que ha aprendido JARVIS del uso")
p.add_argument("--propone", action="store_true")
p.add_argument("--mide", action="store_true")
a = p.parse_args()
manos = Manos()
lineas = lee()
if a.mide:
return mide(manos)
if a.propone:
return propone(lineas, manos)
return resumen(lineas)
if __name__ == "__main__":
sys.exit(main())