README.md
12.8 KB · 226 lines · markdown Raw
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 ![text in → ESP32 → speech out](https://raw.githubusercontent.com/Ampixa/sanoTTS/master/docs/assets/saanotts-mcu-hero.png)
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 ![text → duration → acoustic → decoder → audio](https://raw.githubusercontent.com/Ampixa/sanoTTS/master/docs/assets/saanotts-signal-path.png)
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