aboutsummaryrefslogtreecommitdiffstats

asist-p — local voice assistant

Español

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.

  microphone ──samples──> segmenter ──utterance──> ASR ──text──┐
  (cpal, real time)       (VAD, turns)             (Canary)    │
                                                                v
  speaker <──samples── synthesis <──sentences── conversation <─┘
  (cpal, ring)          (qwentts)                (llama.cpp + tools)

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.

The assistant speaks Spanish: its prompts, tool descriptions and spoken acknowledgements are in Spanish on purpose. The code is in English.

Getting started

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 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.

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

While developing, keep the servers running separately (loading the models takes more than a minute) and restart only the Rust binary:

scripts/servers.sh start
cargo run --release -- run --no-manage

Layout

Crate What it does
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

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.

Configuration

Everything is in 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 (in Spanish).

The most tweaked settings:

[vad]
silence_hold = 0.8     # how much silence ends your utterance
barge_in = false       # interrupt the assistant by talking over it

[general]
history_turns = 8      # conversation memory
system_prompt = "..."  # personality and style

[tools]
shell = false          # run system commands

Interrupting and speaker echo

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.

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.

Latency

A real turn, measured end to end:

Stage Time
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

The binary measures this on every turn and flags the slowest stage. A few configuration settings were worth more than any code change:

  • 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.

All of them, with the reasons and how they were measured, are in docs/RENDIMIENTO.md (in Spanish).

Tools

The assistant can call functions. It ships with these:

Tool What it does Measured cost
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 (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:

scripts/clone-voice.sh recording.wav transcript.txt

They end up in assets/voices/ (outside git). Without a cloned voice, remove the [tts.reference] section from config/asistente.toml and the assistant uses one of the model's own voices.

Tests

More than a hundred unit tests that need neither models nor a microphone, plus integration tests against the real servers.

cargo test                                    # unit tests, no models
scripts/servers.sh start
cargo test --release -p asist-app --test integration -- --nocapture
cargo test --release -p asist-app --test integration -- --ignored  # full loop

The integration tests skip themselves when no server is listening, and they take turns on the GPU through a mutex: both servers share a 4 GB card, and in parallel they measure contention instead of latency.

The --ignored test closes the loop without a microphone: it synthesizes a sentence and checks that the recognizer gets it back.

Requirements

  • Rust 1.85 or later
  • CUDA for the C++ engines (they run on CPU, but barely)
  • PipeWire or ALSA
  • ~6 GB of disk for the models
  • ffmpeg for the camera and to scale down captures
  • For the screen: grim (Wayland) or maim/ImageMagick (X11)
  • For search: a Tavily key in $TAVILY_API_KEY, or uv tool install ddgs

License

MIT