Back to tools

Free · runs on your device · no sign-up

TTS Generator that runs on your device

Type anything and hear it right away. The voice runs in this tab: a 3.5 MB model on a 136 kB WebAssembly engine I wrote for it, so your text never leaves your device. I started from Paradee v1.1, a small open model distilled from Kokoro-82M, and made it 12.9× faster in the browser while it scored slightly better on quality. Below you can race the old and new versions on your own hardware, see what each speed-up was, and take the files home.

12.9×
faster than Paradee v1.1 in the browser
201×
realtime on one desktop CPU core (v2g)
10 ms
to the first sound, streaming (v2g)
136 kB
engine, instead of 21.6 MB of onnxruntime-web
Demo: the first pages of Alice's Adventures in Wonderland (Lewis Carroll, 1865, public domain), about 5 minutes of speech. Press Speak and watch how fast it renders.5,040 / 12,000
Generated0:00
Playing0:00
Render time–
Faster than realtime–

First sound
–
after pressing Speak
Engine speed
–
seconds of speech per second of compute
Engine
pdn
3.5 MB model, loads on first use
Privacy
On device
your text never leaves this tab

How this text to speech works

Step 1
həlˈO·wˈɜɹld

Text becomes phonemes

A JavaScript port of misaki, the phonemizer Kokoro was trained on, looks words up in a lexicon it fetches in small pieces: about 90 kB before the first sentence.

Step 2
pdn.wasm136 kB→v2f.pdn

A tiny model makes the sound

The phonemes go to a 2-4 MB model running on pdn, a WebAssembly engine built for this one model, in a background worker so the page stays smooth.

Step 3

Audio streams to you

Sound starts after the first slice of a sentence is ready, a few tens of milliseconds, and plays while the rest is made. Download the whole thing as a WAV.

Live, on your hardware

Race them on your device

Same passage, same phonemes, one CPU thread each. These numbers come from your browser, not my benchmark box.

The passage every engine reads

“I started with a voice model that ran about fifteen times faster than real time in a browser. Then I rebuilt its sound engine, slimmed down every layer, and wrote a tiny WebAssembly runtime just for it. Now the same voice starts talking before you can blink.”

Your device
CPU: 1 thread per engine
SIMD: checking… · WebGPU: no
v2gmine
pdn, my engine
 
–
realtime
v2fmine
pdn, my engine
 
–
realtime
v2fmine
onnxruntime-web, WASM
 
–
realtime
Paradee v1.1
onnxruntime-web, WASM
 
–
realtime
First run downloads about 22 MB; after that it comes from your browser cache.

Each engine runs in its own fresh Web Worker, one after another, on one CPU thread, with a short warm-up first. Speed is seconds of speech per second of compute. First sound is how long the first sentence takes before it can play: pdn streams, so it's the first chunk; onnxruntime-web can't, so it's the whole sentence. onnxruntime-web is 1.22.0-dev (the build Chickadee ships), loaded from jsDelivr; v1.1 is the published file from Hugging Face. onnxruntime-web needs several hundred MB of memory, so on a phone the tab may reload.

The speed-up, step by step

How I made it 12.9× faster

Every step below is a real model file, measured in the same run as v1.1 on one desktop core (i7-14700K) in Node, which runs WebAssembly on the same V8 engine as Chrome. Every step that changed the weights had to keep quality: 400 dev sentences × 3 seeds, 1,600 more held-out sentences, and a second speech recogniser as a guard. The test set got one look per release.

onnxruntime-web 1.22, the runtime I started onpdn, my engine× realtime, one CPU thread, the same 200 test sentences
  1. Where I started

    v1.1

    Paradee-8M v1.1: Kokoro-82M's af_heart voice distilled into 7.6M parameters. It sounds great, but in onnxruntime-web it ran at 15× realtime and took 3.6 s from a cold start to its first sound.

    15×
    the baseline · 8.46 MB
  2. 1

    Faster DSP, same weights

    A closed-form sine bank instead of a running-sum oscillator, radix-4 block STFTs, and DSP tables rebuilt when the session starts instead of stored in the file.

    28×
    1.88× v1.1 · 8.12 MB
  3. 2

    Slimmer text side, packed weights

    A smaller text model, and every layer stored at the fewest bits it can take (4, 6 or 8), unpacked at load. The file went from 8.46 MB to 4.60 MB.

    33×
    2.15× v1.1 · 4.60 MB
  4. 3

    Copy-free decoder head

    I laid out the head's tensors so the STFT stages never copy data between them. Same function, less memory traffic.

    34×
    2.26× v1.1 · 4.61 MB
  5. 4

    GAN-trained decoder head

    v2d

    A new head with a 300-point noise path, trained adversarially. It sounds better than v1.1's and does less work.

    41×
    2.70× v1.1 · 5.00 MB
  6. 5

    A distilled, slimmer trunk

    The decoder trunk went from 256 to 192 channels, distilled from the wider one.

    45×
    2.98× v1.1 · 4.26 MB
  7. 6

    Quantisation-aware training, int8 math

    v2e

    Fine-tuned for its own 4-bit weights and 8-bit compute, so the biggest matrix multiplies run in int8 without costing quality.

    50×
    3.34× v1.1 · 4.01 MB
  8. 7

    Slimmer again: 128 channels

    A second distillation round. The trunk is now half as wide as v1.1's.

    54×
    3.57× v1.1 · 3.56 MB
  9. 8

    A new DSP head

    v2f

    Harmonics computed on the noise path's grid so one inverse STFT makes both, noise drawn by rows from a small table, and only ops a GPU backend supports.

    73×
    4.85× v1.1 · 3.42 MB
  10. 9

    A smaller text model

    lite

    A narrower text encoder: 0.8 MB smaller and about 15% faster, for a small word-error cost.

    85×
    5.62× v1.1 · 2.64 MB
  11. 10

    No LSTM, fully streamable

    v2g

    A convolutional text side with a sentence vector, and a decoder with fixed per-channel norms. Both run in chunks, so audio can start after about 10 ms.

    97×
    5.96× v1.1 · 2.32 MB
  12. 11

    My own engine: pdn

    pdn

    A C to WebAssembly engine written for exactly this model: 136 kB instead of onnxruntime-web's 21.6 MB, SIMD128 and relaxed-SIMD builds with int8 dot products, FFT-based DSP, its own noise generator, and streaming. Same v2g weights, 2.1× faster again. (v2f on pdn: 149×, 9.6× v1.1.)

    201×
    12.91× v1.1 · 2.32 MB

v2f on pdn is already in production as the read-aloud voice of a web-novel reading site. The browser numbers above are on one thread, because a normal web page can't count on more: the 4-thread onnxruntime-web build needs cross-origin isolation, and pdn doesn't need any of it.

Phone speed, emulated

Who speaks first on a phone?

The same files on a desktop core slowed down to phone speed with Chrome's CPU throttling. The lanes start talking at their measured time.

Who speaks first?

A median 7.4 s sentence, played back at real speed

v2g
pdn
45 ms
v2f
pdn
127 ms
v2f
onnxruntime-web
384 ms
Paradee v1.1
onnxruntime-web
1.8 s
onnxruntime-web can't stream: its first sound waits for the whole sentence.

Speed on the same throttled CPU

× realtime, one thread, 30 test sentences

v2g
pdn
49×
12.6× v1.1
v2f
pdn
37×
9.4× v1.1
v2f
onnxruntime-web
19×
4.9× v1.1
Paradee v1.1
onnxruntime-web
3.9×
the baseline

These are pdn's relaxed-SIMD numbers (Chrome on Android has it). With plain SIMD128, as on Safari and Firefox, v2g gets 35× and 55 ms to first sound.

This is an emulation, not a real phone. I don't have a phone lab, so I used Chrome's standard CPU throttling (4× is Lighthouse's “mid-tier mobile”, 6× is low end) with Chrome pinned to one desktop core. It doesn't model ARM chips, heat or memory bandwidth, so trust the ratios more than the absolute numbers. The race above runs on your actual device. First visit on slow 4G at mid-tier speed, from page load to the first sound: v2g 11.6 s, v2f 16.5 s. Nearly all of that is downloading 2.1 MB or 3.1 MB; after that it's cached.

Download, memory, start-up

Smaller everything

The squares are drawn to scale by area. Cold start means a fresh process to the first sound of a short sentence: 3.6 s with v1.1 on onnxruntime-web, most of it compiling a 21.6 MB WebAssembly runtime.

Runtime download
onnxruntime-web 21.6 MB
pdn 136 kB
Compressed: 3.0 MB vs 38 kB
Model file
v1.1 8.46 MB
v2g 2.32 MB
7.57M → 2.91M parameters. v2f is 3.42 MB.
Memory at start
v1.1 on ORT-web 646 MB
v2g on pdn 100 MB
Peak, fresh process to the first sound of a short sentence
Cold start to first sound
v1.1 on ORT-web 3.6 s
v2g on pdn 60 ms
Runtime compile, model load and a short sentence, local files

WebGPU

What about the GPU?

I measured it once, on a desktop graphics card in Chrome.

v2f on pdn
one CPU core
184×
v2f on onnxruntime-web
WebGPU, RTX 4070 Ti
113×
v2f on onnxruntime-web
WASM, one CPU core
89×

One core of a desktop CPU running pdn beat a desktop RTX 4070 Ti running onnxruntime-web's WebGPU backend, in the same Chrome session. The GPU also needed 1.75 s to start (session plus shader compilation) and can't stream, and a laptop's integrated GPU or a phone would be slower still. So the CPU is the right default for a model this small. v1.1 can't take the GPU route at all. This is one measured run, so read it as a direction, not a decimal.

Quality

Did the voice get worse?

No. Faster and smaller didn't cost quality: the naturalness score went up and word errors stayed within noise. Listen for yourself.

Listen side by side

“White dwarf stars, if they have a near companion, may then become Type Ia supernovae.”

Held-out test sentences, rendered with the released files. 96 kbps MP3.

Scores on 200 held-out sentences

ModelUTMOS (higher)WER (lower)
Kokoro-82M4.5205.67%
Paradee v1.14.3915.85%
v2f4.4625.65%
v2g4.4535.48%

UTMOS predicts how natural speech sounds; v2f's gain over v1.1 is statistically significant. Word error rate is what Whisper mishears, and those differences are within noise. A stronger recogniser, Whisper large-v3-turbo, shows no regression against v1.1 on two larger sets, and on 1,600 more held-out sentences v2f's error rate there came out lower than Kokoro's own.

Do it yourself

Run it on your own site

It's all static files: no server, no API key, no GPU. These are the release files byte for byte, with checksums.

Nothing to install

Drop this into any page. The engine, model and lexicon load from mesmer.tools (CORS is open), so it works from a static site, a CodePen or a local file. Synthesis runs on the page's thread here, which is fine for short text.

hello.html 2 kB
<script type="module">
  import { loadParadee } from "https://mesmer.tools/paradee/v2/paradee.mjs";

  const tts = await loadParadee({
    model: "v2f",                                          // or "v2g", "v2f_lite"
    espeakUrl: "https://mesmer.tools/paradee/espeak.mjs",  // optional, see the licence note
  });
  document.querySelector("#speak").onclick = async () => {
    const audio = await tts.synth("Hello from my own page.");  // Float32Array, 24 kHz
    const ctx = new AudioContext({ sampleRate: tts.sampleRate });
    const buf = ctx.createBuffer(1, audio.length, tts.sampleRate);
    buf.copyToChannel(audio, 0);
    const src = ctx.createBufferSource();
    src.buffer = buf; src.connect(ctx.destination); src.start();
  };
</script>
<button id="speak">Speak</button>
FileWhat it isSizeSHA-256
paradee-web-v2.zipThe browser bundle: pdn engine (SIMD128 and relaxed-SIMD builds), v2f, v2f lite and v2g weights, the misaki-lite phonemizer and lexicon, worker, player, example.html10 MB
paradee_v2f.onnxv2f, the default: 4-bit weights, int8 compute3.4 MB
paradee_v2f_noint8.onnxv2f with fp32 compute, for WebGPU3.4 MB
paradee_v2g.onnxv2g: smallest and fastest, streamable2.3 MB
paradee_v2g_noint8.onnxv2g with fp32 compute, for WebGPU2.3 MB
paradee_v2f_lite.onnxv2f lite: smaller text model2.6 MB
paradee_v2f_lite_noint8.onnxv2f lite with fp32 compute2.6 MB
config.jsonPhoneme vocabulary and model facts3 kB
tokenizer.jsonThe same vocabulary for transformers.js / kokoro-js3 kB
hello.htmlOne-file page that loads everything from mesmer.tools2 kB
say.mjsNode command line (needs the unzipped bundle)1 kB
say.pyPython with onnxruntime and misaki1 kB

Every file inside the bundle has its hash in /paradee/v2/SHA256SUMS; the ONNX files in /paradee/onnx/SHA256SUMS. Check with sha256sum -c SHA256SUMS.

Licences. The models are Apache-2.0. They're derived from Paradee-8M v1.1 by Sahil Mahendrakar (Apache-2.0), which is distilled from Kokoro-82M by hexgrad (Apache-2.0); please keep both credits. The engine and JavaScript are Apache-2.0, and misaki's lexicons are Apache-2.0.

eSpeak NG is not in the bundle. It's GPL-3.0-or-later. Without it, the phonemizer reads every word in its lexicon (96.5% of words in my test set) and throws on the rest, mostly rare names. To add it, pass espeakUrl as above (it loads from jsDelivr only when needed) or npm i phonemizer with a bundler or Node. Check the GPL fits your project first. This page uses it for unknown words.

Credits

The voice is Paradee: Paradee-8M v1.1 by Sahil Mahendrakar (Apache-2.0) is the model I started from, and it was distilled from Kokoro-82M by hexgrad (Apache-2.0), whose af_heart voice it speaks. Text goes through a JavaScript port of hexgrad's misaki phonemizer and its lexicons (Apache-2.0). Words outside the lexicon go through eSpeak NG (GPL-3.0-or-later), which your browser loads from jsDelivr only when it's needed. The comparisons use onnxruntime-web (MIT) from jsDelivr and the Kokoro-82M ONNX export by onnx-community. The v2 models, the pdn engine and this page are mine.