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

123 lines
3.8 KiB
Python

"""Lo que JARVIS oye y que hace con ello. El combustible de todo lo demas.
Sin esto no hay forma de mejorar nada: no se puede arreglar lo que no se mide,
y hasta ahora el nucleo no apuntaba absolutamente nada.
## Por que este y no el de antes
El diario de la epoca de Newelle guardaba `{"texto": "..."}` y nada mas. Con eso
no se puede hacer casi nada:
- no se sabe **que paso** con la frase, asi que no se distingue una orden que
fallo de una que funciono;
- no se sabe **cuando**, asi que no se ve si alguien repitio la misma cosa de
tres formas seguidas —que es la señal mas valiosa que hay—;
- y solo guardaba los FALLOS, asi que no habia con que comparar.
Ademas se llenaba de eco y de alucinaciones de Whisper, y la mejora nocturna
acabo generando alias a partir del ruido. De 504 frases, 306 parecian ordenes
sin atender y al mirarlas casi todas eran JARVIS oyendose a si mismo.
## Que se guarda
Todo lo que llega, con su destino. Una linea por frase, JSON:
t cuando
oido lo que transcribio Whisper
via catalogo | cerebro | ruido | bloqueado | respuesta
accion a que accion fue, si fue a alguna
arg con que argumento
seg lo que tardo en contestar
dicho lo que contesto
Guardar TAMBIEN los aciertos es lo que permite comparar: sin ellos, un dia malo
y un dia bueno tienen el mismo aspecto.
## Lo que NO se guarda
Nada de audio. El diario es texto y se puede leer con `less`; si algun dia
guardara los WAV, el asistente pasaria a ser una grabadora de la habitacion y
eso es otra cosa muy distinta de lo que es.
"""
import json
import os
import threading
import time
from datetime import datetime
AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DIARIO = os.path.join(AQUI, "datos", "diario.jsonl")
# Un diario que crece sin fin acaba siendo un problema en vez de una ayuda. A
# ~150 bytes por linea, 200.000 son unos 30 MB: mas que suficiente para meses de
# uso y poco para el disco.
TOPE_LINEAS = 200_000
_cerrojo = threading.Lock()
def anota(oido: str, via: str, accion: str = "", arg: str = "",
seg: float = 0.0, dicho: str = "", ruta: str = DIARIO):
"""Apunta una frase y su destino. No falla nunca hacia fuera.
Si escribir el diario diera error, lo ultimo que debe hacer es tirar al
asistente: se pierde una linea y ya.
"""
if not oido:
return
linea = {
"t": datetime.now().isoformat(timespec="seconds"),
"oido": oido[:400],
"via": via,
}
if accion:
linea["accion"] = accion
if arg:
linea["arg"] = arg[:200]
if seg:
linea["seg"] = round(seg, 2)
if dicho:
linea["dicho"] = dicho[:300]
try:
with _cerrojo:
os.makedirs(os.path.dirname(ruta), exist_ok=True)
with open(ruta, "a", encoding="utf-8") as f:
f.write(json.dumps(linea, ensure_ascii=False) + "\n")
except OSError:
pass
def lee(ruta: str = DIARIO, desde=None) -> list:
"""Las lineas del diario, ya en diccionarios. Salta las corruptas."""
salida = []
try:
with open(ruta, encoding="utf-8") as f:
for l in f:
try:
d = json.loads(l)
except json.JSONDecodeError:
continue
if desde and d.get("t", "") < desde:
continue
salida.append(d)
except OSError:
pass
return salida
def recorta(ruta: str = DIARIO, tope: int = TOPE_LINEAS):
"""Deja solo las ultimas `tope` lineas."""
lineas = []
try:
with open(ruta, encoding="utf-8") as f:
lineas = f.readlines()
except OSError:
return
if len(lineas) <= tope:
return
try:
with _cerrojo, open(ruta, "w", encoding="utf-8") as f:
f.writelines(lineas[-tope:])
except OSError:
pass