ESP32 Voice Assistant for OpenClaw
Give OpenClaw a hardware voice: an ESP32-S3-BOX-3 running stock ESPHome firmware talks directly to a Docker bridge — on-device wake word, VAD, STT, OpenClaw as the agent brain, then streamed sentence-chunked TTS back to the speaker. No Home Assistant in the voice path, and zero firmware changes: the bridge impersonates the Home Assistant voice client over the ESPHome native API (TCP 6053), so you never reflash or edit device YAML.
wake word (on-device) → ESP32-S3-BOX-3 → bridge (Docker): VAD → STT (xAI|ElevenLabs|FishAudio)
→ spoken ack → OpenClaw /v1/chat/completions → TTS (xAI|ElevenLabs|FishAudio, streamed)
→ device fetches reply audio (HTTP :10400, FLAC)
The author's device answers to "Yo Marvin" — a custom-trained wake word. Yours can use any standard ESPHome wake word (e.g. "Hey Jarvis") or a custom one you train yourself; the process is covered in Hardware Voice Assistant for OpenClaw.
STT and TTS providers are independently switchable between xAI Grok Voice, ElevenLabs, and FishAudio — mix and match (e.g. cheap xAI STT + a FishAudio custom voice for TTS).
Hardware & prerequisites
- ESP32-S3-BOX-3 running an ESPHome voice-assistant firmware with on-device wake word (the stock wake-word-voice-assistants config or a derivative — standard or custom-trained wake word). Other ESPHome voice devices should work; the bridge adapts to the advertised media format.
- Docker host on the same LAN — the device must be able to reach the bridge's HTTP port (default 10400).
- OpenClaw with the chat interface enabled — the bridge talks to OpenClaw's OpenAI-compatible
/v1/chat/completionsendpoint, which is only served when the chat interface is turned on in your OpenClaw configuration. It must be reachable from the bridge (default compose config expects it on the Docker host, port 18789). - xAI, ElevenLabs, and/or FishAudio API key — at least one, matching your
STT_PROVIDER/TTS_PROVIDERselection.ELEVENLABS_VOICE_ID/FISHAUDIO_VOICE_IDis also required when TTS is that provider.
Setup
-
Clone the repo and create the config:
git clone https://github.com/darrenjrobinson/voice-esp32-openclaw.git cd voice-esp32-openclaw cp .env.example .env -
In OpenClaw, make sure the chat interface is enabled so
/v1/chat/completionsis being served, and note its URL and API key. -
Edit
.env— at minimum:STT_PROVIDER/TTS_PROVIDER, the matching API key(s),OPENCLAW_URL,OPENCLAW_API_KEY,ESP32_HOST(andESP32_NOISE_PSKif the device API is encrypted), andBRIDGE_ADVERTISE_HOST(the Docker host's LAN IP — required inside Docker). A commented reference copy ships with this skill as example.env. -
Disconnect Home Assistant's voice subscription for the device (disable the ESPHome integration entry or its assist satellite entity). The protocol allows only one voice-assistant subscriber — with two, the wake word silently does nothing.
-
Start the bridge and verify:
docker compose --profile bridge up -d --build docker compose logs -f voice-bridge # expect: "Connected to <device> (…)" and "device announcement format: flac @ 48000 Hz" -
Say the wake word. Done.
Configuration reference (all in .env)
| Variable | Default | Purpose |
|---|---|---|
STT_PROVIDER / TTS_PROVIDER |
xai |
xai, elevenlabs, or fishaudio, independently — any mix works |
XAI_VOICE |
eve |
Any of the 26 built-in xAI voices or a custom voice ID |
ELEVENLABS_VOICE_ID |
— | Required for ElevenLabs TTS; list yours with python scripts/elevenlabs_voices.py |
ELEVENLABS_TTS_MODEL |
eleven_flash_v2_5 |
Lowest-latency, half-price model |
FISHAUDIO_VOICE_ID |
— | Required for FishAudio TTS; this is the voice's reference_id — list yours with python scripts/fishaudio_voices.py |
FISHAUDIO_TTS_MODEL |
s1 |
Or s2-pro, s2.1-pro, or the free s2.1-pro-free |
OPENCLAW_SESSION_KEY |
agent:main:voice |
Session identity sent to OpenClaw (OPENCLAW_SESSION_MODE=user sends it as the OpenAI user field, header as X-Session-Key) |
OPENCLAW_TIMEOUT_SECONDS |
240 |
Max wait for the agent's reply — tool-heavy turns can run minutes |
BRIDGE_ACK_PHRASE |
On it. |
Spoken while the agent thinks; synthesized once and cached; empty disables |
BRIDGE_VOLUME |
1.0 |
Device volume set before each reply (0 = leave alone) |
VAD_THRESHOLD |
500 |
Speech gate — the BOX-3 mic runs quiet, values near 60 work well; per-turn level stats are logged for tuning |
VAD_SILENCE_SECONDS / VAD_MAX_SECONDS |
0.8 / 10 |
End-of-utterance silence and capture cap |
TTS_STREAMING |
true |
Stream synthesis for fastest time-to-first-audio |
Per-turn cost metrics (USD + ElevenLabs credits) are appended to out/metrics.jsonl. A typical turn costs ~$0.005 (all-xAI or all-FishAudio) to ~$0.015 (ElevenLabs TTS).
Troubleshooting
.envchanges don't apply: containers read.envonly at creation. From the repo directory rundocker compose --profile bridge up -d(add--force-recreateif it reports nothing changed).restartis not enough.- 401 or connection refused from OpenClaw: confirm the chat interface is enabled in OpenClaw (it serves
/v1/chat/completions),OPENCLAW_URLis reachable from inside the container, andOPENCLAW_API_KEYmatches. - Wake word does nothing: something else (usually Home Assistant) still holds the voice-assistant subscription — only one subscriber is allowed.
- No reply audio: check
BRIDGE_ADVERTISE_HOSTis the Docker host's LAN IP and port 10400 is reachable from the device. - Device quirks (chunked-transfer rejection, FLAC-only decoding, wedged announcements, volume handling) are already handled by the bridge — see the README's "Device quirks" table before suspecting the device.
- Rollback:
docker compose --profile bridge down, then re-enable the ESPHome integration in Home Assistant — the device reverts to the HA Assist pipeline immediately; nothing on the device was ever modified.
Further reading
- Going Direct — ESP32 Voice for OpenClaw — the write-up of this integration.
- Hardware Voice Assistant for OpenClaw — device setup and training custom wake words (like "Yo Marvin").
- GitHub: darrenjrobinson/voice-esp32-openclaw — source, full README, cost analysis, and utility scripts.
