aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--.gitmodules8
-rw-r--r--Cargo.toml16
-rw-r--r--LICENSE2
-rw-r--r--README.es.md217
-rw-r--r--README.md299
-rw-r--r--config/asistente.toml173
-rw-r--r--crates/asist-app/src/main.rs275
-rw-r--r--crates/asist-app/src/pipeline.rs176
-rw-r--r--crates/asist-app/src/registry.rs34
-rw-r--r--crates/asist-app/src/render.rs47
-rw-r--r--crates/asist-app/src/session.rs50
-rw-r--r--crates/asist-app/src/supervisor.rs78
-rw-r--r--crates/asist-app/tests/integration.rs (renamed from crates/asist-app/tests/integracion.rs)318
-rw-r--r--crates/asist-asr/Cargo.toml2
-rw-r--r--crates/asist-asr/src/lib.rs110
-rw-r--r--crates/asist-audio/src/capture.rs48
-rw-r--r--crates/asist-audio/src/lib.rs58
-rw-r--r--crates/asist-audio/src/playback.rs117
-rw-r--r--crates/asist-audio/src/vad.rs130
-rw-r--r--crates/asist-core/src/config.rs271
-rw-r--r--crates/asist-core/src/error.rs30
-rw-r--r--crates/asist-core/src/event.rs52
-rw-r--r--crates/asist-core/src/http.rs66
-rw-r--r--crates/asist-core/src/lib.rs10
-rw-r--r--crates/asist-core/src/proc.rs80
-rw-r--r--crates/asist-core/src/telemetry.rs88
-rw-r--r--crates/asist-core/src/text.rs124
-rw-r--r--crates/asist-core/src/tools.rs158
-rw-r--r--crates/asist-llm/src/chat.rs46
-rw-r--r--crates/asist-llm/src/client.rs98
-rw-r--r--crates/asist-llm/src/lib.rs10
-rw-r--r--crates/asist-tools/Cargo.toml6
-rw-r--r--crates/asist-tools/src/camera.rs56
-rw-r--r--crates/asist-tools/src/lib.rs14
-rw-r--r--crates/asist-tools/src/screen.rs88
-rw-r--r--crates/asist-tools/src/search.rs122
-rw-r--r--crates/asist-tools/src/vision.rs66
-rw-r--r--crates/asist-tts/src/lib.rs85
-rw-r--r--docs/EXTENDER.md18
-rw-r--r--docs/RENDIMIENTO.md6
-rwxr-xr-xscripts/bootstrap.sh155
-rwxr-xr-xscripts/buscar-ddgs.sh54
-rwxr-xr-xscripts/capturar-pantalla.sh52
-rwxr-xr-xscripts/capture-screen.sh53
-rwxr-xr-xscripts/clonar-voz.sh47
-rwxr-xr-xscripts/clone-voice.sh47
-rwxr-xr-xscripts/search-ddgs.sh54
-rwxr-xr-xscripts/servers.sh84
-rwxr-xr-xscripts/servidores.sh83
50 files changed, 2260 insertions, 2027 deletions
diff --git a/.gitignore b/.gitignore
index 23c627e..7e83e52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
/target/
/logs/
-# Enlaces a los pesos, que se recrean con scripts/bootstrap.sh
+# Links to the weights, recreated by scripts/bootstrap.sh
/models/*
!/models/.gitkeep
+
+# Cloned voices: they are someone's voice, so they stay local.
+# Create one with scripts/clone-voice.sh.
+/assets/voices/
diff --git a/.gitmodules b/.gitmodules
index 2a8747c..b07c676 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,8 +1,8 @@
-# Los tres motores, fijados al commit contra el que se midió el sistema.
+# The three engines, pinned to the commit the system was measured against.
#
-# «ignore = dirty» es a propósito: scripts/bootstrap.sh aplica sobre cada uno
-# el parche de vendor/patches/, así que su árbol de trabajo queda modificado
-# siempre. Los cambios se versionan ahí, no como commits del submódulo.
+# «ignore = dirty» is on purpose: scripts/bootstrap.sh applies the patch from
+# vendor/patches/ on each of them, so their working tree is always modified.
+# The changes are versioned there, not as submodule commits.
[submodule "canary-rs"]
path = vendor/canary-rs
url = https://github.com/mmende/canary-rs.git
diff --git a/Cargo.toml b/Cargo.toml
index fc2b030..788c0ef 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,10 +9,10 @@ members = [
"crates/asist-tools",
"crates/asist-app",
]
-# Los submódulos son dependencias por ruta, no miembros del workspace: sin esto
-# cargo los adopta por estar dentro del directorio, y `cargo test` acabaría
-# ejecutando las pruebas de integración de canary-rs, que esperan un modelo en
-# su propia ruta por defecto y fallan aquí.
+# The submodules are path dependencies, not workspace members: without this
+# cargo adopts them for being inside the directory, and `cargo test` would end
+# up running canary-rs's integration tests, which expect a model at their own
+# default path and fail here.
exclude = ["vendor"]
[workspace.package]
@@ -31,10 +31,10 @@ asist-tts = { path = "crates/asist-tts" }
asist-tools = { path = "crates/asist-tools" }
canary-rs = { path = "vendor/canary-rs", default-features = false, features = ["cpu", "ort-defaults"] }
-# canary-rs pide "2.0.0-rc.12", que en semver de prelanzamientos también admite
-# rc.13 — y ahí el módulo CoreML pasó a estar tras una feature, así que sus
-# reexportaciones incondicionales dejan de compilar. Se fija la versión contra
-# la que canary-rs se construye de verdad.
+# canary-rs asks for "2.0.0-rc.12", which under prerelease semver also admits
+# rc.13, and there the CoreML module moved behind a feature, so its
+# unconditional re-exports stop compiling. Pin the version canary-rs really
+# builds against.
ort = "=2.0.0-rc.12"
anyhow = "1"
diff --git a/LICENSE b/LICENSE
index a8836d2..acdf183 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 Elvis Claros
+Copyright (c) 2026 Elvis Claros Castro
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.es.md b/README.es.md
new file mode 100644
index 0000000..3bd21ac
--- /dev/null
+++ b/README.es.md
@@ -0,0 +1,217 @@
+# asist-p — asistente de voz local
+
+[English](README.md)
+
+Escucha por el micrófono, piensa y contesta hablando. Todo corre en la
+máquina; sólo la búsqueda en internet, que es opcional, sale a la red.
+
+Une tres motores que ya existían —**Canary** para oír, **llama.cpp** para
+pensar y **qwentts.cpp** para hablar— en un pipeline de hilos y canales en
+Rust donde ninguna etapa espera a la siguiente.
+
+```
+ micrófono ──muestras──> segmentador ──intervención──> ASR ──texto──┐
+ (cpal, tiempo real) (VAD, turnos) (Canary) │
+ v
+ altavoz <──muestras── síntesis <──frases── conversación <──────────┘
+ (cpal, anillo) (qwentts) (llama.cpp + herramientas)
+```
+
+Cada caja es un hilo; cada flecha, un canal. Dos decisiones sostienen el
+resto:
+
+- **Nada bloquea al que va delante.** El micrófono nunca espera al
+ reconocedor, y el modelo nunca espera al sintetizador. Quien va sobrado
+ descarta trabajo en lugar de acumular retraso.
+- **Se responde por frases, no por respuestas.** Cada frase sale hacia el
+ sintetizador en cuanto se cierra, así que el asistente empieza a hablar
+ mientras el modelo sigue escribiendo. Es lo que separa medio segundo de
+ cinco.
+
+## Puesta en marcha
+
+```bash
+git clone --recursive https://git.all.ar/pub/asist-p.git && cd asist-p
+scripts/bootstrap.sh # submódulos, parches, binarios y modelos
+cargo run --release -- check # comprueba que está todo en su sitio
+cargo run --release -- run # a hablar
+```
+
+`bootstrap.sh` es idempotente y no copia modelos: los enlaza desde donde ya
+estén (`~/GIT-MIRRO`, `~/HF`). Si no encuentra los motores construidos, los
+construye, y eso sí tarda.
+
+```bash
+cargo run --release -- devices # listar dispositivos de audio
+cargo run --release -- run --barge-in # permitir cortar al asistente
+cargo run --release -- run --shell # habilitar órdenes del sistema
+cargo run --release -- run --no-manage # no lanzar los servidores
+```
+
+Durante el desarrollo conviene dejar los servidores levantados aparte —cargar
+los modelos cuesta más de un minuto— y reiniciar sólo el binario de Rust:
+
+```bash
+scripts/servers.sh start
+cargo run --release -- run --no-manage
+```
+
+## Cómo está organizado
+
+| Crate | De qué se ocupa |
+|---|---|
+| `asist-core` | Configuración, eventos, errores, cliente HTTP, telemetría y el registro de herramientas |
+| `asist-audio` | Captura y reproducción con cpal, detección de voz, troceado en turnos |
+| `asist-asr` | Canary en ONNX: ventana deslizante y transcripción final |
+| `asist-llm` | llama-server: streaming SSE, historial, llamadas a herramientas |
+| `asist-tts` | qwentts: síntesis en streaming y voces clonadas |
+| `asist-tools` | Buscar en internet, mirar por la cámara y ver la pantalla |
+| `asist-app` | Supervisor de procesos, orquestador y binario `asistente` |
+
+Los tres motores viven en `vendor/` como submódulos fijados a un commit
+concreto. Los cambios locales sobre ellos están en `vendor/patches/`, y
+`bootstrap.sh` los aplica: sin ellos el reconocedor no se comporta como el
+que se midió.
+
+## Configuración
+
+Todo está en [`config/asistente.toml`](config/asistente.toml), comentado. Los
+valores que llevan una cifra en el comentario salen de una medición concreta,
+no de una suposición; el detalle está en
+[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).
+
+Lo que más se toca:
+
+```toml
+[vad]
+silence_hold = 0.8 # cuánto silencio cierra tu intervención
+barge_in = false # cortar al asistente hablando encima
+
+[general]
+history_turns = 8 # memoria de la conversación
+system_prompt = "..." # carácter y estilo
+
+[tools]
+shell = false # ejecutar órdenes del sistema
+```
+
+## Interrumpir y el eco del altavoz
+
+Por defecto el asistente es **media dúplex**: mientras habla, el micrófono
+está cerrado. No es pereza: con altavoces abiertos el micrófono se oye a sí
+mismo, el asistente se transcribe y se responde solo.
+
+Con auriculares, `--barge-in` deja hablar encima para cortarlo. El umbral se
+eleva (`vad.barge_in_factor`) para que el eco no baste y una voz de verdad sí.
+
+## Latencia
+
+Un turno real, medido de punta a punta:
+
+| Etapa | Tiempo |
+|---|---|
+| Reconocimiento (2,5 s de voz) | 200 ms |
+| Modelo, primer token | 1660 ms |
+| Modelo, resto | 1362 ms |
+| Herramientas | 1 ms |
+| **Del final de tu frase al primer audio** | **3213 ms** |
+
+El binario mide esto en cada turno y señala la etapa más lenta. Tres ajustes
+de configuración valieron más que cualquier cambio de código:
+
+- El TTS decodificaba en bloques de 24 s: **4948 ms → 585 ms** al primer audio.
+- La plantilla del modelo dejaba `<think>` abierto: **8630 ms → 413 ms** al
+ primer token.
+- El prompt de estilo anulaba las llamadas a herramientas: **0/8 → 8/8**.
+- La cámara capturaba a 720p: **7,8 s → 2,9 s** por imagen a 640x480.
+- Las herramientas se ejecutaban en silencio: **~8,4 s → 4,1 s** hasta oír algo.
+- La pantalla a 640 px inventaba el texto: **1 de 3 aciertos → 3 de 3** a 1280 px.
+
+Todos, con el porqué y cómo se midieron, en
+[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).
+
+## Herramientas
+
+El asistente puede llamar a funciones. Vienen cuatro:
+
+| Herramienta | Qué hace | Coste medido |
+|---|---|---|
+| `hora_actual` | Fecha y hora del sistema | 0 ms |
+| `buscar_en_internet` | Busca y resume | 0,5–3,6 s |
+| `mirar_por_la_camara` | Hace una foto y responde sobre lo que ve | 4,1–5,2 s |
+| `mirar_la_pantalla` | Captura la pantalla y responde sobre lo que hay | 7,6–12,4 s |
+| `ejecutar_comando` | Órdenes del sistema, **apagada** por defecto | — |
+
+Mientras una herramienta trabaja, el asistente dice «Déjame que lo busque» o
+«Voy a mirar». No es adorno: sin eso el turno pasa seis segundos en silencio y
+se lee como un cuelgue. Con el acuse, el primer audio llega en 3–4 s.
+
+**Buscar** admite tres buscadores: `tavily` (con clave, devuelve una respuesta
+ya redactada), `ddgs` (sin clave, la misma librería que hay bajo
+`duckduckgo-mcp`) y `searxng`. El de comando ejecuta un guion que escriba JSON,
+así que meter otro es escribir un guion, no tocar Rust.
+
+**Ver** —cámara y pantalla— aprovecha que el servidor ya carga el proyector
+multimodal: el mismo modelo que conversa describe la imagen. Ni la foto ni la
+captura se guardan en disco, y no entran en el historial.
+
+La pantalla usa 1280 px y la cámara 640, y la diferencia importa: una escena se
+entiende, pero un texto hay que leerlo. Medido, a 640 px el modelo no dice que
+no lee la pantalla, **se inventa lo que pone** —contestó que la reunión era «a
+las 10:00» cuando ponía 15:30—. Por eso el triple de coste.
+
+`ejecutar_comando` está apagada 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.
+Cuando se enciende, sólo pasan las órdenes de una lista blanca, sin shell que
+interprete metacaracteres y con un plazo máximo.
+
+Añadir una propia son unas veinte líneas:
+[docs/EXTENDER.md](docs/EXTENDER.md).
+
+## Pruebas
+
+Más de cien pruebas unitarias que no necesitan ni modelos ni micrófono, y
+pruebas de integración contra los servidores de verdad.
+
+```bash
+cargo test # unitarias, sin modelos
+scripts/servers.sh start
+cargo test --release -p asist-app --test integration -- --nocapture
+cargo test --release -p asist-app --test integration -- --ignored # bucle completo
+```
+
+Las de integración se saltan solas si no hay servidores escuchando, y se
+turnan la GPU con un mutex: los dos servidores comparten una tarjeta de 4 GB y
+en paralelo miden contención en vez de latencia.
+
+La prueba marcada `--ignored` cierra el bucle sin micrófono: sintetiza una
+frase y comprueba que el reconocedor la recupera.
+
+## Requisitos
+
+- Rust 1.85 o posterior
+- CUDA para los motores en C++ (funcionan en CPU, pero muy justos)
+- PipeWire o ALSA
+- ~6 GB de disco para los modelos
+- `ffmpeg` para la cámara y para reducir las capturas
+- Para la pantalla: `grim` (Wayland) o `maim`/ImageMagick (X11)
+- Para buscar: una clave de Tavily en `$TAVILY_API_KEY`, o `uv tool install ddgs`
+
+## Voz
+
+El repositorio no trae ninguna voz clonada: una voz es de alguien. Para que el
+asistente hable con una, grabá unos segundos de audio con su transcripción y
+extraé los latentes:
+
+```bash
+scripts/clone-voice.sh grabacion.wav transcripcion.txt
+```
+
+Quedan en `assets/voices/` (fuera de git). Sin voz clonada, quitá la sección
+`[tts.reference]` de `config/asistente.toml` y el asistente usa una voz del
+propio modelo.
+
+## Licencia
+
+MIT
+
diff --git a/README.md b/README.md
index bb7c407..6832f32 100644
--- a/README.md
+++ b/README.md
@@ -1,197 +1,224 @@
-# asist-p — asistente de voz local
+# asist-p — local voice assistant
-Escucha por el micrófono, piensa y contesta hablando. Todo en la máquina: no
-sale un byte a internet.
+[Español](README.es.md)
-Une tres motores que ya existían —**Canary** para oír, **llama.cpp** para
-pensar y **qwentts.cpp** para hablar— en un pipeline de hilos y canales en
-Rust donde ninguna etapa espera a la siguiente.
+It listens through the microphone, thinks and answers out loud. Everything
+runs on the machine; only the optional web search goes online.
+
+It joins three existing engines (**Canary** to hear, **llama.cpp** to think
+and **qwentts.cpp** to speak) in a Rust pipeline of threads and channels
+where no stage waits for the next.
```
- micrófono ──muestras──> segmentador ──intervención──> ASR ──texto──┐
- (cpal, tiempo real) (VAD, turnos) (Canary) │
- v
- altavoz <──muestras── síntesis <──frases── conversación <──────────┘
- (cpal, anillo) (qwentts) (llama.cpp + herramientas)
+ microphone ──samples──> segmenter ──utterance──> ASR ──text──┐
+ (cpal, real time) (VAD, turns) (Canary) │
+ v
+ speaker <──samples── synthesis <──sentences── conversation <─┘
+ (cpal, ring) (qwentts) (llama.cpp + tools)
```
-Cada caja es un hilo; cada flecha, un canal. Dos decisiones sostienen el
-resto:
+Each box is a thread; each arrow, a channel. Two decisions carry the rest:
+
+- **Nothing blocks the stage ahead.** The microphone never waits for the
+ recognizer, and the model never waits for the synthesizer. Whoever has
+ spare capacity drops work instead of accumulating delay.
+- **Answers go out per sentence, not per answer.** Each sentence goes to the
+ synthesizer as soon as it closes, so the assistant starts talking while the
+ model is still writing. That is what separates half a second from five.
-- **Nada bloquea al que va delante.** El micrófono nunca espera al
- reconocedor, y el modelo nunca espera al sintetizador. Quien va sobrado
- descarta trabajo en lugar de acumular retraso.
-- **Se responde por frases, no por respuestas.** Cada frase sale hacia el
- sintetizador en cuanto se cierra, así que el asistente empieza a hablar
- mientras el modelo sigue escribiendo. Es lo que separa medio segundo de
- cinco.
+The assistant speaks Spanish: its prompts, tool descriptions and spoken
+acknowledgements are in Spanish on purpose. The code is in English.
-## Puesta en marcha
+## Getting started
```bash
-git clone --recursive <este-repo> && cd asist-p
-scripts/bootstrap.sh # submódulos, parches, binarios y modelos
-cargo run --release -- check # comprueba que está todo en su sitio
-cargo run --release -- run # a hablar
+git clone --recursive https://git.all.ar/pub/asist-p.git && cd asist-p
+scripts/bootstrap.sh # submodules, patches, binaries and models
+cargo run --release -- check # check that everything is in place
+cargo run --release -- run # start talking
```
-`bootstrap.sh` es idempotente y no copia modelos: los enlaza desde donde ya
-estén (`~/GIT-MIRRO`, `~/HF`). Si no encuentra los motores construidos, los
-construye, y eso sí tarda.
+`bootstrap.sh` is idempotent and does not copy models: it links them from
+wherever they already are (`$ASIST_MIRROR`, default `~/GIT-MIRRO`, and
+`$ASIST_HF`, default `~/HF`). If it cannot find the engines built, it builds
+them, and that does take a while.
```bash
-cargo run --release -- devices # listar dispositivos de audio
-cargo run --release -- run --barge-in # permitir cortar al asistente
-cargo run --release -- run --shell # habilitar órdenes del sistema
-cargo run --release -- run --no-manage # no lanzar los servidores
+cargo run --release -- devices # list audio devices
+cargo run --release -- run --barge-in # allow interrupting the assistant
+cargo run --release -- run --shell # enable system commands
+cargo run --release -- run --no-manage # do not start the servers
```
-Durante el desarrollo conviene dejar los servidores levantados aparte —cargar
-los modelos cuesta más de un minuto— y reiniciar sólo el binario de Rust:
+While developing, keep the servers running separately (loading the models
+takes more than a minute) and restart only the Rust binary:
```bash
-scripts/servidores.sh arrancar
+scripts/servers.sh start
cargo run --release -- run --no-manage
```
-## Cómo está organizado
+## Layout
-| Crate | De qué se ocupa |
+| Crate | What it does |
|---|---|
-| `asist-core` | Configuración, eventos, errores, cliente HTTP, telemetría y el registro de herramientas |
-| `asist-audio` | Captura y reproducción con cpal, detección de voz, troceado en turnos |
-| `asist-asr` | Canary en ONNX: ventana deslizante y transcripción final |
-| `asist-llm` | llama-server: streaming SSE, historial, llamadas a herramientas |
-| `asist-tts` | qwentts: síntesis en streaming y voces clonadas |
-| `asist-tools` | Buscar en internet, mirar por la cámara y ver la pantalla |
-| `asist-app` | Supervisor de procesos, orquestador y binario `asistente` |
+| `asist-core` | Configuration, events, errors, HTTP client, telemetry and the tool registry |
+| `asist-audio` | Capture and playback with cpal, voice detection, splitting into turns |
+| `asist-asr` | Canary on ONNX: sliding window and final transcription |
+| `asist-llm` | llama-server: SSE streaming, history, tool calls |
+| `asist-tts` | qwentts: streaming synthesis and cloned voices |
+| `asist-tools` | Web search, looking through the camera and at the screen |
+| `asist-app` | Process supervisor, orchestrator and the `asistente` binary |
-Los tres motores viven en `vendor/` como submódulos fijados a un commit
-concreto. Los cambios locales sobre ellos están en `vendor/patches/`, y
-`bootstrap.sh` los aplica: sin ellos el reconocedor no se comporta como el
-que se midió.
+The three engines live in `vendor/` as submodules pinned to a specific
+commit. The local changes on them are in `vendor/patches/`, and
+`bootstrap.sh` applies them: without them the recognizer does not behave like
+the one that was measured.
-## Configuración
+## Configuration
-Todo está en [`config/asistente.toml`](config/asistente.toml), comentado. Los
-valores que llevan una cifra en el comentario salen de una medición concreta,
-no de una suposición; el detalle está en
-[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).
+Everything is in [`config/asistente.toml`](config/asistente.toml), with
+comments. Values that carry a number in their comment come from a specific
+measurement, not an assumption; the details are in
+[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md) (in Spanish).
-Lo que más se toca:
+The most tweaked settings:
```toml
[vad]
-silence_hold = 0.8 # cuánto silencio cierra tu intervención
-barge_in = false # cortar al asistente hablando encima
+silence_hold = 0.8 # how much silence ends your utterance
+barge_in = false # interrupt the assistant by talking over it
[general]
-history_turns = 8 # memoria de la conversación
-system_prompt = "..." # carácter y estilo
+history_turns = 8 # conversation memory
+system_prompt = "..." # personality and style
[tools]
-shell = false # ejecutar órdenes del sistema
+shell = false # run system commands
```
-## Interrumpir y el eco del altavoz
+## Interrupting and speaker echo
-Por defecto el asistente es **media dúplex**: mientras habla, el micrófono
-está cerrado. No es pereza: con altavoces abiertos el micrófono se oye a sí
-mismo, el asistente se transcribe y se responde solo.
+By default the assistant is **half duplex**: while it talks, the microphone
+is closed. That is not laziness: with open speakers the microphone hears
+itself, and the assistant transcribes itself and answers itself.
-Con auriculares, `--barge-in` deja hablar encima para cortarlo. El umbral se
-eleva (`vad.barge_in_factor`) para que el eco no baste y una voz de verdad sí.
+With headphones, `--barge-in` lets you talk over it to interrupt. The
+threshold is raised (`vad.barge_in_factor`) so the echo is not enough but a
+real voice is.
-## Latencia
+## Latency
-Un turno real, medido de punta a punta:
+A real turn, measured end to end:
-| Etapa | Tiempo |
+| Stage | Time |
|---|---|
-| Reconocimiento (2,5 s de voz) | 200 ms |
-| Modelo, primer token | 1660 ms |
-| Modelo, resto | 1362 ms |
-| Herramientas | 1 ms |
-| **Del final de tu frase al primer audio** | **3213 ms** |
+| Recognition (2.5 s of speech) | 200 ms |
+| Model, first token | 1660 ms |
+| Model, rest | 1362 ms |
+| Tools | 1 ms |
+| **From the end of your sentence to the first audio** | **3213 ms** |
-El binario mide esto en cada turno y señala la etapa más lenta. Tres ajustes
-de configuración valieron más que cualquier cambio de código:
+The binary measures this on every turn and flags the slowest stage. A few
+configuration settings were worth more than any code change:
-- El TTS decodificaba en bloques de 24 s: **4948 ms → 585 ms** al primer audio.
-- La plantilla del modelo dejaba `<think>` abierto: **8630 ms → 413 ms** al
- primer token.
-- El prompt de estilo anulaba las llamadas a herramientas: **0/8 → 8/8**.
-- La cámara capturaba a 720p: **7,8 s → 2,9 s** por imagen a 640x480.
-- Las herramientas se ejecutaban en silencio: **~8,4 s → 4,1 s** hasta oír algo.
-- La pantalla a 640 px inventaba el texto: **1 de 3 aciertos → 3 de 3** a 1280 px.
+- The TTS decoded in 24 s blocks: **4948 ms → 585 ms** to first audio.
+- The model template left `<think>` open: **8630 ms → 413 ms** to first token.
+- The style prompt suppressed tool calls: **0/8 → 8/8**.
+- The camera captured at 720p: **7.8 s → 2.9 s** per image at 640x480.
+- Tools ran in silence: **~8.4 s → 4.1 s** until something is heard.
+- The screen at 640 px made the text up: **1 of 3 right → 3 of 3** at 1280 px.
-Los tres, con el porqué y cómo se midieron, en
-[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md).
+All of them, with the reasons and how they were measured, are in
+[docs/RENDIMIENTO.md](docs/RENDIMIENTO.md) (in Spanish).
-## Herramientas
+## Tools
-El asistente puede llamar a funciones. Vienen cuatro:
+The assistant can call functions. It ships with these:
-| Herramienta | Qué hace | Coste medido |
+| Tool | What it does | Measured cost |
|---|---|---|
-| `hora_actual` | Fecha y hora del sistema | 0 ms |
-| `buscar_en_internet` | Busca y resume | 0,5–3,6 s |
-| `mirar_por_la_camara` | Hace una foto y responde sobre lo que ve | 4,1–5,2 s |
-| `mirar_la_pantalla` | Captura la pantalla y responde sobre lo que hay | 7,6–12,4 s |
-| `ejecutar_comando` | Órdenes del sistema, **apagada** por defecto | — |
-
-Mientras una herramienta trabaja, el asistente dice «Déjame que lo busque» o
-«Voy a mirar». No es adorno: sin eso el turno pasa seis segundos en silencio y
-se lee como un cuelgue. Con el acuse, el primer audio llega en 3–4 s.
-
-**Buscar** admite tres buscadores: `tavily` (con clave, devuelve una respuesta
-ya redactada), `ddgs` (sin clave, la misma librería que hay bajo
-`duckduckgo-mcp`) y `searxng`. El de comando ejecuta un guion que escriba JSON,
-así que meter otro es escribir un guion, no tocar Rust.
-
-**Ver** —cámara y pantalla— aprovecha que el servidor ya carga el proyector
-multimodal: el mismo modelo que conversa describe la imagen. Ni la foto ni la
-captura se guardan en disco, y no entran en el historial.
+| `hora_actual` | System date and time | 0 ms |
+| `buscar_en_internet` | Searches and summarizes | 0.5–3.6 s |
+| `mirar_por_la_camara` | Takes a photo and answers about what it sees | 4.1–5.2 s |
+| `mirar_la_pantalla` | Captures the screen and answers about what is on it | 7.6–12.4 s |
+| `ejecutar_comando` | System commands, **off** by default | — |
+
+The tool names are Spanish because the model reads them in a Spanish
+conversation.
+
+While a tool works, the assistant says «Déjame que lo busque» or «Voy a
+mirar». It is not decoration: without it the turn spends six seconds in
+silence and reads as a hang. With the acknowledgement, the first audio
+arrives in 3–4 s.
+
+**Search** supports three backends: `tavily` (with a key, returns an already
+written answer), `ddgs` (no key, the same library behind `duckduckgo-mcp`)
+and `searxng`. The command backend runs a script that writes JSON, so adding
+another engine means writing a script, not touching Rust.
+
+**Vision** (camera and screen) takes advantage of the server already loading
+the multimodal projector: the same model that converses describes the image.
+Neither the photo nor the capture is saved to disk, and they do not enter the
+history.
+
+The screen uses 1280 px and the camera 640, and the difference matters: a
+scene can be understood, but text has to be read. Measured, at 640 px the
+model does not say it cannot read the screen, **it makes up what it says**
+(it answered that the meeting was «at 10:00» when it said 15:30). Hence the
+threefold cost.
+
+`ejecutar_comando` is off on purpose: giving a shell to a model that obeys
+what it hears through the microphone is a change of security posture. When it
+is on, only allowlisted commands get through, with no shell to interpret
+metacharacters and with a deadline.
+
+Adding your own takes about twenty lines: [docs/EXTENDER.md](docs/EXTENDER.md)
+(in Spanish).
+
+## Voice
+
+The repository ships no cloned voice: a voice belongs to someone. To make the
+assistant speak with one, record a few seconds of audio with its transcript
+and extract the latents:
-La pantalla usa 1280 px y la cámar