1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
|
# 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 = 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", "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/video0"
# 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.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 = []
|