aboutsummaryrefslogtreecommitdiffstats
path: root/config/asistente.toml
blob: bab64c0811301ae2f6c2a78cd090972f026ca715 (plain)
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
232
233
234
# 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 <think> 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 = []