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.
108 lines
4.8 KiB
Bash
Executable file
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"
|