JARVIS/config/puerta.sh
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

108 lines
4.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# La puerta: decide si un cambio automatico se queda o se revierte.
#
# Es la pieza central del pipeline nocturno. Todo lo que el sistema se cambia a
# si mismo pasa por aqui, y lo que no la pasa se deshace ENTERO. Sin esto,
# dejar algo modificando codigo sin nadie delante es una forma elaborada de
# romper un proyecto que funcionaba.
#
# Que comprueba, en orden de lo mas barato a lo mas caro (para fallar pronto):
# 1. que el Python compile
# 2. que el catalogo sea JSON valido y el generador lo reproduzca
# 3. que el emparejador no haya perdido ordenes (08)
# 4. que las acciones sigan respondiendo (repaso completo)
# 5. la suite entera
#
# puerta.sh rapida solo 1-3, para iterar
# puerta.sh completa todo, antes de commitear
set -uo pipefail
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)
MODO=${1:-completa}
cd "$RAIZ"
fallo() { echo "PUERTA: $1" >&2; exit 1; }
# --- 1. compila el Python -------------------------------------------------
python3 -m compileall -q newelle/src config voz/*.py >/dev/null 2>&1 \
|| fallo "hay Python que no compila"
# --- 2. el catalogo es valido y reproducible ------------------------------
python3 -c "
import json,sys
d=json.load(open('newelle/data/acciones/acciones.json'))
acc=d['acciones']
assert len(acc)>100, f'solo {len(acc)} acciones'
ids=[a['id'] for a in acc]
assert len(ids)==len(set(ids)), 'ids repetidos'
for a in acc:
assert a.get('frases'), f\"{a['id']} sin frases\"
assert a.get('acuse'), f\"{a['id']} sin acuse\"
assert 'señor' in a['acuse'].lower(), f\"{a['id']} no trata de señor\"
if a.get('captura') and a.get('comando'):
assert '{argumento' in a['comando'], f\"{a['id']} captura y no usa el argumento\"
" 2>/dev/null || fallo "el catalogo esta mal formado"
# El generador es la fuente: si no reproduce el catalogo, alguien edito el
# producto y el proximo que regenere pierde el trabajo.
cp newelle/data/acciones/acciones.json /tmp/jarvis-puerta-antes.json
(cd newelle/data/acciones && python3 _generar.py >/dev/null 2>&1)
if ! diff -q /tmp/jarvis-puerta-antes.json newelle/data/acciones/acciones.json >/dev/null 2>&1; then
cp /tmp/jarvis-puerta-antes.json newelle/data/acciones/acciones.json
fallo "el generador no reproduce el catalogo (se edito el producto)"
fi
# --- 3. el emparejador no ha perdido ordenes ------------------------------
python3 - <<'FIN' || fallo "el emparejador ha perdido ordenes que antes acertaba"
import importlib.util, sys
spec = importlib.util.spec_from_file_location("acc", "newelle/src/utility/acciones.py")
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
cat = m.Catalogo(["newelle/data/acciones/acciones.json"])
# El minimo innegociable: si alguna de estas deja de funcionar, el cambio se
# revierte pase lo que pase. Son las que el usuario usa de verdad.
IMPRESCINDIBLES = [
("cuanto espacio queda", "disco_libre"),
("cuanta memoria", "memoria"),
("que temperatura hay", "temperatura"),
("abre una terminal", "terminal_abrir"),
("apaga la pantalla", "apagar_pantalla"),
("captura de pantalla", "captura_pantalla"),
("abre una ventana privada", "ff_firefox_privado"),
("pon la ventana a la izquierda", "ventana_izquierda"),
("necesito que pongas la pelicula de iron man", "pelicula_abrir"),
("podrias abrir una terminal", "terminal_abrir"),
]
malas = []
for frase, esperado in IMPRESCINDIBLES:
a, _ = cat.busca(frase)
if a is None or a.id != esperado:
malas.append(f"{frase!r}→{a.id if a else 'ninguna'} (deberia {esperado})")
# Y la conversacion no puede engancharse a una accion
CHARLA = ["que tal estas", "necesito un abrazo", "quiero contarte una cosa",
"cuentame un chiste", "que piensas de la vida", "necesito pensar"]
coladas = [c for c in CHARLA if cat.busca(c)[0] is not None]
if malas or coladas:
for x in malas: print(" perdida:", x, file=sys.stderr)
for c in coladas: print(" charla colada:", c, file=sys.stderr)
sys.exit(1)
FIN
[ "$MODO" = rapida ] && { echo "PUERTA: rapida OK"; exit 0; }
# --- 4. las acciones responden -------------------------------------------
SALIDA=$(timeout 900 python3 config/repasar_acciones.py 2>&1 | tail -3)
echo "$SALIDA" | grep -q "de .* responden bien" || fallo "el repaso de acciones no termino"
ROTAS=$(echo "$SALIDA" | grep -oE "fallan: .*" || true)
[ -n "$ROTAS" ] && fallo "hay acciones rotas → $ROTAS"
# --- 5. la suite ----------------------------------------------------------
# Se salta la 07, que abre la app y se queda colgada sin nadie delante.
SUITE=$(cd pruebas && for t in [0-9][0-9]_*.sh; do
case "$t" in 05_*|07_*) continue;; esac
timeout 400 "./$t" 2>&1 | tail -1
done)
FALLOS=$(echo "$SUITE" | grep -oE "[0-9]+ fallan" | awk '{s+=$1} END{print s+0}')
[ "${FALLOS:-0}" -gt 0 ] && fallo "la suite tiene $FALLOS comprobaciones en rojo"
echo "PUERTA: completa OK"