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

135 lines
5.4 KiB
Bash
Executable file

#!/usr/bin/env bash
# Lanzador del JARVIS naranja: el nucleo, fuera del flatpak.
#
# arranca.sh levanta ollama si hace falta, abre el panel y escucha
# arranca.sh --panel solo el panel, sin microfono
# arranca.sh --stop lo para y libera la grafica
# arranca.sh --estado dice si esta vivo
#
# Comparado con el lanzador del JARVIS azul, aqui sobra casi todo: no hay que
# adivinar la rama del flatpak, ni leer gsettings con escapado de GVariant, ni
# preguntarle a la aplicacion que modelo tiene puesto. El nucleo lo sabe porque
# es suyo.
#
# Lo que SI se conserva, porque en esta maquina importa: al parar se le dice a
# ollama que suelte el modelo. Son 2,1 GB de una tarjeta de 4, y sin esto whisper
# se queda sin sitio la proxima vez.
set -uo pipefail
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
PYTHON=$RAIZ/venv/bin/python
[ -x "$PYTHON" ] || PYTHON=$(command -v python3)
OLLAMA_URL=${JARVIS_OLLAMA:-http://127.0.0.1:11434}
REGISTRO=$RAIZ/jarvis.log
PUERTO=${JARVIS_PUERTO:-8790}
# Fichero de PID, no pgrep.
#
# El primer intento fue marcar el proceso con una variable de entorno y buscarla
# con `pgrep -f`. No funciona: pgrep mira la LINEA DE ORDENES, y `env VAR=1 cmd`
# no deja la variable ahi. El sintoma fue peor que un simple "no lo encuentra":
# el lanzador creia que no habia nada corriendo, arrancaba un segundo, y ese
# moria con "address already in use" mientras el primero seguia vivo.
#
# Y buscar por el nombre del script tiene el problema de siempre: el patron
# aparece en la linea de ordenes del propio pgrep. Un fichero de PID no se
# equivoca.
PID=${XDG_RUNTIME_DIR:-/tmp}/jarvis-nucleo.pid
aviso() { command -v notify-send >/dev/null && \
notify-send -i "$RAIZ/jarvis-naranja.svg" "JARVIS" "$1"; }
modelo() {
grep -oP 'MODELO = os\.environ\.get\("JARVIS_MODELO", "\K[^"]+' \
"$RAIZ/cerebro/ollama.py" 2>/dev/null || echo "qwen3.5:4b-jarvis"
}
# Vivo = hay un pid apuntado Y ese proceso existe. Un pid huerfano de un
# reinicio no puede hacernos creer que sigue en marcha.
vivo() {
[ -f "$PID" ] || return 1
local p; p=$(cat "$PID" 2>/dev/null)
[ -n "$p" ] && kill -0 "$p" 2>/dev/null
}
ollama_up() { curl -s --max-time 2 "$OLLAMA_URL/api/version" >/dev/null 2>&1; }
puerto_ocupado() { curl -s --max-time 2 "http://127.0.0.1:$PUERTO/" >/dev/null 2>&1; }
case "${1:-arranca}" in
--stop)
# Tambien lo que este ocupando el puerto sin fichero de pid: si no, un
# huerfano impide arrancar de nuevo y --stop parece no hacer nada.
if ! vivo && puerto_ocupado && command -v fuser >/dev/null; then
fuser -k "$PUERTO/tcp" >/dev/null 2>&1
fi
if vivo; then
kill "$(cat "$PID")" 2>/dev/null
# margen para que suelte el puerto antes de que nadie lo reclame
for _ in 1 2 3 4 5 6 7 8 9 10; do vivo || break; sleep 0.3; done
vivo && kill -9 "$(cat "$PID")" 2>/dev/null
fi
rm -f "$PID"
if ollama_up; then
# keep_alive 0: suelta el modelo ya, no dentro de cinco minutos
curl -s "$OLLAMA_URL/api/generate" \
-d "{\"model\":\"$(modelo)\",\"keep_alive\":0}" >/dev/null 2>&1
aviso "Detenido, grafica liberada"
else
aviso "Detenido"
fi
exit 0
;;
--estado)
# Se mira el pid Y el puerto. Con solo el pid, una instancia arrancada a
# mano —o superviviente de un lanzador que murio— daba "parado" mientras
# el panel contestaba tan tranquilo, que es la peor respuesta posible:
# la que hace dudar de si el problema es el programa o quien pregunta.
if vivo; then
echo "JARVIS naranja: vivo"
exit 0
fi
if puerto_ocupado; then
echo "JARVIS naranja: vivo, pero arrancado por fuera de este lanzador"
echo " (no hay $PID; para pararlo: fuser -k $PUERTO/tcp)"
exit 0
fi
echo "JARVIS naranja: parado"
exit 1
;;
esac
if vivo || puerto_ocupado; then
# Ya esta abierto: en vez de levantar otro —que moriria con "address already
# in use" y dejaria un rastro confuso— se trae el panel al frente.
aviso "Ya estaba en marcha"
# La MISMA ventana que abre el arranque normal. Antes esto era xdg-open, que
# abre el navegador POR DEFECTO en una pestaña: segun si JARVIS ya estaba
# abierto o no, el mismo icono daba una aplicacion o una pestaña de Firefox.
setsid "$RAIZ/ventana.sh" "http://127.0.0.1:$PUERTO/" >/dev/null 2>&1 &
exit 0
fi
# ollama, si no esta ya. En segundo plano y sin esperar: el catalogo contesta
# las ordenes conocidas sin tocar el modelo, asi que JARVIS es util desde el
# primer segundo aunque ollama tarde en calentar.
if ! ollama_up && command -v ollama >/dev/null; then
setsid ollama serve >/dev/null 2>&1 &
fi
# El indice de COFRE, si no esta. En segundo plano: JARVIS arranca sin el y el
# cerebro lo usa en cuanto este listo. Reconstruirlo a mano: saber/indexa.py
if [ ! -f "$RAIZ/datos/saber.jsonl" ]; then
setsid "$PYTHON" "$RAIZ/saber/indexa.py" >/dev/null 2>&1 &
fi
: > "$REGISTRO" 2>/dev/null || REGISTRO=/dev/null
aviso "Arrancando"
MODO=(--panel --escucha)
[ "${1:-}" = "--panel" ] && MODO=(--panel)
# El pid se apunta ANTES del exec y es el de este mismo shell, que con exec
# pasa a ser el de Python: asi el fichero nunca apunta a un proceso que no es.
echo $$ > "$PID"
trap 'rm -f "$PID"' EXIT
exec "$PYTHON" -u "$RAIZ/jarvis.py" "${MODO[@]}" >>"$REGISTRO" 2>&1