A Firefox extension that reads web pages aloud using Kokoro
running locally on your own machine. No cloud TTS, no API keys, no telemetry — the page text
goes to 127.0.0.1 and nowhere else.
It extracts the article body (not the nav and footer), splits it into sentences, and synthesizes the next few while the current one plays, so playback is continuous instead of pausing between sentences.
Firefox extension ──POST /tts──> kokoro_server.py ──> Kokoro-82M
extract.js page text → chunks local HTTP, WAV 24 kHz mono
background.js queue + prefetch + playback
Two parts, both required: the extension is the front end; the Python server does the synthesis. The extension alone does nothing.
- Firefox 140+
- Python 3.10+
- ~400 MB disk for the model (downloaded once, automatically)
espeak-nginstalled system-wide (Kokoro falls back to it for unusual words) —sudo dnf install espeak-ng/sudo apt install espeak-ng/brew install espeak-ng- A GPU is optional. CPU synthesis is roughly real-time; a GPU is comfortably faster.
git clone https://github.com/fswolf/kokoro-reader.git
cd kokoro-reader/server
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
./start_server.shFirst start downloads the model and takes a minute. When it prints
Kokoro reader server on http://127.0.0.1:8899 it's ready. Check it:
curl -s localhost:8899/healthLeave it running. If you'd rather not keep a terminal open, see Tray app below, or the systemd unit at the bottom of this file.
Grab the signed .xpi from Releases,
then in Firefox: about:addons → gear icon → Install Add-on From File.
Or load it from source (for development)
about:debugging#/runtime/this-firefox → Load Temporary Add-on → pick
extension/manifest.json. This version disappears when Firefox restarts.
| Ctrl+Shift+U | read page / pause / resume |
| Ctrl+Shift+Y | stop |
| Toolbar button | same, plus voice, speed and volume |
| Right-click a selection | Read selection with Kokoro |
The dot beside the popup title is green when the server is reachable. Changing voice or speed re-synthesizes the current sentence immediately, so you can audition voices mid-page.
Default voice is am_adam. Also good: am_michael, am_onyx, bm_george, bm_lewis
(male), af_bella, af_heart, bf_emma (female). The popup lists whatever the server
reports.
Optional. server/kokoro_tray.py runs the server as a child process and puts it in the
system tray, so it starts from your app launcher instead of a terminal. The menu has a live
status line, start/stop/restart, test voice, and quit (which stops the server too).
sudo dnf install python3-gobject libayatana-appindicator-gtk3
# Debian/Ubuntu: sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1
cp server/kokoro-reader-tray.desktop ~/.local/share/applications/
update-desktop-database ~/.local/share/applicationsThen launch "Kokoro Reader" from your launcher.
- The
.desktopfile assumes the repo is at~/kokoro-reader. Elsewhere, edit itsExec=line — and quote the path if it contains spaces, becauseExecsplits on whitespace and an unquoted path fails silently:Exec=python3 "/home/you/My Projects/kokoro-reader/server/kokoro_tray.py" - On Wayland the tray is your bar's job: Waybar needs
"tray"inmodules-right, or the app runs but nothing draws the icon. KOKORO_TRAY_AUTOSTART=0starts the tray without starting the server.- Nothing happens when launched? Run
python3 server/kokoro_tray.pyto see the error.
Server and extension work the same. Three differences.
Dependencies via Homebrew:
brew install python espeak-ngNo tray app — kokoro_tray.py needs GTK and AppIndicator, which are Linux-only. Run
server/start_server.sh from Terminal, or start it at login with launchd:
<!-- ~/Library/LaunchAgents/local.kokoro-reader.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key> <string>local.kokoro-reader</string>
<key>ProgramArguments</key> <array>
<string>/Users/YOU/kokoro-reader/server/start_server.sh</string>
</array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <true/>
</dict>
</plist>launchctl load ~/Library/LaunchAgents/local.kokoro-reader.plistApple Silicon — set KOKORO_DEVICE=mps in start_server.sh to use the GPU via Metal.
Leave it unset for CPU if torch complains.
Running the server on a NAS keeps it always-on and lets every machine on the LAN use it.
A prebuilt image is published to ghcr.io/fswolf/kokoro-reader on every push, so there's
nothing to build.
Docker tab → Add Container, then:
| Field | Value |
|---|---|
| Name | kokoro-reader |
| Icon URL | https://raw.githubusercontent.com/fswolf/kokoro-reader/main/docs/icon.png (Advanced View) |
| Repository | ghcr.io/fswolf/kokoro-reader:latest |
| Network Type | Host |
Add two entries with Add another Path, Port, Variable, Label or Device:
| Config Type | Name | Key / Container Path | Value / Host Path |
|---|---|---|---|
| Path | Model Cache | /cache/huggingface |
/mnt/user/appdata/kokoro-reader/cache |
| Variable | CPU Threads | OMP_NUM_THREADS |
6 |
Apply. The image is ~1.5 GB, and the first start downloads the model (~330 MB), so give it a minute, then:
curl -s http://YOUR-SERVER:8899/healthPut that URL in the extension popup under Server. Firefox prompts for permission to reach the host — the manifest grants only loopback up front.
- Network Type. Host needs no port mapping. With Bridge, add a Port: container
8899, host8899, TCP. - Model Cache keeps the weights across container updates. Without it they re-download every time.
- CPU Threads — set it to your number of performance cores, not total threads. On hybrid Intel chips (12th gen and later), extra threads land on E-cores and every sentence waits for the slowest one. Measured on an i5-12600K (6 P-cores): 6 threads ≈ 0.5 s per sentence, 8 threads ≈ 0.9 s.
- Updates. Push to
main→ GitHub rebuilds the image → Unraid shows "update ready".
Check synthesis speed before committing to it. Latency lands right before each sentence, and a NAS CPU is usually slower than a desktop:
time curl -s -X POST http://YOUR-SERVER:8899/tts \
-H 'Content-Type: application/json' \
-d '{"text":"The quick brown fox jumps over the lazy dog."}' -o /dev/nullUnder a second for that sentence and the extension's prefetch hides it entirely.
Building the image yourself / Docker Compose
docker/docker-compose.yml builds from source — use it with Unraid's Compose Manager plugin
or plain docker compose. Clone the repo, point context at it, and Compose Up.
git clone https://github.com/fswolf/kokoro-reader.git /mnt/user/appdata/kokoro-reader/src
docker build -f docker/Dockerfile -t kokoro-reader:latest /mnt/user/appdata/kokoro-reader/srcKOKORO_HOST must be 0.0.0.0 inside the container or a published port reaches nothing —
the image sets it. If a build fails on a dependency, try python:3.11-slim in the Dockerfile.
Server, via env vars (set them in server/start_server.sh):
| Variable | Default | |
|---|---|---|
KOKORO_PORT |
8899 |
|
KOKORO_VOICE |
am_adam |
default voice |
KOKORO_LANG |
a |
a American, b British, plus others Kokoro supports |
KOKORO_DEVICE |
auto | set cuda to force GPU |
KOKORO_HOST |
127.0.0.1 |
leave it — see Security |
Extension, in the popup: voice, speed, volume, and the server URL.
- Chunk size —
MAX = 300/HARD = 420inextension/extract.js. Smaller starts talking sooner but breaks prosody more often between sentences. - Lookahead —
lookahead: 2inextension/background.js. Each chunk in flight is one request; raise it on a GPU so synthesis stays ahead of playback. - Extraction misses the article —
pickRoot()scores candidates by paragraph text length with a link-density penalty and prefers<article>/<main>. Sites with unusual markup may need a selector added to theexplicitquery. - Abbreviations read as letters —
protect()hides dots insidee.g.,U.S.,Dr., decimals and initials so the sentence splitter can't break them. Add cases toABBR.
The server binds to 127.0.0.1 and has no authentication, which is fine for a loopback
socket. Don't change KOKORO_HOST to 0.0.0.0 on a shared or untrusted network — that
would let anyone on it synthesize arbitrary text on your machine.
Running it in Docker does expose it, by necessity — the container binds 0.0.0.0 so the
published port works. That's reasonable on a home LAN and the same posture as most NAS
services, but keep the port off the internet: don't forward it, and don't put it behind a
public reverse proxy without adding auth in front.
The extension declares data_collection_permissions: none and means it: page text is
POSTed to your local server, nothing is stored, nothing leaves the machine.
- Can't read PDFs or
about:pages — Firefox doesn't allow script injection there. - No word or sentence highlighting yet.
- Reads the whole extracted article from the top; no read-from-click-point.
- If the server logs
200but you hear nothing, it's Firefox's autoplay policy on the background page — setmedia.autoplay.defaultto0inabout:config.
# ~/.config/systemd/user/kokoro-reader.service
[Unit]
Description=Kokoro Reader TTS server
[Service]
ExecStart=%h/kokoro-reader/server/start_server.sh
Restart=on-failure
[Install]
WantedBy=default.targetsystemctl --user enable --now kokoro-readerFirefox won't permanently install an unsigned add-on. To build your own signed copy, get API credentials from AMO:
npm i -g web-ext
cd extension
export WEB_EXT_API_KEY='user:...' WEB_EXT_API_SECRET='...'
web-ext sign --channel=unlistedIf Kokoro Reader is useful to you, you can sponsor its development:
MIT — see LICENSE.
Kokoro-82M is by hexgrad, Apache 2.0.
