Skip to the content
PCBKitNew board
2 layers31 × 43 mmNEO Lite, the printed circuit board in 3D

Open-source ESP32-S3 voice assistant: offline wake word with TinyML (TensorFlow Lite Micro + ESP-NN), Whisper speech-to-text, local LLM (Ollama, Gemma, Qwen) or Groq, streaming TTS. A DIY Spanish-speaking smart speaker with measured results.

Size
31 × 43 mm
Copper layers
2
Thickness
1.6 mm
Parts
35
Nets
54
Pads
180
Tracks, vias
297, 89
Pours
3
Routed
Yes
KiCad
KiCad 9.0 (file 20241229)
Created
22 Apr 2026
Updated
31 Oct 2026

Overview

NEO Lite is an open-source printed circuit board by techmiguel, from the techmiguel/neo-robot repository on GitHub, published under the MIT licence. It is a 2-layer board of 31 × 43 mm with 35 parts on 54 nets, routed with 297 tracks and 89 vias.

It is built around the ESP32-S3-WROOM-1-N16R8, MAX98357AETE+T and TLV1117LV33DCYR, with the BSMD1206-110-6V, TPSC476K016R0350 and TYPE-C-31-M-12 among its other parts. Its parts include 10 resistors, 9 capacitors, 4 connectors, 3 ICs and 2 switches. It belongs with the audio and music and home automation and IoT boards and the ESP32 boards of this library.

Main components

7

README

README.md

NEO — open-source ESP32-S3 voice assistant with an offline wake word and a local LLM

Tests License: MIT ESP32-S3 Python Language

NEO is a DIY smart speaker built around an ESP32-S3. You say "Hola NEO": a 15 KB TinyML model (TensorFlow Lite Micro, int8) recognizes the wake word on the chip, offline. Then you ask anything, hands-free. A Python server on your PC transcribes the question with Whisper, answers it with a local LLM (Ollama: Gemma, Qwen) or the cloud (Groq), and talks back through the speaker with streaming text-to-speech.

It is a privacy-friendly, hackable Alexa / Google Home alternative you can build on a breadboard for the price of a few modules, and every number in this README was measured on the real board, with the raw data in the repo.

mermaid
flowchart TB
    subgraph board["ESP32-S3 board"]
        direction LR
        mic["INMP441 mic"] --> ww["Wake word 'Hola NEO'<br/>TFLite Micro + ESP-NN"] --> vad["VAD<br/>300 ms pre-roll"]
        spk["DAC + amp<br/>speaker"]
    end
    subgraph server["Python server: your PC or Hugging Face Spaces"]
        direction LR
        stt["STT<br/>faster-whisper / Groq"] --> intent{"Intent"}
        intent -- command --> api["Weather, crypto,<br/>news, rates"]
        intent -- question --> llm["LLM<br/>Ollama / Groq"]
        api --> tts["TTS by fragments<br/>edge-tts / Piper"]
        llm -- tokens --> tts
    end
    vad -- "PCM 16 kHz, WebSocket" --> stt
    tts -- "PCM, streamed" --> spk

Highlights

  • Offline wake word on a microcontroller. A CNN on MFCC features, quantized to int8 and accelerated with Espressif's ESP-NN SIMD kernels: 0.32–0.40 s per inference, ~1.1 s from the end of "Hola NEO" to NEO answering. Test accuracy 95.7 %; the full training pipeline is included.
  • Local-first. The board talks to your PC on the LAN; if it is not there, it falls back to a cloud deployment. The server can run fully local (faster-whisper + Ollama) or on free cloud tiers (Groq + edge-tts).
  • Streaming responses. The LLM's tokens are cut into phrases, each one is synthesized and sent as soon as it is ready, and the firmware starts playing after 256 ms of buffered audio, without waiting for the full reply.
  • Commands without an LLM. Weather, crypto prices, tech news and Cuban informal exchange rates are answered by keyword intent detection and real APIs: faster, cheaper and no hallucinations.
  • Robust on bad networks. Prerecorded phrases tell you when WiFi, the server or the internet is down. WiFi sleeps after 10 min idle and the CPU drops to 80 MHz while the wake word keeps listening.
  • Measured, not claimed. Word error rate, latency per pipeline stage, wake word confusion matrix and power draw per state, from 31-phrase sessions read to the board.

Quick start

You need the hardware, a PC on the same network (Windows, macOS or Linux) and about 30 minutes.

1. Server

bash
git clone https://github.com/techmiguel/neo-robot.git
cd neo-robot/servidor
python -m venv .venv
.venv\Scripts\activate          # Windows; on macOS/Linux: source .venv/bin/activate
pip install -e ".[dev]"
cp .env.example .env            # Windows: copy .env.example .env

Pick a profile and edit .env:

ProfileWhat runs whereYou need
nube (cloud, easiest)Groq Whisper + Groq LLM + edge-ttsA free Groq API key in GROQ_API_KEY
localfaster-whisper + Ollama on your PC, edge-tts voice (Piper offline fallback)pip install -e ".[local]", Ollama with ollama pull gemma4:e2b (or qwen2.5:3b)

The command APIs are optional: OPENWEATHER_API_KEY, NEWSAPI_KEY and ELTOQUE_API_KEY (free tiers; CoinGecko needs no key). Every variable is explained in servidor/.env.example.

bash
python -m src.server.ws_server --perfil nube     # or --perfil local

The server listens on ws://0.0.0.0:8765. Note your PC's LAN IP address.

2. Firmware

Install PlatformIO (VS Code extension or pip install platformio).

bash
cd neo-robot/firmware
cp src/config_local.example.h src/config_local.h   # Windows: copy; set NEO_SERVIDOR_HOST to your PC's IP
pio run -e esp32dev --target upload
pio device monitor -e esp32dev

The first build downloads the ESP32 toolchain and TensorFlow Lite Micro and takes a few minutes. Without config_local.h the firmware builds with a placeholder IP (192.168.1.100) and no cloud fallback.

3. First boot

  1. NEO has no WiFi yet, so it opens an access point called NEO-Config. Join it from your phone and open http://192.168.4.1.
  2. Enter your WiFi name and password. NEO saves them, connects, and finds the server.
  3. Say "Hola NEO", wait for the greeting and ask, in Spanish: "¿Cuál es la capital de Japón?", "¿Va a llover mañana en La Habana?", "¿Cuánto vale el bitcoin?".

No wake word? Press the BOOT button briefly to ask without it. The model was trained on two voices; if it does not wake for yours, train it on your voice.

Troubleshooting

SymptomFix
Could not install package 'links2004/WebSockets' on WindowsThe path is too long for Windows (260 characters). Clone to a short path such as C:\neo.
This chip is ESP8266, not ESP32-S3 while flashingAnother board is on the default port. Pass it explicitly: --upload-port COM8 (the Freenove shows up as USB-Enhanced-SERIAL CH343 in pio device list).
Serial log full of ASSOC_TOOMANYYour access point (often a phone hotspot) has no free client slots. Disconnect a device.
time: missing unit in duration "-1" from OllamaUse OLLAMA_KEEP_ALIVE=-1m, with a unit.
NEO hears the wake word but misses the start of your questionWait for the greeting to finish. The VAD calibrates the background noise for 0.5 s after it.
The first answer takes minutes with the local profileOllama is loading the model from disk; later answers are fast. Keep it loaded with OLLAMA_KEEP_ALIVE=-1m.

Hardware

PartNotes
Freenove ESP32-S3 WROOM CAM (FNK0085), N8R8 module8 MB flash, 8 MB PSRAM
INMP441 I2S microphoneI2S_NUM_0
PCM5100A + PAM8403 I2S DAC/amplifier module, 8 Ω speakerI2S_NUM_1
SSD1306 128 × 64 I2C OLEDAnimated eyes and connection status

Wiring, pin tables and the mistakes to avoid are in docs/hardware.md. A compact custom PCB (NEO Lite, 31 × 43 mm, USB-C, ready for JLCPCB assembly) is in hardware/neo-lite/, designed in KiCad 9 but not yet fabricated.

What you can ask

NEO speaks Spanish. After the wake word it detects the intent of the transcription:

Intent (keywords)AnswerSource
clima, "qué tiempo hace", temperatura, pronósticoCurrent weather (default city: La Habana)OpenWeatherMap
dólar, euro, tasa, El ToqueInformal Cuban peso (CUP) exchange ratesEl Toque API
bitcoin, ethereum, criptoCrypto priceCoinGecko
noticias, titularesTech news (Apple, Google, Nvidia, OpenAI…), one headline per outletNewsAPI
hola, "qué tal"Greeting—
anything elseFree answer in NEO's voice: a stranded alien traveller, curious, with dry humour, ≤ 20 wordsLLM

Adding a command is a Python class on the server (servidor/src/commands/); the firmware does not change.

Measured results

All numbers come from the board. Method, raw data and analysis are linked.

Wake word "Hola NEO", CNN int8 on the ESP32-S3, two speakers (experiments, metrics)

MetricValue
Test accuracy (int8, 302 clips)95.7 %
True / false positive rate per window (threshold 0.7618)95.6 % / 0.6 %
Inference time with ESP-NN kernels0.32–0.40 s (2.6 s with the reference kernels)
End of "Hola NEO" → confirmed, on the board1.1 s median (1.11 s on 24/10, n = 4; 1.09 s on 25/10, n = 5)

Wake word confusion matrix

Conversation, 31 phrases read to the board, same audio sent to every server variant. Word error rate ignores accents. Latency runs from the end of the recording to the first audio of the reply, median (p90).

SessionSTT + LLM + TTSWERIntentFree questionsCommands
24/10 (analysis)Cloud: Groq Whisper + gpt-oss-20b + edge-tts3.1 %96.8 %5.7 s (8.0 s)5.1 s (6.1 s)
24/10Local: Whisper small + qwen2.5:3b + edge-tts6.2 %96.8 %8.8 s (9.6 s)6.2 s (9.5 s)
25/10 (analysis)Local: Whisper small + gemma4:e2b, streaming5.1 %100 %18.0 s with the long prompt*—

* The 25/10 run exposed that gemma4:e2b re-processes the system prompt on every question (~37 tokens/s on a laptop i5): the 409-token prompt cost ~8 s. The local profile now uses a 139-token prompt (2.7–3.3 s in Ollama, not yet re-measured end to end). Streaming saves ~1.9 s once answers are long enough to split; edge-tts costs ~1.9 s per call from Cuba. On the board, add 1.5 s of end-of-speech silence and ~0.6 s of WiFi upload.

Power at 5 V (INA219, method, data, preliminary)

StateMean currentPeakMean power
Rest, listening for the wake word119 mA249 mA0.60 W
Wake word capture + inference137 mA255 mA0.69 W
Sending audio over WiFi153 mA408 mA0.76 W
Speaking302 mA797 mA1.44 W

Current during one interaction

Train the wake word on your voice

The model ships in the firmware (firmware/src/models/) and was trained on two speakers. The recordings are not published (they are people's voices), but the pipeline is:

bash
cd firmware
pio run -e dataset_capture --target upload               # firmware that streams clips over serial
pip install -r tools/requirements_dataset.txt
python tools/capture_dataset.py --port COM8              # guided recording per class
pip install -r tools/requirements_train.txt
python tools/train_model.py                              # trains, quantizes to int8, exports TFLite
python tools/gen_vocabulario.py --umbrales models/umbrales.json
python tools/tflite_to_header.py                         # writes src/models/*.h
pio run -e esp32dev --target upload

tools/vocabulario.json (classes, phrases, thresholds) is the single source of truth for capture, training and inference. The experiments behind the current model (cross-validation, leave-one-speaker-out, negative speech) are in firmware/models/experimentos/.

Other firmware environments

pio run -e …Purpose
esp32devFull firmware: wake word, WebSocket, audio, OLED
hw_testChecks OLED, microphone, speaker and button
loopback_testPlays the microphone through the speaker
wake_word_testWake word inference without the server
dataset_captureRecords training clips over serial
demo_ojos / demo_bocinaEyes animation / speaker playback demos
consumo_loggerPower logger on an ESP8266 + INA219

Benchmark your own setup

The server includes the validation panel used for every number above: it relays the board's audio to several server variants (models, providers, streaming on/off), scores the transcriptions against the corpus and reads the latency from the board's serial log.

bash
cd servidor
python -m src.benchmark.panel --variante "gemma=local,OLLAMA_MODEL=gemma4:e2b" --variante "nube=nube"

Open http://localhost:8080, read the phrase it shows to NEO, and press Generar informe at the end. Details in servidor/benchmark/README.md.

Repository layout

text
firmware/            ESP32-S3 firmware (C++, PlatformIO + Arduino)
  src/               audio, display, network, commands (TFLite inference), main.cpp
  tools/             dataset capture, training, header generation, log analysis
  models/            trained models and the experiments behind them
  logs/              serial logs of the measured sessions
servidor/            Python server: STT, intent, LLM, TTS, WebSocket, benchmark panel
hardware/neo-lite/   custom PCB: KiCad project, generator scripts, Gerbers, BOM
docs/                design notes, experiments and measured metrics (in Spanish)
ROADMAP.md           phases and status (in Spanish)

Documentation

The design notes and experiment reports are written in Spanish.

TopicDocument
Wiring and partsdocs/hardware.md
Software strategy: voice, prerecorded phrases, network on demanddocs/estrategia-software.md
Streaming responses and where the latency goesdocs/streaming-latencia.md
Conversation memorydocs/diseno-memoria.md
Experiments: latency, wake word latency, false positives, full flow, powerdocs/experimento-*.md
Measured sessionsdocs/metrics/
Custom PCB, phase by phasehardware/neo-lite/README.md
Status and next stepsROADMAP.md

Design decisions

  • Only the wake word runs on the chip. A multi-command local classifier would need a dataset per phrase and a reflash per change. Commands live on the server and grow without touching the firmware; the cost is that every question needs the network.
  • Keywords before the LLM. Common commands skip the LLM: faster, free and deterministic. The LLM only gets free-form questions.
  • Two I2S buses. Microphone and DAC each get their own peripheral; no time multiplexing.
  • Inference buffers in internal SRAM. PSRAM access with WiFi active tripped the watchdog.
  • One voice. NEO always speaks with edge-tts es-CU-ManuelNeural, including its prerecorded phrases; Piper only answers when edge-tts is unreachable.

Failure modes

Every message below is prerecorded in the firmware, so NEO can say it without a server.

SituationWhat NEO does
Server unreachable (LAN and cloud)Says "No encuentro mi servidor", and "Ya estoy conectado y listo" when it is back
WiFi downSays it cannot connect; the wake word keeps working
No internet on the serverThe server sends sin_internet; NEO says "No puedo acceder a internet ahora"
Empty transcription"No te entendí, ¿me lo repites?" and listens again
Server silent for 30 s"Está tardando demasiado, inténtalo otra vez"
edge-tts unreachableThe server answers with Piper (different voice)
LLM drifts into another script (qwen2.5:3b → Chinese)The server asks again once, then strips non-Latin text
Wake word false positiveNEO greets, hears nothing for 10 s and goes back to rest

Security and limitations

  • The WebSocket has no authentication; it assumes a trusted LAN. Add a token or TLS before exposing the server. API keys live in servidor/.env (gitignored).
  • Spanish only: STT, prompts and voice.
  • Short memory: the last 5 exchanges per WebSocket session, in RAM.
  • USB power: speaking peaks at ~0.8 A at 5 V; a single boosted 18650 cell browns out.
  • No OTA updates yet; flash over USB.
  • The local profile is slower and less accurate than the cloud one on a laptop CPU.

Contributing

Issues and pull requests are welcome; see CONTRIBUTING.md. Run the tests before sending changes:

bash
cd servidor && pytest
cd firmware/tools && pytest

License and acknowledgements

MIT. Built on TensorFlow Lite Micro, ESP-NN, faster-whisper, Ollama, Groq, edge-tts, Piper, Adafruit SSD1306 and arduinoWebSockets.

¿Hablas español? Lee el README en español.

Parts

35
QtyValueFootprintReferences
3100nFCapacitor_SMD:C_0402_1005MetricC4, C7, C10
310uFCapacitor_SMD:C_0805_2012MetricC2, C5, C9
20Resistor_SMD:R_0402_1005MetricR7, R8
222Resistor_SMD:R_0402_1005MetricR4, R5
24.7kResistor_SMD:R_0402_1005MetricR9, R10
25.1kResistor_SMD:R_0402_1005MetricR1, R2
2M2MountingHole:MountingHole_2.2mm_M2H1, H2
1+3V3TestPoint:TestPoint_Pad_D1.0mmTP2
1+5VTestPoint:TestPoint_Pad_D1.0mmTP1
11.1AFuse:Fuse_1206_3216MetricF1
110kResistor_SMD:R_0402_1005MetricR3
11uFCapacitor_SMD:C_0402_1005MetricC6
122uFCapacitor_SMD:C_0805_2012MetricC3
1470kResistor_SMD:R_0402_1005MetricR6
147uF 16VCapacitor_Tantalum_SMD:CP_EIA-6032-28_Kemet-CC1
1ALTAVOZ 8 ΩConnector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_VerticalJ4
1BOOTButton_Switch_SMD:SW_Push_1P1T_XKB_TS-1187ASW2
1ESP32-S3-WROOM-1-N16R8RF_Module:ESP32-S3-WROOM-1U2
1GNDTestPoint:TestPoint_Pad_D1.0mmTP3
1MAX98357APackage_DFN_QFN:TQFN-16-1EP_3x3mm_P0.5mm_EP1.23x1.23mmU4
1MIC INMP441Connector_PinHeader_2.54mm:PinHeader_1x06_P2.54mm_VerticalJ3
1OLEDConnector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_VerticalJ2
1RESETButton_Switch_SMD:SW_Push_1P1T_XKB_TS-1187ASW1
1TLV1117LV33Package_TO_SOT_SMD:SOT-223-3_TabPin2U1
1USB-CConnector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12J1
1USBLC6-2SC6Package_TO_SOT_SMD:SOT-23-6D1

Questions

How big is the NEO Lite PCB?

It measures 31 × 43 mm and has 2 copper layers, 1.6 mm thick.

What parts does the NEO Lite use?

35 parts, built around the ESP32-S3-WROOM-1-N16R8, MAX98357AETE+T, TLV1117LV33DCYR and BSMD1206-110-6V. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.

Where can I download the NEO Lite KiCad files?

From the techmiguel/neo-robot repository on GitHub, at the commit this page was made from: the board is hardware/neo-lite/kicad/NEO_Lite.kicad_pcb and its schematic hardware/neo-lite/kicad/NEO_Lite.kicad_sch. PCBKit opens the board in the browser, with nothing to install.

Can I use the NEO Lite design in my own project?

Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

Similar boards

  • 6 layers68.8 × 74.6 mmFurbyBoard, the printed circuit board in 3D

    FurbyBoard

    Guilleloma/Furby-GPT

    This project transforms a 1998 Furby into an interactive and advanced desktop assistant using a Raspberry Pi W. It incorporates OpenAI and Whisper for natural language processing, primarily relying on Google Cloud Text…

    158 parts26
  • 4 layers89.5 × 38 mmRoom node, the printed circuit board in 3D

    Room node

    boernerc20/esphome-room-node

    ESPHome-based smart room node for Home Assistant — temp/humidity, e-ink display, voice satellite, LED ring

    57 parts
  • 4 layers46 × 86.4 mmOpenCycle, the printed circuit board in 3D

    OpenCycle

    Vishvak365/OpenCycle

    Everything is generated from design.py — do not hand-edit the board and then regenerate, you will lose the edit. If you do route by hand in KiCad, stop using the pipeline for that board and say so in docs/DECISIONS.md.

    79 parts
  • 4 layers68 × 60 mmRoboCore, the printed circuit board in 3D

    RoboCore

    Plastal-Bot-Builders/RoboCoreDocumentation

    RoboCore is a classroom robot kit and coding platform, designed and built in Zambia by Plastal-Bot Builders (Lusaka).

    104 partsMIT
  • 2 layers35 × 100 mmWalkieTalkie, the printed circuit board in 3D

    WalkieTalkie

    GuzziFlipFlops/ESP32-Walkie-Talkie

    An open source ESPNOW based walkie talkie with a 600 meter tested distance, with an OLED and multiple buttons to interact with

    66 parts7MIT
  • 4 layers246.5 × 95 mmViaggio, the printed circuit board in 3D

    Viaggio

    dataehora/beatconfused

    A small web studio of fast, elegant, install-free music tools. Every tool is plain HTML, CSS and vanilla JavaScript with the Web Audio API — no build step, no dependencies, no accounts, works offline after first load.

    215 parts1
  • 2 layers45 × 100 mmWalkiepcb, the printed circuit board in 3D

    Walkiepcb

    houseofthe051010/ESP32-Walkie-Talkie-V2

    This ESP32 Walkie Talkie is redesigned with a SX1262 radio, a PCB, and a full case redesign.

    93 parts1
  • 4 layers40 × 60 mmTrinketcore, the printed circuit board in 3D

    Trinketcore

    alph4345/test

    TrinketCore puts everything a small rechargeable gadget needs on one 40 × 60 mm board, ready to order assembled from PCBWay:

    113 parts