Articles

Free, on-device text-to-speech in the browser (and a scam-call sound board)

Leveraging Supertonic TTS and ONNX Runtime Web

Text-to-speech in the browser. 10 voices. Runs offline. No API key. $0/char.  Free, on-device speech. Nothing leaves the page.
July 1, 2026

Supertonic Text-to-Speech Demo

On-device TTS in the browser. Nothing heavy loads until you click Enable.

I get a lot of junk calls. Fake warranty renewals, "Your account has been compromised," the whole catalog, several times a day. Somewhere between the third and fourth one, I started wondering how hard it would be to build a little soundboard of stall tactics I could play back at them for my own amusement.

That was the fun excuse. The real reason I started poking at this was simpler: I've written CFML-based AI chatbots using an older perpetual-licensed version of ColdFusion and wanted to integrate text-to-speech that I would not have to pay a cloud vendor for. Every hosted TTS service meters you by the character, wants an API key, and sends your text off to someone else's server. This seemed like overkill for a proof-of-concept. I wanted to type a sentence and hear it, with nothing leaving the browser.

It turns out that is now possible, and the whole thing runs client-side.

Disclaimer

I have used the built-in SpeechSysnthesis interface from the Web Speech API in the past, but it sounds fake and kinda robotic to me... like 1982's SAM (Software Automated Mouth) from the Commodore 64. Your mileage may vary with this demo. I've received reports that it doesn't work on mobile Chrome devices. I immediately encountered an "out of memory" error when using an emulated BrowserStack iPhone device. (It works with my iPhone 15.) If used in production for the general public, feature testing may be required in order to determine if these more advanced speech models can be used.

What it does

The demo is a single static page. You click "Enable," it downloads a set of ONNX models once, and after that you can type any text and synthesize speech locally. No account, no per-character billing, no network round trip for the actual audio. There are ten voices (five male, five female) and speed and step controls.

On top of that, I built the soundboard. It is a grid of call-screening lines grouped by category: greetings, stalling, "hold on, let me get my wife," a few absurd ones ("City Morgue, you kill 'em, we chill 'em"), and polite ways to end a call. Tap a line and it speaks in the currently selected voice. There is also a "Surprise me" button that picks a random line in a random voice, which is exactly as dumb and entertaining as it sounds.

I also added a spot to type your own line, sanitize it, and save it to the browser so it becomes a reusable button. More on that below.

The stack

The speech engine is Supertonic, an on-device TTS project from Supertone Inc. Specifically, I am using their Supertonic-3 model generation (tts_version 1.7.3), which supports 31 languages. The models are plain ONNX files hosted on Hugging Face, so the browser fetches them directly.

The thing that actually runs the models is ONNX Runtime Web (the onnxruntime-web package), version 1.23.0. It executes the ONNX graph in the browser using WebGPU when the machine has it and falls back to WebAssembly when it does not. You can force either path with a ?ep=wasm or ?ep=webgpu URL parameter, which was handy while I was debugging.

A word on that debugging, because it cost me an evening. Mixed or multi-threaded execution gave me garbled, robotic, half-scrambled audio that sounded like a Cylon reading a ransom note. The fix was to pin the runtime to single-threaded WASM (numThreads = 1, proxy = false) and match the exact runtime the reference implementation uses. Multi-threaded WASM needs cross-origin isolation that a plain static host does not hand you, and without it you get a degraded threading fallback that quietly corrupts the output. Once I forced one thread and the correct model generation, it produced clean speech.

There is no bundler. It is ES modules plus a browser import map, which keeps the whole project readable and copy-pasteable.

Four ways to hand off the audio

The engine gives you back raw PCM samples. What you do with those depends on how you want to play them, so I wired up four approaches side by side to compare them:

  • A Blob URL fed into an <audio controls> element. This is the primary one. You get native play, pause, scrub, and replay for free, and it is the least fussy.
  • The Web Audio API, building an AudioBuffer and playing it through a gain node. Lowest latency and full routing control, but the source nodes are one-shot so you rebuild them on every play.
  • A downloadable .wav file, encoded as 16-bit PCM.
  • A base64 data URI, mostly to show how much larger the encoded string gets versus the blob (about 33 percent).

Clicking any sound-board line now fills all four panels from the same result, so you can see the tradeoffs against real audio.

The user-generated part

The custom-line feature is where a bit of CFML instinct crept in. Anything a user types gets run through a sanitizer that strips tag-like sequences, drops stray angle brackets, removes control characters, and collapses whitespace before it is stored or displayed. Everything renders through textContent, never innerHTML, so markup can never execute even if something slipped past. It is defense in depth for what is otherwise a toy.

Saved lines go into localStorage as a JSON array, so they survive a refresh. Each one shows up as a play button with an X next to it to delete it. There are two save buttons: "Save" quietly adds the line, and "Save and play" also synthesizes it, so you can build up a batch without sitting through each one.

Licenses

Since this leans on other people's work, the terms matter:

  • The Supertonic models are released under the OpenRAIL-M license, which is a responsible-AI license with use-based restrictions. Read it before you ship anything commercial.
  • Supertonic's own sample code is MIT. My code is adapted from it.
  • ONNX Runtime Web is MIT, from Microsoft.

So the code side is permissively licensed, and the model side carries the OpenRAIL-M restrictions. For a personal proof-of-concept that is fine. For production, the OpenRAIL-M terms are the part to actually read.

This hasn't been uploaded to any repositories yet.

Was it worth it

For the stated goal, yes. I can generate speech in the browser, offline after the first load, without paying anyone per character. The quality is good enough that I was genuinely surprised it was running locally.

The soundboard is a gimmick, but it is my gimmick, and the next time someone calls to tell me my car's extended warranty is expiring, I will at least have options.