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.
188 lines
7.4 KiB
Bash
Executable file
188 lines
7.4 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# JARVIS — lanzador del asistente local (Newelle)
|
|
#
|
|
# sin argumentos abre JARVIS a pantalla completa, sin pasar por el chat
|
|
# --chat abre Newelle normal, con sus pestañas y su historial
|
|
# --stop la cierra y libera el modelo
|
|
# --status que corre, con que cerebro y cuanto ocupa
|
|
#
|
|
# El lanzador NO decide el motor: lo lee de gsettings (language-model) y se
|
|
# adapta. Asi no hay que tocarlo cada vez que se cambia de cerebro, que es
|
|
# justo lo que lo dejo desincronizado antes: precargaba Ollama mientras la
|
|
# app usaba su llamacpp interno, ocupando 2.9 GB de VRAM para nada.
|
|
#
|
|
# Y el MODELO tampoco, por lo mismo. Estaba escrito a mano aqui y volvio a
|
|
# pasar exactamente lo de antes: al cambiar la app a qwen3.5:4b-jarvis, el
|
|
# lanzador seguia precargando qwen3.5:4b. Los dos no caben en 3717 MiB, asi
|
|
# que ollama tenia que descargar uno para cargar el otro con el panel ya
|
|
# esperando: 6,55 s hasta la primera respuesta contra 3,70 con los dos de
|
|
# acuerdo. El sintoma no era "va lento", era "no se abre el panel solo".
|
|
|
|
set -uo pipefail
|
|
|
|
APPID=io.github.qwersyk.Newelle
|
|
OLLAMA_URL=http://localhost:11434
|
|
|
|
# La salida de la app iba entera a /dev/null. Cuando el panel dejo de abrirse,
|
|
# el traceback que lo explicaba —una linea— no estaba en ningun sitio, y el
|
|
# fallo se diagnostico dos veces mal antes de mirarlo. Ahora se guarda.
|
|
# misma idea que en los scripts de voz: la raiz sale de donde vive esto
|
|
RAIZ=$(cd -P "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)
|
|
REGISTRO=$RAIZ/jarvis-app.log
|
|
: > "$REGISTRO" 2>/dev/null || REGISTRO=/dev/null
|
|
GGUF=$HOME/.var/app/$APPID/config/models/custom_models/qwen3.5-4b.gguf
|
|
|
|
# Elige rama: //master es la version propia (jarvis-skin, con reactor arc);
|
|
# //stable es la de Flathub, la red de seguridad.
|
|
#
|
|
# El chequeo mira la typelib de Vte en lib/: si un build deja mal el libdir,
|
|
# la app arranca y muere con "Namespace Vte not available". Asi el icono
|
|
# nunca abre una version rota, y se pasa solo a la propia cuando este sana.
|
|
pick_branch() {
|
|
local base=$HOME/.local/share/flatpak/app/$APPID/x86_64
|
|
if [ -f "$base/master/active/files/lib/girepository-1.0/Vte-3.91.typelib" ]; then
|
|
echo "master"
|
|
else
|
|
echo "stable"
|
|
fi
|
|
}
|
|
RAMA=$(pick_branch)
|
|
|
|
motor() {
|
|
flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" language-model 2>/dev/null \
|
|
| tr -d "'" | tr -d '\n'
|
|
}
|
|
|
|
# El modelo que la app tiene elegido de verdad, leido de sus ajustes. Si no se
|
|
# puede leer se cae al 4b pelado, que es mejor que precargar algo que no existe.
|
|
modelo() {
|
|
flatpak run --command=gsettings "$APPID//$RAMA" get "$APPID" llm-settings 2>/dev/null \
|
|
| python3 -c "
|
|
import json, sys
|
|
s = sys.stdin.read().strip()
|
|
try:
|
|
d = json.loads(s[1:-1].replace('\\\\\\\\', '\\\\').replace(\"\\\\'\", \"'\"))
|
|
print(d.get('ollama', {}).get('model') or 'qwen3.5:4b')
|
|
except Exception:
|
|
print('qwen3.5:4b')
|
|
" 2>/dev/null || echo qwen3.5:4b
|
|
}
|
|
MODEL=$(modelo)
|
|
|
|
notify() { command -v notify-send >/dev/null && notify-send -i jarvis "JARVIS" "$1"; }
|
|
|
|
ollama_up() { curl -s --max-time 2 "$OLLAMA_URL/api/version" >/dev/null 2>&1; }
|
|
|
|
app_corriendo() { pgrep -f "bin/newelle" >/dev/null; }
|
|
|
|
# El llama-server interno de la app, no el de Ollama: el suyo vive en /app/bin.
|
|
servidor_interno() { pgrep -f "/app/bin/llama-server" | head -1; }
|
|
|
|
case "${1:-start}" in
|
|
--stop)
|
|
# flatpak kill y no pkill: el patron "bin/newelle" tambien casa con la
|
|
# propia linea de comandos cuando esto se invoca desde un bash -c inline,
|
|
# y el script se mataba a si mismo antes de llegar aqui.
|
|
flatpak kill "$APPID" 2>/dev/null || pkill -f "bin/newelle" 2>/dev/null
|
|
if [ "$(motor)" = "ollama" ] && ollama_up; then
|
|
curl -s "$OLLAMA_URL/api/generate" \
|
|
-d "{\"model\":\"$MODEL\",\"keep_alive\":0}" >/dev/null 2>&1
|
|
notify "Detenido, VRAM liberada"
|
|
else
|
|
notify "Detenido"
|
|
fi
|
|
echo "JARVIS detenido."
|
|
;;
|
|
|
|
--status|--status-notify)
|
|
MOTOR=$(motor)
|
|
app_corriendo && S1="app: abierta (//$RAMA)" || S1="app: cerrada"
|
|
S2="cerebro: ${MOTOR:-?}"
|
|
|
|
if [ "$MOTOR" = "ollama" ]; then
|
|
S3=$(curl -s "$OLLAMA_URL/api/ps" 2>/dev/null \
|
|
| python3 -c 'import json,sys
|
|
ms = json.load(sys.stdin).get("models", [])
|
|
print("\n".join("cargado: %s %.1f GB (%.1f en GPU)"
|
|
% (m["name"], m.get("size",0)/1e9, m.get("size_vram",0)/1e9) for m in ms)
|
|
or "sin modelos cargados")' 2>/dev/null)
|
|
[ -n "$S3" ] || S3="ollama: caido"
|
|
else
|
|
PID=$(servidor_interno)
|
|
if [ -n "$PID" ]; then
|
|
S3="cargado en RAM ($(awk '/VmRSS/{printf "%.1f GB", $2/1048576}' \
|
|
"/proc/$PID/status" 2>/dev/null))"
|
|
else
|
|
S3="sin modelos cargados"
|
|
fi
|
|
fi
|
|
|
|
S4=$(nvidia-smi --query-gpu=memory.used,memory.free --format=csv,noheader 2>/dev/null \
|
|
| sed 's/^/VRAM usada\/libre: /')
|
|
|
|
if [ "$1" = "--status-notify" ]; then
|
|
notify "$S1 · $S2"$'\n'"$S3"$'\n'"$S4"
|
|
else
|
|
printf '%s\n%s\n\n%s\n%s\n' "$S1" "$S2" "$S3" "$S4"
|
|
fi
|
|
;;
|
|
|
|
*)
|
|
MOTOR=$(motor)
|
|
|
|
if [ "$MOTOR" = "ollama" ]; then
|
|
if ! ollama_up; then
|
|
systemctl start ollama 2>/dev/null || sudo systemctl start ollama
|
|
for _ in $(seq 20); do ollama_up && break; sleep 0.5; done
|
|
fi
|
|
ollama_up || { notify "Ollama no arranca"; echo "ERROR: ollama no responde" >&2; exit 1; }
|
|
# precargar en GPU para que la primera respuesta no tarde.
|
|
# think:false o se va a 40 s pensando en voz alta.
|
|
curl -s "$OLLAMA_URL/api/generate" \
|
|
-d "{\"model\":\"$MODEL\",\"prompt\":\"ok\",\"stream\":false,\"think\":false,\"keep_alive\":\"30m\"}" \
|
|
>/dev/null 2>&1 &
|
|
else
|
|
[ -f "$GGUF" ] || { notify "Falta el modelo: $(basename "$GGUF")"
|
|
echo "ERROR: no esta $GGUF" >&2; exit 1; }
|
|
fi
|
|
|
|
# --chat abre Newelle tal cual; sin argumentos va derecho al asistente
|
|
DIRECTO=si
|
|
[ "${1:-}" = "--chat" ] && DIRECTO=no
|
|
|
|
if app_corriendo; then
|
|
command -v xdotool >/dev/null && \
|
|
xdotool search --name "^newelle$" windowactivate 2>/dev/null
|
|
elif [ "$DIRECTO" = "no" ]; then
|
|
exec flatpak run "$APPID//$RAMA"
|
|
else
|
|
# En segundo plano y no con exec: hay que esperar a que exista la ventana
|
|
# antes de pedir la pestaña. --run-action se procesa antes de que la
|
|
# ventana este construida, asi que lanzarlo de golpe no serviria.
|
|
#
|
|
# Y esperar al PROCESO no basta: el proceso existe en cuanto python
|
|
# arranca, varios segundos antes de que haya ventana. Con `sleep 3` a
|
|
# ojo, un arranque un poco mas lento pierde la carrera y la accion
|
|
# revienta con "'MyApp' object has no attribute 'win'" — la app abre,
|
|
# pero en el chat y sin panel. Se espera a la VENTANA, que es la
|
|
# condicion de verdad.
|
|
flatpak run "$APPID//$RAMA" >>"$REGISTRO" 2>&1 &
|
|
for _ in $(seq 60); do
|
|
if command -v xdotool >/dev/null; then
|
|
xdotool search --name "^newelle$" >/dev/null 2>&1 && break
|
|
else
|
|
pgrep -f "bin/newelle" >/dev/null && break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
# sin xdotool no hay forma de saberlo: se vuelve al margen a ojo
|
|
command -v xdotool >/dev/null || sleep 3
|
|
fi
|
|
|
|
if [ "$DIRECTO" = "si" ]; then
|
|
# jarvis_full y no jarvis+hud: el segundo alterna, asi que pulsar el
|
|
# icono con la app ya abierta te sacaria de pantalla completa.
|
|
flatpak run "$APPID//$RAMA" --run-action=jarvis_full >>"$REGISTRO" 2>&1
|
|
fi
|
|
;;
|
|
esac
|