Claudia
Version 1.0.0 — documentation + config repo
Claudia
Version 1.0.0 — documentation + config repo
Build your own always-on voice assistant in an afternoon — a Raspberry Pi Zero 2 WH with a USB microphone for conversation audio and the Hiwonder WonderEcho module as a hardware wake-word trigger, wired straight to the Claude API. Sits on your shelf, listens for "Claudia", and Claude answers out loud in seconds. No Alexa account, no surveillance, no subscription — just a Claude API key and hardware you own.
Why two audio devices? The WonderEcho is a command-word recognizer, not a microphone: its CI1302 chip recognizes the wake word on-device and reports a short event ID over I²C. It never streams raw audio, so Whisper can't transcribe through it. The WonderEcho handles the always-listening wake word; the USB mic (a standard ALSA device) records what you actually say.
WH, not W. The WonderEcho connects to four GPIO pins (SDA / SCL / 5V / GND), so the build needs the WH variant with pre-soldered headers. Buying the plain "W" means soldering 40 pins yourself before anything works.
Before you start, gather: a Windows / macOS / Linux computer to flash the microSD and SSH in, a way to plug a microSD into it (the SanDisk Ultra ships with a full-size SD adapter but no USB reader — most modern ultrabooks and MacBooks need a USB microSD reader, ~$8), and a 2.4 GHz Wi-Fi network (the Pi Zero 2 WH has no 5 GHz radio). The smart-plug options below ship with US plugs; each vendor (Kasa, Shelly, Sonoff) also sells EU/UK/AU variants that speak the same local API — pick your region at checkout. No soldering iron needed, but you'll need 4 female-to-female jumper wires to link the WonderEcho to the Pi — the WonderEcho does not include any cable, so the shopping list below adds a cheap Dupont wire kit. The USB microphone plugs into the Pi's data port through a micro-USB OTG adapter (also in the shopping list — the Pi Zero has no full-size USB-A port).
Stock check. The Pi Zero 2 WH is supply-constrained; if all the US retailers on the cards below show out-of-stock, rpilocator.com tracks live availability across the official reseller network.
What this repo is
Claudia is a documentation + config repo, not an application. There is no compiled source, no
.sln, no package.json, and no build step in the traditional sense — what ships is this
README.md (the build guide you're reading, and the source document the public landing page is
rendered from), a hardware/software parts catalog, two Pi-side shell scripts, and a small set of
Windows PowerShell tools that keep the whole thing internally consistent. The actual assistant
runtime — PiSugar/whisplay-ai-chatbot — is
cloned onto the Raspberry Pi at build time by the guide/installer; this repo never vendors or
forks it. See docs/BIBLE.md {#CLA-§1}–§3 for the
canonical "is / is not" statement and §4 for the full architecture canon —
this section is a practical map of the repo, not a restatement of that canon.
Architecture at a glance
This repo (Claudia) On the Pi (after following the guide)
┌─────────────────────────┐ ┌──────────────────────────────────────┐
│ README.md (build guide │ builder follows │ Raspberry Pi OS 64-bit │
│ + landing-page source) │ ───────────────▶ │ whisplay-ai-chatbot (cloned upstream)│
│ config/parts.json (L5) │ │ ASR ▸ Claude (LLM) ▸ TTS pipeline │
│ config/versions.json │ │ systemd chatbot.service │
│ config/env.template │ └───────┬──────────────┬────────────────┘
│ config/asoundrc.usbmic │ │ I²C (4 wires) │ USB OTG
│ scripts/pi/*.sh │ ▼ ▼
│ docs/ (Codex canon) │ WonderEcho (0x52) USB mic (ALSA)
└─────────────────────────┘ wake word "Claudia" conversation audio
│ rendered by sibling MindAttic.Deploy │
▼ ▼ HTTPS
mindattic.com/claudia.htm api.anthropic.com (Claude — the brain)
Only the Claude API call is mandatory cloud traffic; ASR/TTS/smart-home each have a local option
({#CLA-LAW-1}). The full canonical diagram, domain model, and verbs live in
docs/BIBLE.md §4.
Directory layout
Claudia/
README.md the build guide (12 numbered parts + troubleshooting) — also the
source document the public landing page is rendered from
CLAUDE.md Codex working agreement for AI agents in this repo
config/
parts.json L5 canon: shopping catalog + landing-page configurator axes
versions.json pinned upstream dependency version labels (Node, Python, Claude model)
env.template example .env for ~/whisplay-ai-chatbot/.env on the Pi
asoundrc.usbmic ALSA ~/.asoundrc profile for the USB conversation mic
images/ part photos for the landing-page catalog cards
scripts/pi/
install-claudia.sh idempotent Pi installer — automates guide parts 5–10
healthcheck.sh 4-layer smoke test (I²C · ALSA mic · network · Claude API)
tools/
codex.ps1 doctor (validate canon) + digest (regenerate BIBLE.digest.md)
build-readme.ps1 wrapper: regenerates README.htm from README.md (this file)
docs/ Codex canon — see "Docs canon" below
BIBLE.md L0 — architecture, laws, verified state, glossary
AMENDMENTS.md L1 — append-only change log (CLA-A<n>)
USER_STORIES.md L2 — stories (CLA-US-<Epic><n>), each ✅ cites its check
BIBLE.digest.md GENERATED — never hand-edit; regenerate via tools/codex.ps1 digest
rfc/0001-config-axis-contract.md design note on the configurator-axis contract
data/parts.index.json mirrors config/parts.json's stable ids (part.<slug>)
data/_schema/part.schema.json JSON Schema validating config/parts.json entries
.claude/ Claude Code project wiring (hooks, slash commands, statusline)
commands/ checkpoint.md, commit.md, deploy.md, do.md
hooks/ inject-digest.ps1 (SessionStart), restore-handoff.ps1 (/clear)
settings.json hook + statusline registration
statusline.ps1 context-window gauge shown in the CLI status bar
Scripts, tools & CLI entry points
Every runnable entry point in this repo, what it does, and where it's documented:
| Entry point | Runs on | Purpose | Documented at |
|---|---|---|---|
tools/codex.ps1 doctor |
Windows (PS 5.1+) | Validates the whole Codex canon: front-matter, unique {#...} ids, resolving cross-refs, JSON + schema validity for config/parts.json/config/versions.json, catalog id uniqueness against docs/data/parts.index.json, that cited repo paths exist, and that docs/BIBLE.digest.md isn't stale. Must exit 0. |
docs/BIBLE.md §6, docs/USER_STORIES.md CLA-US-D3 |
tools/codex.ps1 digest |
Windows (PS 5.1+) | Regenerates docs/BIBLE.digest.md from docs/BIBLE.md + the latest amendment. Run before doctor if it reports the digest stale. |
docs/AMENDMENTS.md CLA-A1 |
tools/build-readme.ps1 |
Windows (PS 5.1+) | Thin wrapper that calls the shared engine ../codex-standard/build-readme.ps1 to render this README.md into a standalone, dark-themed, sidebar-TOC README.htm at the repo root. Every MindAttic repo shares the one engine so all README.htm pages look and behave identically — edit the engine, not this wrapper. |
This README, "Build, verify & test" below |
scripts/pi/install-claudia.sh |
Raspberry Pi (bash) | Idempotent installer automating guide Parts 5–10: apt update/upgrade, trims RAM-hungry services, installs build deps, enables I²C, writes ~/.asoundrc if absent, clones + builds whisplay-ai-chatbot, guards against a placeholder ANTHROPIC_API_KEY, runs the healthcheck, and registers the chatbot.service boot unit. Safe to re-run. |
docs/BIBLE.md §4.1, guide Parts 05–10 below |
scripts/pi/healthcheck.sh |
Raspberry Pi (bash) | 4-layer smoke test: WonderEcho on the I²C bus → USB mic visible to ALSA → network reaches api.anthropic.com → the configured API key + model return HTTP 200. Also embedded (abbreviated header) in guide Part 09 as the copy-paste a builder pastes onto the Pi as ~/healthcheck.sh. |
Guide Part 09, docs/USER_STORIES.md CLA-US-D1 |
.claude/hooks/inject-digest.ps1 |
Claude Code SessionStart hook | Injects docs/BIBLE.digest.md as context at the start of every Claude Code session in this repo. |
docs/AMENDMENTS.md CLA-A1 |
.claude/hooks/restore-handoff.ps1 |
Claude Code SessionStart (matcher: clear) |
Re-ingests .claude/checkpoint.md (written by /checkpoint) as resume context the instant /clear fires, then deletes it — a one-shot context handoff across /clear. |
.claude/commands/checkpoint.md |
There is no dotnet build/npm install/npm test here — this repo has no compiled or transpiled
source. "Build" and "test" instead mean the checks below.
Configuration reference
| File | What it holds | Consumed by |
|---|---|---|
config/parts.json |
The canonical L5 shopping catalog and the configAxes block that drives the landing-page configurator (battery, mic, asr, tts, case, smarthome). Every part has id, category, price, pricesAsOf, specs, buy-link tiers, and an optional when gate. Prices are non-authoritative estimates dated by a top-level pricesAsOf ({#CLA-LAW-3}). |
Sibling MindAttic.Deploy reads this file in place at this exact path to render the shopping-list widget and configurator on mindattic.com/claudia.htm; do not move it. Ids are mirrored (not duplicated) into docs/data/parts.index.json as part.<slug> and validated against docs/data/_schema/part.schema.json. |
config/versions.json |
Pinned upstream dependency version labels (current Node major, Python system version, Raspberry Pi OS label, default Claude model + label), each dated by versionsAsOf. |
Hand-kept in sync with what README.md hardcodes in prose — there is no automatic injection pipeline (a prior build-html.js placeholder-injection step was retired when rendering moved to MindAttic.Deploy). |
config/env.template |
Example .env for ~/whisplay-ai-chatbot/.env on the Pi — LLM/ASR/TTS provider keys and the system prompt. |
Guide Part 08; copied by install-claudia.sh when the Pi's own .env.template is missing. |
config/asoundrc.usbmic |
ALSA ~/.asoundrc profile that makes the USB conversation mic the default capture device (card 1) while leaving playback on the default output (card 0). |
Guide Part 5.5; written verbatim by install-claudia.sh if ~/.asoundrc doesn't already exist. |
config/images/ |
Part photos (pi-zero-2-wh.png, hiwonder-wonderecho.png, sunfounder-mic.png, respeaker-xvf3800.png, microsd-32gb.png, pi-power-supply.png, elegoo-dupont-wires.png) referenced by imageFile in config/parts.json. |
Landing-page catalog cards (MindAttic.Deploy). |
Configurator axis contract. An axis key=value is only valid if it agrees in all three
places at once: the configAxes block above, each part's when gate, and this README's
<!-- when: key=value --> markers. This is currently enforced by hand/review
({#CLA-LAW-4}); RFC 0001
proposes teaching codex.ps1 doctor to check it mechanically.
Build, verify & test
There's no compiler and no test runner — "build" means regenerating generated artifacts, and "test" means the checks below all pass. Run from the repo root, Windows PowerShell 5.1:
# Regenerate docs/BIBLE.digest.md from BIBLE.md + the latest amendment
powershell -NoProfile -ExecutionPolicy Bypass -File tools/codex.ps1 digest
# Validate the whole Codex canon (front-matter, ids, cross-refs, JSON+schema,
# catalog id uniqueness, cited paths exist, digest freshness) — must exit 0
powershell -NoProfile -ExecutionPolicy Bypass -File tools/codex.ps1 doctor
# Regenerate README.htm from this README.md via the shared workspace-wide engine
powershell -NoProfile -ExecutionPolicy Bypass -File tools/build-readme.ps1
Additional checks reviewed by hand (not currently automated in this repo):
- Pi-side shell scripts (
scripts/pi/*.sh) are syntax-checked withbash -nand kept idempotent. - Any new/edited config axis is checked to agree across
parts.json, partwhengates, and this README's markers (see the axis contract above). - On-hardware behavior (WonderEcho detection, wake→Claude→speak round trip, boot service) can't be
exercised in CI — those stories stay
🟡indocs/USER_STORIES.mduntil proven on a real Pi. Seedocs/BIBLE.md §6for the full verified-state table.
The public landing page (mindattic.com/claudia.htm) is rendered from this README.md +
config/parts.json by the sibling MindAttic.Deploy repo — that rendering + deploy pipeline is
outside this repo's scope. README.htm (generated above) is a separate, internal engineering
rendering of this same file, shared in look-and-feel across every MindAttic repo.
Docs canon
This repo uses the MindAttic Codex documentation standard — layered, single-home-per-fact docs
under docs/. Read docs/BIBLE.md first; everything else links off it.
| Layer | File | What it's for |
|---|---|---|
| L0 | docs/BIBLE.md |
What Claudia IS / is NOT, architecture canon, the Laws ({#CLA-LAW-n}), verified state, glossary. |
| L1 | docs/AMENDMENTS.md |
Append-only change log (CLA-A<n>); an amendment wins over the bible. |
| L2 | docs/USER_STORIES.md |
Stories CLA-US-<Epic><n> across Epics A–D (Configure & shop, Assemble & flash, Install & converse, Verify & operate); every ✅ cites its verifying check. |
| rfc | docs/rfc/ |
Design notes; graduate into L0+L2 then mark superseded. Currently: RFC 0001. |
| L5 | docs/data/ |
parts.index.json mirrors the canonical catalog config/parts.json; _schema/part.schema.json validates it. |
| generated | docs/BIBLE.digest.md |
Regenerated by tools/codex.ps1 digest; injected at Claude Code session start by .claude/hooks/inject-digest.ps1. Never hand-edit. |
Org-wide laws are inherited by reference from ../MindAttic.HouseRules.md
via docs/BIBLE.md §5 — not restated here.
Versioning
Whole-number, major-only: this README is stamped Version 1.0.0 at the top of this file. A
release bumps the major only (1.0.0 → 2.0.0 → 3.0.0, …); minor and patch stay 0
(HOUSE-LAW-1, inherited via docs/BIBLE.md §5).
01. Configure
<!-- CONFIG-WIDGET -->
02. Shopping list
<!-- PARTS-GALLERY -->
03. Assemble
Total time: ~3 minutes. No soldering.
- Do not insert the microSD yet. Flash it first in section 04.
- Connect the WonderEcho to the Pi's I²C header pins with 4 female-to-female Dupont jumper wires (from the kit in the shopping list — the WonderEcho does not include any cable):
SDA → BCM 2 (pin 3),SCL → BCM 3 (pin 5),5V → pin 2,GND → pin 6. - Plug the micro-USB OTG adapter into the Pi's middle port labeled
USB(the data port — not the cornerPWR INport), then plug the USB microphone into the adapter.
<!-- when: mic=basic -->
- The SunFounder mini mic is a thumb-size dongle — it just hangs off the OTG adapter. Point its grille roughly toward where you'll be speaking.
<!-- end -->
<!-- when: mic=array -->
- Set the reSpeaker XVF3800 array flat, mics facing the room — its beamforming works best with an unobstructed 360° view. It connects to the OTG adapter with its own USB cable.
<!-- end -->
- Make sure the WonderEcho's speaker face is unobstructed (its on-board mic listens for the wake word).
<!-- when: battery=yes --> 6. Snap the PiSugar 3 battery onto the underside of the Pi using its magnetic pogo pins. No soldering — the spring-loaded pogo pins align themselves.
Final stack: WonderEcho (via I²C cable) ←→ Pi Zero 2 WH ←→ USB mic (via OTG) → PiSugar 3 <!-- end --> <!-- when: battery=no --> Final layout: WonderEcho (via I²C cable) ←→ Pi Zero 2 WH ←→ USB mic (via OTG), wall-powered <!-- end -->
✅ Checkpoint: The four I²C wires are seated firmly, nothing wobbles, the USB mic is in the USB (middle) port via the OTG adapter, and the WonderEcho's speaker grille is unobstructed.
04. Flash microSD
4.1 Install Raspberry Pi Imager
If your laptop has no SD-card slot — common on recent ultrabooks and every modern MacBook — plug in a USB microSD reader now. The card itself ships with a full-size SD adapter, but that only helps you if the host has a full-size SD slot.
Download from raspberrypi.com/software (Windows, macOS, Linux).
4.2 Flash
- Open Raspberry Pi Imager.
- Choose Device →
Raspberry Pi Zero 2 W(Imager doesn't distinguish W from WH — the OS image is the same). - Choose OS →
Raspberry Pi OS (other)→ Raspberry Pi OS (64-bit) (the full version, not Lite).- The chatbot repo's install script expects packages from the full image. Lite will work but you'll need extra apt installs and may hit surprises.
- Choose Storage → your microSD card.
- Click the gear icon (⚙) for Edit Settings and configure:
- Hostname:
claudia - Username: anything other than
pi— Pi OS Bookworm deprecated the defaultpiuser, and current Imager builds warn (or refuse) when you try to set it. Useclaudia, your first name, or any other identifier you'll remember. - Password: something secure
- Enable SSH: ✅ password auth
- Wireless LAN: SSID + password for your home Wi-Fi
- Locale: your timezone (e.g.
America/Chicago), keyboard your layout (e.g.us)
- Hostname:
- Save, then Write. Takes 2–5 minutes.
4.3 First boot
Insert the microSD into the Pi.
Plug the official power supply into the
PWR INmicro-USB port (the one nearest the corner, labeledPWR INon the silkscreen). Not the middle port labeledUSB.Wait 60–90 seconds.
From your PC:
ssh <your-username>@claudia.localIf
claudia.localdoesn't resolve, find the Pi's IP in your router's admin page and usessh <your-username>@192.168.x.x.
✅ Checkpoint: You see the <your-username>@claudia:~ $ prompt. Run cat /etc/os-release and confirm it says Debian/Raspberry Pi OS. Run free -h — you should see ~430 MB of Mem: (the Pi Zero 2 WH has 512 MB total).
05. System setup
Run these from the SSH session. One at a time. Wait for each to finish.
5.1 Update
sudo apt update && sudo apt full-upgrade -y
This takes 5–15 minutes on a Pi Zero 2 WH. Be patient.
5.2 Free up RAM (Pi Zero only has 512 MB)
The Pi Zero 2 WH is RAM-constrained. Disable services you don't need:
# Disable Bluetooth (not used by this build)
sudo systemctl disable hciuart bluetooth
# Disable triggerhappy (gamepad daemon, not needed)
sudo systemctl disable triggerhappy
5.3 Install build dependencies
sudo apt install -y git curl build-essential python3-pip python3-venv \
portaudio19-dev libsndfile1 ffmpeg alsa-utils libatlas-base-dev
5.4 Enable I²C and detect the WonderEcho
The WonderEcho is an I²C device. Turn the bus on, install i2c-tools, then verify the module answers on the bus.
# Enable I²C non-interactively
sudo raspi-config nonint do_i2c 0
# Tools + Python bindings
sudo apt install -y i2c-tools python3-smbus
sudo reboot
After it reboots, SSH back in and run:
i2cdetect -y 1
You should see a device address show up (commonly 0x52 for the WonderEcho — verify against the sticker on the module).
✅ Checkpoint: i2cdetect -y 1 lists at least one device address — the WonderEcho is talking to the Pi.
5.5 Verify the USB microphone
The USB mic is a standard USB Audio Class device — no driver needed. Confirm ALSA sees it:
arecord -l
You should see the mic listed as a capture card (typically card 1 — card 0 is the Pi's HDMI output, which has no capture side). Then make it the default capture device so the chatbot's recorder finds it without extra flags:
nano ~/.asoundrc
Paste (this file also ships in the repo as config/asoundrc.usbmic):
pcm.!default {
type asym
playback.pcm {
type plug
slave.pcm "hw:0,0"
}
capture.pcm {
type plug
slave.pcm "hw:1,0"
}
}
ctl.!default {
type hw
card 1
}
If arecord -l showed your mic on a different card number, change the hw:1,0 (and card 1) to match.
<!-- when: mic=array -->
Array bonus: the reSpeaker XVF3800 also has a playback side — a 3.5 mm jack plus a JST connector driving up to 5 W speakers (Seeed wiki). Point
playback.pcmat the reSpeaker's card too and one device covers both mic and speaker. <!-- end -->
Now record a 3-second test clip:
arecord -d 3 -f S16_LE -r 16000 /tmp/mictest.wav
✅ Checkpoint: arecord -l lists the USB mic, and the test recording completes without audio open error. (Playback of the clip is covered in Part 10 — the Pi's only speaker at this point may be an HDMI display.)
06. Install chatbot
This is the PiSugar whisplay-ai-chatbot repo — we use it as the LLM/ASR/TTS plumbing even though we're not using the Whisplay HAT itself. Wake-word detection goes through the WonderEcho; conversation audio is recorded from the USB mic, which the chatbot picks up as the default ALSA capture device (set in Part 5.5).
cd ~
git clone https://github.com/PiSugar/whisplay-ai-chatbot.git
cd whisplay-ai-chatbot
bash install_dependencies.sh
source ~/.bashrc
The dependency install pulls Node.js, Python packages, and audio libraries. This takes 15–25 minutes on a Pi Zero 2 WH. Let it finish.
The
source ~/.bashrcline is important — the installer sets PATH entries you need in your current shell session.
✅ Checkpoint: install_dependencies.sh finishes without errors. Test that Node is on PATH:
node --version
You should see v24.x or newer (upstream's installer pulls in the current Node LTS).
07. API key
- Go to console.anthropic.com and sign in (or create an account).
- Add a payment method and put a small amount of credit on the account (e.g., $5 — that lasts a long time on Haiku).
- Navigate to API Keys → Create Key.
- Name it
claudia. Copy the key now — you can't see it again later. - Treat the key like a password.
Approximate cost: Casual personal use on claude-haiku-4-5-20251001 typically runs a few dollars per month at most. Check current pricing at anthropic.com/pricing.
Which model to pick
| Model ID | Speed | Quality | When to use |
|---|---|---|---|
claude-haiku-4-5-20251001 |
Fastest | Good | Default for this device. Latency matters more than essay-grade prose for a voice assistant. |
claude-sonnet-4-6 |
Medium | Excellent | If you want richer answers and don't mind a slightly slower response. |
claude-opus-4-7 |
Slowest | Best | Overkill for spoken Q&A. Use for hard reasoning tasks only. |
Model IDs change over time. The current list lives at docs.claude.com.
08. Configure chatbot
8.1 Create your .env
cd ~/whisplay-ai-chatbot
cp .env.template .env
nano .env
The template ships with many fields for different ASR/LLM/TTS providers. For a Claude-based build, you need the LLM section set to Anthropic. Find and set:
# === LLM (the AI brain) ===
LLM_SERVER=anthropic
ANTHROPIC_API_KEY=sk-ant-YOUR-KEY-HERE
ANTHROPIC_MODEL=claude-haiku-4-5-20251001
# === System prompt — shapes the assistant's voice ===
SYSTEM_PROMPT=You are a concise, friendly voice assistant. Answer in plain spoken English — no markdown, no bullet lists, no headings. Keep responses to 1–3 sentences unless the user explicitly asks for more.
The wake-word listener does not run on the Pi — it's handled in hardware by the WonderEcho (see section 08.3 below). The Pi only polls the WonderEcho's wake-event register over I²C, so no WAKE_WORD_* env keys are needed. When a wake event fires, the chatbot records your question from the USB mic (the default ALSA capture device you set in Part 5.5) — the WonderEcho's own mic is only used by its on-chip wake-word detector and is never seen by the Pi.
Env-key naming: upstream uses
LLM_SERVER,ASR_SERVER,TTS_SERVER(not*_PROVIDER). The plugin registry switches on the lowercase value — seesrc/cloud-api/server.tsin the upstream repo.
<!-- when: asr=whisper-cpp -->
ASR (speech-to-text): Whisper, local. Already wired up by the template defaults. Slowest option on a Pi Zero 2 WH (~3–6 s per utterance) but no API key required and works offline.
<!-- end -->
<!-- when: asr=openai -->
ASR (speech-to-text): OpenAI Whisper API. Add to your .env:
ASR_SERVER=openai
OPENAI_API_KEY=sk-REPLACE-ME
Round-trip latency drops to ~0.5–1 s. Costs a few cents per hour of speech.
<!-- end -->
<!-- when: asr=google -->
ASR (speech-to-text): Google Cloud STT. Add to your .env:
ASR_SERVER=google
GOOGLE_APPLICATION_CREDENTIALS=/home/pi/google-stt-key.json
Drop the service-account JSON from Google Cloud Console at the path above. Generally fastest cloud STT on US-region traffic. <!-- end -->
<!-- when: tts=piper -->
TTS (text-to-speech): Piper, local. Free, runs on the Pi. Voice quality is "robot but understandable" — fine for short replies. Add to your .env:
TTS_SERVER=piper
PIPER_BINARY_PATH=/usr/local/bin/piper
PIPER_MODEL_PATH=/home/pi/piper/voices/en_US-amy-low.onnx
<!-- end -->
<!-- when: tts=openai -->
TTS (text-to-speech): OpenAI gpt-4o-mini-tts (recommended). Near-state-of-the-art quality, supported by upstream out-of-the-box. Add to your .env:
TTS_SERVER=openai
OPENAI_API_KEY=sk-REPLACE-ME
OPENAI_VOICE_MODEL=gpt-4o-mini-tts
OPENAI_VOICE_TYPE=nova
The new gpt-4o-mini-tts model and the 4o-series voices (alloy, nova, onyx, marin, cedar, plus older echo/fable/shimmer/ash/ballad/coral/sage/verse) are dramatically more natural than the older tts-1. Costs roughly $0.015 per minute of speech.
<!-- end -->
<!-- when: tts=elevenlabs --> TTS (text-to-speech): ElevenLabs (best quality, requires a one-time patch).
ElevenLabs has the most natural voices on the market right now, but the upstream chatbot doesn't ship an ElevenLabs handler. You add one yourself — about 40 lines of TypeScript and a single registration entry.
Step 1 — handler. Create ~/whisplay-ai-chatbot/src/cloud-api/elevenlabs/elevenlabs-tts.ts with:
import mp3Duration from "mp3-duration";
import { TTSResult } from "../../type";
// The chatbot already loads .env at startup, so process.env is populated
// by the time this plugin's activate() runs — no need to call dotenv here.
const apiKey = process.env.ELEVENLABS_API_KEY || "";
const voiceId = process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL"; // "Bella"
const modelId = process.env.ELEVENLABS_MODEL_ID || "eleven_turbo_v2_5"; // low-latency
const stability = parseFloat(process.env.ELEVENLABS_STABILITY || "0.5");
const similarity = parseFloat(process.env.ELEVENLABS_SIMILARITY || "0.75");
const elevenLabsTTS = async (text: string): Promise<TTSResult> => {
if (!apiKey) { console.error("ELEVENLABS_API_KEY is not set."); return { duration: 0 }; }
const url = `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}`;
let res: Response;
try {
res = await fetch(url, {
method: "POST",
headers: {
"xi-api-key": apiKey,
"Content-Type": "application/json",
"Accept": "audio/mpeg",
},
body: JSON.stringify({
text,
model_id: modelId,
voice_settings: { stability, similarity_boost: similarity },
}),
});
} catch (e) {
console.log("ElevenLabs TTS request failed:", e);
return { duration: 0 };
}
if (!res.ok) {
console.log("ElevenLabs TTS HTTP " + res.status + ": " + (await res.text().catch(() => "")));
return { duration: 0 };
}
const buffer = Buffer.from(await res.arrayBuffer());
const duration = await mp3Duration(buffer);
// mp3-duration returns undefined if it can't parse the stream; coerce to
// 0 so downstream code never sees NaN.
return { buffer, duration: (duration ?? 0) * 1000 };
};
export default elevenLabsTTS;
Step 2 — register the plugin. Open ~/whisplay-ai-chatbot/src/plugin/builtin/tts.ts and add this block alongside the other pluginRegistry.register(...) calls:
pluginRegistry.register({
name: "elevenlabs",
displayName: "ElevenLabs TTS",
version: "1.0.0",
type: "tts",
audioFormat: "mp3",
description: "ElevenLabs text-to-speech (high-quality cloud voices)",
activate: () => {
const ttsProcessor = require("../../cloud-api/elevenlabs/elevenlabs-tts").default;
return { ttsProcessor };
},
} as TTSPlugin);
Step 3 — .env.
TTS_SERVER=elevenlabs
ELEVENLABS_API_KEY=sk_REPLACE_ME
ELEVENLABS_VOICE_ID=EXAVITQu4vr4xnSDxMaL
ELEVENLABS_MODEL_ID=eleven_turbo_v2_5
ELEVENLABS_STABILITY=0.5
ELEVENLABS_SIMILARITY=0.75
Step 4 — rebuild + restart.
cd ~/whisplay-ai-chatbot
bash build.sh
sudo systemctl restart chatbot.service
Voice IDs: log into elevenlabs.io, open VoiceLab, and copy the ID of any voice you've cloned or one of their stock voices. eleven_turbo_v2_5 is recommended for the Pi Zero 2 WH — it has the lowest latency. Cost is roughly $0.18 per 1000 chars (~7-8 cents per minute of speech).
<!-- end -->
The
.env.templateevolves. If your file looks different from this guide, the live template at github.com/PiSugar/whisplay-ai-chatbot/blob/master/.env.template is the source of truth.
Save: Ctrl+X, Y, Enter.
8.2 Build the project
bash build.sh
This compiles the TypeScript and prepares assets. ~5–10 minutes on a Pi Zero 2 WH.
✅ Checkpoint: build.sh exits cleanly with no errors.
8.3 Configure the WonderEcho wake word
The WonderEcho module runs its own on-device wake-word detector — the Pi doesn't have to listen. You program the trigger phrase ("Claudia") once over I²C, then the module flags a wake event on the bus whenever it hears the word; the Pi polls that register and starts a recording session each time it fires.
Verify before running. The exact I²C register layout (
0x10as the "set-trigger" opcode below) depends on your WonderEcho firmware revision. Check the Hiwonder WonderEcho wiki for the register map matching your unit before running this — the snippet is the canonical pattern, not a guaranteed copy-paste for every shipping firmware.
# Reference snippet: writes the trigger word to the WonderEcho's "set-trigger"
# register. Confirm the register/opcode against the Hiwonder wiki for your
# firmware revision before relying on this in production.
cd ~/whisplay-ai-chatbot
python3 - <<'PY'
import smbus2 as smbus, time
bus = smbus.SMBus(1) # I²C bus 1 on the Pi Zero
ADDR = 0x52 # WonderEcho default — confirm with i2cdetect
WORD = b"claudia"
bus.write_i2c_block_data(ADDR, 0x10, list(WORD) + [0]) # 0x10 = set-trigger
time.sleep(0.2) # let the WonderEcho commit the trigger to its on-board flash before we close the bus
print("Wake word programmed:", WORD.decode())
PY
The chatbot service polls the WonderEcho's wake-event register over I²C and starts a recording session each time the word fires. No Python venv, no openWakeWord, no training.
✅ Checkpoint: speak "Claudia" near the module — journalctl -u chatbot.service -f shows a wake event within ~300 ms.
The exact register map can vary by firmware revision. If your unit reports a different I²C address (verify with
i2cdetect -y 1) or uses a different "set-trigger" opcode, check the WonderEcho wiki for the map matching your firmware.
09. Healthcheck
Before launching the full chatbot, run a 90-second healthcheck that verifies four layers: the WonderEcho is present on the I²C bus, the USB mic is visible to ALSA, the network can reach Anthropic, and your API key + chosen model actually return a response. (The full audio round trip — record, transcribe, speak — is exercised by the manual launch in Part 10.)
Create the script:
nano ~/healthcheck.sh
Paste:
#!/bin/bash
# claudia healthcheck — quick end-to-end smoke test
# Usage: bash ~/healthcheck.sh
set -u
ENV_FILE="$HOME/whisplay-ai-chatbot/.env"
PASS="\033[0;32m✓\033[0m"
FAIL="\033[0;31m✗\033[0m"
exit_code=0
step() { printf "\n%s\n" "── $1 ──"; }
ok() { printf " $PASS %s\n" "$1"; }
bad() { printf " $FAIL %s\n" "$1"; exit_code=1; }
step "1. WonderEcho module on I2C"
# The WonderEcho is the wake-word frontend and talks to the Pi over I2C bus 1.
# It is NOT an audio device — it never appears in ALSA.
if command -v i2cdetect >/dev/null 2>&1; then
if i2cdetect -y 1 2>/dev/null | grep -qE ' 5[234] '; then
ok "WonderEcho detected on I2C bus 1"
else
bad "WonderEcho NOT detected on I2C bus 1 (check 4-pin wiring + 'sudo raspi-config nonint do_i2c 0')"
fi
else
bad "i2c-tools not installed - run 'sudo apt install -y i2c-tools' (see Part 05.4)"
fi
step "2. USB microphone in ALSA"
# Conversation audio comes from the USB mic — a standard USB Audio Class
# device that must show up as an ALSA capture card (see Part 5.5).
if command -v arecord >/dev/null 2>&1; then
if arecord -l 2>/dev/null | grep -q '^card '; then
ok "ALSA capture device present: $(arecord -l 2>/dev/null | grep '^card ' | head -1)"
else
bad "no ALSA capture device — is the USB mic in the middle 'USB' port via the OTG adapter? (Part 03 / 5.5)"
fi
else
bad "alsa-utils not installed - run 'sudo apt install -y alsa-utils' (see Part 05.3)"
fi
step "3. Network reachability"
# Use HTTPS instead of ping — many networks/APIs drop ICMP but pass TLS.
# A 4xx response still proves we got a real reply from api.anthropic.com.
net_code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 https://api.anthropic.com/ 2>/dev/null || echo "000")
if [ "$net_code" != "000" ]; then
ok "api.anthropic.com responded (HTTP $net_code)"
else
bad "cannot reach api.anthropic.com (Wi-Fi, DNS, or TLS issue)"
fi
step "4. Claude API call"
if [ ! -f "$ENV_FILE" ]; then
bad "$ENV_FILE not found — finish Part 08 first"
else
# shellcheck disable=SC1090
set -a; source "$ENV_FILE"; set +a
if [ -z "${ANTHROPIC_API_KEY:-}" ]; then
bad "ANTHROPIC_API_KEY is empty in .env"
else
response=$(curl -s -w "\n%{http_code}" https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d "{\"model\":\"${ANTHROPIC_MODEL:-claude-haiku-4-5-20251001}\",\"max_tokens\":50,\"messages\":[{\"role\":\"user\",\"content\":\"Say hello in exactly 5 words.\"}]}")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "200" ]; then
ok "Claude API responded HTTP 200"
# Prefer jq if available — it handles escaped quotes correctly. Fall
# back to a grep+sed that breaks on escapes but is good enough for a
# smoke-test "did Claude reply" sanity check.
if command -v jq >/dev/null 2>&1; then
reply=$(echo "$body" | jq -r '.content[0].text // empty' 2>/dev/null)
else
reply=$(echo "$body" | grep -o '"text":"[^"]*"' | head -1 | sed 's/"text":"//;s/"$//')
fi
echo " Reply: $reply"
else
bad "Claude API returned HTTP $http_code"
echo " $body" | head -3
fi
fi
fi
echo
if [ $exit_code -eq 0 ]; then
printf "$PASS All checks passed. You're ready for Part 10.\n"
else
printf "$FAIL One or more checks failed. Fix above before running the chatbot.\n"
fi
exit $exit_code
Run it:
chmod +x ~/healthcheck.sh
bash ~/healthcheck.sh
✅ Checkpoint: All four sections print green check marks. If anything fails, fix that piece before moving on — running the full chatbot before this passes just makes debugging harder.
10. Run
Manual launch (foreground, for testing)
cd ~/whisplay-ai-chatbot
bash run_chatbot.sh
Say "Claudia" — the WonderEcho hears the wake word, the chatbot starts a recording session on the USB mic, you ask your question, and Claude answers out loud. Sessions end automatically after 60 seconds of silence or when you say a stop word (byebye, goodbye, or stop).
Stop the foreground process with Ctrl+C.
Set it to start on boot
The repo provides an opinionated startup installer that registers a chatbot.service systemd unit and sets the system to multi-user (headless) mode. Use it:
cd ~/whisplay-ai-chatbot
bash startup.sh
After this, the chatbot starts automatically on every boot. Verify:
sudo systemctl status chatbot.service
You should see Active: active (running).
Live logs
tail -f ~/whisplay-ai-chatbot/chatbot.log
# or
journalctl -u chatbot.service -f
Tuning wake-word reliability
The WonderEcho exposes a few I²C registers for tuning:
- Too many false wakes (TV, conversations) → raise the detection threshold via the threshold register.
- Missing real wakes (you have to say it twice) → lower the threshold, or move the module closer to where you sit.
Reference: Hiwonder WonderEcho wiki for the exact register map for your firmware revision.
<!-- when: smarthome=kasa,shelly,sonoff -->
10.5 Smart-home
You picked a smart plug. Teach Claudia to flip it by giving the chatbot a tool — a small shell command it can invoke when the user's request matches.
<!-- when: smarthome=kasa -->
TP-Link Kasa (HS103 / KP125M) — local control via python-kasa
pip install python-kasa --break-system-packages
# Find your plug on the LAN
kasa discover
# Toggle it (replace IP)
kasa --host 192.168.1.42 on
kasa --host 192.168.1.42 off
Wire that into the chatbot by exposing kasa --host <ip> on / off as a tool the LLM can call. No vendor account, no cloud hop — works even if the Kasa cloud is down.
<!-- end -->
<!-- when: smarthome=shelly -->
Shelly Plug US — local HTTP
Find your plug's IP in your router admin or via the Shelly app. Then any HTTP client can flip it:
# On
curl "http://192.168.1.42/relay/0?turn=on"
# Off
curl "http://192.168.1.42/relay/0?turn=off"
No vendor account, no SDK — wire those two curl calls into the chatbot as tools.
<!-- end -->
<!-- when: smarthome=sonoff -->
Sonoff S31 + Tasmota — local MQTT / HTTP
Out-of-the-box the S31 uses the eWeLink cloud, which means latency and a dependency on someone else's servers. Reflash with Tasmota (no soldering needed for the S31 — there's a serial header) to expose a local HTTP endpoint:
curl "http://192.168.1.42/cm?cmnd=Power%20On"
curl "http://192.168.1.42/cm?cmnd=Power%20Off"
Slightly more work to flash, but you get full local control + power-usage telemetry over MQTT. <!-- end --> <!-- end -->
11. Case
<!-- when: case=none --> You picked no case. PiSugar publishes free STL files if you change your mind — flip the 3D-printed case config above to FDM or SLA and the right link will appear here. <!-- end --> <!-- when: case=fdm,sla --> PiSugar publishes free STL files for case shells: <!-- end -->
<!-- when: case=fdm -->
- pi02 Whisplay chatbot case — FDM (filament print) <!-- end --> <!-- when: case=sla -->
- pi02 Whisplay chatbot case — SLA (resin print) <!-- end -->
<!-- when: case=fdm,sla --> No printer? Upload the STL to a print service like JLC3DP or Craftcloud — a few dollars shipped. <!-- end -->
12. Troubleshooting
Nothing plays through the speaker
- TTS playback goes to the Pi's default ALSA output (
aplay -lshows it), not to the WonderEcho — the WonderEcho's on-board speaker can only play its own canned firmware phrases and cannot render Claude's replies. Check which card playback is routed to in~/.asoundrc(Part 5.5) and that an actual speaker is attached to it. - Check
journalctl -u chatbot.service -ffor "TTS" or "speak" lines — if Claude is replying but you hear nothing, the playback device is wrong or muted (alsamixer, F6 to pick the card).
Mic captures silence or garbage
- Run
arecord -l— if the USB mic is missing, reseat the OTG adapter in the middleUSBport (the corner port is power-only) and checkdmesg | tailfor USB enumeration errors. - If the card number changed after a reboot (USB enumeration order isn't stable), update
hw:1,0in~/.asoundrcto matcharecord -l, or lock the mic to index 1 via/etc/modprobe.d/alsa-base.conf. - Test in isolation:
arecord -d 3 -f S16_LE -r 16000 /tmp/mictest.wav— if this errors, the problem is ALSA config, not the chatbot. - If the wake event never fires (
journalctl -u chatbot.service -fstays silent when you speak), that's the WonderEcho, not the mic — the wake word may have been reset on cold boot; re-run the I²C programming snippet from Part 08.3.
Build fails out of memory
- The Pi Zero 2 WH only has 512 MB. Add swap if
build.shgets OOM-killed:sudo dphys-swapfile swapoff sudo sed -i 's/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=1024/' /etc/dphys-swapfile sudo dphys-swapfile setup sudo dphys-swapfile swapon
Service won't start
sudo systemctl status chatbot.service --no-pager
journalctl -u chatbot.service -n 60 --no-pager
Look for the first ERROR line — usually a missing .env key or a wrong path.
Claude API returns 401
- API key is invalid or expired. Re-copy from console.anthropic.com → API Keys.
Claude API returns 429
- You're rate-limited. Add credit at console.anthropic.com → Billing.
WonderEcho doesn't respond
- Run
i2cdetect -y 1and confirm the module's address still shows up. - Re-run the wake-word programming script in Part 08.3 — flashes can be lost on cold boots.
- Check
journalctl -u chatbot.service -fwhile you speak — if the wake event never fires, the 4-pin I²C cable may have come loose or the module's mic input is occluded.
Wake word triggers on TV / unrelated speech
- Increase the WonderEcho's detection threshold via I²C — see the Hiwonder wiki for the register address on your firmware revision.
Responses feel slow
- Use
claude-haiku-4-5-20251001(Part 07 — it's the recommended default for this reason). - The Pi Zero 2 WH's Wi-Fi antenna is weak. Move it closer to the router.
- Local Whisper STT is the slowest step on a Pi Zero 2 WH. If you have a cloud STT key (OpenAI, Google), switching to one of those in
.envcuts perceived latency dramatically.
Need to re-run the healthcheck
bash ~/healthcheck.sh
SD card filling up
df -h
sudo apt clean
# clear chatbot recordings:
rm -f ~/whisplay-ai-chatbot/data/recordings/*.wav 2>/dev/null
Reference
- WonderEcho module: https://www.hiwonder.com/products/wonderecho
- SunFounder USB mini mic: https://www.sunfounder.com/products/mini-usb-microphone
- reSpeaker XVF3800 mic array: https://wiki.seeedstudio.com/respeaker_xvf3800_introduction/
- Chatbot repo: https://github.com/PiSugar/whisplay-ai-chatbot
- Claude API docs: https://docs.claude.com
- Claude model catalog: https://docs.claude.com/en/docs/about-claude/models/overview
- Pricing: https://anthropic.com/pricing
Summary stack
| Layer | What it is |
|---|---|
| Hardware | Pi Zero 2 WH + USB mic (SunFounder mini or reSpeaker XVF3800 array) + Hiwonder WonderEcho (I²C wake word) (+ optional PiSugar 3 battery) |
| OS | Raspberry Pi OS 64-bit |
| Wake word | "Claudia" — runs on the WonderEcho, no Pi-side listener |
| Microphone | USB mic via OTG adapter — the default ALSA capture device (the WonderEcho never streams audio) |
| Speech → text | Local Whisper-cpp, or cloud STT if configured |
| LLM | Claude API (Anthropic) |
| Text → speech | OpenAI gpt-4o-mini-tts (recommended), Piper (local), or ElevenLabs (with patch) |
| Service manager | systemd (chatbot.service, set up by startup.sh) |
Only Claude (and your chosen TTS, if cloud) runs in the cloud. Everything else can run on-device.