Distributed home voice-assistant: kenzy-node, kenzy-server, kenzy-stt, kenzy-tts, kenzy-llm, kenzy-speaker
Project description
KENZY ·

A distributed home voice assistant built as six independently deployable microservices. Kenzy runs wake-word detection locally on room nodes (Orange Pi Zero 3 / 3W or Raspberry Pi 3 / 4 / 5), streams audio to a central server for transcription, runs it through an LLM with tool-calling skills, and streams synthesized speech back to the room.
Architecture
Node (mic) ──PCM over WebSocket──► Server
│
┌─────────────────┘ on session end
▼
STT ──┐ (parallel)
Speaker ID ──┘
│
▼
LLM ◄──► Skills (weather, news, home control, …)
│
▼
TTS
│
PCM over WebSocket ──► Node (speaker)
| Service | Command | Default port | Role |
|---|---|---|---|
| node | kenzy-node |
— | Wake word + audio capture, TTS playback |
| server | kenzy-server |
8765 | WebSocket hub, pipeline orchestrator |
| stt | kenzy-stt |
8767 | Speech-to-text via faster-whisper |
| tts | kenzy-tts |
8769 | Text-to-speech via OpenAI or local Kokoro |
| llm | kenzy-llm |
8766 | LLM + skill tool-calling via LiteLLM |
| speaker | kenzy-speaker |
8768 | Speaker identification via SpeechBrain |
Requirements
- Python 3.11+
- On Raspberry Pi OS / Debian:
sudo apt-get install libportaudio2 portaudio19-dev - API keys: OpenAI (TTS + LLM), Home Assistant (home control skill). The weather skill uses the National Weather Service API — no key required.
Setup
Kenzy installs from PyPI — the default configs, built-in skills, and .env.example
ship as package data, so a service runs from a bare install with no source checkout:
pipx install "kenzy[node]" # or use the one-line installer at kenzy.ai/install.sh
kenzy-setup # download wake-word / speaker-ID models (run once)
kenzy-init # scaffold a config home (~/.config/kenzy)
For development from a checkout, use an editable install instead:
# Create and activate a virtualenv
python3 -m venv .venv
source .venv/bin/activate
# Install the services you need
pip install -e ".[node]" # room node only
pip install -e ".[server,stt,tts,llm,speaker]" # full server stack
pip install -e ".[node,server,stt,tts,llm,speaker,dev]" # everything
# Download wake-word and speaker-ID models (run once after install)
kenzy-setup
# Configure API keys
cp .env.example .env
# Edit .env and fill in OPENAI_API_KEY, HA_API_KEY (as needed)
Running
The config-path argument is optional — each service resolves its config from the config home automatically. Start the server first: the backend services and nodes pull their config from it on startup and block until it answers.
# Server host first
kenzy-server [configs/server.yaml]
kenzy-stt [configs/stt.yaml]
kenzy-tts [configs/tts.yaml]
kenzy-llm [configs/llm.yaml]
kenzy-speaker [configs/speaker.yaml]
kenzy-node [configs/node.yaml] # then each room device (discovers + pulls from the server)
Speaker enrollment
To enable speaker identification, enroll each person once:
kenzy-enroll [configs/speaker.yaml]
To identify the correct audio device and sample rates for a node:
kenzy-devices
Remote deployment
kenzy-deploy manages installation and updates across a fleet of remote hosts over SSH. See configs/deploy.yaml for host configuration.
kenzy-deploy init # one-time OS setup on all hosts
kenzy-deploy install # first full deployment (source or PyPI mode)
kenzy-deploy upgrade # install updates and restart services
kenzy-deploy status # check service health
kenzy-deploy uninstall # stop, remove units + venv (--purge also removes the install dir)
Prerequisites on each remote host: SSH key auth and passwordless sudo. Backend services are deployed in pull mode (they fetch config from the server) and per-node config lives in the server-owned central store (configs/nodes/, configs/services/), so a deployed fleet is managed from the dashboard. See the deployment docs for the central-config model, per-host node_id, and --reseed.
Dashboard
kenzy-server can serve an opt-in web fleet manager (off by default). Enable it in
server.yaml (dashboard.enabled: true, controls: true, logs: true) and open
http://127.0.0.1:8770/dashboard. It gives you one place to:
- See live node + backend-service health and each host's installed version
- Configure each node, rename its room, and run a guided audio-calibration wizard
- Manage skills (enable/disable live) and speaker profiles (rename / delete / enroll from a room)
- Watch pipeline activity (transcripts, latency, fast-path hit rate) and read server / service / node logs
- Trigger / stop / restart nodes and send TTS announcements to every room
- Upgrade the server, backend services, and nodes in place — one click, with an "update available" check against PyPI
- Edit a safe subset of the server's own config and change the dashboard password
All /api reads and actions require login; mutating actions also need controls. Login
defaults to admin / password — change it with kenzy-passwd (server host only) or
from the Settings page. It is plaintext HTTP on a LAN bind, so do not port-forward it.
The Settings page also shows the node join token to copy when provisioning new nodes.
See the Dashboard guide.
Configuration
The server is the configuration authority for the whole fleet. Nodes and the backend services pull their config from it at boot and are edited from the dashboard; the YAML files below are the server-side store and the seed defaults.
Key settings:
configs/node.yaml— bootstrap-only (identity + how to reach the server + early logging). A node auto-generates a stablenode_id, then blocks until the server pushes its full operational config (audio device, wake-word threshold/VAD, sounds, room name) and initializes audio from that. Per-node overrides live inconfigs/nodes/<node_id>.yaml; the room name is server-owned and set from the dashboard.configs/server.yaml— URLs for each downstream service (omit a URL to disable that stage),node_defaults, discovery, and the dashboard blockconfigs/services/<svc>.yaml— server-owned overrides for the backend services (stt/tts/llm/speaker), edited from the dashboard's Services tab; each service pulls its effective config (packaged default + this override, secrets stripped) from the server at bootconfigs/llm.yaml/stt.yaml/tts.yaml/speaker.yaml— packaged seed defaults for those services (model/voice/thresholds/etc.)
Secrets stay in each host's environment / .env — never in the config store.
Skills
Skills are async Python functions in skills/ decorated with @skill. They are discovered and loaded automatically at startup — no registration required. The LLM calls them as tools based on their docstrings and type signatures.
Included skills:
| Skill file | What it does |
|---|---|
weather.py |
Current conditions and forecast via NWS |
news.py |
RSS headlines and article summaries |
stocks.py |
Stock quotes via yfinance |
home_assistant.py |
Smart home control via Home Assistant REST API (secure actions require a recognized speaker) |
datetime_skill.py |
Current date and time (with a deterministic fast path) |
announce.py |
Speak a message in every room (broadcast) |
intercom.py |
Start a live two-way voice call between two rooms |
volume.py |
Set / adjust a room's playback volume or mute |
enroll.py |
Voice speaker enrollment ("enroll me as Alice") |
random_tools.py |
Coin flip, dice, random number, pick from list |
about.py |
Reports the installed Kenzy version |
Adding a skill
# skills/my_skill.py
from kenzy.llm.skills import skill
@skill
async def my_skill(query: str) -> str:
"""One-line description the LLM uses to decide when to call this."""
return "result"
Per-skill config lives under skills.<name> in llm.yaml. Secrets come from environment variables in .env.
Home Assistant device topology
Smart home control pulls your device topology live from Home Assistant — which entities exist, their friendly names, domains, and floor/area placement — so there's no device-map file to maintain. Add a device in HA and it's voice-controllable on the next refresh. Commands resolve in two tiers: a deterministic fast path (padacioso + rapidfuzz, no LLM) for everyday imperatives, and a sub-LLM fallback that reads the live floor → area → type → entity outline for harder requests.
The only hand-authored input is an optional data/home_assistant/curation.yaml — the voice layer HA can't store: spoken aliases, per-device notes, room group-defaults, and voice-control exclusions. Edit it directly or from the dashboard's Home Assistant tab. Run kenzy-ha-devices to print the live tree with each entity ID and its included/excluded status. (If HA is unreachable and legacy device_ids.yaml/device_ids.json files are present, the skill falls back to them.)
Development
source .venv/bin/activate
ruff check src/ # lint
ruff format src/ # format
mypy src/ # type-check
pytest # run tests
Environment variables
See .env.example for the full list. Required variables:
| Variable | Used by |
|---|---|
OPENAI_API_KEY |
TTS service, LLM service (if using OpenAI models) |
HA_API_KEY |
Home Assistant skill |
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kenzy-3.6.0.tar.gz.
File metadata
- Download URL: kenzy-3.6.0.tar.gz
- Upload date:
- Size: 3.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9bb175879ee665e88f3d79e4b1f1bb99deee5005b9bc21273bd72c3981eb15f4
|
|
| MD5 |
a41e3b0437fd76ad18ec8dc5008adabb
|
|
| BLAKE2b-256 |
ee10fda445d04501c012039980c7cd1a62f21a3c43135f0723700494192579f1
|
File details
Details for the file kenzy-3.6.0-py3-none-any.whl.
File metadata
- Download URL: kenzy-3.6.0-py3-none-any.whl
- Upload date:
- Size: 3.7 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.25
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aec8cddfe4305b332b60009b6ff6915832608cf48b0b25a3cd37ee17aa8193e4
|
|
| MD5 |
8fedf23b48010f993345d464a40bcb9f
|
|
| BLAKE2b-256 |
2b466af148249c33cf56a705932a7bd364247051d8e015504660395f8ebf9e16
|