aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/servidores.sh
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-06 19:21:16 -0300
committerelvis <elvis@claros.ar>2026-09-06 19:23:37 -0300
commitf8f98e83481e7376a235fb305a095552433f79ab (patch)
tree6d0ecddb26bef8430a086431de40f8ce9b1039e6 /scripts/servidores.sh
downloadasist-p-f8f98e83481e7376a235fb305a095552433f79ab.tar.gz
asist-p-f8f98e83481e7376a235fb305a095552433f79ab.zip
Local voice assistant on top of Canary, llama.cpp and qwentts
Pipeline en Rust de hilos y canales que une los tres motores: el micrófono alimenta un segmentador con VAD, las intervenciones cerradas van al reconocedor, la transcripción al modelo y cada frase que este cierra sale hacia el sintetizador sin esperar al resto de la respuesta. Dos reglas sostienen el diseño: ninguna etapa bloquea a la anterior —quien va sobrado descarta trabajo en lugar de acumular retraso— y todo lo que viaja por los canales lleva el turno al que pertenece, así que interrumpir es subir el contador y levantar dos banderas de cancelación. Seis crates: core (configuración, eventos, HTTP, telemetría, herramientas), audio (cpal, VAD, anillo de reproducción), asr, llm, tts y app (supervisor de procesos y orquestador). Los motores van como submódulos fijados a un commit, con los cambios locales en vendor/patches. Midiendo el pipeline aparecieron tres cuellos de botella de configuración que valieron más que cualquier cambio de código, todos documentados en docs/RENDIMIENTO.md: - tts-server decodificaba el audio en bloques de 24 s, de modo que el modo «streaming» llegaba de una pieza: 4948 ms -> 585 ms hasta el primer audio. - La plantilla de chat del modelo abre <think> y no lo cierra nunca, sin variable que lo apague: 8630 ms -> 413 ms hasta el primer token, con una copia de la plantilla que deja el bloque cerrado de entrada. - Cualquier indicación de estilo junto a la guía de herramientas hace que este modelo de 2B deje de llamarlas y se invente el dato (8/8 aciertos con la guía sola, 0/8 con la persona de asistente de voz). El turno alterna ahora entre dos instrucciones de sistema. La ejecución de órdenes del sistema queda implementada y apagada, tras cuatro barreras: lista blanca sobre el ejecutable, rutas rechazadas, sin shell que interprete metacaracteres y plazo máximo. 68 pruebas unitarias sin modelos, más seis de integración que se saltan solas si no hay servidores y se turnan la GPU: en paralelo, los dos servidores no caben en 4 GB y miden contención en vez de latencia. Claude-Session: https://claude.ai/code/session_01FNxz5cSdQSscJH9H7b8uGU
Diffstat (limited to 'scripts/servidores.sh')
-rwxr-xr-xscripts/servidores.sh83
1 files changed, 83 insertions, 0 deletions
diff --git a/scripts/servidores.sh b/scripts/servidores.sh
new file mode 100755
index 0000000..2e3b769
--- /dev/null
+++ b/scripts/servidores.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+# Arranca o para los dos servidores por separado, sin el asistente.
+#
+# Útil para desarrollar: cargar los modelos cuesta más de un minuto y así se
+# reinicia el binario de Rust las veces que haga falta sin volver a pagarlo.
+# El asistente detecta que ya están escuchando y los reutiliza.
+#
+# scripts/servidores.sh arrancar | parar | estado
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+LOGS="${ASIST_LOGS:-logs}"
+LLM_PUERTO="${ASIST_LLM_PUERTO:-8012}"
+TTS_PUERTO="${ASIST_TTS_PUERTO:-8013}"
+mkdir -p "$LOGS"
+
+vivo() { curl -sf -m 2 "http://127.0.0.1:$1/health" >/dev/null 2>&1; }
+
+esperar() { # puerto nombre segundos
+ local fin=$(( SECONDS + $3 ))
+ while [ $SECONDS -lt $fin ]; do
+ vivo "$1" && { echo " $2 listo"; return 0; }
+ sleep 1
+ done
+ echo " $2 NO respondió en $3 s; mira $LOGS/$2.log" >&2
+ return 1
+}
+
+arrancar() {
+ if vivo "$LLM_PUERTO"; then
+ echo " llama-server ya estaba arriba"
+ else
+ echo " arrancando llama-server…"
+ nohup vendor/llama.cpp/build/bin/llama-server \
+ --model models/Qwen3.5-2B.Q5_K_M.gguf \
+ --mmproj models/mmproj-BF16.gguf \
+ --host 127.0.0.1 --port "$LLM_PUERTO" \
+ --threads 10 --threads-batch 10 \
+ --batch-size 512 --ubatch-size 256 \
+ --gpu-layers 10 --split-mode layer --tensor-split 1 --main-gpu 0 \
+ --no-mmap --ctx-size 8192 --parallel 2 --cache-ram 6144 \
+ --rope-freq-base 1000000 --rope-freq-scale 0.25 \
+ --jinja --chat-template-file config/qwen35-no-think.jinja \
+ > "$LOGS/llama-server.log" 2>&1 &
+ fi
+
+ if vivo "$TTS_PUERTO"; then
+ echo " tts-server ya estaba arriba"
+ else
+ echo " arrancando tts-server…"
+ # --codec-chunk-dur 1.0 es lo que hace que el audio salga por bloques
+ # según se genera en vez de todo al final. Ver docs/RENDIMIENTO.md.
+ nohup vendor/qwentts.cpp/build/tts-server \
+ --model models/qwen-talker-1.7b-base-Q8_0.gguf \
+ --codec models/qwen-tokenizer-12hz-Q8_0.gguf \
+ --host 127.0.0.1 --port "$TTS_PUERTO" \
+ --lang spanish --codec-chunk-dur 1.0 \
+ > "$LOGS/tts-server.log" 2>&1 &
+ fi
+
+ esperar "$LLM_PUERTO" llama-server 240
+ esperar "$TTS_PUERTO" tts-server 240
+}
+
+parar() {
+ # SIGTERM, no SIGKILL: hay que darles ocasión de soltar la GPU.
+ pkill -TERM -f 'llama-server .*--port '"$LLM_PUERTO" 2>/dev/null && echo " llama-server parado" || true
+ pkill -TERM -f 'tts-server .*--port '"$TTS_PUERTO" 2>/dev/null && echo " tts-server parado" || true
+}
+
+estado() {
+ vivo "$LLM_PUERTO" && echo " llama-server arriba :$LLM_PUERTO" || echo " llama-server parado"
+ vivo "$TTS_PUERTO" && echo " tts-server arriba :$TTS_PUERTO" || echo " tts-server parado"
+ command -v nvidia-smi >/dev/null && \
+ nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader | sed 's/^/ GPU: /'
+}
+
+case "${1:-estado}" in
+ arrancar|start) arrancar ;;
+ parar|stop) parar ;;
+ estado|status) estado ;;
+ *) echo "uso: $0 {arrancar|parar|estado}" >&2; exit 1 ;;
+esac