Server Configuration¶
File: configs/server.yaml
Command: kenzy-server [config_path]
The server is the central WebSocket hub. It accepts connections from room nodes, runs the STT → LLM → TTS pipeline, and streams audio responses back. Each downstream service is optional — omit its url to disable that stage.
Full reference¶
| Key | Default | Description |
|---|---|---|
host |
"0.0.0.0" |
Bind address. 0.0.0.0 listens on all interfaces. |
port |
8765 |
WebSocket port |
log_level |
"info" |
Log verbosity |
Discovery and config-pull¶
| Key | Default | Description |
|---|---|---|
discovery.enabled |
true |
Advertise the server as _kenzy._tcp over mDNS so nodes auto-discover it without a hardcoded server_url |
discovery.instance |
"kenzy-server" |
mDNS instance name |
discovery.token |
(generated by kenzy-init) |
Shared secret required in each node's hello (mismatching nodes are rejected) and the service-to-service bearer. kenzy-init generates one by default and matches it in node.yaml + .env (KENZY_SERVICE_TOKEN); the dashboard shows it under Settings for copy-paste. Clear it to allow unauthenticated joins. |
node_defaults |
{} |
Node tuning defaults (wake-word thresholds, VAD timing) pushed to every node on connect. Per-node overrides live in configs/nodes/<node_id>.yaml and shallow-merge over these. |
On connect, a node's hello carries its stable node_id and its room name; the server replies with the node's effective config = node_defaults merged with configs/nodes/<node_id>.yaml. The node blocks until this first frame arrives, then builds its audio stack from it (so hardware keys — audio device, sample rates, wakeword models, sounds — apply on that first pull); a later hardware change takes effect on restart, while live-tunable keys (thresholds, VAD timing, log levels) and the room name apply immediately. The per-node file is keyed by node_id, so a node keeps its config even if its room is renamed; pre-existing room-named files migrate automatically on first connect. This is how a room device runs with a bootstrap-only local file — see Node Configuration.
Central config for backend services¶
The server is also the config authority for the backend HTTP services. It exposes an always-on endpoint GET /config/<service> on the node WebSocket port (it runs whenever the server runs, independent of the dashboard), returning that service's effective config = the packaged default deep-merged with the server-owned override at configs/services/<service>.yaml. Secret-like keys are stripped, so secrets never leave the server — they stay in each host's environment / .env.
At boot, kenzy-stt/kenzy-tts/kenzy-llm/kenzy-speaker discover the server the same way a node does (mDNS, or an explicit KENZY_SERVER_URL), pull their config from this endpoint, and block with retry/backoff until the server answers — so the server must come up first (set After=kenzy-server in systemd units; the installer does this). The endpoint is gated by the service-to-service bearer (discovery.token / KENZY_SERVICE_TOKEN) when one is set. Each service also exposes a token-protected POST /restart that re-execs it to re-pull fresh config. Passing an explicit config path to a service (e.g. kenzy-stt configs/stt.yaml) bypasses the pull and loads locally — a dev/offline escape hatch.
Edit it all from the dashboard's Services tab: it reads each service's secret-stripped effective config, writes your changes to configs/services/<service>.yaml on the server, and restarts the service to apply. Secrets stay in the service host's environment and are never shown or stored.
Announce endpoint¶
The server exposes an always-on GET /announce on the node WebSocket port so external automations (e.g. Home Assistant) and scripts can make Kenzy speak in your rooms:
GET http://<server>:8765/announce?text=Dinner%20is%20ready&rooms=kitchen,office
Authorization: Bearer <discovery.token / KENZY_SERVICE_TOKEN>
text is required; rooms is an optional comma-separated list of room names (omit for every room). Returns {"announced": <node count>, …}. It must be a GET with query parameters (the websockets HTTP hook only accepts GET and exposes no request body), gated by the service-to-service bearer when one is configured.
For a ready-to-use Home Assistant rest_command, see Home Assistant Integration → Calling Kenzy from Home Assistant.
Dashboard¶
Opt-in web fleet manager served by kenzy-server. Off by default; when disabled nothing is wired up (no route, no overhead). When enabled it provides a live fleet/health view, a per-node config editor (with room rename), node controls (trigger/stop/restart), TTS announcements, a log viewer, and a settings page. See the Dashboard guide for the full walkthrough.
| Key | Default | Description |
|---|---|---|
dashboard.enabled |
false |
Master switch. false ⇒ nothing below is mounted. |
dashboard.bind |
"127.0.0.1" |
Listener address — keep it on localhost or the LAN; do not port-forward it (login is plaintext HTTP) |
dashboard.port |
8770 |
Dashboard HTTP port (separate from the node WS port) |
dashboard.auth.username / dashboard.auth.password_hash |
admin / (hash of password) |
Browser login. Change it with the server-only kenzy-passwd CLI (or the dashboard's Settings page); never edit the hash by hand. |
dashboard.auth_token |
null |
Optional bearer token for API/CLI clients (the browser uses the login cookie, not this) |
dashboard.controls |
false |
Enable mutating actions — config edits, room rename, trigger/stop/restart, announcements. false ⇒ read-only. |
dashboard.logs |
false |
Enable the pull-based log viewer (server, services, and per-node buffers) and the Activity tab |
dashboard.allowed_hosts |
[] |
Optional list of hostnames the dashboard will accept in the Host header (DNS-rebinding defense). Empty = no Host restriction; the cross-site Origin check always applies. Set it when serving under a fixed name (e.g. ["kenzy.local"]). |
Keep the dashboard off the public internet
Login runs over plaintext HTTP on a LAN bind and defaults to admin / password. Bind it to localhost or the LAN only, change the password with kenzy-passwd, and do not port-forward the dashboard port.
STT service¶
| Key | Default | Description |
|---|---|---|
stt.url |
— | URL of the kenzy-stt /transcribe endpoint. Omit or set to null to skip transcription. |
stt.timeout |
60.0 |
HTTP timeout in seconds |
Speaker identification service¶
| Key | Default | Description |
|---|---|---|
speaker.url |
— | URL of the kenzy-speaker /identify endpoint. Omit to disable speaker ID. |
speaker.timeout |
10.0 |
HTTP timeout in seconds |
speaker.unknown_speaker |
"unknown" |
Name used when no enrolled speaker is identified |
LLM service¶
| Key | Default | Description |
|---|---|---|
llm.url |
— | URL of the kenzy-llm /process endpoint. Omit to disable LLM processing. |
llm.timeout |
30.0 |
HTTP timeout in seconds |
TTS service¶
| Key | Default | Description |
|---|---|---|
tts.url |
— | URL of the kenzy-tts /speak endpoint. Omit to disable TTS. |
tts.timeout |
60.0 |
HTTP timeout in seconds |
tts.chunk_size |
4096 |
Bytes per PCM chunk streamed to the node. At 24 kHz int16 mono, 4096 bytes ≈ 85 ms of audio. |
Example¶
host: "0.0.0.0"
port: 8765
discovery:
enabled: true
instance: "kenzy-server"
# token: "change-me" # require this in every node's hello
node_defaults: # pushed to nodes on connect (config-pull)
wakeword_threshold: 0.5
silence_rms_threshold: 50
silence_ms: 400
dashboard:
enabled: false # opt-in; nothing is wired up while false
bind: "127.0.0.1"
port: 8770
stt:
url: "http://127.0.0.1:8767/transcribe"
timeout: 60.0
speaker:
url: "http://127.0.0.1:8768/identify"
timeout: 10.0
unknown_speaker: "unknown"
llm:
url: "http://127.0.0.1:8766/process"
timeout: 30.0
tts:
url: "http://127.0.0.1:8769/speak"
timeout: 60.0
chunk_size: 4096
Disabling stages
You can run a partial pipeline for development. For example, omit llm.url and tts.url to transcribe audio and log the results without generating responses.