A multi-model Muse: personal AI voice assistant on ESP32 - wake word or knob, any OpenAI-compatible brain (STT/chat/TTS mix-and-match), streaming replies, out of the box. Built on muse-gadget-sdk.
The project's README from github.com/raullenchai/esp32-thinks. As one markdown file: README.md.
A multi-model Muse — a personal AI assistant on ESP32, talking out of the box.
Think of it as a Muse that thinks with your models: say "Hi, ESP" (or press the knob), ask anything, and it answers out loud — streaming sentence-by-sentence while the model is still writing, in English, Chinese or Korean, on a small gadget with a mic, a speaker and a round display.
No vendor lock on the brain: speech, transcription and chat are three independent endpoints, each pointed at any OpenAI-compatible server — one provider for all three, or a different one per task. Run it bare against QuickSilver, or put the in-repo openmuse agent in front to give it location, weather, time and memory.
Cloud assistants rent you a brain: one vendor's models, one vendor's pricing, one vendor's uptime, and your voice on their servers. The gadget hardware is fine — the lock is in the firmware.
esp32-thinks flips that. The device runs your stack: pick the cheapest transcription, the smartest chat model, the voice you like — per task, changeable in one config line and a rebuild. The implementation is a reviewable patch against a pinned open-source SDK, so you can read it, change it, or point it at a local server. The optional openmuse agent keeps the extra context (location, memory, tools) on your machine too.
openai, quicksilver, openmuse, custom.openmuse/ adds location, clock, weather, persistent memory and optional web search, owns the system prompt, and proxies STT so STT + chat share one TLS session. TTS still goes straight to your provider.# 0) One-time: ESP-IDF v6.0.x for esp32s3 (docs/REFERENCE.md §Prerequisites)
git clone https://github.com/facebookincubator/muse-gadget-sdk.git
git -C muse-gadget-sdk checkout 812c46f
git clone https://github.com/raullenchai/esp32-thinks.git
cd esp32-thinks
# 1) Back up the factory NVS once — your restore path if anything goes sideways
./scripts/backup-nvs.sh ../muse-gadget-sdk /dev/ttyACM1 # use your port: ACM0 or ACM1
# 2) Apply the patch + configure your brain (pick a preset)
./apply.sh ../muse-gadget-sdk --preset quicksilver # or openai / openmuse / custom
# 3) Build + flash (CH342-safe pacing; allow several minutes)
./build-and-flash.sh ../muse-gadget-sdk /dev/ttyACM1
With a preset + headless Wi-Fi in step 2, that's the whole setup: unplug, re-plug, and talk — press the knob, or just say "Hi, ESP".
| Command | Does |
|---|---|
./scripts/backup-nvs.sh <sdk> [port] |
Dump the factory NVS partition (0x9000, 0x32000) — do this once |
./apply.sh <sdk> [--preset ...] |
Copy pinned CAs, apply patches/muse-brain.patch, generate provider_config.h (asks for keys, Wi-Fi, wake word) |
./build-and-flash.sh <sdk> [port] |
idf.py build + paced flash (firmware + wake-word model); never interrupt it |
python3 scripts/tts_probe.py <sdk> [port] ["question"] |
Typed chat/TTS probe; opens a serial reader and resets the device |
./openmuse/run.sh [--funnel] |
Optional agent: Docker or Node, --funnel exposes HTTPS via Tailscale |
QuickSilver alone is the fastest path. If you want the gadget to know your city and time, answer weather questions, and remember things across turns, run the agent that ships in this repo:
./openmuse/run.sh # first run writes openmuse/.env, then exits
$EDITOR openmuse/.env # set UPSTREAM_API_KEY + OPENMUSE_TOKEN
./openmuse/run.sh --funnel # Docker or Node; --funnel exposes HTTPS via Tailscale
./apply.sh ../muse-gadget-sdk --preset openmuse
# chat URL: https://<host>/v1
# token: OPENMUSE_TOKEN
openmuse sits between the device and any OpenAI-compatible LLM: it owns the
system prompt, adds location/clock/weather/memory tools, proxies STT (one TLS
session for STT + chat), and streams the reply through untouched. TTS still goes
directly to QuickSilver. It needs no domain — a Tailscale Funnel gives it a real
Let's Encrypt certificate. Memory lives in openmuse/data/memories.json.
Details: openmuse/README.md. For the protocol, the
CopilotKit/OpenMuse boundary, and troubleshooting, see
docs/OPENMUSE.md.
QuickSilver preset, typed warm turns: 2.2–2.5 s for English/Korean and about 6.8 s for Chinese (longer text, slower synthesis). The nightly soak on 2026-10-19 measured 2.2 s / 6.8 s / 2.8 s (EN/CN/KO). Voice turns add recording/VAD: roughly 3–5 s from end of speech to first sound.
With the openmuse agent in front, add one hop and tool work: typed warm turns measured ~3.9 s, a voice turn ~4.7 s; the first turn after boot can be slower while DNS/TLS warm up. These are individual samples, not latency guarantees — provider load and reply length dominate.
See REFERENCE for historical measurements and configuration, and docs/SOAK.md for the nightly stability check.
"Hi, ESP" / knob ──▶ record (pre-roll holds the last second)
│ ends: voice VAD (~1.4 s silence) or knob release
▼
trim to speech ──▶ POST WAV/ADPCM ─▶ STT endpoint (or openmuse proxy)
│ (¼-size upload, keep-alive)
▼
POST /chat/completions (SSE) ──────────────▶ chat endpoint (or openmuse)
│ split at sentence boundaries (ASCII + 。!?…)
▼
per sentence: POST /audio/speech ─▶ MP3 appends to the buffer
│ already decoding + playing
▼
first word out loud while the model is still writing
Connection preparation starts after Wi-Fi comes up. A first question can still pay connection costs if preparation has not finished, or when a longer reply needs a separate speech connection. See REFERENCE for historical measurements and connection behavior.
| Device | Seeed SenseCAP Watcher W1-B (ESP32-S3, 1.45" 412×412 round LCD, ES8311 speaker codec, ES7243E mic, PTT knob) — more boards the SDK supports are candidates |
| Base firmware | muse-gadget-sdk @ 812c46f (2026-10-09) + patches/muse-brain.patch (see what's theirs, what's ours) |
| ESP-IDF | v6.0.x (CI pins espressif/idf:v6.0.3) |
| Brain | Any OpenAI-compatible servers: STT (/audio/transcriptions), chat (/chat/completions, SSE streaming), speech (/audio/speech) |
| Presets | openai (all three) · quicksilver (qwen3.7-flash + whisper-large-v3-turbo + chatterbox/qwen3-tts) · openmuse (in-repo agent for STT + chat, QuickSilver TTS) · custom |
| Wake word | esp-sr WakeNet10 wn10_hiesp ("Hi, ESP", on-chip, model flash partition) — custom phrases trainable via Espressif's TTS Pipeline V3 |
| Build host | Linux or macOS with ESP-IDF v6.0.x |
The USB serial port is a full console, handy for bench testing. Ports can
renumber on replug — use the one that answers >status (/dev/ttyACM0 or
/dev/ttyACM1):
| Input | Does |
|---|---|
hold knob / d…u |
simulate PTT down/up |
k |
audible self-test: plays the wake phrase through the speaker, then a full voice turn |
>chat=hello? |
typed turn — same chat/TTS pipeline |
>tasks |
dump every task backtrace (works even while wedged) |
>status |
heartbeats: Wi-Fi, heap, DMA, tunnel, firmware version |
z / w |
sleep / wake |
p |
screenshot over serial |
Use >chat= for routine checks and d/u for silent PTT input. Reserve k for a deliberate acoustic wake check; avoid loops that repeatedly play the wake phrase.
Credit where it's due — and clarity about the split:
The SDK provides the device framework, LVGL UI and round-LCD drivers, audio stack (codec, resampler, minimp3), input/console plumbing, OTA and partitioning. The patch also wraps peripheral I/O to monitor stalls. This recipe targets a pinned SDK commit so patches apply cleanly.
Ours (patches/muse-brain.patch): the brain session (muse_chat_session.cpp — three-endpoint config, SSE sentence streaming, TTS pipeline, connection warm-up + idle probes, turn cancellation), the wake-word integration (muse_voice.c — direct WakeNet10 on idle chunks, wake→PTT synthesis, VAD end-of-turn, quiet-recording guard, stall coroner), the I2C expander watchdog, the model flash partition and the esp-dl kernel fix, plus the tooling (apply.sh, build-and-flash.sh, NVS backup, probes) and the optional openmuse/ agent.
Nothing else from the Muse cloud is required: the vendor agent works as an optional fallback (its token is optional), and with headless provisioning the device never contacts it.
Everything lives in esp32/components/muse/provider_config.h (written by apply.sh):
| Setting | What |
|---|---|
TTS_URL / TTS_MODEL / TTS_VOICE |
Speech endpoint, model, voice |
TTS_MODEL_CJK |
Optional second speech model, selected for Chinese/Korean reply text |
BRAIN_STT_WAV_ADPCM |
1 uploads IMA ADPCM WAV at about one quarter of PCM size; enabled for the verified quicksilver and openmuse presets, 0 otherwise |
TTS_API_KEY / TTS_CA |
Its key and pinned root (OPENAI_CA or ISRG_CA) |
BRAIN_STT_URL / BRAIN_STT_MODEL |
Transcription endpoint (shares the chat key); openmuse points this at the agent's /audio/transcriptions proxy |
BRAIN_STT_LANG / BRAIN_STT_PROMPT |
Empty by default: omit both fields and auto-detect language; optionally pin an ISO 639-1 language or provide a prompt |
BRAIN_CHAT_URL / BRAIN_CHAT_KEY / BRAIN_MODEL |
Chat endpoint; leave the URL empty to use the Muse cloud agent. openmuse uses the agent URL and token |
BRAIN_CHAT_EXTRA |
Extra JSON merged into the chat body (provider quirks, e.g. DeepSeek's reasoning.exclude) |
BRAIN_SYSTEM |
Language-following persona; one or two sentences, ~40 words / 60 Chinese characters; greeting-only input returns Please ask. (openmuse replaces this with its own prompt) |
WAKE_WORD |
1 = "Hi, ESP" wake word + button; 0 = button only |
WAKE_VAD_DB |
End-of-turn silence threshold for wake turns (raise toward -35 if it keeps recording after you stop) |
DEV_WIFI_SSID / DEV_WIFI_PASS |
Headless provisioning; empty = normal BLE setup |
Presets: openai, quicksilver, openmuse (STT + chat through the in-repo openmuse agent — location, weather, time, memory — with QuickSilver for TTS), and custom.
Changed a define? Re-run ./build-and-flash.sh — it rebuilds and reflashes.
provider_config.h (it's generated; apply.sh keeps it out of the repo).openmuse/.env holds its keys too and is gitignored; only openmuse/.env.example is tracked.files/*.pem); the ESP cert bundle alone doesn't carry them (the v0.1 story — see REFERENCE — now with a second root for Let's Encrypt hosts).Build details and historical receipts: docs/REFERENCE.md. For contributing and validation, see CONTRIBUTING.md.
Top four (full checklist in REFERENCE):
brain heard: (transcription worked) and TTS ok: — quiet recordings are normally discarded with DIDN'T CATCH THAT. BRAIN STT FAILED can also indicate transport or response errors; inspect the HTTP log before changing credentials.coroner: (a 20 s peripheral stall) or expander wedged (v0.2.11+) just before rst:. The Wi-Fi ... reason=8 line right before the reset is the shutdown side-effect, not the cause; see issue #5.device reports readiness to read but returned no data — two processes fighting over the same /dev/ttyACM* (monitor + probe). One at a time.esptool at high baud. The CH342 bridge drops whole packets; the recipe uses paced_esptool at 115200 for a reason.version: line from >status, the turn that failed, and >tasks if the device froze (or the lines just before rst: if it rebooted).good first issue are scoped to be self-contained.See CHANGELOG.md and the releases page.
A multi-model Muse: personal AI voice assistant on ESP32 - wake word or knob, any OpenAI-compatible brain (STT/chat/TTS mix-and-match), streaming replies, out of the box. Built on muse-gadget-sdk.