Every article on this site has a “Listen to this article” button, and none of that audio comes from a cloud voice service. It is made on a Mac by Kokoro, an 82 million parameter text-to-speech model, run by a 396-line Python script that checks the site every ten minutes. We timed it on 14 September: a 1,047-word article became 6 minutes 17 seconds of speech in 53.3 seconds. This guide covers running Kokoro TTS on a Mac, from install to automatic narration, using the setup that has produced 165 narrations so far.
The test machine is an M4 Pro with 48 GB of unified memory and 14 CPU cores.
How to install Kokoro TTS on a Mac without ffmpeg
Kokoro is published on Hugging Face as hexgrad/Kokoro-82M. The model card lists 82 million parameters, an Apache 2.0 licence, a 24,000 Hz sample rate and 54 voices across 8 languages. We run it through the kokoro-onnx Python package, which uses ONNX Runtime and needs no GPU setup.
The whole install is six Python packages in a virtual environment:
python3 -m venv .venv
source .venv/bin/activate
pip install kokoro-onnx numpy lameenc huggingface_hub requests beautifulsoup4Two of those replace system tools. lameenc encodes MP3 inside Python, so there is no ffmpeg to install, and huggingface_hub fetches the model files on first run. Our script downloads kokoro-v1.0.onnx and voices-v1.0.bin from the fastrtc/kokoro-onnx repository. On disk they are 325.5 MB and 28.2 MB.
Speech then takes three lines:
from kokoro_onnx import Kokoro
kokoro = Kokoro("kokoro-v1.0.onnx", "voices-v1.0.bin")
samples, sample_rate = kokoro.create("Listen to this article.", voice="af_heart", speed=1.0, lang="en-us")create returns raw audio samples and the sample rate. Our voice is af_heart, a US English female voice; the other 53 are selected the same way by name.
Our script keeps the voice, language, speed and MP3 bitrate in a small config.json beside it, with af_heart, en-us, 1.0 and 48 kbps as the defaults. Other English voices in the same file include af_bella, am_michael, bf_emma and bm_george. Changing the voice only affects articles narrated from then on, so the script has an --all option that regenerates audio for every post. On a site with 165 narrations that is a long run, so try a new voice on one article with --limit 1 first.
How fast Kokoro TTS runs on an M4 Pro: one article measured
We narrated our published article on running ARC-AGI-3 on a Mac, writing the MP3 to a scratch folder so nothing was uploaded, and timed each stage with the system time command.
| Measure | Result |
|---|---|
| Text sent to Kokoro | 1,047 words, 5,632 characters, 13 chunks |
| Model load | 0.8 seconds |
| Synthesis | 53.3 seconds |
| Audio produced | 377 seconds |
| Speed | 7.1 times real time |
| MP3 size, mono 48 kbps | 2.26 MB |
| Peak memory | 1.61 GB |
| CPU time | 326.7 seconds over 55.0 seconds of wall clock |
The CPU figure means the run used close to six cores on average. Kokoro through ONNX Runtime ran on the CPU here, which is also why a narration can slow other work on the same machine. LM Studio had Gemma 4 31B loaded but idle during the test.
Earlier narrations logged by the same script sit in the same range: seven articles took between 48.7 and 95.6 seconds, and the longest produced a 4.22 MB file. Across all 165 narrations the MP3s total 411.7 MB, with a median of 2.35 MB. For a sense of scale, Piper TTS is the other local voice we have set up; we have not run the two side by side on this machine, so we give no comparison here.
Turning a web article into clean speech for Kokoro
Kokoro reads whatever text it is given, so the text has to be cleaned first. Our script takes the article’s HTML from the WordPress REST API and parses it with BeautifulSoup.
It deletes code blocks, tables, figures, scripts, styles and asides before reading anything, because a table read aloud is a list of numbers with no labels. It then reads headings, paragraphs, list items and quotations in order, and adds a full stop to any heading that lacks one, so the voice pauses instead of running the heading into the next sentence.
Long text goes to Kokoro in chunks. The script splits on sentence endings and packs sentences into chunks of at most 480 characters, then joins the audio with a quarter of a second of silence between chunks. The article we timed became 13 chunks.
The last step converts Kokoro’s floating point samples to 16-bit audio and encodes a mono MP3 at 48 kbps with lameenc. Speech does not need stereo or a high bitrate, and a six-minute narration comes in around 2.3 MB, small enough that the site’s player loads nothing until a reader presses play.
How to narrate new posts automatically with launchd
WordPress runs on a web host and cannot reach a Mac on a home network, so the Mac asks instead. A launchd agent runs the script every 600 seconds and once at login, with StartInterval set to 600 and RunAtLoad set to true in its plist.
Each run makes one request for published posts and checks a custom field, sn_audio_url, on each one. A post that already has audio is skipped, and when every post has audio the run prints “Nothing to do” and exits. The Kokoro model is only loaded when there is an article to narrate, so an idle check costs one HTTP request.
For a post without audio the script narrates it, uploads the MP3 to the WordPress media library, and writes the file’s address into sn_audio_url. The site theme shows the player on any post where that field is set. A new article is narrated within about ten minutes of going live, while the Mac is awake.
Two details keep it safe to leave running. A file lock stops a scheduled run and a manual run from narrating at the same time, and the WordPress credentials sit in a local config file excluded from version control. After editing the text of a narrated post, the old audio still matches the old text, so we regenerate that one post with a single-post option rather than waiting for the watcher, which only fills gaps.
This is the same principle behind why we run our models locally: the site holds a 2 MB file per article, and the model that made it never leaves the Mac. For the reverse direction, turning speech into text on the same machine, our WhisperX transcription guide covers that setup.
