Skip to content

Node Configuration

File: configs/node.yaml
Command: kenzy-node [config_path]

The node service runs on each room device. It captures microphone audio, detects the wake word, streams PCM to the server, and plays back TTS responses.

node.yaml is bootstrap-only: it holds just what the node needs to start up, log, and reach the server (log_level/verbose, server_url/discovery, and a stable node_id). On every boot the node pulls its full operational config from the server — audio device, sample rates, wakeword models/threshold/VAD, sounds, VAD timing, and its room name — and does not initialize audio until that config arrives. Configure all of it from the dashboard, keyed by the node's node_id. The operational keys below may still be set locally as a pre-connect fallback for any key the server does not push, but the dashboard/server is authoritative.

Full reference

Key Default Description
server_url null WebSocket URL of the kenzy-server. Leave null/empty to auto-discover the server on the LAN via mDNS; set an explicit ws:// (or wss:// for a TLS-enabled server) URL to skip discovery (e.g. across VLANs that block multicast).
discovery.enabled true Browse for the server over mDNS when server_url is unset
tls_verify false When connecting over wss:// (a TLS-enabled server): false = encrypted but the certificate is not verified — the right posture for a self-signed home-LAN cert, no CA install needed. true = require a certificate trusted by the system store.
tls_ca Path to the server's certificate (or a CA bundle) to pin: connections verify against this file specifically. Implies verification.
node_id (generated) Stable primary identifier for this node. Leave unset — one is generated and written back to node.yaml on first run (or assigned at install with kenzy-init --node-id ID), then kept across restarts. The server keys the registry, per-node config, and all controls on it, so a node's identity (and its config) survives even when the room name changes or the device is reimaged.
room_id null Human room name (e.g. kitchen). Server-owned: set it from the dashboard. Until the server provides one it falls back to the hostname. Sent to the assistant as context (used in conversation history).
audio_device null PortAudio device name substring or integer index. null uses the system default. Use kenzy-devices to find the correct value.
capture_sample_rate 16000 Sample rate for microphone capture. Set to the device's native rate if it does not support 16000 Hz; audio is resampled automatically.
playback_sample_rate 24000 Sample rate for speaker output. Set to the device's native rate if it does not support 24000 Hz; TTS audio is resampled automatically.
volume 100 Playback volume [0–100]. Server-owned, applies live (config-pull). Settable from the dashboard or by voice ("turn it up", "set the volume to 40"). Affects TTS, intercom, and announcements.
muted false Runtime only — not persisted. Mutes all playback except the wake-word ready chime, which stays audible (at a floor level) so you can tell the device is listening and knowingly unmute. Toggle from the dashboard or by voice ("mute"/"unmute"). A node always comes back un-muted after a restart.
log_level "info" What the node prints to its console (debug/info/warning/error). Live-tunable from the dashboard.
log_capture_level "debug" Server-owned. How deep the dashboard log viewer can see for this node (trace/debug/…), independent of log_level. Captured only while the dashboard's logs flag is on (otherwise zero overhead). Set trace to include per-frame audio logs.
verbose false Also enables debug output from websockets and asyncio internals

Let the dashboard tune these for you

wakeword_threshold, wakeword_vad_threshold, and silence_rms_threshold are mic- and room-specific. Rather than guessing, use the Calibration panel in the dashboard (or kenzy-node --calibrate on a headless node) to measure your room and apply suggested values. See Calibrating a node's audio.

Wake word

Key Default Description
wakeword_models [] List of paths to .tflite or .onnx model files. Empty uses the bundled hey_ken_zee.tflite
wakeword_threshold 0.5 Confidence threshold [0.0–1.0] above which a detection fires
wakeword_vad_threshold 0.0 openwakeword Silero VAD gate [0.0–1.0]. Wake-word predictions are discarded unless the voice-activity score exceeds this. 0 disables it. Set to ~0.5 to suppress false detections on near-silence/noise. With it enabled you can safely lower wakeword_threshold (e.g. 0.4) for better real-speech sensitivity without reintroducing silence false-positives. The Silero VAD model is downloaded automatically by kenzy-setup.

Voice activity detection (VAD)

Key Default Description
vad_enabled true When false, the node streams until the server sends STOP. Hard cap does not apply.
silence_rms_threshold 50 RMS amplitude [0–32767] below which a frame is considered silent
silence_ms 400 Consecutive silence (ms) that ends an active session, once speech_min_ms has been heard
speech_min_ms 400 Minimum speech (ms) that must be detected before silence detection activates. Prevents the session ending on the pause after the wake word.
no_speech_timeout_ms 15000 Timeout (ms) if no speech is heard after activation. Prevents indefinite streaming when the wake word fires accidentally.
hard_cap_ms 30000 Unconditional session ceiling (ms). The session ends regardless of VAD state.

Sound files

Key Default Description
sound_ready null WAV file played on activation (the "chime"). null uses the bundled ready.wav. Accepts an absolute path or a bare filename loaded from the bundled sounds directory.
sound_waiting null WAV file played while waiting for the server response. Plays once and stops naturally or is interrupted when TTS begins. null (or an empty string) disables it — pure silence while waiting. Provide a filename or path to enable it.
sound_connect "connect.wav" Chime played when an intercom call connects (bundled default; path, or empty/null to disable).
sound_disconnect "disconnect.wav" Chime played when an intercom call ends (bundled default; path, or empty/null to disable).
sound_ringback "ringback.wav" The ring the caller hears while an intercom target room is being rung and asked to accept — loops until the call connects, is declined, or times out. Empty/null = silent wait.
sound_timer "timer.wav" Tone played before a timer announcement. Unlike the other sounds, this is prepended by the server at fire time — a path is read on the server host, and changes apply live (no node restart). Empty/null = voice only.
sound_alarm "alarm.wav" Tone played before each alarm ring — same server-side, live-apply behavior as sound_timer. The tone still plays even if TTS synthesis fails. Empty/null = voice only.
sound_error "error.wav" Spoken cue when a request fails mid-pipeline ("I'm sorry, but I'm having trouble…"). Pre-recorded — not synthesized — so it still plays when TTS itself is the broken part. Same server-side, live-apply semantics. Empty/null = stay silent on failure.
dialog_no_speech_timeout_ms 8000 How long Kenzy waits for your answer during a multi-turn dialog before giving up (the soft end cue = "I stopped waiting"). Live-applied.
dialog_onset_ms 300 Speech required to start a dialog turn — either sustained speech of this length, or a shorter complete word ("yes", "Boo") that ends in silence. A cough or clink can't trip the mic; your first word is buffered and kept whole. Live-applied.
dialog_onset_vad_threshold 0.5 Voice-activity confidence gate for dialog onset (falls back to energy sensing if the VAD model is absent). Live-applied.
hardware_aec true Whether this room's speaker does acoustic echo cancellation. Calibration detects and sets this automatically (it plays a known sound through the node's speaker and measures the echo); set it manually only to override an ambiguous reading. Set false for a non-AEC speaker and the room runs half-duplex: see the table below for exactly what changes. Live-applied; shown as a "no AEC" badge on the room's fleet card.

Rooms without echo cancellation (hardware_aec: false)

Kenzy assumes an echo-cancelling speakerphone (see the hardware guide) — it's what lets her hear you while she's making sound. On a speaker without AEC, her own audio floods the microphone, so features that depend on listening-during-playback are turned off cleanly rather than left to misbehave:

Feature Without AEC
Voice interrupt while Kenzy is speaking/playing Off — the wake word works again the instant playback ends
Talk-over (barge-in during a dialog) Off — she can't hear you over herself; you answer after she finishes (3.6.0 strict turns). With AEC, you can talk over her and she yields
Intercom (live two-way calls) Unavailable — Kenzy politely refuses (a two-way call without echo cancellation is a feedback loop)
Alarms Refused at set time with an offer of a timer or reminder instead (an alarm's ring loop can only be silenced by voice — impossible over the ringing). An already-set alarm still fires, once, timer-style
Timers, reminders, announcements, all voice commands, multi-turn dialogs Work normally

Zero-config nodes (discovery + config-pull)

A node needs no operational local config. With server_url unset it finds the server via mDNS, generates a stable node_id on first run (or one assigned at install via kenzy-init --node-id), and blocks until the server answers — it connects, sends hello, and waits for the server's config frame before initializing audio. That effective config is the server's node_defaults plus any per-node override in configs/nodes/<node_id>.yaml. Hardware keys (audio_device, sample rates, wakeword_models/VAD gate, sounds) are applied as the audio stack is built on this first pull; a later change to a hardware key needs a restart (one click in the dashboard). Live-tunable keys (wake-word threshold, silence RMS, VAD timing) apply immediately on every push. So a room device can run with an essentially empty node.yaml, and everything — including its room name — is configured from the dashboard and centralised on the server. Pre-seed a node by creating configs/nodes/<node_id>.yaml on the server before the device first connects. See Server Configuration.

Env-only bootstrap (no node.yaml)

A node can start from the environment alone — no config file at all — which is the tidiest way to provision a room device:

Variable Purpose
KENZY_SERVER_URL How to reach the server (ws:///wss://; http(s):// and a bare host:port are accepted and normalized). Omit to auto-discover via mDNS.
KENZY_SERVER_TOKEN The shared join token (if the server requires one). KENZY_SERVICE_TOKEN is still accepted as a legacy alias.
KENZY_NODE_ID A stable id for this node. When set it is authoritative — used as-is and never written to disk — so two node instances can run on one machine (two speakerphones, two units, two ids). Omit and the node generates + persists one on first run.

These override any node.yaml value, so a systemd unit with a few Environment= lines is a complete node install; everything operational is still pulled from the server. (KENZY_NODE_ID unset falls back to the generate-and-persist behavior described above.)

Finding the right device name

Run kenzy-devices after install. It tests every PortAudio device against Kenzy's required sample rates and prints ready-to-paste node.yaml settings including capture_sample_rate and playback_sample_rate if resampling is needed.

Prefer a speakerphone with hardware AEC

Use a USB speakerphone with built-in acoustic echo cancellation as the node's mic+speaker. Kenzy does not cancel echo itself, so without hardware AEC the node hears its own TTS playback and may falsely wake or interrupt — otherwise you must handle echo cancellation outside Kenzy.

Custom wake word

Custom wake word models can be trained at openWakeWord and pointed to via wakeword_models. Both .tflite and .onnx formats are supported.

Example

A typical node only needs the bootstrap keys — audio and tuning come from the server:

log_level: "info"
server_url: null                      # null = discover the server via mDNS
discovery:
  enabled: true
# node_id is generated and written here automatically on first run — leave it unset

The operational keys may still be set locally as a pre-connect fallback for any key the server does not push (e.g. to pin a device before the node is configured in the dashboard):

audio_device: "Anker PowerConf S330"  # substring of name shown by kenzy-devices
capture_sample_rate: 48000            # device native rate; resampled to 16000 Hz
playback_sample_rate: 48000           # device native rate; resampled to 24000 Hz
wakeword_threshold: 0.4               # lower is safe once VAD gating is on
wakeword_vad_threshold: 0.5           # reject wake-word hits on near-silence/noise