From f8f98e83481e7376a235fb305a095552433f79ab Mon Sep 17 00:00:00 2001 From: elvis Date: Sun, 6 Sep 2026 19:21:16 -0300 Subject: Local voice assistant on top of Canary, llama.cpp and qwentts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- config/asistente.toml | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 config/asistente.toml (limited to 'config/asistente.toml') diff --git a/config/asistente.toml b/config/asistente.toml new file mode 100644 index 0000000..b638acd --- /dev/null +++ b/config/asistente.toml @@ -0,0 +1,153 @@ +# Configuración del asistente de voz. +# +# Las rutas relativas se resuelven contra la carpeta de este fichero, no contra +# el directorio desde el que se lanza el binario. +# +# Los valores que llevan un comentario con una medición vienen de +# docs/RENDIMIENTO.md: son los que se midieron en esta máquina, no adivinanzas. + +[general] +language = "es" +history_turns = 8 +report_latency = true +system_prompt = """ +Eres un asistente de voz en español. Tus respuestas se leen en voz alta, así \ +que responde en una o dos frases cortas, en texto plano corrido. No uses \ +markdown, ni listas, ni asteriscos, ni emojis, ni encabezados. No escribas \ +URLs ni código salvo que te lo pidan explícitamente. Si no sabes algo, dilo \ +en una frase. +""" + +# SUSTITUYE a system_prompt en la pasada en que el modelo decide si llamar a +# una herramienta. Va sola a propósito: medido con Qwen3.5-2B, añadirle +# cualquier indicación de estilo —dos palabras bastan— hace que deje de llamar +# a las herramientas y se invente el dato. La guía sola acierta 8 de 8; con +# «Responde breve.» detrás, 1 de 8; con la persona de asistente de voz, 0 de 8. +# Ver docs/RENDIMIENTO.md. Se desactiva con tools.dedicated_prompt = false. +tools_prompt = """ +Antes de responder, comprueba si alguna de tus herramientas te da el dato. Si \ +es así, llámala primero y espera su resultado; no contestes de memoria. Sólo \ +cuando tengas el resultado, resúmelo en una frase. +""" + +[audio] +# Vacío = el dispositivo por defecto. `asistente devices` los lista. +input_device = "" +output_device = "" +playback_prebuffer = 0.20 +output_gain = 1.0 + +[vad] +frame_seconds = 0.1 +preroll_seconds = 0.3 +silence_hold = 0.8 # silencio que cierra una intervención +min_utterance = 0.3 # se mide sobre la voz, sin contar el preroll +max_utterance = 20.0 +threshold_factor = 3.0 +min_threshold = 0.0008 +max_threshold = 0.02 +# Cortar al asistente hablando encima. Apagado por defecto: con altavoces +# abiertos el micrófono se oye a sí mismo y el asistente se interrumpe solo. +# Enciéndelo con auriculares, o con --barge-in. +barge_in = false +barge_in_factor = 4.0 + +[asr] +model_dir = "../vendor/canary-rs/models/canary-180m-flash-onnx" +source_lang = "es" +target_lang = "es" +window = 6.0 +step = 0.4 +stability = 2 +partials = true +dedicated_final_model = false +# CPU a propósito: la GPU de 4 GB la ocupa el hablante del TTS y disputársela +# sale más caro que decodificar aquí. Ver docs/RENDIMIENTO.md. +execution_provider = "cpu" +inter_threads = 2 +intra_threads = 4 + +[llm] +host = "127.0.0.1" +port = 8012 +model = "" +temperature = 0.7 +top_p = 0.9 +top_k = 40 +min_p = 0.1 +repeat_penalty = 1.1 +max_tokens = 300 # una respuesta hablada larga cansa, y la síntesis es lo caro +max_tool_rounds = 4 +request_timeout_secs = 120 + +[tts] +host = "127.0.0.1" +port = 8013 +voice = "asistente" +language = "spanish" +temperature = 0.9 +top_k = 50 +top_p = 1.0 +repetition_penalty = 1.05 +max_new_tokens = 2048 +warmup = true # la primera síntesis cuesta ~3,5 s más que el resto +request_timeout_secs = 180 + +# Voz clonada, registrada en el servidor al arrancar. Los tres ficheros salen +# de `qwen-codec --talker` sobre una grabación de referencia; hay un guion en +# scripts/clonar-voz.sh. +[tts.reference] +name = "asistente" +speaker = "../assets/voices/asistente.spk" +codes = "../assets/voices/asistente.rvq" +transcript = "../assets/voices/asistente.txt" + +[tools] +enabled = true +# Alterna entre tools_prompt (para decidir) y system_prompt (para redactar). +# Es lo único que hace que las herramientas funcionen con este modelo; a +# cambio, las respuestas que no usan herramienta pierden la guía de estilo. +# Ponlo a false para priorizar el estilo sobre las herramientas. +dedicated_prompt = true +# Ejecución de órdenes del sistema. Apagada por defecto 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, no una comodidad. Actívala aquí o con --shell. +shell = false +shell_allowlist = ["date", "uptime", "free", "df", "ls"] +shell_timeout_secs = 10 +shell_dry_run = false +shell_working_dir = "" + +[supervisor] +manage = true # lanzar los servidores; --no-manage los supone arriba +startup_timeout_secs = 180 + +[supervisor.llama] +binary = "../vendor/llama.cpp/build/bin/llama-server" +model = "../models/Qwen3.5-2B.Q5_K_M.gguf" +mmproj = "../models/mmproj-BF16.gguf" +# Copia de la plantilla del modelo con el bloque cerrado de entrada. +# Sin esto el modelo razona entre 7 y 9 s antes de la primera palabra audible. +chat_template = "qwen35-no-think.jinja" +extra_args = [ + "--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", + # 8192 en vez del contexto completo del modelo (262144): la caché KV de ese + # tamaño no cabe en 4 GB junto al TTS. Ver docs/RENDIMIENTO.md. + "--ctx-size", "8192", "--parallel", "2", + "--cache-ram", "6144", + "--rope-freq-base", "1000000", "--rope-freq-scale", "0.25", +] + +[supervisor.tts] +binary = "../vendor/qwentts.cpp/build/tts-server" +model = "../models/qwen-talker-1.7b-base-Q8_0.gguf" +codec = "../models/qwen-tokenizer-12hz-Q8_0.gguf" +# El ajuste con más efecto de todo el sistema: de fábrica son 24 s, y con eso +# el servidor no devuelve nada hasta terminar la frase entera. Medido, baja el +# primer audio de 4948 ms a 585 ms. +codec_chunk_dur = 1.0 +extra_args = [] -- cgit v1.2.3