← A Voice Reader for Simply Broken
v1 · researchListening to thirteen years of writing
The brief was a reader on every page: press play, keep listening when the phone is in a pocket, skip with the AirPods, change the speed. This is what that actually takes — which engine speaks the words, which code plays them, and the two places where the obvious answer is the wrong one.
01 · The corpusThe site is smaller than it feels
Every estimate in this exploration is measured, not assumed. The whole of Simply Broken — twenty-four posts and pages, nineteen public exploration pages — is about 176,000 characters. Add a short summary for each piece and the job is roughly 202,000 characters, which is four and a quarter hours of speech.
That number decides more than it looks. At this size every commercial engine costs less than the price of a lunch to narrate the entire site, one time. Cost is therefore not the question. The question is which voice you want to live with, and how much control you have over how it reads.
The longest piece is the fundraising playbook at about eighteen minutes. The median piece is under four. Ten of the nineteen exploration pages are under 1,700 characters and are really index pages wrapped around a gallery of images — and that turns out to matter a great deal. See section 05.
The explorations were expected to carry a lot of Hebrew, which would have forced a cloud engine and dragged in every niqqud and homograph problem the story pipeline has been fighting for a month. They do not. Across all nineteen pages there are eighteen Hebrew characters. This is an English-only job, and the hardest known risk simply is not present.
02 · The voiceChoosing an engine when price is not the tiebreak
Costs below are the one-time cost to narrate the whole site at each engine's public rate. Nothing here is a subscription you keep paying: the audio is rendered once and the file is yours.
| Engine | Whole site | Pause control | The trade |
|---|---|---|---|
| Azure Neural HD | $4.45 | Full SSML | Audiobook-grade, and the pause control that makes headings sound deliberate rather than run-on. |
| Google Chirp 3 HD | $6.06 | Full SSML | The same class as Azure. Pick on voice taste, not on capability. |
| OpenAI tts-1-hd | $6.06 | Style prompts, no SSML | Simplest API of the lot. A notch below on naturalness, and you steer it with words instead of tags. |
| Amazon Polly Generative | $6.06 | Full SSML | Good, but Amazon has retired voices before, which is a risk for a corpus meant to sound consistent for years. |
| ElevenLabs | ~$10–20 | Pause tags only | The best-sounding voice, the weakest control, and it collides with the story pipeline's quota. See below. |
| Local Chatterbox | $0 | None — silence is inserted by hand | Free and already installed on the mini. Watermarks every file it produces. This page is reading you one. |
| Browser speech synthesis | $0 | None | Fails the brief outright — the voice differs on every visitor's device and playback stops when the tab loses focus. |
| Apple system voices | — | Limited | Disqualified. Apple's licence forbids publishing recordings of the system voices. |
The obvious move was to use the ElevenLabs subscription that already exists for the Shmili stories — Creator tier, 136,319 credits a month, already paid for. Checked against the live account rather than the notes: 11,009 credits remain, and the month resets on 25 August.
The site needs about 202,000. So this month holds five per cent of the job, and even a completely fresh month does not cover one pass — it would take roughly six weeks of quota and would consume the entire story budget on the way. The free ride is closed unless capacity gets bought.
Which leaves a genuine choice rather than a default. A neutral narrator from Azure or Google costs about five dollars, sounds professional and gives full control over pacing. Or the author reads his own work: a cloned voice is the only option that sounds like the person who wrote the sentences. Worth noting that every failure recorded in the voice ledger — the flipped homograph, niqqud coming out as Arabic, a model that was “totally confused” — was Hebrew. English is where cloning is strongest, and English is all this corpus needs.
03 · The prototypeHear it before deciding anything
Below is a real post — 3 Must follow rules for fundraising storytelling from July 2021 — rendered by the free local engine on the Mac mini, and played by the player this exploration recommends building. It is the cheapest option on the table, so it sets the floor: if this is good enough, nothing needs buying.
Rendered locally in about eleven minutes on the M4. Every file this engine makes carries an inaudible watermark, which is a reason to weigh it carefully before it becomes the site voice.
04 · The playerFour requirements, and the one that forces a choice
The recommendation is to build it: roughly two hundred lines of plain JavaScript over a
normal <audio> element. That matches how the rest of the site is built
— no framework, no build step — and every alternative has a defect.
- The turnkey embeds — ElevenLabs AudioNative, BeyondWords, Play.ht, Trinity — sell voice generation with a player attached. They are the wrong shape for audio you already own, and several inject their own branding or audio advertising.
- Plyr is being archived into Video.js v10.
- howler.js defaults to the Web Audio API, which is precisely the thing that breaks background playback on iOS.
Each requirement maps onto one mechanism, and each has a way to get it wrong:
| Requirement | How it is met | What breaks it |
|---|---|---|
| Keeps playing in a pocket | A plain <audio> element |
Using the Web Audio API, which iOS suspends the moment the tab is backgrounded |
| AirPods and lock-screen controls | The Media Session API | Nothing much — supported on iOS since Safari 15 |
| Playback speed | playbackRate, pitch preserved by default |
Nothing. The claim that iOS does not support this is a stale myth from an old Apple doc |
| Starting at all | The first play() inside the tap handler |
Calling it later, from a promise or a timer — iOS silently refuses |
The lock screen shows either skip-fifteen-seconds buttons or next-and-previous-track buttons. Never both. Registering one pair suppresses the other, and iOS always draws “10” on the button whatever interval the code actually skips.
So: keep skip, and the choice between summary and full article lives on the page, as it does in the prototype above. Or give the AirPods that toggle, and lose skipping. Skip is the assumption here, because it is what was asked for.
The files sit in R2, mono at 64kbps, which is right for speech and indistinguishable from anything larger. A twelve-minute article is about five megabytes; the whole site is comfortably under 150.
The research warned that Cloudflare sometimes answers a range request with
200 instead of 206. Tested against the very file playing
above: it returns 200, sends the whole 1.5 MB, and offers no
Accept-Ranges header at all. Static assets do not serve ranges.
At three minutes nothing is visibly wrong — the file arrives quickly and seeking
works once it has. At twelve minutes on a slow connection it will not: the listener
cannot skip ahead until the whole file has landed, and the lock-screen scrubber is the
first thing to feel it. The fix is to serve audio through the Worker straight from R2
and pass the Range header through, rather than as a static asset. That is
a change to the serving layer, not to the player, and it is not made yet.
05 · ImagesThe part where the instinct is wrong
The brief asked for images to be mentioned or described as the voice reads. The research says the opposite, and the reason is worth keeping: Medium and the New York Times skip images entirely in their narrated articles. Where description is done properly — in the EPUB and DAISY standards built for exactly this — it is a separate optional track, because a listener cannot skim past a description they did not want.
But the two halves of this site are not the same thing, and a single rule would be wrong for one of them.
| Surface | Images | What they are | What the voice should do |
|---|---|---|---|
| Blog posts | 33 | Decorative headers. Only two have alt text and both merely repeat the post title. | Skip them. Nothing is lost. The seven real captions are visible text and get read anyway. |
| Explorations | 453 | The images are the content — icon sets, mockups, pricing tables. 43% have real alt text. | Describe them, or do not narrate these pages at all. Text alone describes nothing. |
The convention worth adopting now
One rule, which costs nothing extra and serves both a screen reader and this player:
Write every alt text as one complete sentence you would be happy to hear spoken
aloud, leading with the takeaway. Use an empty alt="" for images
that are purely decorative.
- A chart: “Line chart: NoonFox signups grew from 200 to 4,000 users between June and August 2026.” Not “chart.png”.
- A mockup: “Mockup of the NoonFox onboarding screen, showing the three-question flow before the paywall.”
- Decoration:
alt="", and the reader passes over it in silence.
Applied to what already exists, that is about 288 descriptions to write. That is authoring work, not code, and it is the real cost of the image half of this brief.
06 · The rest of the pipelineSummaries, silence, and knowing when audio has gone stale
The summary track
150 to 250 words — about ninety seconds. Hook, what the piece argues, who it is for. Written from the article by a model told to lift the author's own phrases rather than paraphrase them, to keep the real numbers and examples, and banned from opening with “This article explores…”.
How the words are prepared
Headings are read as ordinary text, never announced as headings; they get a longer silence before them instead. Links read their anchor text and never the URL. Pull-quotes are dropped, because they duplicate a sentence from the body and would otherwise be heard twice. Code blocks and complex tables become a single spoken line pointing back to the written page.
The local engine has no SSML, so those pauses are real silence cut in between the pieces — 500 milliseconds between paragraphs, 800 before a heading. A paid engine would express the same thing as a tag.
Knowing when to re-render
Hash the extracted spoken text of each page and put the hash in the file name. Edit an essay and the hash changes, so the build can say which audio is stale without anyone remembering to check.
07 · OpenWhat is still to decide
- The voice. A neutral narrator for about five dollars with full pause control — or the author's own cloned voice, which costs more, controls less, and needs a clean English sample.
- The scope. All 43 pieces, the blog only, or the blog plus the four substantial explorations. Ten exploration pages do not earn a reader.
- The images. Skip them as the industry does, or build the separate description track — and whether the 288 missing descriptions get written.
- The AirPods. Skip fifteen seconds, or switch between summary and full. iOS permits exactly one.