Skip to content
Gabriel Debarnot
Back to home

A local-first AI assistant orchestrator.

Hermes

2025 — presentSolo — architecture & implementation
  • Qwen 2.5 14B
  • ROCm
  • Claude Haiku
  • FastAPI
  • WebSocket
  • faster-whisper
  • Piper TTS
  • openWakeWord
  • systemd
NFC · /triggeropenWakeWordwake wordfaster-whisperSTT · FRLLMorchestratorPiper TTSspeechQwen 2.5 14B · ROCmClaude Haiku · fallback

Problem

Private by default, hands-free, always on: a voice assistant whose brain runs on my own hardware, not someone else's cloud.

I wanted an assistant that is genuinely mine. Most cloud assistants send every utterance and every intent to someone else's servers, which rules them out for anything I actually care about. My goal was a system where the model runs locally, voice control is reliable enough to trust hands-free, and the cloud is an opt-in fallback rather than the default path.

Constraints

A 14B model on a consumer AMD GPU, a strict end-to-end latency budget, and services that must survive reboots unattended.

  • Consumer hardware, not datacenter. The model runs on an AMD RX 7900 XTX via ROCm, which is noticeably less mature than the CUDA ecosystem — driver quirks, fewer prebuilt wheels, and more manual tuning to get Qwen 2.5 14B stable at ~63 tok/s.
  • Latency budget. A hands-free loop only feels usable if wake-word, transcription, inference, and speech synthesis stay under a tight combined budget, so every stage had to be fast on its own.
  • Unattended operation. This isn't a script I babysit — it has to survive reboots and run as background services with no terminal open.

Approach

A FastAPI orchestrator chains fully local voice components into systemd services, with one rule: 95% reliability on a few workflows before adding more.

The backend is FastAPI, exposing both REST and a WebSocket channel for streaming interactions, with a modular tool registry so capabilities can be added without touching the core. The voice pipeline is a chain of local components: openWakeWord for activation, faster-whisper for French STT, the LLM for reasoning, and Piper for TTS — each wired up as a systemd user service so the whole thing comes up on login. Qwen 2.5 14B runs locally with Claude Haiku as a cloud fallback for the cases where the local model isn't confident or capable enough. NFC tags hit a /trigger endpoint backed by an intent routing table that deliberately separates direct intents (fixed action, no ambiguity) from contextual intents (need interpretation). My guiding rule was reliability over breadth: reach 95% reliability on three or four workflows before adding a fifth.

What shipped

A daily driver, not a demo: voice end to end, NFC automations, and a cloud that only steps in when the local model taps out.

A working local-first orchestrator: hands-free voice control end to end, NFC-triggered automations via tap, and a graceful degradation path to the cloud when the local model falls short. The tool registry made it cheap to add new capabilities, and the systemd services mean it's just there — no manual startup. The discipline of deepening a handful of workflows instead of sprawling paid off in something I actually trust.

What I'd do differently

The intent router should have been data, not code — and the boring reliability work constantly needs defending from the lure of new features.

The intent routing table grew organically, one if at a time, and it's now the part of the system I'm least happy with. What started as a clean split between direct and contextual intents has accumulated special cases that live in code rather than data — which means adding an intent is an edit-and-redeploy instead of a config change. I'd redesign it as a declarative schema: intents, their triggers, their routing, and their fallbacks described as data the router interprets, not as branching logic. The other honest tension is the pull toward adding a fifth and sixth workflow when the first four aren't yet at 95% — the interesting new thing is always more tempting than the boring reliability work, and I have to keep talking myself back into the boring work.