On-device speech-to-text: what we gained
HardwareOne P4 + G2 glasses, Oct 3-11 2026. Every number below was measured in this project unless marked as an estimate. Version numbers are labels for the test builds, in the order they were made; the firmware's own version did not change.
| 1.1 | Oct 10 | deployed | The firmware on the P4 (gen2-25397). The model is rebuilt for every phrase. |
| 1.2 | Oct 10, afternoon | c3x | Design version: early final, short-reply rule, model kept for the session with its bookkeeping in PSRAM. |
| 1.3 | Oct 10, evening | c4a | Faster depthwise layers on both cores; head start for short replies. |
| 1.4 | Oct 10, 18:30 | c5 | Faster language-model decoder (identical text). |
| 1.5 | Oct 11, 00:45 | c7 | Each layer's setup prepared once per session; 200 ms tail confirmed. |
| 1.6 | Oct 11, 03:15 | c9 | Head start grabs the audio at once, even while a preview runs. |
| 1.7 | Oct 11, 05:30 | c11 | Continuous phrases capped at 10 s (half the session audio buffers). |
| 1.8 | Oct 11, 08:20 | c12 + card LM | 1.6 MB language model with 9.2k tech terms (43k words) instead of 2.5 MB. |
| 1.9 | Oct 11, 08:30 | c13, first | Speaker labels ("Person N"), computed in the speech worker. |
| 2.0 | Oct 11, 09:20-10:45 | c13, final | Speaker labels in a background task, ESP-IDF fix, trimmed kernels, labelled transcript lines. |
1. The model: accuracy and noise
Broad error rate on the full test sets (lower is better)
Words invented on pure background noise (lower is better)
The noise-only training clips cut invented words by about 70% at no accuracy cost (general + noise clips: 23.18 vs 23.11). The new data and the 500-hour run gave the deployed model its accuracy edge. The 2e-5 learning rate was the mistake to avoid.
2. Training speed on the Mac
Training throughput (audio seconds processed per second)
One checkpoint scoring pass
Where a 500-hour run's time goes
The cause: one operation (the backward pass of the depthwise convolutions) was very slow on the Mac's GPU. A replacement written in this project gives the same gradients to within a millionth.
3. Caption delay on the P4
End of speech to caption ready (median, P4 mic)
From 1.3 on, each change saved tens of milliseconds, which is smaller than the spread between sessions (about ±0.1 s, more for one-session versions). So the medians from 1.4 on move up and down by noise; what each change did is in the per-change chart below, measured clip by clip. 1.4 and 1.8 were run once, so their medians rest on 14-18 phrases.
Short replies
Longer phrases
Speech model time per second of audio (median phrase)
Language-model step per phrase (median)
Model time fell about 40% from 1.1 to 1.5 and then stayed flat: 1.6-1.8 changed when the model starts and how much memory it needs, not its speed. The language-model step halved with the new decoder in 1.4; its size (2.5 or 1.6 MB) does not change its speed.
Long unbroken speech: the 10 s phrase cap (12-17 s utterances)
Medians over the same 14 clips, 3 sessions each. With a 20 s cap a long utterance is transcribed in one piece after you stop; with 10 s the first part is cut and transcribed while you are still talking. For a typical clip that changes little (-34 ms), but some finish much sooner (95% range -416 to +19 ms). The main gain is memory (section 4).
Where the time goes, step by step: all phrases
Short replies (same colours)
Longer phrases (same colours)
The model's split by layer type uses each build's own per-layer timing on the chip: 1.4 changed only the language-model decoder, so it uses 1.3's; 1.5 to 1.8 share one model build, timed in the 1.8 test sessions. 1.4 and 1.8 were run once (7-18 phrases per bar), so their bars are rougher; 2.0 is not shown because it was tested on conversation recordings, where it matched the same build without speaker labels clip by clip (-2 ms). Each bar starts when you stop talking and ends when the caption is ready, and the pieces are in the order they happen. Each bar is the average of the middle half of that group's phrases (so one odd phrase cannot skew it), so totals sit close to the medians above. With the head start, the model runs while the end-of-phrase wait is still going, so the grey wait mostly disappears; the light grey piece is the 200 ms of quiet the P4 needs before it starts. The model's split by layer type comes from each version's own per-layer timing on the chip (1.1: an older build of the same model).
Inside the model run, by layer type (a 5-second phrase)
Numbers in brackets are how many layers of that type the model has. Depthwise layers are 9% of the model's arithmetic but took about a third of its time: each step read more data than the chip's 64 KB fast cache holds. Working in 64-channel blocks and splitting the channels across both cores fixed that, with bit-identical output. 1.5's prepared layer setups took a further 30 ms off, mostly in the smaller layers, where setup is a larger share; the 512-wide pointwise layers are now 60% of the run. *1.1's split comes from an older per-layer profile of the same model.
What each change did (compared one at a time)
The chip has no neural-net accelerator; its SIMD units were mostly waiting on memory. That is why the cache line was the biggest early win, and why using both cores only paid off once each core worked on its own channels. The last five rows were measured on fixed test audio (model time) or, for short replies, in speaker sessions.
3b. Through the G2 glasses (Bluetooth)
With the glasses, the speech engine on the P4 is the same; Bluetooth adds the trip in (mic audio) and the trip out (the caption). Green boxes are the parts this work changed.
End of speech to caption on the glasses (longer phrases)
Short replies ("yeah", "okay")
Measured: speech processing (P4, same engine as with the glasses), the caption timer (0-200 ms, about 0.11 s on average; from 1.2 on the sender wakes as soon as a caption is ready) and the caption write to the glasses with its acknowledgement (30-102 ms). Estimated: the mic audio trip over Bluetooth (packets every 50 ms plus radio transfer; not measured directly) and the glasses' own drawing time (not included). From 1.2 on the end-of-phrase wait and the processing overlap (the final starts early), so they are one blue bar. With the glasses 1.2 should land at about 2.1 s for longer phrases and 1.3 at about 1.9 s (short replies about 1.0 s), and 1.4-1.8 at about 1.8-1.9 s (short replies 0.7-1.0 s), against about 3.7 s; none of them has been timed on the glasses yet. 2.0 is not shown: its speaker labels add nothing to the caption path (section 6), so it behaves like 1.8. Earlier glasses logs reported 3.6-5.5 s because up to ~1 s of that was a clock offset in the glasses' audio stream, not real delay.
4. Memory and accuracy by version
Lowest free PSRAM during a session (standard playlist)
Lowest free PSRAM on long unbroken speech
Out of 32 MB. 1.1 rebuilt the model for every phrase, so it held less between phrases. From 1.2 the model and its working area stay for the session (about 2.6 MB more). 1.7 halves the five session audio buffers and the feature caches: 2.7 MB more free after warm-up, and 3.8 MB more at the lowest point on long speech, where phrases used to reach 20 s. 1.8's smaller LM gives back 0.9 MB. 2.0's speaker model takes 3.4 MB: with the 1.6 MB LM about 3 MB stays free at the lowest point.
Lowest free internal RAM during a session
Internal RAM is the scarce kind (about 260-280 KB free at boot). 1.1 dipped to about 44 KB while it rebuilt the model for each phrase; from 1.2 the model is built once and its bookkeeping is requested from PSRAM. From test batch 17 on (during 1.6's tests) the same firmware booted with about 17 KB more free internal RAM, so the step to ~140 KB is a test-setup change, not the 10 s cap; compare versions within a batch. In the same batch, 2.0's speaker labels lower the low point by 6-11 KB (about 144 → 134 KB), set while ESP-DL builds the speaker model at session start.
Word errors, scored clip by clip (standard playlist, lower is better)
Clips whose console text line was lost in any of that version's sessions are left out (a test-harness fault, not the firmware), so each version is scored on 21-26 of the 27 clips. Every version lands between 18.2% and 20.1%; sessions of the same build vary by about ±2 points. The speed changes are bit-identical on fixed test audio, so their text cannot change; the changes that alter what the model hears (1.2's early final and short-reply rule, 1.7's cut at 10 s) show no clear accuracy cost here. The one change that did cost accuracy, the 100 ms tail, was rejected.
5. The language model (1.8)
Everyday speech: word errors by LM size (P4 recordings re-decoded on the Mac)
Tech speech: 180 synthesized sentences (word errors; right column: unknown words recognised)
The P4 logged each caption's raw speech-model output, and the Mac re-ran the device's exact decoder on it (it reproduces every device caption), so every LM was compared on identical audio. The LM is worth about 3 points; its size barely matters, because the speech model gets most words right on its own and the LM mainly breaks ties that depend on common word pairs, which every size keeps. Decode time is set by the speech, not by memory or the session's load, and is the same for every size. The tech term list doubles the unknown words recognised (8 → 16 of 126), but most tech words still fail: the speech model has never heard them and spells them by sound. The next training run adds synthesized tech speech to fix that at the source.
6. Speaker labels (1.9, 2.0)
Each finished phrase gets a session label, and saved transcript lines read "[Person N] text". The model (clova Fast ResNet-34, 1.4 M weights, 8-bit weights with 16-bit activations) turns a phrase into a voiceprint; phrases join the closest voice heard so far, or start a new one. A different speaker model can be dropped onto the card without new firmware.
Phrases labelled with the right person
Labelling time per phrase
Device accuracy is from the four known-speaker conversations (2, 4, 3 and 3 people); repeat runs in the background build landed at 76-85% per conversation, within the run-to-run spread, and the device's voiceprints match the Mac's (cosine 0.997). Two similar audiobook readers merged into one person: their voiceprints are too close for any threshold, a limit of this small model. Turns under 2 s never start a new person. In 2.0, captions did not wait at all: matched clip by clip against the same build without labels, speech end to caption changed by -2 ms (95% range -11 to +4 ms). Labels reach the transcript 0.8-1.9 s after the caption (median); every saved line got its label, in order. Costs: 3.4 MB of PSRAM, about 110 KB of flash, about 0.5 s at session start, and 6-11 KB of internal RAM at the low point. The 3x3 layers of the speaker model got their own cache-sized blocking in ESP-DL (195 → 39 ms each, bit-identical).
7. Trade-offs still open
Accuracy: the 1.3 report flagged about 2.5 points more word errors for 1.2 (18.2% → 20.5% in raw session averages). Scored clip by clip with lost console lines left out, 1.1 to 1.8 all land between 18.2% and 20.1%, so that loss does not reproduce. Tech words are the clear weak spot (about 50% errors on tech sentences); the next training run targets them.
Memory: correction to the 1.3 report: 1.1's internal RAM fell to about 44 KB during each phrase's model rebuild, not 117 KB; from 1.2 the low point is about 112-117 KB (about 143 KB in later test batches). The speaker model's load at session start lowers it by 6-11 KB; moving ESP-DL's short-lived build buffers to PSRAM explicitly would win that back. A 256 KB cache was measured 11-22% faster but costs 131 KB of internal RAM, so it was rejected for excessive RAM consumption; the cache stays at 128 KB.
Speaker labels: similar voices can merge (a whole-conversation relabelling pass or a larger model would help, if memory allows); overlapping speech gets one label.
Not used yet: the faster memory clock (-68 ms) needs a long stability test first. Not yet timed on the glasses themselves. 1.4-2.0 ran in test builds; the stt-speedups branch carries all of it but has not run on the P4 as a whole yet (planned for 0.99.98, after the 15x5 switch).
8. How it went
| Oct 3 | General model on the P4 (Broad 23.11). Data moved to one external SSD. |
| Oct 5 | Noise-only clip bug found and fixed (they were ~90% of the training signal). User: train from the ground up. |
| Oct 6-7 | LR test: 2e-5 hurt early and never fully recovered; 1e-5 matched then beat the old model. |
| Oct 8 | general2 1e-5: best model yet (Broad 22.80, invented words 30 -> 9.5 per minute). |
| Oct 9 | Deployed to the P4. Speed audit: 9 measurements, 54 ideas, 126 agents. Trainer 3x faster. |
| Oct 10 | Six rounds of on-chip tests through the speaker and the P4 mic: caption delay 3.6 s -> ~1.7 s. |
| Oct 10 (eve) | Per-layer timing on the chip: depthwise layers were 9% of the math but 34% of the time. Reordered (bit-identical) and head start for short replies: ~1.7 s -> ~1.1 s. Bigger cache measured, rejected (RAM). |
| Oct 10 (18:30) | 1.4: the language-model decoder rewritten, 63% faster on the P4 with identical text. All of 1.3 ported to the local stt-speedups branch. |
| Oct 11 (00:00-03:30) | 1.5: every layer's setup prepared once per session (no internal RAM). Its first build pushed free PSRAM under the LM's 6 MB load rule, so the LM did not load; fixed. Tail tests: 100 ms cost 1.1 points of accuracy, 150 ms gained nothing clear; 200 ms kept. |
| Oct 11 (03:00-05:10) | 1.6: the head start takes the audio straight away even while a preview runs (wait 77 -> 45 ms). 16 ms mic chunks measured and rejected: the model ran 3% slower. |
| Oct 11 (05:30) | 1.7: continuous phrases capped at 10 s. On long unbroken speech the lowest free PSRAM went from 1.6 to 5.4 MB. |
| Oct 11 (06:00-08:20) | LM probe: decode time is set by the speech, not by memory or load, and LM size changes memory, not speed. Tech vocabulary (9.2k terms) built; 1.8 = a 1.6 MB LM with the terms: same accuracy, 0.9 MB less PSRAM. |
| Oct 11 (08:30-09:35) | 1.9: speaker labels on the P4 (91% of phrases right). 2.0: labelling moved to a background task; that exposed an ESP-IDF bug (a vector register not saved on task switches), fixed with the upstream patch. |
| Oct 11 (10:15-11:00) | 1.6 MB LM installed on the card, speaker kernels trimmed, labelled transcript lines, diagnostics compiled out. Everything ported to the stt-speedups branch (builds, host tests pass). |