# 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. """ # Se añade a system_prompt para redactar la respuesta cuando una herramienta ya # devolvió su resultado. Aquí sí se puede añadir estilo —la llamada ya ocurrió— # y hace falta: sin esto el modelo anuncia lo que acaba de hacer («he tomado una # foto, ahora puedo responderte sobre el color») en vez de decir lo que averiguó. tool_result_prompt = """ Acabas de recibir el resultado de una herramienta. Contesta a la pregunta \ usando ese resultado y nada más. No anuncies lo que has hecho ni lo que \ podrías hacer: di directamente lo que has averiguado. Si el resultado viene en \ otro idioma, tradúcelo al español. """ [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 = 400 # 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", "mkdir", "cat"] shell_timeout_secs = 10 shell_dry_run = false shell_working_dir = "" # Búsqueda en internet. La clave NO va aquí: se lee de la variable de entorno # que indique api_key_env, porque este fichero se versiona y un secreto dentro # acaba en el historial de git. # # Tres buscadores, medidos: # tavily ~2,4 s. Devuelve una respuesta YA REDACTADA además de los enlaces, # que es lo que se puede leer en voz alta sin gastar otra vuelta del # modelo en resumir. Necesita clave. # ddgs 1,5-5,8 s. Sin clave. Consulta DuckDuckGo, Brave, Mojeek, Startpage # y compañía a través de scripts/buscar-ddgs.sh. Sólo devuelve # fragmentos: la síntesis la tiene que hacer el modelo, y con 2B sale # algo peor que con tavily. # searxng Sin clave, pero hace falta una instancia propia. Como ddgs, sólo # devuelve resultados. [search] enabled = true backend = "tavily" # "tavily", "ddgs" o "searxng" api_key_env = "TAVILY_API_KEY" base_url = "" # sólo para searxng, p. ej. "http://127.0.0.1:8888" # Programa del backend "ddgs". {consulta} y {max} se sustituyen antes de # ejecutar, y tiene que escribir JSON por la salida estándar. Vale cualquier # otro programa que respete eso, incluido un puente a un servidor MCP. command = ["../scripts/buscar-ddgs.sh", "{consulta}", "{max}"] max_results = 5 timeout_secs = 20 # Mirar por la cámara. El servidor ya carga el proyector multimodal, así que el # mismo modelo que conversa describe lo que capta. [camera] enabled = true device = "/dev/video2" # La resolución manda en la latencia. Medido en esta máquina: 1,3 s a 320x240, # 2,9 s a 640x480 y 7,8 s a 1280x720. 640x480 es el equilibrio. width = 640 height = 480 # Fotogramas descartados para que se asiente la exposición automática; el # primero suele salir quemado y descartar unos pocos es casi gratis. warmup_frames = 5 timeout_secs = 15 # Vacío = no se guarda ningún fotograma en disco, que es lo que corresponde. # Ponle una carpeta sólo para depurar qué está viendo el modelo. save_dir = "" # Mirar la pantalla. Es la misma visión que la cámara, pero con un ajuste # distinto porque el problema es distinto: una pantalla es TEXTO. # # Medido con tipografía de interfaz de 13 px, preguntando por datos concretos: # 1280 px 7,6 s acierta 3 de 3 # 960 px 4,5 s acierta 2 de 3 # 640 px 2,4 s acierta 1 de 3 # # Y cuando falla no dice que no lo lee: se lo inventa. A 640 px contestó que el # error era «no se pudo abrir el archivo involution» y que la reunión era «a # las 10:00»; ninguna de las dos cosas estaba en la imagen. Por eso 1280 aunque # cueste el triple que la cámara. [screen] enabled = true # Detecta el entorno gráfico (grim en Wayland, maim/imagemagick en X11), captura # y reduce a {ancho}. Editar el guion es más fácil que recompilar. command = ["../scripts/capturar-pantalla.sh", "{ancho}", "{salida}"] width = 1280 output = "" # monitor concreto; vacío = todo timeout_secs = 20 # Vacío = no se guarda ninguna captura. Aquí pesa más que en la cámara: en una # captura de pantalla caben contraseñas, mensajes privados y correo abierto. save_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.Q8_0.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 = []