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