aboutsummaryrefslogtreecommitdiffstats

asist-p — asistente de voz local

English

Escucha por el micrófono, piensa y contesta hablando. Todo corre en la máquina; sólo la búsqueda en internet, que es opcional, sale a la red.

Une tres motores que ya existían —Canary para oír, llama.cpp para pensar y qwentts.cpp para hablar— en un pipeline de hilos y canales en Rust donde ninguna etapa espera a la siguiente.

  micrófono ──muestras──> segmentador ──intervención──> ASR ──texto──┐
  (cpal, tiempo real)      (VAD, turnos)                 (Canary)    │
                                                                     v
  altavoz <──muestras── síntesis <──frases── conversación <──────────┘
  (cpal, anillo)         (qwentts)          (llama.cpp + herramientas)

Cada caja es un hilo; cada flecha, un canal. Dos decisiones sostienen el resto:

  • Nada bloquea al que va delante. El micrófono nunca espera al reconocedor, y el modelo nunca espera al sintetizador. Quien va sobrado descarta trabajo en lugar de acumular retraso.
  • Se responde por frases, no por respuestas. Cada frase sale hacia el sintetizador en cuanto se cierra, así que el asistente empieza a hablar mientras el modelo sigue escribiendo. Es lo que separa medio segundo de cinco.

Puesta en marcha

git clone --recursive https://git.all.ar/pub/asist-p.git && cd asist-p
scripts/bootstrap.sh          # submódulos, parches, binarios y modelos
cargo run --release -- check  # comprueba que está todo en su sitio
cargo run --release -- run    # a hablar

bootstrap.sh es idempotente y no copia modelos: los enlaza desde donde ya estén (~/GIT-MIRRO, ~/HF). Si no encuentra los motores construidos, los construye, y eso sí tarda.

cargo run --release -- devices          # listar dispositivos de audio
cargo run --release -- run --barge-in   # permitir cortar al asistente
cargo run --release -- run --shell      # habilitar órdenes del sistema
cargo run --release -- run --no-manage  # no lanzar los servidores

Durante el desarrollo conviene dejar los servidores levantados aparte —cargar los modelos cuesta más de un minuto— y reiniciar sólo el binario de Rust:

scripts/servers.sh start
cargo run --release -- run --no-manage

Cómo está organizado

Crate De qué se ocupa
asist-core Configuración, eventos, errores, cliente HTTP, telemetría y el registro de herramientas
asist-audio Captura y reproducción con cpal, detección de voz, troceado en turnos
asist-asr Canary en ONNX: ventana deslizante y transcripción final
asist-llm llama-server: streaming SSE, historial, llamadas a herramientas
asist-tts qwentts: síntesis en streaming y voces clonadas
asist-tools Buscar en internet, mirar por la cámara y ver la pantalla
asist-app Supervisor de procesos, orquestador y binario asistente

Los tres motores viven en vendor/ como submódulos fijados a un commit concreto. Los cambios locales sobre ellos están en vendor/patches/, y bootstrap.sh los aplica: sin ellos el reconocedor no se comporta como el que se midió.

Configuración

Todo está en config/asistente.toml, comentado. Los valores que llevan una cifra en el comentario salen de una medición concreta, no de una suposición; el detalle está en docs/RENDIMIENTO.md.

Lo que más se toca:

[vad]
silence_hold = 0.8     # cuánto silencio cierra tu intervención
barge_in = false       # cortar al asistente hablando encima

[general]
history_turns = 8      # memoria de la conversación
system_prompt = "..."  # carácter y estilo

[tools]
shell = false          # ejecutar órdenes del sistema

Interrumpir y el eco del altavoz

Por defecto el asistente es media dúplex: mientras habla, el micrófono está cerrado. No es pereza: con altavoces abiertos el micrófono se oye a sí mismo, el asistente se transcribe y se responde solo.

Con auriculares, --barge-in deja hablar encima para cortarlo. El umbral se eleva (vad.barge_in_factor) para que el eco no baste y una voz de verdad sí.

Latencia

Un turno real, medido de punta a punta:

Etapa Tiempo
Reconocimiento (2,5 s de voz) 200 ms
Modelo, primer token 1660 ms
Modelo, resto 1362 ms
Herramientas 1 ms
Del final de tu frase al primer audio 3213 ms

El binario mide esto en cada turno y señala la etapa más lenta. Tres ajustes de configuración valieron más que cualquier cambio de código:

  • El TTS decodificaba en bloques de 24 s: 4948 ms → 585 ms al primer audio.
  • La plantilla del modelo dejaba <think> abierto: 8630 ms → 413 ms al primer token.
  • El prompt de estilo anulaba las llamadas a herramientas: 0/8 → 8/8.
  • La cámara capturaba a 720p: 7,8 s → 2,9 s por imagen a 640x480.
  • Las herramientas se ejecutaban en silencio: ~8,4 s → 4,1 s hasta oír algo.
  • La pantalla a 640 px inventaba el texto: 1 de 3 aciertos → 3 de 3 a 1280 px.

Todos, con el porqué y cómo se midieron, en docs/RENDIMIENTO.md.

Herramientas

El asistente puede llamar a funciones. Vienen cuatro:

Herramienta Qué hace Coste medido
hora_actual Fecha y hora del sistema 0 ms
buscar_en_internet Busca y resume 0,5–3,6 s
mirar_por_la_camara Hace una foto y responde sobre lo que ve 4,1–5,2 s
mirar_la_pantalla Captura la pantalla y responde sobre lo que hay 7,6–12,4 s
ejecutar_comando Órdenes del sistema, apagada por defecto —

Mientras una herramienta trabaja, el asistente dice «Déjame que lo busque» o «Voy a mirar». No es adorno: sin eso el turno pasa seis segundos en silencio y se lee como un cuelgue. Con el acuse, el primer audio llega en 3–4 s.

Buscar admite tres buscadores: tavily (con clave, devuelve una respuesta ya redactada), ddgs (sin clave, la misma librería que hay bajo duckduckgo-mcp) y searxng. El de comando ejecuta un guion que escriba JSON, así que meter otro es escribir un guion, no tocar Rust.

Ver —cámara y pantalla— aprovecha que el servidor ya carga el proyector multimodal: el mismo modelo que conversa describe la imagen. Ni la foto ni la captura se guardan en disco, y no entran en el historial.

La pantalla usa 1280 px y la cámara 640, y la diferencia importa: una escena se entiende, pero un texto hay que leerlo. Medido, a 640 px el modelo no dice que no lee la pantalla, se inventa lo que pone —contestó que la reunión era «a las 10:00» cuando ponía 15:30—. Por eso el triple de coste.

ejecutar_comando está apagada a conciencia: darle una shell a un modelo que obedece a lo que oye por el micrófono es un cambio de postura de seguridad. Cuando se enciende, sólo pasan las órdenes de una lista blanca, sin shell que interprete metacaracteres y con un plazo máximo.

Añadir una propia son unas veinte líneas: docs/EXTENDER.md.

Pruebas

Más de cien pruebas unitarias que no necesitan ni modelos ni micrófono, y pruebas de integración contra los servidores de verdad.

cargo test                                    # unitarias, sin modelos
scripts/servers.sh start
cargo test --release -p asist-app --test integration -- --nocapture
cargo test --release -p asist-app --test integration -- --ignored  # bucle completo

Las de integración se saltan solas si no hay servidores escuchando, y se turnan la GPU con un mutex: los dos servidores comparten una tarjeta de 4 GB y en paralelo miden contención en vez de latencia.

La prueba marcada --ignored cierra el bucle sin micrófono: sintetiza una frase y comprueba que el reconocedor la recupera.

Requisitos

  • Rust 1.85 o posterior
  • CUDA para los motores en C++ (funcionan en CPU, pero muy justos)
  • PipeWire o ALSA
  • ~6 GB de disco para los modelos
  • ffmpeg para la cámara y para reducir las capturas
  • Para la pantalla: grim (Wayland) o maim/ImageMagick (X11)
  • Para buscar: una clave de Tavily en $TAVILY_API_KEY, o uv tool install ddgs

Voz

El repositorio no trae ninguna voz clonada: una voz es de alguien. Para que el asistente hable con una, grabá unos segundos de audio con su transcripción y extraé los latentes:

scripts/clone-voice.sh grabacion.wav transcripcion.txt

Quedan en assets/voices/ (fuera de git). Sin voz clonada, quitá la sección [tts.reference] de config/asistente.toml y el asistente usa una voz del propio modelo.

Licencia

MIT