A standalone AI voice assistant that connects directly to ESPHome devices over WiFi or BLE. No Home Assistant required.
Handles the full voice pipeline: wake word detection (on-device) → speech-to-text → AI agent → text-to-speech, with audio streamed back to the device speaker.
ESPHome Device (wake word) ──► mic audio over WiFi/BLE ──► Ovi Server
│
STT (Nemotron / Whisper)
│
Agent (OpenAI Agents SDK)
│
TTS (Kokoro / Piper)
│
ESPHome Device (speaker) ◄── encoded audio ◄───────────────┘
- STT: Nemotron Speech 600M streaming RNNT (default) or faster-whisper. Both use Silero VAD. CPU inference.
- Agent: OpenAI Agents SDK — works with any OpenAI-compatible endpoint (OpenAI, ollama, vLLM, LM Studio, etc.). Supports MCP tools, sub-agents, and session memory.
- TTS: Kokoro 82M ONNX int8 (default) or Piper ONNX voices. CPU inference.
- Transport: WiFi (plain TCP) or BLE (GATT). Audio codecs: PCM, LC3, Opus.
- Music: YouTube Music, Spotify, Apple Music via browser automation. Multi-room synchronized playback.
- Memory: Persistent fact extraction and recall (SQLite + embeddings).
- Automations: Cron-based proactive announcements.
| Device | Transport | Codec | Notes |
|---|---|---|---|
| Voice Preview Edition | WiFi, BLE | LC3 | 12-LED ring, rotary volume, mute switch |
| M5Stack ATOM Echo | WiFi | PCM | Shared audio bus, single LED |
| ESP32-S3-BOX-3 | WiFi | LC3 | Dual mics, ES8311 DAC |
| Elecrow CrowPanel 9" | WiFi | LC3 | ESP32-P4 + C6, touchscreen |
| Elecrow CrowPanel Advance 5" | WiFi | LC3 | ESP32-S3, touchscreen |
- Python 3.14+, uv
- An ESPHome-compatible device (see above)
- An OpenAI-compatible LLM endpoint
uv sync --group devEdit esphome/secrets.yaml with your WiFi credentials:
wifi_ssid: "YourNetwork"
wifi_password: "YourPassword"Flash the device:
uv run esphome run esphome/voice-pe.yamlOn first run, Ovi launches an interactive setup wizard that walks you through LLM, STT, TTS, device, and codec selection:
oviThe wizard saves configuration to ~/.ovi/config.yaml. You can re-run it anytime:
ovi --setupOr edit the YAML directly:
# ~/.ovi/config.yaml
llm:
base_url: http://localhost:11434/v1 # ollama, LM Studio, etc.
model: llama3.2
stt:
provider: nemotron # nemotron or whisper
model: int8-dynamic
tts:
provider: kokoro # kokoro or piper
model: af_heart
devices: voice-pe-XXXX.local
transport:
codec: lc3 # lc3, opus, or pcmoviOr pass devices on the command line:
ovi voice-pe-XXXX.localDiscover devices on the network:
ovi --scanBLE mode (single device):
ovi --transport bleOvi uses a layered configuration system. Sources are applied in this order (later overrides earlier):
- YAML config —
~/.ovi/config.yaml(created byovi --setup) .envfile — dotenv in the working directory- Environment variables —
OVI_prefix with__for nesting - CLI arguments —
--agent-model,--codec, etc.
The config file lives at ~/.ovi/config.yaml and uses nested sections:
llm:
api_key: sk-...
base_url: http://localhost:11434/v1
model: llama3.2
mcp_servers: '@~/.ovi/mcp.json'
agents: '@~/.ovi/agents.json'
stt:
provider: nemotron # nemotron, whisper
model: int8-dynamic # int8-dynamic, int8-static, fp16, fp32
device: cpu # cpu, cuda
tts:
provider: kokoro # kokoro, piper
model: af_heart
devices: voice-pe-XXXX.local
transport:
type: wifi # wifi, ble
codec: lc3 # lc3, opus, pcm
memory:
enabled: trueOverride any config value using the OVI_ prefix and __ as the nesting delimiter:
OVI_LLM__MODEL=gpt-4o-mini ovi # override LLM model
OVI_STT__PROVIDER=nemotron ovi # switch STT provider
OVI_TRANSPORT__CODEC=opus ovi # change codec
OVI_LLM__API_KEY=sk-... ovi # set API key without saving to fileCLI arguments take highest priority:
ovi --agent-model gpt-4o --codec opus --stt-model small.enRun ovi --help for all options.
| Path | Contents |
|---|---|
~/.ovi/config.yaml |
Configuration file |
~/.ovi/memory.db |
Persistent memory (SQLite) |
~/.ovi/automations.json |
Scheduled automations |
~/.cache/ovi/ |
Model caches (whisper, nemotron, embeddings) |
~/.cache/kokoro/ |
Kokoro TTS model cache |
~/.cache/piper-voices/ |
Piper TTS voice cache |
Generate an encryption key:
ovi --gen-keyThis writes the key to esphome/secrets.yaml. Uncomment the encryption block in the device YAML:
api:
encryption:
key: !secret api_encryption_keyReflash and connect:
ovi voice-pe-XXXX.local::KEYThe voice assistant includes 19 built-in tools:
- Speech:
say(immediate speech) - Timers:
set_timer,check_timer,cancel_timer - Time:
get_current_time - Math:
calculate,unit_convert - Random:
roll_dice,random_number,flip_coin - Music:
play_music,pause_music,resume_music,skip_track,stop_music,now_playing - Automations:
create_automation,list_automations,delete_automation,toggle_automation
Additional tools via MCP servers and sub-agents.
The ESPHome firmware provides:
- On-device wake word detection (microWakeWord)
- "Stop" wake word to interrupt responses
- LED state feedback (listening, thinking, replying, error, muted)
- Volume control (rotary encoder or software)
- Hardware mute switch
- Conversation follow-up (
[LISTEN]token) - Multi-device wake word arbitration (closest device wins)
- Synchronized multi-room music playback (NTP-based)
src/ovi_voice_assistant/
__main__.py CLI entry point (ovi command)
config.py Settings (OVI_ env prefix)
voice_assistant.py STT → Agent → TTS pipeline
device_connection.py Device transport bridge + codec
device_manager.py Multi-device management + wake arbitration
discovery.py mDNS device discovery
scheduler.py Cron automations
pipeline_output.py Pipeline event types
codec/ PCM, LC3, Opus encoding/decoding
transport/ WiFi (TCP) and BLE (GATT) transports
stt/ Nemotron + Whisper speech-to-text
tts/ Kokoro + Piper text-to-speech
agent/ OpenAI Agents SDK + MCP tools
memory/ SQLite fact store + embeddings
music/ YouTube, Spotify, Apple Music
esphome/
components/ Custom ESPHome components
ovi_voice_assistant/ Device-side voice pipeline
ovi_audio_codec/ LC3/Opus codec for ESP32
voice-pe.yaml Voice PE (WiFi)
voice-pe-ble.yaml Voice PE (BLE)
atom-echo.yaml ATOM Echo
s3-box-3.yaml S3-BOX-3
crowpanel-9.yaml CrowPanel 9" (ESP32-P4)
crowpanel-s3-5.yaml CrowPanel Advance 5" (ESP32-S3)