aboutsummaryrefslogtreecommitdiffstats
path: root/config
diff options
context:
space:
mode:
Diffstat (limited to 'config')
-rw-r--r--config/asistente.toml153
-rw-r--r--config/qwen35-no-think.jinja91
2 files changed, 244 insertions, 0 deletions
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 <think> 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 = []
diff --git a/config/qwen35-no-think.jinja b/config/qwen35-no-think.jinja
new file mode 100644
index 0000000..40f64c2
--- /dev/null
+++ b/config/qwen35-no-think.jinja
@@ -0,0 +1,91 @@
+{%- if tools %}
+ {{- '<|im_start|>system\n' }}
+ {%- if messages[0].role == 'system' %}
+ {{- messages[0].content + '\n\n' }}
+ {%- endif %}
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
+ {%- for tool in tools %}
+ {{- "\n" }}
+ {{- tool | tojson }}
+ {%- endfor %}
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
+{%- else %}
+ {%- if messages[0].role == 'system' %}
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
+ {%- endif %}
+{%- endif %}
+{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
+{%- for message in messages[::-1] %}
+ {%- set index = (messages|length - 1) - loop.index0 %}
+ {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
+ {%- set ns.multi_step_tool = false %}
+ {%- set ns.last_query_index = index %}
+ {%- endif %}
+{%- endfor %}
+{%- for message in messages %}
+ {%- if message.content is string %}
+ {%- set content = message.content %}
+ {%- else %}
+ {%- set content = '' %}
+ {%- endif %}
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
+ {%- elif message.role == "assistant" %}
+ {%- set reasoning_content = '' %}
+ {%- if message.reasoning_content is string %}
+ {%- set reasoning_content = message.reasoning_content %}
+ {%- else %}
+ {%- if '</think>' in content %}
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
+ {%- endif %}
+ {%- endif %}
+ {%- if loop.index0 > ns.last_query_index %}
+ {%- if loop.last or (not loop.last and reasoning_content) %}
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
+ {%- else %}
+ {{- '<|im_start|>' + message.role + '\n' + content }}
+ {%- endif %}
+ {%- else %}
+ {{- '<|im_start|>' + message.role + '\n' + content }}
+ {%- endif %}
+ {%- if message.tool_calls %}
+ {%- for tool_call in message.tool_calls %}
+ {%- if (loop.first and content) or (not loop.first) %}
+ {{- '\n' }}
+ {%- endif %}
+ {%- if tool_call.function %}
+ {%- set tool_call = tool_call.function %}
+ {%- endif %}
+ {{- '<tool_call>\n{"name": "' }}
+ {{- tool_call.name }}
+ {{- '", "arguments": ' }}
+ {%- if tool_call.arguments is string %}
+ {{- tool_call.arguments }}
+ {%- else %}
+ {{- tool_call.arguments | tojson }}
+ {%- endif %}
+ {{- '}\n</tool_call>' }}
+ {%- endfor %}
+ {%- endif %}
+ {{- '<|im_end|>\n' }}
+ {%- elif message.role == "tool" %}
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
+ {{- '<|im_start|>user' }}
+ {%- endif %}
+ {{- '\n<tool_response>\n' }}
+ {{- content }}
+ {{- '\n</tool_response>' }}
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
+ {{- '<|im_end|>\n' }}
+ {%- endif %}
+ {%- endif %}
+{%- endfor %}
+{%- if add_generation_prompt %}
+ {{- '<|im_start|>assistant
+<think>
+
+</think>
+
+' }}
+{%- endif %} \ No newline at end of file