Turn the daily TLDR newsletters into a podcast you listen to instead of read.
Pick a day, hit Broadcast, and a few minutes later there are chapterized episodes waiting in your browser β one per newsletter, each story its own track. Runs entirely on your own machine: no accounts, no API keys, no cloud, no LLM, $0.
tldr-radio-demo-readme.mp4
Two minutes of the real app β pick a date, hit Broadcast, then listen: story chapters, sponsor reads and all. Turn the sound on β GitHub starts embedded video muted, and this one is worth hearing.
Featured at 22:22 in Top 10 GitHub on The Next New Thing.
TLDR publishes 14 excellent newsletters. Reading all of them is a chore; hearing them while you cook or drive is not. Existing "listen to your newsletter" tools want an account, a subscription, and your inbox. This wants a Docker daemon.
The thing that makes it trustworthy is what it doesn't do: no language model touches the text. The pipeline is deterministic end to end β fetch the public archive page, parse it by structure, assemble a script from templates and a pronunciation dictionary, and hand that exact script to a local text-to-speech model that only reads. Nothing is summarized, rewritten, or hallucinated. What you hear is what TLDR published.
Requirements: Docker Desktop (or Docker Engine + Compose v2), ~8 GB free disk, and a CPU. No GPU needed. Works on Apple Silicon, Intel, and amd64 Linux.
git clone https://github.com/<you>/tldr-radio.git
cd tldr-radio
make doctor # optional: checks Docker, disk, and the port before you pull 5 GB
make up # or: docker compose up -d --buildThen open http://localhost:7777, pick a date, and hit Broadcast.
The first run downloads ~5 GB and takes a while. The text-to-speech image bundles its model, and the container needs a minute or two to warm up before it answers. This is normal and only happens once β
make logswill showModel warmed up on cpu. Later runs start in seconds.
You don't need a config file to start: every setting has a working default. Copy .env.example
to .env when you want your own timezone, an overnight schedule, or notifications.
make help # all available commands
make logs # follow both services
make down # stopThe app itself is platform-agnostic β same images, same commands, nothing to configure differently. Two practical notes:
- Windows: Docker Desktop with the WSL2 backend.
makeisn't installed by default, so use the Compose commands directly (or run them from WSL, wheremakeis oneapt installaway). - Linux: Docker Engine plus the Compose v2 plugin.
docker-composev1 is EOL and won't parse this file.
| Instead of | Run |
|---|---|
make up |
docker compose up -d --build |
make down |
docker compose down |
make logs |
docker compose logs -f |
make ps |
docker compose ps |
make doctor |
bash scripts/doctor.sh β Git Bash (ships with Git for Windows) or WSL |
For best filesystem performance on Windows, clone into the WSL filesystem (e.g. ~/tldr-radio)
rather than a /mnt/c/... path.
Pick a date and the newsletters you want, then watch each one fetch, parse, script and synthesize live β one card per edition, with a progress meter during synthesis.
All 14 TLDR newsletters, each with two switches: put it on the Desk for manual runs, and/or include it in the overnight build. The line at the bottom estimates how much audio your overnight selection adds up to, measured from your own episodes.
A library grouped by day, one chapter per story with its section heading, the source summary and a link to the original. Transport with Β±15 s, speed up to 2.5Γ, auto-advance, resume where you left off, and keyboard shortcuts. Every clip tells you roughly how much reading time it saved.
Download hands you the whole episode as a single tagged mp3 β album, title and author set, so
it shelves properly in a podcast or audiobook app instead of showing up as a filename. The chapter
files are joined without re-encoding, so there's no quality loss and no ffmpeg in the image.
TLDR is funded by its sponsors, so this reads them out rather than quietly deleting them. Each one becomes its own chapter you can see coming and skip β the audio equivalent of your eye sliding past the ad block in the email β marked with a pill in the list, read in a different voice, and opened with a spoken "a word from our sponsor" so an ad is never mistaken for the news.
Sponsors are never counted as stories: the story numbering, the intro line and the episode's story count all step over them and keep matching the printed issue.
It's on by default. Turn it off in Settings β Sponsor reads β but note it's applied when an episode is built, so it affects the next broadcast rather than what's already in your Library.
Daytime Paper and Night Broadcast, both contrast-checked. Follows your system by default; the toggle in the header overrides it and is remembered.
| Night Broadcast | Daytime Paper |
|---|---|
Two containers on a private network β app (FastAPI + a build-less vanilla JS frontend) and
kokoro (Kokoro-82M via
Kokoro-FastAPI, CPU-only).
tldr.tech archive β parse β script β synthesize β SQLite + mp3s β player
(cached HTML) structure templates Kokoro-82M ./data
- Parse keys on page structure, never a hardcoded list of section names, so a new newsletter needs no code. If a layout change ever breaks it, the job fails loudly with a story count rather than quietly producing a short episode.
- Script adds a templated intro and outro, announces section changes, and applies a pronunciation dictionary so "CVE-2026-1234" and "GPT-4o" are read the way you'd say them.
- Synthesize runs chapters in parallel within an episode; editions build one at a time.
- Everything lands in
./data(SQLite + mp3s + cached pages) and prunes itself after 14 days.
68 voices are available; audition any of them in Settings.
Optional β docker-compose.yml provides a default for everything. Copy .env.example β .env to
override.
| Variable | Default | What it does |
|---|---|---|
TZ |
UTC |
Your timezone, so the overnight schedule means local time (e.g. America/Chicago). |
MAX_CONCURRENT_SYNTH |
4 |
Chapters synthesized at once within an episode. Lower to 2 to keep the machine responsive. |
KOKORO_CPUS |
6 |
Hard cap on cores the TTS container may use. |
AUTO_BROADCAST_TIME |
(off) | HH:MM local time to build episodes overnight, e.g. 06:00. Blank disables. |
AUTO_BROADCAST_EDITIONS |
tech,ai,infosec |
Seed only β the initial line-up on a fresh install. Afterwards Settings β Stations owns it. |
AUTO_BROADCAST_RETRY_HOURS |
4 |
How long to keep re-checking a newsletter that isn't posted yet (~25 min apart). 0 = one shot. |
RETENTION_DAYS |
14 |
Seed only β days to keep episodes, mp3s and cache before the nightly prune. Afterwards Settings owns it. |
DEFAULT_VOICE |
af_heart |
Seed only β default voice; changeable per broadcast and in Settings. |
INCLUDE_SPONSORS |
true |
Seed only β read sponsor segments as their own skippable chapters. Afterwards Settings β Sponsor reads owns it. Applied at build time, so it affects the next broadcast. |
SPONSOR_VOICE |
bm_george |
Seed only β voice for sponsor reads, deliberately distinct from DEFAULT_VOICE so an ad is audible as one. |
NTFY_URL |
(off) | Webhook pinged when the overnight run finishes or fails. An ntfy topic works as-is. |
PLEX_URL / PLEX_TOKEN |
(off) | Only if you share the machine with a Plex server β warns before a manual broadcast if Plex is streaming. |
APP_UID / APP_GID |
1000 |
Who the app runs as inside the container. Only needed on a Linux host where ./data belongs to someone other than uid 1000 β see below. |
The app runs as a non-root user, uid/gid 1000. Since ./data is a bind mount from your
machine, the host directory's owner is what actually decides whether episodes can be written.
On macOS and Windows this never comes up β Docker Desktop remaps mount permissions. On Linux, if
./data belongs to a different account, run id -u and id -g and set APP_UID / APP_GID in
.env to match (or sudo chown -R 1000:1000 data). If you get it wrong the app says so and
refuses to start, rather than failing halfway through a broadcast.
The 14 newsletters are tech, dev, ai, infosec, product, devops, founders, design,
marketing, crypto, fintech, it, data, hardware. In Settings β Stations each gets:
- Desk β offered on the Broadcast Desk for manual runs.
- Auto β included in the overnight build. Auto implies Desk.
They're separate so a newsletter can sit one click away without joining every night's run. Since editions build one at a time, the night scales linearly β the panel shows how much audio your current selection adds up to, measured from your own episodes. Turning a station off never hides episodes you already made.
Set AUTO_BROADCAST_TIME and TZ and episodes are waiting when you wake up. TLDR doesn't publish
at a fixed hour, and doesn't publish at all on weekends or holidays β so anything not yet up (or
that failed transiently) is retried every ~25 minutes for AUTO_BROADCAST_RETRY_HOURS, stopping
the moment it lands. Days with no issue pass quietly: nothing queued, nothing cached, no
notificatio
71B8
n. Only the final state is reported, so a run that recovers never pings you about the
attempts that didn't.
This is happy on modest, older hardware β it's CPU-bound but bursty, working hard for a few minutes a few times a day and idling otherwise. Two things scale with the number of stations you enable:
- Time. Editions build sequentially. A few minutes each on a modern CPU; 8β15 minutes each on an older server chip without AVX2. Three newsletters is a short night, all fourteen is a long one.
- Disk. Roughly
RETENTION_DAYSΓ stations Γ ~5 MB of mp3s, on top of the ~5 GB image.
If it shares the box with something else that wants the CPU, lower MAX_CONCURRENT_SYNTH and
KOKORO_CPUS. Budget ~2 GB RAM at peak.
docker run -d -p 8880:8880 --name kokoro ghcr.io/remsky/kokoro-fastapi-cpu # TTS
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements-dev.txt
KOKORO_URL=http://localhost:8880 uvicorn app.main:app --reload --port 7777Open http://127.0.0.1:7777.
- Tests:
pytestΒ· Lint:ruff check . - UI + accessibility QA:
python qa/qa.pyβ drives every page in both themes with Playwright, measures real contrast ratios, asserts no horizontal overflow at 390 px, and exercises the voice audition under a simulated slow backend. Needsplaywright install chromium webkitonce. - Audio integrity:
python qa/audio_qa.py
Frontend is deliberately build-less: no framework, no bundler, no CDN, self-hosted fonts. Edit the
files in app/static/ and reload.
Longer-form docs live in docs/ β spec.md is the full specification,
lessons_learned.md is a log of the bugs worth remembering, and
CHANGELOG.md is the changelog.
"Broadcast Desk" β warm oat paper, a dark radio faceplate, one signal-orange accent, and a distinct colour per station. Daytime and Night themes, both contrast-checked. Fonts are Bricolage Grotesque, Newsreader and Space Mono, self-hosted.
- Newsletter content belongs to TLDR β this only reads their public archive pages, and it reads the sponsor segments too (see Sponsor reads), because their sponsors are what pay for the writing. If you enjoy the newsletters, subscribe.
- Keep it to yourself. This is a personal listening tool. Publishing the audio β a public feed, a podcast directory, YouTube β would be redistributing someone else's work, and crediting them isn't a licence. Ask them first.
- Speech by Kokoro-82M (Apache-2.0), served via Kokoro-FastAPI.
This project is MIT licensed. It's a personal, local-first tool β not affiliated with or endorsed by TLDR.