# asist-p — asistente de voz local Escucha por el micrófono, piensa y contesta hablando. Todo en la máquina: no sale un byte a internet. 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 ```bash git clone --recursive && 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. ```bash 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: ```bash scripts/servidores.sh arrancar 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-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`](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](docs/RENDIMIENTO.md). Lo que más se toca: ```toml [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 `` abierto: **8630 ms → 413 ms** al primer token. - El prompt de estilo anulaba las llamadas a herramientas: **0/8 → 8/8**. Los tres, con el porqué y cómo se midieron, en [docs/RENDIMIENTO.md](docs/RENDIMIENTO.md). ## Herramientas El asistente puede llamar a funciones. Viene con `hora_actual` y con `ejecutar_comando`, esta última **apagada**: 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](docs/EXTENDER.md). ## Pruebas 68 pruebas unitarias que no necesitan ni modelos ni micrófono, y seis de integración contra los servidores de verdad. ```bash cargo test # unitarias, sin modelos scripts/servidores.sh arrancar cargo test --release -p asist-app --test integracion -- --nocapture cargo test --release -p asist-app --test integracion -- --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