Optimized ESP32-S3/P4 fully offline speech to text and text to speech system for Micropython and AtomVM, featuring a finetuned STT model for noisy environments, plus a data generation/field capture/model finetuning framework. Includes high performance 4/8bit int kernels for Xtensa/RiscV PIE vector units
C
44
139 commits
updated Oct 13, 2026

BabyTalk makes an ESP32-S3 or ESP32-P4 that understands what you say and talks back, with no cloud and no internet, from C, MicroPython, or Erlang/Elixir on AtomVM:
import stt, tts
text = stt.transcribe(pcm) # 16 kHz audio -> "what's the weather like today"
audio = tts.say("You said: " + text) # text -> 24 kHz audio for the speaker
Memory, bottom line: speech to text needs 6 MB of flash and 84.5 KB of internal RAM; text to speech adds 1.9 MB of flash. Each borrows a few hundred KB of PSRAM while it works.
| flash | PSRAM | internal RAM | |
|---|---|---|---|
| Speech to text (4-bit model) | 6.0 MB: the model, read in place | 0.2 MB, transcribing 4 s (more for longer clips) | 84.5 KB, shared with text to speech |
| Text to speech (nano voice) | 1.9 MB: voice 0.34 MB, pronunciations 1.5 MB | 0.5 MB, a 4 s reply | the same 84.5 KB |
heart voice (2.4 MB of flash, loaded into PSRAM).
Every option: Models. The full breakdown: Performance and memory.Made by Thomas Lackner and Claude Opus 5.5 (Anthropic). MIT licensed; the speech models belong to their authors (see Credits and licenses).
Most "voice on ESP32" projects do one of two things:
BabyTalk is different: a general-purpose English speech recognizer that runs entirely on the chip. It transcribes any sentence, with no training and no command list. A wake phrase is any phrase you type ("wake up, tomato face"), also with no training. Optionally the same firmware speaks replies with an on-device neural voice.
You'll want it if you need voice input that keeps working offline, keeps audio private, or can't depend on a server. For example:
Skip it if you need very high accuracy, languages other than English, or it has to run on a smaller chip (see below).
(This may also be the only project that involves both LoRa radios and LoRA,
Low-Rank Adaptation. The original research plan in docs/historical/ explored LoRA-style
expert adapters for the speech model. They aren't used yet.)
NVIDIA's Citrinet-256, compressed
to 4 or 8 bits, in two versions: the original, and one fine-tuned for background noise.
All four are ready to flash from models/ (SHA-256, licence and how each was made:
models/README.md).
| file | flash | noisy rooms* | clean speech** | fits |
|---|---|---|---|---|
citrinet256_int4.mmrt (default) | 6.0 MB | 58.1% | 8.2% | the standard 6 MB model partition |
citrinet256_int8.mmrt | 9.8 MB | 51.5% | 6.3% | a 10 MB model partition |
citrinet256_noisy_int4.mmrt | 6.0 MB | 42.7% | 11.8% | the standard 6 MB model partition |
citrinet256_noisy_int8.mmrt | 9.8 MB | 33.8% | 7.8% | a 10 MB model partition |
Word error rate, lower is better. *82 recordings made on the boards with music, highway noise or a coffee-shop loop in the background (see Training for the real world). **LibriSpeech test-clean (clean, read English); the original full-precision model scores 3.8%.
The noisy models are an alternative, not an upgrade: a quarter to a third fewer words
wrong with noise in the room, more wrong on clean speech. Pick by where the board will
listen. They are the same size as the originals, and need no firmware or code change: the
file records its own format.
Wake phrases need no model of their own: the speech model's output is scored against the phrase.
| voice | flash | PSRAM | sounds* | runs on |
|---|---|---|---|---|
nano (default), 294k parameters | 1.9 MB with the pronunciation dictionary | 0.5 MB | 2.13 | every board, compiled into the firmware |
heart, 2.27M parameters | 2.4 MB more, in its own partition | 2.4 MB more | 3.36 | ESP32-P4 only, at half real time |
*SCOREQ, a predicted listener rating (higher is better). Both are
sanoTTS voices; details and the 4-bit heart4:
docs/TTS_VOICES.md.
Building your own (fine-tuning on your recordings, other languages, other voices):
docs/MODELS.md.
All three ways in share one engine (components/) and give the same transcripts.
A MicroPython v1.27.0 firmware with stt and tts modules. You bring the audio with
machine.I2S; codec drivers for the ES7210 and ES8311 are in mpy/drivers/. Transcription
can run in the background while asyncio keeps going, and a wake phrase is a list of
spellings. Full API: mpy/README.md.
import stt
stt.phrase(["wake up tomato face"]) # score every run for this phrase
text = stt.transcribe(pcm)
stt.last()["score"] # 0 = the phrase is the best reading
1. Build the firmware (MicroPython v1.27.0 with a small patch that lets machine.I2S
drive the codec's master clock):
REPO=$PWD # this repository
. ~/esp/esp-idf/export.sh # wherever your ESP-IDF v5.5.1 lives
mkdir -p ~/build/sentry-fw && cd ~/build/sentry-fw
git clone --depth 1 -b v1.27.0 https://github.com/micropython/micropython
git -C micropython apply $REPO/mpy/patches/micropython-i2s-mck.patch
make -C micropython/ports/esp32 submodules
git clone https://github.com/Ampixa/sanoTTS ~/build/tts/sanoTTS # optional: text to speech,
git -C ~/build/tts/sanoTTS checkout 18e26b2b365bff41d211e516b0760021451438f1 # at the audited commit
cd $REPO && mpy/build.sh
2. Flash the firmware and a model, then clear the filesystem area once:
# a 6 MB model (default layout)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
0x410000 models/citrinet256_int4.mmrt
esptool.py --chip esp32s3 erase_region 0xA10000 0x5F0000
# or: a 9.8 MB model (int8 layout: same firmware, bigger model partition)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
0x410000 models/citrinet256_int8.mmrt
esptool.py --chip esp32s3 write_flash 0x8000 ~/build/sentry-fw/out/partition-table-int8.bin
esptool.py --chip esp32s3 erase_region 0xE10000 0x1F0000
3. Try it: press Enter, talk, and the board says back what it heard.
uv run --with mpremote tools/echo.py
mpy/examples/ also has a wake-phrase demo, a speaker demo and model_tools.py (install a
model from an SD card: docs/MODELS.md).
AtomVM v0.7 has no I2S driver yet, so BabyTalk brings its own audio drivers (ES7210, ES8311,
a GPIO- or expander-switched amp). Pins and parts are chosen at run time, so there is one
firmware per chip and your app picks a board preset or its own pins
(boards and pins). It adds a supervised gen_server
that listens for a wake phrase with audible chimes, and MMRT as a standalone library of
int8/int4 vector kernels (matvec, matmul, top-k...) for any AtomVM project.
ok = babytalk:audio_config(waveshare_s3_cam), % or your own pins: a map
{ok, Pcm} = babytalk:record(3),
{ok, Text, _Info} = babytalk:transcribe_sync(Pcm, 10000),
ok = babytalk:speak([<<"You said: ">>, Text]).
Build, flash and the demo apps: atomvm/README.md.
Add components/stt_engine (with mmrt and sram_pool) and, for speech,
components/sanotts to your ESP-IDF project. The engine maps the model from a model
flash partition; you hand it 16 kHz PCM.
stt_engine_open();
stt_result_t r;
stt_engine_run(pcm, n_samples, &r); // r.text: "what's the weather like today"
int16_t *out; int n_out;
tts_say(r.text, 0.8f, &out, &n_out, NULL); // 24 kHz PCM (in PSRAM) for your I2S driver
Setup, build options and the full API: components/README.md. The
inference runtime on its own: mmrt/README.md.
mic -> 16 kHz audio -> log-mel features -> Citrinet-256 (neural net) -> letters/word pieces -> text
\-> wake phrase score
text -> pronunciation dictionary -> phonemes -> sanoTTS voice -> 24 kHz audio -> speaker
export/kws_validate.py checks a phrase against hours of other speech
to pick a threshold that avoids false wakes.The model runs on MMRT (mmrt/, the "micromodels runtime"), a small inference engine
written for BabyTalk and meant to host other small models later.
We started with Espressif's ESP-DL. It gave correct results but took 1.33 s to process
each second of audio, because it re-read every layer's weights from flash for every frame
of audio and used one core. Fixing that meant patching ESP-DL, which nobody else could easily
build, so we wrote our own. MMRT is about 13 times faster than stock ESP-DL and half its
size, with identical output:
Details, a C usage example and the measurements behind each design choice:
mmrt/README.md.
The original model uses 32-bit floats (~39 MB). It is compressed twice:
Each step costs some accuracy (3.8% -> 6.3% -> 8.2% word error rate on clean speech; see
Models). Rebuilding them yourself, from NVIDIA's checkpoint or your own
fine-tuned one: docs/MODELS.md.
| folder | what |
|---|---|
mmrt/ | the int8/int4 inference runtime (C + ESP32-S3 and ESP32-P4 SIMD assembly) |
components/ | the engines as ESP-IDF components, for C apps and both firmwares: mmrt, stt_engine, sram_pool, sanotts (README) |
mpy/ | the MicroPython firmware: stt and tts modules, codec drivers, examples |
atomvm/ | the AtomVM (Erlang/Elixir) firmware: NIFs, libraries, demo apps |
stt/ | a plain ESP-IDF test firmware, where the engine is developed and measured |
bench/ | ESP32-S3 micro-benchmarks (memory, kernels, the mic) |
export/ | PC side: model port, quantization, the .mmrt exporter, bit-exact checks |
train/, field/, datagen/ | fine-tuning, field recording sessions, synthetic speech |
tools/ | laptop tools that drive a board over WiFi or USB (transcribe, record, wake phrases) |
models/ | the speech models, ready to flash |
docs/ | MODELS.md (using, building and replacing the models; other languages), ROADMAP.md, BENCHMARKS.md (the ESP32-S3's memory and arithmetic speeds, a glossary), board notes, project history |
Measured on the Waveshare ESP32-S3-CAM, from MicroPython, 4-bit model. Times include the audio feature step.
Speed
| task | time |
|---|---|
| Transcribe 1 s of speech | 0.35 s |
| Transcribe 2 s of speech | 0.51 s |
| Transcribe 4 s of speech | 0.83 s |
| Transcribe 10 s of speech | 1.72 s |
| Transcribe 1 s, with the 6 MB weight cache | 0.31 s |
| Transcribe 4 s, with the 6 MB weight cache | 0.79 s |
| Speak 1 s of speech | 0.26 s |
The optional weight cache (stt.cache(6144)) leaves little PSRAM free, so with it, clips
are limited to about 4 s under MicroPython.
Flash (16 MB)
| item | size |
|---|---|
| Speech model, 4-bit | 6.0 MB |
| Speech model, 8-bit (int8 layout) | 9.8 MB |
| Text to speech: nano voice 0.34 MB, pronunciation dictionary 1.5 MB | 1.9 MB |
| MicroPython itself, with the camera module | 1.7 MB |
| The whole MicroPython firmware (MicroPython + text to speech) | 3.6 MB |
| Left for your files, 4-bit layout | 6 MB |
| Left for your files, int8 layout | 2 MB |
PSRAM (8 MB)
| item | size |
|---|---|
| Free when idle, speech loaded | 7.9 MB |
| Used while transcribing 4 s | 0.2 MB |
| Used by a 4 s spoken reply | 0.4 MB |
| Optional weight cache | 6 MB |
Internal RAM (the scarce one)
| item | size |
|---|---|
| Shared block for listening / speaking (they take turns) | 84.5 KB |
| Text to speech, permanent buffers (PSRAM under AtomVM) | 104 KB |
| Free for your program | ~42 KB |
Bluetooth is disabled in this firmware to make that room. Fitting BabyTalk next to WiFi, a
camera or a screen from AtomVM (moving the shared block and task stacks to PSRAM, fewer
WiFi buffers): atomvm/README.md. The chip's raw memory and
arithmetic speeds: docs/BENCHMARKS.md.
The speech model learned from clean audiobooks. At a desk it does well, but with an engine, music or people talking in the background, it falls apart: on our recordings over a diesel truck idling, even the full-precision model got about half the words wrong. So we record real-world audio on the board itself and fine-tune the model on it.
Recording (field/): a session plays a background sound (a TV, a coffee-shop loop, a
truck engine, or the real thing) and shows sentences on screen. You read each one aloud,
then the laptop reads the same sentence in one of ~35 synthetic voices at a random speed,
and the board records both over WiFi. Sentences come from audiobooks, today's news,
popular Wikipedia articles, Hacker News headlines, and your own list of words
(field/terms.txt: names, commands, jargon). Background-only recordings are saved too,
for mixing into training.
Hands free: with a board that streams its mic over USB (atomvm/apps/mic_stream, e.g. on
the LilyGO T-LoRa Pager), capture.py --link usb --auto records nothing but laptop voices,
with no prompts, so a session can run during a drive. The laptop cuts each clip out of the
stream where its playback starts (found by cross-correlation) and keeps the whole stream too.
Measuring (export/field_eval.py): one command scores every model on the recordings.
A fifth of the sentences are reserved for testing and never used in training.
export/hard_words.py keeps a running list of the words the model gets wrong, and the
recorder can target them (--hard) or redo the sentences it missed (--retake).
Training (train/finetune.py, ~15 minutes on a laptop GPU; without one --device cpu
works but takes ~1.5 days): the model keeps learning from 100 hours of audiobooks, plus
synthetic speech of modern text and the field recordings, with the recorded background
noise and other damage (muffling, clipping, dropouts) mixed in.
Results so far: the noisy models in Models. On 82 recordings none of the
models had heard, the full-precision model went from 47.2% of words wrong to 30.8%, and the
4-bit model on the board from 58.1% to 42.7%; on clean audiobooks they lost some accuracy.
Training that keeps clean-speech accuracy is next (docs/ROADMAP.md).
cd field && uv run prompts.py # build the sentence pool
uv run capture.py --condition truck-idle --noise "diesel idling" # a recording session
uv run capture.py --link usb --board lilygo-t-lora-pager --mic es8311 --auto --minutes 45 \
--condition truck-drive --noise "diesel, open doors, traffic" # hands free
cd ../export && uv run field_eval.py --split all # score the models
cd ../train && uv run make_tts.py && uv run finetune.py # fine-tune (GPU if present; --device cpu|mps|cuda)
Converting the result for the board: docs/MODELS.md.
noisy models get a third to two fifths of the
words wrong in our noisy recordings, and trade away some clean-speech accuracy to do it.babytalk:audio_config/1); other chips need a C driver
(board_audio.h). MicroPython's drivers (mpy/drivers/) work on the I2C bus and I2S port
your script opens, so their pins are already yours to choose.The fuller list, with what each item takes: docs/ROADMAP.md.
docs/TTS_VOICES.md).BabyTalk was made by Thomas Lackner and Claude Opus 5.5 (Anthropic's AI model), working together.
The code in this repository is released under the MIT license, copyright Thomas Lackner. Other people's work used here keeps its own license:
models/): derived from NVIDIA's
stt_en_citrinet_256_ls,
CC-BY-4.0. Converted and quantized by this project, and for the noisy models
fine-tuned; see models/README.md.components/sanotts/LICENSES.md.bench/ firmware uses esp-nn (Apache-2.0).boot.avm) are part of the AtomVM firmware image (atomvm/). Downloaded at
build time; our local patch is in atomvm/patches/.mpy/drivers/,
stt/main/mic.c, bench/main/bench_rec.c and atomvm/components/atomvm_babytalk/ board_audio.c are derived from Espressif's esp-bsp drivers (Apache-2.0,
licenses/Apache-2.0.txt); each file says so.Project history and early design notes: docs/historical/.
97 followers · starred Oct 2026
23 followers · starred Oct 2026
25 followers · starred Oct 2026
Optimized ESP32-S3/P4 fully offline speech to text and text to speech system for Micropython and AtomVM, featuring a finetuned STT model for noisy environments, plus a data generation/field capture/model finetuning framework. Includes high performance 4/8bit int kernels for Xtensa/RiscV PIE vector units
C
44
139 commits
updated Oct 13, 2026

BabyTalk makes an ESP32-S3 or ESP32-P4 that understands what you say and talks back, with no cloud and no internet, from C, MicroPython, or Erlang/Elixir on AtomVM:
import stt, tts
text = stt.transcribe(pcm) # 16 kHz audio -> "what's the weather like today"
audio = tts.say("You said: " + text) # text -> 24 kHz audio for the speaker
Memory, bottom line: speech to text needs 6 MB of flash and 84.5 KB of internal RAM; text to speech adds 1.9 MB of flash. Each borrows a few hundred KB of PSRAM while it works.
| flash | PSRAM | internal RAM | |
|---|---|---|---|
| Speech to text (4-bit model) | 6.0 MB: the model, read in place | 0.2 MB, transcribing 4 s (more for longer clips) | 84.5 KB, shared with text to speech |
| Text to speech (nano voice) | 1.9 MB: voice 0.34 MB, pronunciations 1.5 MB | 0.5 MB, a 4 s reply | the same 84.5 KB |
heart voice (2.4 MB of flash, loaded into PSRAM).
Every option: Models. The full breakdown: Performance and memory.Made by Thomas Lackner and Claude Opus 5.5 (Anthropic). MIT licensed; the speech models belong to their authors (see Credits and licenses).
Most "voice on ESP32" projects do one of two things:
BabyTalk is different: a general-purpose English speech recognizer that runs entirely on the chip. It transcribes any sentence, with no training and no command list. A wake phrase is any phrase you type ("wake up, tomato face"), also with no training. Optionally the same firmware speaks replies with an on-device neural voice.
You'll want it if you need voice input that keeps working offline, keeps audio private, or can't depend on a server. For example:
Skip it if you need very high accuracy, languages other than English, or it has to run on a smaller chip (see below).
(This may also be the only project that involves both LoRa radios and LoRA,
Low-Rank Adaptation. The original research plan in docs/historical/ explored LoRA-style
expert adapters for the speech model. They aren't used yet.)
NVIDIA's Citrinet-256, compressed
to 4 or 8 bits, in two versions: the original, and one fine-tuned for background noise.
All four are ready to flash from models/ (SHA-256, licence and how each was made:
models/README.md).
| file | flash | noisy rooms* | clean speech** | fits |
|---|---|---|---|---|
citrinet256_int4.mmrt (default) | 6.0 MB | 58.1% | 8.2% | the standard 6 MB model partition |
citrinet256_int8.mmrt | 9.8 MB | 51.5% | 6.3% | a 10 MB model partition |
citrinet256_noisy_int4.mmrt | 6.0 MB | 42.7% | 11.8% | the standard 6 MB model partition |
citrinet256_noisy_int8.mmrt | 9.8 MB | 33.8% | 7.8% | a 10 MB model partition |
Word error rate, lower is better. *82 recordings made on the boards with music, highway noise or a coffee-shop loop in the background (see Training for the real world). **LibriSpeech test-clean (clean, read English); the original full-precision model scores 3.8%.
The noisy models are an alternative, not an upgrade: a quarter to a third fewer words
wrong with noise in the room, more wrong on clean speech. Pick by where the board will
listen. They are the same size as the originals, and need no firmware or code change: the
file records its own format.
Wake phrases need no model of their own: the speech model's output is scored against the phrase.
| voice | flash | PSRAM | sounds* | runs on |
|---|---|---|---|---|
nano (default), 294k parameters | 1.9 MB with the pronunciation dictionary | 0.5 MB | 2.13 | every board, compiled into the firmware |
heart, 2.27M parameters | 2.4 MB more, in its own partition | 2.4 MB more | 3.36 | ESP32-P4 only, at half real time |
*SCOREQ, a predicted listener rating (higher is better). Both are
sanoTTS voices; details and the 4-bit heart4:
docs/TTS_VOICES.md.
Building your own (fine-tuning on your recordings, other languages, other voices):
docs/MODELS.md.
All three ways in share one engine (components/) and give the same transcripts.
A MicroPython v1.27.0 firmware with stt and tts modules. You bring the audio with
machine.I2S; codec drivers for the ES7210 and ES8311 are in mpy/drivers/. Transcription
can run in the background while asyncio keeps going, and a wake phrase is a list of
spellings. Full API: mpy/README.md.
import stt
stt.phrase(["wake up tomato face"]) # score every run for this phrase
text = stt.transcribe(pcm)
stt.last()["score"] # 0 = the phrase is the best reading
1. Build the firmware (MicroPython v1.27.0 with a small patch that lets machine.I2S
drive the codec's master clock):
REPO=$PWD # this repository
. ~/esp/esp-idf/export.sh # wherever your ESP-IDF v5.5.1 lives
mkdir -p ~/build/sentry-fw && cd ~/build/sentry-fw
git clone --depth 1 -b v1.27.0 https://github.com/micropython/micropython
git -C micropython apply $REPO/mpy/patches/micropython-i2s-mck.patch
make -C micropython/ports/esp32 submodules
git clone https://github.com/Ampixa/sanoTTS ~/build/tts/sanoTTS # optional: text to speech,
git -C ~/build/tts/sanoTTS checkout 18e26b2b365bff41d211e516b0760021451438f1 # at the audited commit
cd $REPO && mpy/build.sh
2. Flash the firmware and a model, then clear the filesystem area once:
# a 6 MB model (default layout)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
0x410000 models/citrinet256_int4.mmrt
esptool.py --chip esp32s3 erase_region 0xA10000 0x5F0000
# or: a 9.8 MB model (int8 layout: same firmware, bigger model partition)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
0x410000 models/citrinet256_int8.mmrt
esptool.py --chip esp32s3 write_flash 0x8000 ~/build/sentry-fw/out/partition-table-int8.bin
esptool.py --chip esp32s3 erase_region 0xE10000 0x1F0000
3. Try it: press Enter, talk, and the board says back what it heard.
uv run --with mpremote tools/echo.py
mpy/examples/ also has a wake-phrase demo, a speaker demo and model_tools.py (install a
model from an SD card: docs/MODELS.md).
AtomVM v0.7 has no I2S driver yet, so BabyTalk brings its own audio drivers (ES7210, ES8311,
a GPIO- or expander-switched amp). Pins and parts are chosen at run time, so there is one
firmware per chip and your app picks a board preset or its own pins
(boards and pins). It adds a supervised gen_server
that listens for a wake phrase with audible chimes, and MMRT as a standalone library of
int8/int4 vector kernels (matvec, matmul, top-k...) for any AtomVM project.
ok = babytalk:audio_config(waveshare_s3_cam), % or your own pins: a map
{ok, Pcm} = babytalk:record(3),
{ok, Text, _Info} = babytalk:transcribe_sync(Pcm, 10000),
ok = babytalk:speak([<<"You said: ">>, Text]).
Build, flash and the demo apps: atomvm/README.md.
Add components/stt_engine (with mmrt and sram_pool) and, for speech,
components/sanotts to your ESP-IDF project. The engine maps the model from a model
flash partition; you hand it 16 kHz PCM.
stt_engine_open();
stt_result_t r;
stt_engine_run(pcm, n_samples, &r); // r.text: "what's the weather like today"
int16_t *out; int n_out;
tts_say(r.text, 0.8f, &out, &n_out, NULL); // 24 kHz PCM (in PSRAM) for your I2S driver
Setup, build options and the full API: components/README.md. The
inference runtime on its own: mmrt/README.md.
mic -> 16 kHz audio -> log-mel features -> Citrinet-256 (neural net) -> letters/word pieces -> text
\-> wake phrase score
text -> pronunciation dictionary -> phonemes -> sanoTTS voice -> 24 kHz audio -> speaker
export/kws_validate.py checks a phrase against hours of other speech
to pick a threshold that avoids false wakes.The model runs on MMRT (mmrt/, the "micromodels runtime"), a small inference engine
written for BabyTalk and meant to host other small models later.
We started with Espressif's ESP-DL. It gave correct results but took 1.33 s to process
each second of audio, because it re-read every layer's weights from flash for every frame
of audio and used one core. Fixing that meant patching ESP-DL, which nobody else could easily
build, so we wrote our own. MMRT is about 13 times faster than stock ESP-DL and half its
size, with identical output:
Details, a C usage example and the measurements behind each design choice:
mmrt/README.md.
The original model uses 32-bit floats (~39 MB). It is compressed twice:
Each step costs some accuracy (3.8% -> 6.3% -> 8.2% word error rate on clean speech; see
Models). Rebuilding them yourself, from NVIDIA's checkpoint or your own
fine-tuned one: docs/MODELS.md.
| folder | what |
|---|---|
mmrt/ | the int8/int4 inference runtime (C + ESP32-S3 and ESP32-P4 SIMD assembly) |
components/ | the engines as ESP-IDF components, for C apps and both firmwares: mmrt, stt_engine, sram_pool, sanotts (README) |
mpy/ | the MicroPython firmware: stt and tts modules, codec drivers, examples |
atomvm/ | the AtomVM (Erlang/Elixir) firmware: NIFs, libraries, demo apps |
stt/ | a plain ESP-IDF test firmware, where the engine is developed and measured |
bench/ | ESP32-S3 micro-benchmarks (memory, kernels, the mic) |
export/ | PC side: model port, quantization, the .mmrt exporter, bit-exact checks |
train/, field/, datagen/ | fine-tuning, field recording sessions, synthetic speech |
tools/ | laptop tools that drive a board over WiFi or USB (transcribe, record, wake phrases) |
models/ | the speech models, ready to flash |
docs/ | MODELS.md (using, building and replacing the models; other languages), ROADMAP.md, BENCHMARKS.md (the ESP32-S3's memory and arithmetic speeds, a glossary), board notes, project history |
Measured on the Waveshare ESP32-S3-CAM, from MicroPython, 4-bit model. Times include the audio feature step.
Speed
| task | time |
|---|---|
| Transcribe 1 s of speech | 0.35 s |
| Transcribe 2 s of speech | 0.51 s |
| Transcribe 4 s of speech | 0.83 s |
| Transcribe 10 s of speech | 1.72 s |
| Transcribe 1 s, with the 6 MB weight cache | 0.31 s |
| Transcribe 4 s, with the 6 MB weight cache | 0.79 s |
| Speak 1 s of speech | 0.26 s |
The optional weight cache (stt.cache(6144)) leaves little PSRAM free, so with it, clips
are limited to about 4 s under MicroPython.
Flash (16 MB)
| item | size |
|---|---|
| Speech model, 4-bit | 6.0 MB |
| Speech model, 8-bit (int8 layout) | 9.8 MB |
| Text to speech: nano voice 0.34 MB, pronunciation dictionary 1.5 MB | 1.9 MB |
| MicroPython itself, with the camera module | 1.7 MB |
| The whole MicroPython firmware (MicroPython + text to speech) | 3.6 MB |
| Left for your files, 4-bit layout | 6 MB |
| Left for your files, int8 layout | 2 MB |
PSRAM (8 MB)
| item | size |
|---|---|
| Free when idle, speech loaded | 7.9 MB |
| Used while transcribing 4 s | 0.2 MB |
| Used by a 4 s spoken reply | 0.4 MB |
| Optional weight cache | 6 MB |
Internal RAM (the scarce one)
| item | size |
|---|---|
| Shared block for listening / speaking (they take turns) | 84.5 KB |
| Text to speech, permanent buffers (PSRAM under AtomVM) | 104 KB |
| Free for your program | ~42 KB |
Bluetooth is disabled in this firmware to make that room. Fitting BabyTalk next to WiFi, a
camera or a screen from AtomVM (moving the shared block and task stacks to PSRAM, fewer
WiFi buffers): atomvm/README.md. The chip's raw memory and
arithmetic speeds: docs/BENCHMARKS.md.
The speech model learned from clean audiobooks. At a desk it does well, but with an engine, music or people talking in the background, it falls apart: on our recordings over a diesel truck idling, even the full-precision model got about half the words wrong. So we record real-world audio on the board itself and fine-tune the model on it.
Recording (field/): a session plays a background sound (a TV, a coffee-shop loop, a
truck engine, or the real thing) and shows sentences on screen. You read each one aloud,
then the laptop reads the same sentence in one of ~35 synthetic voices at a random speed,
and the board records both over WiFi. Sentences come from audiobooks, today's news,
popular Wikipedia articles, Hacker News headlines, and your own list of words
(field/terms.txt: names, commands, jargon). Background-only recordings are saved too,
for mixing into training.
Hands free: with a board that streams its mic over USB (atomvm/apps/mic_stream, e.g. on
the LilyGO T-LoRa Pager), capture.py --link usb --auto records nothing but laptop voices,
with no prompts, so a session can run during a drive. The laptop cuts each clip out of the
stream where its playback starts (found by cross-correlation) and keeps the whole stream too.
Measuring (export/field_eval.py): one command scores every model on the recordings.
A fifth of the sentences are reserved for testing and never used in training.
export/hard_words.py keeps a running list of the words the model gets wrong, and the
recorder can target them (--hard) or redo the sentences it missed (--retake).
Training (train/finetune.py, ~15 minutes on a laptop GPU; without one --device cpu
works but takes ~1.5 days): the model keeps learning from 100 hours of audiobooks, plus
synthetic speech of modern text and the field recordings, with the recorded background
noise and other damage (muffling, clipping, dropouts) mixed in.
Results so far: the noisy models in Models. On 82 recordings none of the
models had heard, the full-precision model went from 47.2% of words wrong to 30.8%, and the
4-bit model on the board from 58.1% to 42.7%; on clean audiobooks they lost some accuracy.
Training that keeps clean-speech accuracy is next (docs/ROADMAP.md).
cd field && uv run prompts.py # build the sentence pool
uv run capture.py --condition truck-idle --noise "diesel idling" # a recording session
uv run capture.py --link usb --board lilygo-t-lora-pager --mic es8311 --auto --minutes 45 \
--condition truck-drive --noise "diesel, open doors, traffic" # hands free
cd ../export && uv run field_eval.py --split all # score the models
cd ../train && uv run make_tts.py && uv run finetune.py # fine-tune (GPU if present; --device cpu|mps|cuda)
Converting the result for the board: docs/MODELS.md.
noisy models get a third to two fifths of the
words wrong in our noisy recordings, and trade away some clean-speech accuracy to do it.babytalk:audio_config/1); other chips need a C driver
(board_audio.h). MicroPython's drivers (mpy/drivers/) work on the I2C bus and I2S port
your script opens, so their pins are already yours to choose.The fuller list, with what each item takes: docs/ROADMAP.md.
docs/TTS_VOICES.md).BabyTalk was made by Thomas Lackner and Claude Opus 5.5 (Anthropic's AI model), working together.
The code in this repository is released under the MIT license, copyright Thomas Lackner. Other people's work used here keeps its own license:
models/): derived from NVIDIA's
stt_en_citrinet_256_ls,
CC-BY-4.0. Converted and quantized by this project, and for the noisy models
fine-tuned; see models/README.md.components/sanotts/LICENSES.md.bench/ firmware uses esp-nn (Apache-2.0).boot.avm) are part of the AtomVM firmware image (atomvm/). Downloaded at
build time; our local patch is in atomvm/patches/.mpy/drivers/,
stt/main/mic.c, bench/main/bench_rec.c and atomvm/components/atomvm_babytalk/ board_audio.c are derived from Espressif's esp-bsp drivers (Apache-2.0,
licenses/Apache-2.0.txt); each file says so.Project history and early design notes: docs/historical/.
97 followers · starred Oct 2026
23 followers · starred Oct 2026
25 followers · starred Oct 2026