# Voice assistant configuration. # # Relative paths are resolved against the directory of this file, not against # the directory the binary is launched from. # # Values with a comment carrying a measurement come from docs/RENDIMIENTO.md: # they were measured on this machine, not guessed. # # The prompts are in Spanish because the assistant speaks Spanish. [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. """ # REPLACES system_prompt in the pass where the model decides whether to call # a tool. It goes alone on purpose: measured with Qwen3.5-2B, adding any style # instruction (two words are enough) makes it stop calling tools and make the # data up. The guide alone gets 8 of 8; with «Responde breve.» after it, 1 of 8; # with the voice-assistant persona, 0 of 8. See docs/RENDIMIENTO.md. Turn it # off with 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. """ # Appended to system_prompt to write the answer once a tool has returned its # result. Style can be added here (the call already happened) and it is needed: # without it the model announces what it just did («he tomado una foto, ahora # puedo responderte sobre el color») instead of saying what it found out. 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] # Empty = the default device. `asistente devices` lists them. 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 # silence that ends an utterance min_utterance = 0.3 # measured on speech, not counting the preroll max_utterance = 20.0 threshold_factor = 3.0 min_threshold = 0.0008 max_threshold = 0.02 # Interrupt the assistant by talking over it. Off by default: with open # speakers the microphone hears itself and the assistant interrupts itself. # Turn it on with headphones, or with --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 on purpose: the 4 GB GPU is taken by the TTS talker and fighting over # it costs more than decoding here. See 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 # a long spoken answer is tiring, and synthesis is the expensive part 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 # the first synthesis costs ~3.5 s more than the rest request_timeout_secs = 180 # Cloned voice, registered with the server at startup. The three files come # from `qwen-codec --talker` on a reference recording; there is a script in # scripts/clone-voice.sh. [tts.reference] name = "asistente" speaker = "../assets/voices/asistente.spk" codes = "../assets/voices/asistente.rvq" transcript = "../assets/voices/asistente.txt" [tools] enabled = true # Alternates between tools_prompt (to decide) and system_prompt (to write). # It is the only thing that makes tools work with this model; in exchange, # answers that use no tool lose the style guide. # Set it to false to favour style over tools. dedicated_prompt = true # System command execution. Off by default on purpose: giving a shell to a # model that obeys what it hears through the microphone is a change of # security posture, not a convenience. Turn it on here or with --shell. shell = false shell_allowlist = ["date", "uptime", "free", "df", "ls", "mkdir", "cat"] shell_timeout_secs = 10 shell_dry_run = false shell_working_dir = "" # Web search. The key does NOT go here: it is read from the environment # variable named by api_key_env, because this file is versioned and a secret # in it ends up in the git history. # # Three backends, measured: # tavily ~2.4 s. Returns an ALREADY WRITTEN answer besides the links, # which can be read aloud without spending another model round # summarizing. Needs a key. # ddgs 1.5-5.8 s. No key. Queries DuckDuckGo, Brave, Mojeek, Startpage # and friends through scripts/search-ddgs.sh. Only returns # snippets: the model has to summarize, and with 2B the result is # somewhat worse than tavily. # searxng No key, but needs your own instance. Like ddgs, it only returns # results. [search] enabled = true backend = "tavily" # "tavily", "ddgs" o "searxng" api_key_env = "TAVILY_API_KEY" base_url = "" # searxng only, e.g. "http://127.0.0.1:8888" # Program for the "ddgs" backend. {query} and {max} are substituted before # running, and it must write JSON to standard output. Any other program that # honours that works too, including a bridge to an MCP server. command = ["../scripts/search-ddgs.sh", "{query}", "{max}"] max_results = 5 timeout_secs = 20 # Looking through the camera. The server already loads the multimodal # projector, so the same model that converses describes what it captures. [camera] enabled = true device = "/dev/video2" # Resolution rules latency. Measured on this machine: 1.3 s at 320x240, # 2.9 s at 640x480 and 7.8 s at 1280x720. 640x480 is the balance. width = 640 height = 480 # Frames discarded so auto-exposure settles; the first one is usually # blown out and discarding a few is almost free. warmup_frames = 5 timeout_secs = 15 # Empty = no frame is saved to disk, which is right. # Set a directory only to debug what the model is seeing. save_dir = "" # Looking at the screen. Same vision as the camera, but tuned differently # because the problem is different: a screen is TEXT. # # Measured with 13 px UI type, asking for specific details: # 1280 px 7.6 s 3 of 3 right # 960 px 4.5 s 2 of 3 right # 640 px 2.4 s 1 of 3 right # # And when it fails it does not say it cannot read it: it makes it up. At # 640 px it answered that the error was «no se pudo abrir el archivo # involution» and the meeting was «at 10:00»; neither was in the image. Hence # 1280 even though it costs three times the camera. [screen] enabled = true # Detects the graphical environment (grim on Wayland, maim/imagemagick on # X11), captures and scales down to {width}. Editing the script is easier than # recompiling. command = ["../scripts/capture-screen.sh", "{width}", "{output}"] width = 1280 output = "" # a specific monitor; empty = all timeout_secs = 20 # Empty = no capture is saved. It weighs more here than for the camera: a # screenshot can hold passwords, private messages and open email. save_dir = "" [supervisor] manage = true # start the servers; --no-manage assumes they are up 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" # Copy of the model template with the block closed from the start. # Without it the model reasons for 7 to 9 s before the first audible word. 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 instead of the model's full context (262144): a KV cache that size # does not fit in 4 GB next to the TTS. See 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" # The single most effective setting in the system: the stock value is 24 s, # and with it the server returns nothing until the whole sentence is done. # Measured, it lowers the first audio from 4948 ms to 585 ms. codec_chunk_dur = 1.0 extra_args = []