Skip to content

Repository files navigation

sanoTTS — a tiny neural voice that runs anywhere

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.

sanoTTS — nine tiny voices, six languages, browser + $3 chip

  • 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

Live demo

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.

Install & use

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

Python

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.wav
import sanotts
result = sanotts.synthesize("Hello world", voice="amy")   # numpy audio @ 22.05 kHz

Voices: 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.

Arduino / PlatformIO

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.git

See arduino/README.md for board support (ESP32-S3 ✓), memory guidance, and flashing the model blobs (mcu-kristin-745k-q8.tar.gz).

Hugging Face

The voice packages — including audio samples — are mirrored at huggingface.co/ampixa/sanoTTS.

Browser

Nothing to install: ampixa.github.io/sanoTTS.

Deploy on your own site

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.

How it stacks up

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.

Size comparison: sanoTTS 0.75M-1.8M params vs TinyTTS 1.62M vs Inflect Nano 4.63M vs Kokoro 82M, linear axis

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.

The size-quality frontier: SCOREQ rising from 3.70 to 4.16 as decoder size grows from 1.09M to 1.84M params, same voice

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.

The voices

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.

How it works

text → duration → acoustic → decoder → audio

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).

Train your own voice

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

Deploy

Verify your result

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.

Layout

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).

License

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.

About

sanoTTS (सानो = 'small' in Nepali): a ~1.4M-param neural TTS that runs on a $3 chip or in the browser. Leads SCOREQ/UTMOS in the sub-15M class.

Resources

Stars

106 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages