README.md
| 1 | --- |
| 2 | license: gpl-3.0 |
| 3 | language: [en, ne, hi, vi, id, zh] |
| 4 | pipeline_tag: text-to-speech |
| 5 | tags: [text-to-speech, tiny, microcontroller, esp32, wasm, piper, distillation] |
| 6 | library_name: sanotts |
| 7 | --- |
| 8 | |
| 9 | # sanoTTS — a tiny neural voice that runs anywhere |
| 10 | |
| 11 | ***sano*** (सानो) — Nepali for **"small."** A family of tiny neural |
| 12 | text-to-speech voices — **294k to 2.27M parameters** — that run with |
| 13 | **no cloud and no NPU**: real-time on a ~$3 ESP32-S3 (out a GPIO into an |
| 14 | LM386 and a speaker), or live in the browser via WASM. |
| 15 | |
| 16 |  |
| 17 | |
| 18 | | | | |
| 19 | | --- | --- | |
| 20 | | Parameter range | **294,279 – 2,272,145** | |
| 21 | | Runs real-time on a $3 microcontroller | ESP32-S3, out a GPIO into an LM386 | |
| 22 | | Runs in the browser | WebAssembly, no server | |
| 23 | | Per-voice footprint | **337 KB to 8.7 MB**, zero dependencies (espeak-ng phonemizer included) | |
| 24 | | Coverage | **11 voices** across **6 languages** — English, Nepali (नेपाली), Hindi (हिन्दी), Vietnamese (Tiếng Việt), Indonesian (Bahasa), Chinese (中文) | |
| 25 | | License | open source, **GPL-3.0** | |
| 26 | |
| 27 | **Live demo:** [tts.ampixa.com/sanoTTS](https://tts.ampixa.com/sanoTTS) — |
| 28 | every voice synthesizes your text live in the browser, no server, no upload. |
| 29 | |
| 30 | ## Download |
| 31 | |
| 32 | Both packages stream their weights from **this repo** by default. |
| 33 | |
| 34 | ```python |
| 35 | import sanotts |
| 36 | sanotts.synthesize("Hello world", voice="heart") # downloads heart/ from here |
| 37 | ``` |
| 38 | |
| 39 | ```js |
| 40 | import { SanoTTS } from 'sanotts-web'; // fetches web/voices/ from here |
| 41 | const tts = await SanoTTS.load(); |
| 42 | const result = await tts.synthesize('Hello world', { voice: 'heart' }); |
| 43 | ``` |
| 44 | |
| 45 | Python needs `sanotts` >= 0.3.0, the browser `sanotts-web` >= 0.3.0. Both fall |
| 46 | back to the GitHub releases or the Pages host if Hugging Face cannot be |
| 47 | reached, so an outage here cannot break an install. Python packages land in |
| 48 | `~/.cache/sanotts/`; set `SANOTTS_VOICE_SOURCE=hf` or `=github` to pin one |
| 49 | host. In the browser, passing `voiceBase` yourself turns the fallback off, so |
| 50 | a self-hosted deployment never quietly reaches back to our servers. |
| 51 | |
| 52 | ## Samples |
| 53 | |
| 54 | Two clips per voice are in this repo's `samples/` folder; one is embedded |
| 55 | below. "Package here" means this repo carries that voice's weights. The three |
| 56 | languages without one currently ship only through the |
| 57 | [browser demo](https://tts.ampixa.com/sanoTTS) and the GitHub repo's |
| 58 | `web/voices/` — their packages haven't been exported yet. |
| 59 | |
| 60 | | Voice | Language | Params | SCOREQ | Package here | Sample | |
| 61 | | --- | --- | ---: | :---: | :---: | --- | |
| 62 | | heart | English 🇺🇸 | 2.27 M | 3.48 | [`heart/`](https://huggingface.co/ampixa/sanoTTS/tree/main/heart) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-heart-1.mp3"></audio> | |
| 63 | | hfc | English 🇺🇸 | 1.83 M | 3.94 | [`hfc-en-1p8m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/hfc-en-1p8m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-hfc-1.mp3"></audio> | |
| 64 | | amy | English 🇺🇸 | 1.46 M | **4.13** | [`amy-en-1p46m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/amy-en-1p46m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-amy-1.mp3"></audio> | |
| 65 | | kristin | English 🇺🇸 | 1.40 M | 4.09 | [`kristin-en-1p4m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/kristin-en-1p4m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-kristin-1.mp3"></audio> | |
| 66 | | amy-small | English 🇺🇸 | 1.08 M | 3.70 | [`amy-en-1p1m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/amy-en-1p1m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-amy-small-1.mp3"></audio> | |
| 67 | | robot (on-device, int8) | English 🇺🇸 | 567 k | — | not packaged here (int8 MCU format) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/mcu-567k.mp3"></audio> | |
| 68 | | heart-nano | English 🇺🇸 | 294 k | 2.29 | [`heartnano/`](https://huggingface.co/ampixa/sanoTTS/tree/main/heartnano) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/english-heart-nano-1.mp3"></audio> | |
| 69 | | Indonesian | Bahasa | 1.46 M | — | [`id-newstts-1p46m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/id-newstts-1p46m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/indonesian-1.mp3"></audio> | |
| 70 | | Vietnamese | Tiếng Việt | 1.46 M | — | [`vi-vais1000-1p46m/`](https://huggingface.co/ampixa/sanoTTS/tree/main/vi-vais1000-1p46m) | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/vietnamese-1.mp3"></audio> | |
| 71 | | Nepali | नेपाली | 1.47 M | — | not exported yet — see `web/voices/nepali/` in the GitHub repo | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/nepali-1.mp3"></audio> | |
| 72 | | Hindi | हिन्दी | 1.50 M | — | not exported yet — see `web/voices/hindi/` in the GitHub repo | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/hindi-1.mp3"></audio> | |
| 73 | | Chinese | 中文 | 1.50 M | — | not exported yet — see `web/voices/chinese/` in the GitHub repo | <audio controls src="https://huggingface.co/ampixa/sanoTTS/resolve/main/samples/chinese-1.mp3"></audio> | |
| 74 | |
| 75 | SCOREQ is a no-reference naturalness predictor, higher is better; it is only |
| 76 | reported for the English voices, which share one 24-sentence eval set. The |
| 77 | other languages haven't been scored against a comparable reference yet. |
| 78 | |
| 79 | **Size does not order this table, and we are not going to pretend it does.** |
| 80 | `amy` at 1.46M scores higher than `heart` at 2.27M because they come from |
| 81 | different teachers and different architectures, not because one is bigger. |
| 82 | `amy` and the voices above it are the **piperlite** lineage, distilled from a |
| 83 | Piper/VITS teacher at 22.05 kHz. `heart` and `heart-nano` are the newer |
| 84 | **nano** lineage — 24 kHz, mel-100 → ConvNeXt1D → iSTFT, distilled from a |
| 85 | Kokoro teacher through a frozen Vocos — which is the one that quantizes to |
| 86 | int8 and runs on a microcontroller. The two are not interchangeable and the |
| 87 | `sanotts` package picks the right runtime for you. |
| 88 | |
| 89 | The `heart` and `heart-nano` scores were re-measured on 2026-09-04; the eval |
| 90 | set, checkpoint hashes, exact commands and all 24 per-clip scores are in |
| 91 | [`evidence/heart-diverse24-remeasure-20260904.json`](https://huggingface.co/ampixa/sanoTTS/blob/main/evidence/heart-diverse24-remeasure-20260904.json). |
| 92 | They were scored on the float32 reference render. The shipped `heart-nano` |
| 93 | int8 export tracks that render at 0.981 waveform correlation, and `heart` |
| 94 | ships as float32 because its int8 export reached only 0.951 against a 0.98 |
| 95 | gate. |
| 96 | |
| 97 | The "robot" row is the 567,008-parameter model that runs on the ESP32-S3. |
| 98 | |
| 99 | ## Install & use |
| 100 | |
| 101 | | Platform | Install | Then | |
| 102 | | --- | --- | --- | |
| 103 | | Python | `pip install sanotts` | `sanotts say "Hello" --voice heart -o hello.wav` | |
| 104 | | Web (npm) | `npm install sanotts-web` | `const tts = await SanoTTS.load(); await tts.synthesize('Hello', {voice:'heart'})` | |
| 105 | | Web (no build) | copy `dist/` + `voices/` | see [Deploy on your own site](https://github.com/Ampixa/sanoTTS#deploy-on-your-own-site) in the GitHub README | |
| 106 | | Arduino / PlatformIO | zip-install or `lib_deps = https://github.com/Ampixa/sanoTTS.git` | [`arduino/README.md`](https://github.com/Ampixa/sanoTTS/blob/master/arduino/README.md) | |
| 107 | | Hugging Face | this repo | the voice packages above, downloaded for you by `pip install sanotts` | |
| 108 | | Browser | nothing | [tts.ampixa.com/sanoTTS](https://tts.ampixa.com/sanoTTS) | |
| 109 | |
| 110 | Pip voices: `heart`, `hfc`, `amy-1p8m`, `amy`, `kristin`, `vi`, `id`, |
| 111 | `amy-1p1m`, `heart-nano`. Pure numpy inference, no torch, no onnxruntime. |
| 112 | |
| 113 | ## How it stacks up |
| 114 | |
| 115 | Open small-scale TTS on an honest gate — a diverse 24-sentence set scored |
| 116 | with the **same** no-reference suite (SCOREQ / UTMOS are naturalness |
| 117 | predictors, DNSMOS-SIG is signal quality; higher is better). Parameter |
| 118 | counts are inference-time and exclude the shared external G2P. |
| 119 | |
| 120 | | System | Params | SCOREQ | UTMOS | DNS-SIG | |
| 121 | | --- | ---: | :---: | :---: | :---: | |
| 122 | | **sanoTTS (amy)** | **1.46 M** | **4.13** | **4.10** | 3.61 | |
| 123 | | TinyTTS | 1.62 M | 3.94 | 3.65 | **3.62** | |
| 124 | | Inflect Nano | 4.63 M | 3.81 | 3.65 | 3.58 | |
| 125 | | Kitten TTS nano | 15 M | 3.02 | 3.58 | 3.43 | |
| 126 | | _Piper (our teacher)_ | _~15 M_ | _4.71_ | _4.47_ | _3.65_ | |
| 127 | | _Kokoro_ | _82 M_ | _4.89_ | _4.52_ | _3.69_ | |
| 128 | |
| 129 | sanoTTS is the **smallest** model here and the **best on naturalness |
| 130 | (SCOREQ and UTMOS) among everything up to 15M params** — beating TinyTTS |
| 131 | while being smaller. On DNSMOS-SIG, TinyTTS edges us by 0.01 — no single |
| 132 | metric tells the whole story. It's the only one that runs a full neural |
| 133 | stack on a $3 MCU. The frontier only pulls ahead at Piper, the ~15M teacher |
| 134 | we distilled from, and Kokoro (82M, 56× larger) — a gap we don't claim to |
| 135 | close. Reproduce it with `tools/eval_mos_all.py` + `tools/eval_scorecard.py` |
| 136 | in the GitHub repo. |
| 137 | |
| 138 | Shipped-file sizes: sanoTTS amy 2.8 MB fp16 and TinyTTS 3.5 MB fp16, both |
| 139 | verified from the released files; Kokoro's ~330 MB fp32 is its widely cited |
| 140 | public figure. |
| 141 | |
| 142 | ## How it works |
| 143 | |
| 144 |  |
| 145 | |
| 146 | espeak-ng provides phoneme IDs; a duration model predicts timing; an |
| 147 | acoustic model predicts generator latents; a decoder renders audio. The |
| 148 | piperlite voices (amy, kristin, hfc, and the other languages) use a compact |
| 149 | time-domain decoder running in fp32 at 22.05 kHz. The nano voices (heart, |
| 150 | heart-nano) and the 567,008-parameter on-device model use an iSTFT decoder |
| 151 | at 24 kHz, quantized to int8 where it has to fit and run in real time on the |
| 152 | ESP32-S3. Models are distilled from a Piper/VITS or Kokoro teacher — see |
| 153 | [`docs/distillation-recipe.md`](https://github.com/Ampixa/sanoTTS/blob/master/docs/distillation-recipe.md) |
| 154 | in the GitHub repo for the full recipe. |
| 155 | |
| 156 | ## Deploy |
| 157 | |
| 158 | - **ESP32-S3 talking device** — a standalone WiFi dashboard: type text, the |
| 159 | board phonemizes (on-chip espeak-ng) and speaks. See |
| 160 | [`mcu/ports/esp32s3/`](https://github.com/Ampixa/sanoTTS/tree/master/mcu/ports/esp32s3). |
| 161 | Board-by-board measurements are in |
| 162 | [`BOARDS.md`](https://github.com/Ampixa/sanoTTS/blob/master/BOARDS.md); the |
| 163 | silicon figures there are for the `en_us_e12nano` lineage, which is a |
| 164 | sibling of `heart-nano`, not the same weights. |
| 165 | - **Browser** — the full stack in WASM, no server. **[▶ Hear and synthesize |
| 166 | all 11 voices live](https://tts.ampixa.com/sanoTTS)**; source in |
| 167 | [`web/`](https://github.com/Ampixa/sanoTTS/tree/master/web). |
| 168 | - **Other MCUs** — which chips can run it and how well: |
| 169 | [`docs/mcu-classes-and-porting.md`](https://github.com/Ampixa/sanoTTS/blob/master/docs/mcu-classes-and-porting.md). |
| 170 | |
| 171 | ## Links |
| 172 | |
| 173 | - Source, recipes, eval tooling: [github.com/Ampixa/sanoTTS](https://github.com/Ampixa/sanoTTS) |
| 174 | - Live browser demo: [tts.ampixa.com/sanoTTS](https://tts.ampixa.com/sanoTTS) |
| 175 | - npm package: [sanotts-web](https://www.npmjs.com/package/sanotts-web) |
| 176 | - PyPI package: [sanotts](https://pypi.org/project/sanotts/) |
| 177 | |
| 178 | ## License |
| 179 | |
| 180 | GPLv3 — see [`LICENSE`](https://github.com/Ampixa/sanoTTS/blob/master/LICENSE). |
| 181 | The pipeline builds on GPLv3 components (notably |
| 182 | [espeak-ng](https://github.com/espeak-ng/espeak-ng) for G2P, and |
| 183 | [piper](https://github.com/OHF-Voice/piper1-gpl)), so the project as a |
| 184 | whole is GPLv3. |
| 185 | |
| 186 | Copyright (C) 2026 Ampixa. |
| 187 | |
| 188 | ## Files here |
| 189 | |
| 190 | Two package layouts, because there are two graphs. |
| 191 | |
| 192 | **piperlite** (`amy-en-1p46m/`, `kristin-en-1p4m/`, `hfc-en-1p8m/`, |
| 193 | `amy-en-1p1m/`, `id-newstts-1p46m/`, `vi-vais1000-1p46m/`) — a flat fp16 |
| 194 | blob addressed by manifest offsets: |
| 195 | |
| 196 | ``` |
| 197 | manifest.json weights.fp16.bin |
| 198 | piper-phoneme-config.json (+ sibilant-injection calibration where applicable) |
| 199 | ``` |
| 200 | |
| 201 | **nano** (`heart/`, `heartnano/`) — the mel-100 stack, two blobs plus the |
| 202 | generated offset header: |
| 203 | |
| 204 | ``` |
| 205 | meta.json lineage, per-file sha256, sample rate, vocab |
| 206 | front_*.bin duration + acoustic |
| 207 | model_*.bin decoder |
| 208 | nano_q8_meta.h tensor offsets |
| 209 | ``` |
| 210 | |
| 211 | `heartnano/` ships `*_q8.bin` (int8, 345,232 bytes total); `heart/` ships |
| 212 | `*_f32.bin` (float32, 9,137,920 bytes) because its int8 export missed the |
| 213 | fidelity gate. Both layouts are consumed by the `sanotts` Python package and |
| 214 | the portable C runtime. |
| 215 | |
| 216 | **`web/voices/`** is a third thing and exists for the browser. The piperlite |
| 217 | voices ship there as `front_f32.bin` + `dec_f32.bin`, a different artifact |
| 218 | from the `weights.fp16.bin` the Python package reads, which is why the same |
| 219 | voice appears twice in this repo under two names. It mirrors `web/` in the |
| 220 | GitHub repo byte for byte and is what `sanotts-web` fetches. The nano voices |
| 221 | are not duplicated: `web/voices/heart/` and `heart/` hold the same blobs. |
| 222 | |
| 223 | `samples/` holds the audio clips embedded above (mp3, one or two per voice, |
| 224 | plus the on-device `mcu-567k.mp3`), and `evidence/` the eval report behind the |
| 225 | heart scores. |
| 226 | |