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, largest to smallest: heart (2.27M), hfc and amy-1p8m (1.8M),
amy, kristin, vi and id (1.46M), amy-1p1m (1.1M), and heart-nano
(294k, 337 KB of weights).
They are fetched from
huggingface.co/ampixa/sanoTTS into
~/.cache/sanotts/, and fall back to the GitHub
voices-v1 and
voices-v2 releases
if Hugging Face cannot be reached. Set SANOTTS_VOICE_SOURCE=hf or =github
to pin one host. 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).
Every voice package, plus audio samples, lives at
huggingface.co/ampixa/sanoTTS. That is
where pip install sanotts downloads from by default; the GitHub releases are
the fallback.
Nothing to install: tts.ampixa.com/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.48 | 3.38 | 3.51 |
| sanoTTS (heart-nano) | 0.29 M | 2.29 | 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.48 | |
| heart-nano (int8, 24 kHz) | 294 k | 2.29 | |
| 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).
The inference runtime is MIT. The project as a whole is GPLv3.
| Licence | |
|---|---|
Runtime + language bindings (mcu/src/snt_*.c, mcu/include/, mobile/) |
MIT — see LICENSE.MIT |
| Everything else, including the espeak-ng G2P ports and the training tooling | GPL-3.0-or-later — see LICENSE |
The copyleft comes from espeak-ng alone, which is used for grapheme-to-phoneme. An earlier version of this note said piper was GPLv3 too; it is not — piper and piper-phonemize are MIT.
The runtime files were audited against that boundary: none of them reference
espeak, and the espeak-ng code lives entirely in the G2P and port layers, which
stay GPLv3. LICENSE.MIT lists every covered file and shows the reasoning,
including the upstream licences it was checked against. So you can embed the
runtime in a permissively-licensed project; you cannot embed the espeak-ng G2P
without taking GPLv3 with it.
Copyright (C) 2026 Ampixa.