Repository navigation
Installation
Murdock ships as a Home Assistant add-on and as a standalone container — same image, same Web UI, same database layout.
-
Settings → Add-ons → Add-on Store → ⋮ → Repositories and add
https://github.com/BobMcGlobus/Murdock. - Refresh the store, open Murdock, click Install (the image is built locally on first install — give it a few minutes).
-
Configuration tab:
-
stt_backend/upstream_uri: only seed the first speech-to-text service on the first start — e.g.upstreamandtcp://core-whisper:10300. Afterwards services are managed in the Web UI (see below), and these options have no effect. -
mqtt_enabled: leavetrue— with the Mosquitto add-on installed the broker is auto-wired (host, port, credentials), nothing to enter.
-
- Start the add-on, then Open Web UI (served through ingress — no extra port needed).
Updates appear in HA's normal update flow whenever a new version is
released; the add-on's /data volume (speakers, settings, models)
survives updates and reinstalls.
Microphone note: HA ingress serves the UI over plain HTTP, so the browser microphone (
getUserMedia) is unavailable inside the add-on panel. Use file upload or train over your voice satellite instead — the UI detects this and guides you.
Using the prebuilt multi-arch image:
services:
murdock:
image: ghcr.io/bobmcglobus/murdock:latest
container_name: murdock
ports:
- "10350:10350" # Wyoming — HA connects here
- "8099:8099" # Web UI
volumes:
- murdock_data:/app/data
- murdock_models:/app/models
environment:
- UPSTREAM_URI=tcp://<your-stt-host>:10300
# Optional MQTT bootstrap (also configurable in the Web UI):
# - MQTT_ENABLED=true
# - MQTT_HOST=192.168.1.100
# - MQTT_USERNAME=...
# - MQTT_PASSWORD=...
restart: unless-stopped
volumes:
murdock_data:
murdock_models:Or build from source: clone the repo and docker compose up -d (uses
Dockerfile.standalone). The CAM++ and Silero VAD models (~30 MB) are
baked into the image at build time and re-downloaded at startup if
missing.
- HA usually auto-discovers Murdock as a Wyoming service; otherwise add
Settings → Devices & Services → Add Integration → Wyoming Protocol
with host + port
10350. - In your Assist pipeline, select
murdock-proxyas the speech-to-text engine. - Set up the MQTT integration for the speaker sensors and context push.
Since 0.11 every transcription engine is a named service under Settings → Transcription → Speech-to-text services, set up once with its own address, key, model and optional timeout. Switching engines is a click, not retyping credentials. Four types exist:
| Type | What it is | Notes |
|---|---|---|
| Wyoming server | any Wyoming STT server (Kroko, faster-whisper, Parakeet bridge, …) | as main it streams live while you speak — lowest latency, fully local |
| Voxtral | Mistral Cloud API |
voxtral-mini-latest (fast) / voxtral-small-latest (more accurate) |
| OpenAI-compatible | any /v1/audio/transcriptions endpoint |
OpenAI (https://api.openai.com + gpt-4o-transcribe), Groq (https://api.groq.com/openai + whisper-large-v3-turbo), OpenRouter (https://openrouter.ai + openai/whisper-large-v3-turbo or microsoft/mai-transcribe-2 — the deviating request format is auto-detected), or a local server such as speaches (http://host:8000, key may stay empty) |
| Home Assistant STT entity | any stt.* entity, including Home Assistant Cloud |
see below |
Each service takes one or more roles:
- Main — exactly one. It answers Home Assistant.
- Fallback — an ordered chain, tried when the main fails. A local service at the end is the offline fallback: when a service that needs the internet cannot even be reached, the remaining internet fallbacks are skipped instead of each burning its own timeout, and the chain goes straight to the local ones. With "Ask the fallbacks when the main heard nothing" (on by default) an empty answer goes down the chain too — an unsure engine returns nothing rather than guessing.
- Patience — how long a cloud main may take before the first fallback answers instead (For all services, default 2.5 s). A local first fallback is asked in parallel right away, so its answer is ready when the patience runs out; a remote one is only asked then. A slow main therefore costs the patience, not its timeout plus the fallback — the difference between an answer and Home Assistant giving up on the turn. 0 restores waiting for the main's full timeout.
- Shadow — any number, for comparison. Shadows transcribe the same recording only after the answer has gone out, one after another, so they never compete with the main for the network or the CPU and never skew each other's timings. Every shadow's transcript, time or error appears under the utterance in the recognition log, differing readings highlighted.
Each service shows what it measured on your own utterances over the last seven days — p50, p95 and error rate — which is what the choice of main should rest on.
The Test button asks a Wyoming server or Home Assistant what it supports, without transcribing anything, and warns when your language is missing. HTTP APIs have no such call, so they get one second of silence — the smallest request that proves key, model and address, and billed like any short request. The test uses what is in the form, so it works before saving; an empty key field means the stored key.
Upgrading from an earlier version converts the old settings once: the backend becomes the main, the local fallback a Wyoming fallback, the A/B shadow a shadow — and, if "let the shadow answer when the primary heard nothing" was on, a fallback as well. The dual transcript is gone.
Cloud transcription is not a Wyoming service and has no API of its own,
so it cannot be reached the way the other backends are. It is, however,
an ordinary stt.* entity, and Home Assistant exposes every such entity
over POST /api/stt/{entity_id} — the endpoint its own frontend uses.
Add a Home Assistant STT entity service with the entity, e.g.
stt.home_assistant_cloud. It uses the URL and token already configured
under Home Assistant, so nothing else is needed.
The same route reaches every other STT entity in the instance — a Whisper add-on, anything an integration provides. One thing it will not do is point at Murdock's own entity: the request would come straight back here, and that is refused rather than allowed to recurse.
A transducer (RNN-T/TDT) has no re-decode cascade and no way to
hallucinate in silence, so its latency is bounded in a way an
encoder-decoder's is not — for an assistant the ceiling matters more
than the average. ghcr.io/achetronic/parakeet
serves parakeet-tdt-0.6b-v3 as int8 ONNX behind
/v1/audio/transcriptions:
docker run -d --name parakeet -p 5092:5092 ghcr.io/achetronic/parakeet:latestDo not mount /models — the models are baked into the image, and a
bind mount hides them (config.json: no such file or directory). Then
add an OpenAI-compatible service at http://host:5092, model
parakeet-tdt-0.6b-v3, key empty unless you set PARAKEET_API_KEY.
Measured on CPU with 3.4 s of audio: ~260–290 ms, spread 1.12× between best and worst run.
Home Assistant forwards whatever the pipeline is configured with, often
a full tag like de-DE, while these endpoints document ISO-639-1. Tags
are reduced to their primary subtag automatically, and the STT
language setting fills in when Home Assistant sends nothing at all.
This matters more than it sounds: an endpoint that cannot place the language does not fail, it guesses — and nearly all of them guess English. A German sentence then comes back as fluent English nonsense.
Full detail, including the automatic name correction added in 0.8.3: Transcript Correction.
Two independently toggleable weapons against systematic misrecognitions (custom names like "Fehenlichter"):
-
Custom vocabulary — your terms are sent to whisper-family cloud
backends as the OpenAI
promptfield, fixing them at the source. Not supported by Voxtral/OpenRouter (silently skipped there); for a local wyoming-faster-whisper setinitial_prompton that container. -
Correction dictionary — one entry per line:
fehlende Lichter -> Fehenlichterreplaces deterministically (keeps HA's local intent matching working),Bad-Lightstrip ~> Bed-Lightstripannotates the transcript with[oder: …]for an LLM agent. Case-insensitive, word-boundary matching;#comments.
Everything except these is configured in the Web UI and persisted in the database:
| Variable | Default | Description |
|---|---|---|
LISTEN_URI |
tcp://0.0.0.0:10350 |
Wyoming bind address |
UPSTREAM_URI |
tcp://localhost:10300 |
seeds the first Wyoming service on a fresh install |
WEB_PORT |
8099 |
Web UI port |
STT_BACKEND |
upstream |
kind of the first service on a fresh install (upstream, voxtral, openai, ha) |
MQTT_* |
— | broker bootstrap (host, port, username, password) |
ADVERTISED_LANGUAGES |
— | e.g. de,en; empty = auto-detect |
LOG_LEVEL |
info |
debug / info / warning / error
|