sano (सानो) — Nepali for "small." A family of tiny neural text-to-speech voices — 294k to 2.3M parameters — that run with no cloud and no NPU: real-time on a ~$3 ESP32-S3 (out a GPIO into an LM386 and a speaker), or live in the browser via WASM.
- smallest neural TTS family known — 294k to 2.3M parameters
- runs real-time on a $3 microcontroller (ESP32-S3)
- runs right in your browser — WebAssembly, no server
- under 4 MB per voice, zero dependencies (espeak-ng phonemizer included)
- 11 voices across 6 languages — English, Nepali (नेपाली), Hindi (हिन्दी), Vietnamese (Tiếng Việt), Indonesian (Bahasa), Chinese (中文)
- new: heart, our best-sounding voice at 2.27M parameters (24 kHz), and heart-nano, the same voice in 294k parameters — a complete text-to-speech stack, int8, in 337 KB. Both synthesize live in the browser demo
- open source, GPL-3.0
ampixa.github.io/sanoTTS — every voice synthesizes your text live in the browser. No server, no upload: text goes through an espeak-ng-in-WASM phonemizer and that voice's own neural stack, all client-side.
| Platform | Install | Then |
|---|---|---|
| Python | pip install sanotts |
sanotts say "Hello" --voice amy -o hello.wav |
| Web (npm) | npm install sanotts-web |
const tts = await SanoTTS.load(); await tts.synthesize('Hello', {voice:'amy'}) |
| Web (no build) | copy dist/ + voices/ |
see Deploy on your own site |
| Arduino / PlatformIO | zip-install or lib_deps = https://github.com/Ampixa/sanoTTS.git |
arduino/README.md |
| Hugging Face | huggingface.co/ampixa/sanoTTS |
voice packages + samples |
| Browser | nothing | ampixa.github.io/sanoTTS |
CLI + library; voices download on first use:
pip install sanotts
sanotts say "Hello from a two megabyte voice." --voice amy -o hello.wav
sanotts say "Xin chào!" --voice vi -o xinchao.wavimport sanotts
result = sanotts.synthesize("Hello world", voice="amy") # numpy audio @ 22.05 kHzVoices: amy, amy-1p1m, amy-1p8m, kristin, hfc, vi, id — fetched
from the voices-v1 release
into ~/.cache/sanotts/. Pure numpy inference, no torch, no onnxruntime.
The SanoTTS library lives in arduino/: add it to the Arduino
IDE as a .zip library, or in platformio.ini:
lib_deps = https://github.com/Ampixa/sanoTTS.gitSee arduino/README.md for board support (ESP32-S3 ✓),
memory guidance, and flashing the model blobs
(mcu-kristin-745k-q8.tar.gz).
The voice packages — including audio samples — are mirrored at huggingface.co/ampixa/sanoTTS.
Nothing to install: ampixa.github.io/sanoTTS.
sanoTTS's browser demo runs entirely client-side — WebAssembly, no server — so "deploying" it just means hosting a handful of static files. Two ways to do it:
Option A — npm install sanotts-web (on npm)
import { SanoTTS, playAudio } from 'sanotts-web';
const tts = await SanoTTS.load({
assetBase: 'https://your-cdn.example.com/sanotts/', // where you copied dist/
});
const result = await tts.synthesize('Hello from my own server.', {
voice: 'amy',
voiceBase: 'https://your-cdn.example.com/sanotts/', // where you copied voices/
});
playAudio(result);Copy the package's dist/ (the wasm runtime) and this repo's web/voices/
directory to your own static host, then point assetBase/voiceBase at it.
Everything else — phonemization, synthesis, playback — happens in the
visitor's browser.
Option B — no build, no npm
Copy web/snt_g2p.js, web/snt_g2p.wasm, web/snt_g2p.data,
web/snt_voice.js, web/snt_voice.wasm, and web/voices/ from this repo
(or scrape them straight from ampixa.github.io/sanoTTS) onto your static
host, and load them the same way web/index.html does:
<script src="/sanotts/snt_g2p.js"></script>
<script src="/sanotts/snt_voice.js"></script>
<script type="module">
const [G2P, Voice] = await Promise.all([SaanoG2P(), SaanoVoice()]);
G2P._snt_g2p_init();
// ...set voice, phonemize, synthesize — see web/index.html for the full sequence.
</script>Sizes to plan around:
- wasm runtime: ~700 KB total gzipped over the wire (espeak-ng G2P
ships ~2.5 MB uncompressed including its phoneme-table
.data, ~700 KB gzipped; the acoustic/decoder wasm adds another ~40 KB) - per-voice weights: 4–7 MB, fp32 (
front_f32.bin+dec_f32.bin), fetched lazily on first use of that voice, not bundled with the runtime — int8 quantized voices (~4x smaller) are planned but not yet shipped
CSP note: the wasm runtime needs 'wasm-unsafe-eval' (or
'unsafe-eval' on older browsers) in your script-src Content-Security-Policy,
for WebAssembly.instantiate/instantiateStreaming. Nothing else needs
relaxing — the runtime never eval()s JavaScript. Most default/modern CSPs
(including having no explicit script-src) already allow this.
Open small-scale TTS on an honest gate — a diverse 24-sentence set scored with the same no-reference suite (SCOREQ / UTMOS are naturalness predictors, DNSMOS-SIG is signal quality; higher is better). Parameter counts are inference-time and exclude the shared external G2P.
Kokoro is 36x larger than our largest voice (heart, 2.27M), and 279x larger than our smallest (heart-nano, 294k). Shipped-file sizes: sanoTTS amy 2.8 MB fp16 and TinyTTS 3.5 MB fp16, both verified from the released files; Kokoro's ~330 MB fp32 is its widely cited public figure.
| System | Params | SCOREQ | UTMOS | DNS-SIG |
|---|---|---|---|---|
| sanoTTS (amy) | 1.46 M | 4.13 | 4.10 | 3.61 |
| sanoTTS (heart) | 2.27 M | 3.51 | 3.42 | 3.50 |
| sanoTTS (heart-nano) | 0.29 M | 2.30 | 2.45 | 3.35 |
| TinyTTS | 1.62 M | 3.94 | 3.65 | 3.62 |
| Inflect Nano | 4.63 M | 3.81 | 3.65 | 3.58 |
| Kitten TTS nano | 15 M | 3.02 | 3.58 | 3.43 |
| reference (~15 M) | ~15 M | 4.71 | 4.47 | 3.65 |
| Kokoro | 82 M | 4.89 | 4.52 | 3.69 |
sanoTTS is the smallest model here and the best on naturalness (SCOREQ and
UTMOS) among everything up to 15M params — beating TinyTTS while being smaller.
On DNSMOS-SIG, TinyTTS edges us by 0.01 — no single metric tells the whole story.
It's the only one that runs a full neural stack on a $3 MCU. Parameter count
isn't destiny at this scale: Kitten TTS at 10x the size scores a full SCOREQ
point lower. The frontier only pulls ahead at ~15M-class models and Kokoro (82M,
60x larger) — a gap we don't claim to close. Reproduce it with
tools/eval_mos_all.py + tools/eval_scorecard.py.
Same voice (amy), same duration/acoustic recipe — only decoder size changes. Quality lives in the decoder: doubling it from 1.09M to 1.84M params moves SCOREQ from 3.70 to 4.16.
| Language | Voice | Params | SCOREQ |
|---|---|---|---|
| English 🇺🇸 | amy | 1.46 M | 4.13 |
| kristin | 1.40 M | 4.09 | |
| hfc | 1.83 M | 3.94 | |
| amy-small | 1.08 M | 3.70 | |
| heart (24 kHz) | 2.27 M | 3.51 | |
| heart-nano (int8, 24 kHz) | 294 k | 2.30 | |
| robot (on-device, int8) | 567 k | — | |
| Nepali नेपाली | Nepali | 1.47 M | — |
| Hindi हिन्दी | Hindi | 1.50 M | — |
| Vietnamese Tiếng Việt | Vietnamese | 1.46 M | — |
| Indonesian Bahasa | Indonesian | 1.46 M | — |
| Chinese 中文 | Chinese | 1.50 M | — |
The "robot" row is the same 567,008-parameter model that runs on the ESP32-S3 —
bit-exact with the chip's own output. (Some older packaging, including the
mcu-kristin-745k-q8.tar.gz filename, carries a "745k" label; that was a
directory name, never a parameter count for these binaries.) SCOREQ is only reported for the English
voices, which share a common eval set; the other languages haven't been scored
against a comparable reference yet.
heart and heart-nano are a second recipe: a 100-band mel interface between
the acoustic model and a noise-shaping ConvNeXt + iSTFT decoder, at 24 kHz.
heart-nano is the smallest complete neural TTS stack we have built —
duration 22,858 + acoustic 65,299 + decoder 206,122 = 294,279 parameters, shipped
as 337 KB of int8 blobs (web/voices/heartnano/) and run in the browser with the
int8 arithmetic of the microcontroller build unchanged (mcu/src/snt_nano.c,
golden fixture mcu/test/fixtures/en_us_e13b). heart ships float32 weights
(web/voices/heart/, 9.1 MB): its int8 form fails the 0.98 golden gate at 0.951
minimum correlation, the float build reproduces the training-side output at
1.000000 (mcu/test/fixtures/en_us_r227f32). Rebuild both with
mcu/ports/wasm/build_nano.sh, gate with mcu/ports/wasm/verify_nano_node.mjs.
espeak-ng provides phoneme IDs; a duration model predicts timing; an acoustic
model predicts generator latents; a decoder renders 22 kHz audio.
The web voices (amy, kristin, hfc, and the other languages) use a compact
time-domain decoder running in fp32 WASM; the on-device model instead uses
a quantized int8 iSTFT decoder, sized to fit and run in real time on the
ESP32-S3. heart / heart-nano predict a 100-band mel spectrogram and render it
with a noise-fed ConvNeXt + iSTFT decoder at 24 kHz (mcu/src/snt_nano.c).
The end-to-end recipe is in the docs:
build a probe pack → train the duration, acoustic-latent, and decoder models →
joint finetune → export int8. New-language porting is
docs/roota-language-porting-recipe.md.
pip install -e .
# then follow the training recipe in docs/ to make a new voice- ESP32-S3 talking device — a standalone WiFi dashboard: type text, the board
phonemizes (on-chip espeak-ng) and speaks. See
mcu/ports/esp32s3/. - Browser — the full stack in WASM, no server. ▶ Hear and synthesize all 9
voices live (GitHub Pages); source in
web/. - Other MCUs — which chips can run it and how well:
docs/mcu-classes-and-porting.md.
The eval loop measures what actually matters — intelligibility (Whisper WER),
phoneme-class fidelity, and G2P parity — not just a gameable MOS score:
tools/eval_scorecard.py, tools/eval_phoneme_class_fidelity.py,
tools/eval_g2p_parity.py.
docs/repository-layout.md. In short: src/saanotts/
(package), tools/ (pipeline + eval commands), mcu/ (portable C runtime + device
ports), web/ (browser demo), configs/ + data/textsets/ (contracts).
GPLv3 — see LICENSE. The pipeline builds on GPLv3 components
(notably espeak-ng for G2P), so the
project as a whole is GPLv3.
Copyright (C) 2026 Ampixa.

