Long text becomes narrated audio, and the job tells you the truth while it runs
A production tool that pulls text out of a source, narrates it, and reports honestly on a job that takes minutes, without pretending to be instant.
Who, what, and how long
Media & Publishing
12 weeks
Time and materials
Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.
Versioned extraction
Three extraction strategies kept live and comparable, because clean text out of arbitrary sources is the genuinely hard part.
Getting clean prose out of an arbitrary URL is what decides whether the audio is listenable, so all three strategies stay live: readability heuristics over the DOM, structural extraction with Cheerio against known site shapes, and a plain-text path. Each job records which strategy produced its text, so a bad render can be diagnosed instead of shrugged at.
- Three extraction strategies kept live and comparable
- Every job records which one produced its text
- A bad render is traceable to its extractor
The extraction lab: readability heuristics, structural extraction with Cheerio and the plain-text path run on the same web page side by side, what each kept that isn't the article, and the strategy recorded on each job.
One job's timeline: four named stages of very different lengths, drawn to shape, the transition each stage wrote, and why a bar giving each stage a quarter would sit at 75% through all of encoding.
Named stages
Extracting, segmenting, synthesizing, encoding: progress that says exactly what's happening.
Progress is reported as the stage the job is in (extracting, segmenting, synthesizing, encoding), because those stages run for very different lengths and a percentage that treats them as equal lies. Each stage writes its own transition, so the screen can name what's happening. Long stages report their own internal position, so encoding a two-hour file never looks stalled.
- Four named stages in place of an averaged percentage
- Each stage writes its own transition
- Long stages report internal position so they never look stalled
A real status endpoint
A job that stopped in synthesizing, with the stage and reason, exactly what resuming will and won't redo, the failed status response, and a different job shown as stalled in encoding with no progress.
The working screen polls the pipeline's actual position. No plausible-looking bar on a timer.
The working screen polls an endpoint that reads the job's actual row. If the pipeline stalls, the screen stalls with it and says so. That's the whole point: a progress bar that keeps moving through a dead job is worse than no progress bar. Failures surface the stage they failed in and the reason.
- Polls the job's real row; no timer-driven animation
- A stalled pipeline shows as stalled
- Failures name the stage and the reason
The jobs list with every job's id, extractor and state, two renders of the same article started a minute apart kept apart, and the export view for the one that has output.
Per-job screens
Every job has an id, a working view and an export view, so two renders never get confused for each other.
Identity is what makes a long render survivable. The job is addressable, so it can be linked, reopened on another device or picked up tomorrow, and two jobs started a minute apart can't be mistaken for each other. The export view appears only once there's something to collect, so it never sits empty as a promise from the moment work begins.
- Per-job id with its own working and export routes
- A job survives being closed, linked, or reopened elsewhere
- The export view appears only when there is output
Explicit cleanup
Artifacts are listed and deleted deliberately. An audio pipeline fills a disk faster than anyone expects.
Intermediate WAVs, segment files and encoded output are listed with their sizes and deleted deliberately by a person. An audio pipeline fills a disk far faster than anyone plans for, and a silent background cleanup that removes the wrong artifact is worse than a full disk. Retention is stated per job type, never assumed.
- Artifacts listed with sizes and deleted deliberately
- No background sweep that could remove the wrong file
- Retention stated per job type
Artifacts by job: intermediate WAVs, segment files and encoded output with sizes, files locked while a job runs or needs them to resume, retention stated per job type, and a deletion someone has to confirm.
What we were brought in to do
The production tool turns written material into narrated audio at volume. We built the extraction, the conversion pipeline, and the job surface that turns a multi-minute render into something a producer can watch instead of guess at.
A publisher turning written material into narrated audio at volume, where a single render takes minutes and a producer running a queue needs to know whether to wait or go do something else. Two genuinely hard problems sat side by side: getting clean text out of arbitrary source material, and reporting the progress of a long job honestly.
Backend & Pipeline Engineering
Where the old way broke
- 01
Two hard problems sat side by side. Getting clean text out of arbitrary source material is genuinely difficult: the extractor was rewritten three times. And a render that takes minutes can't hide behind a spinner, because a producer needs to know whether to wait or go and do something else.
Extraction was the one that fought back. Source material arrived as web pages, documents and pasted text, and every strategy that worked well on one kind produced garbage on another. The extractor was rewritten three times before the answer turned out to be keeping all three. Meanwhile the working screen showed a spinner, which on a six-minute job looks exactly like a hang.
A staged pipeline with a real status endpoint behind it, so the working screen shows which stage a job is in and roughly what remains, plus an export screen that appears only when there's something to collect.
What we built together
- 01
Built extraction as a versioned endpoint and kept all three versions live while they were compared
Extraction is a versioned endpoint with all three strategies live, and each job records which one produced its text, so a bad render can be traced to its source.
- 02
Split conversion into named stages so progress means something specific
Conversion is split into extracting, segmenting, synthesizing and encoding, because those stages run for different lengths and one averaged percentage across them lies.
- 03
Polled a real status endpoint in place of an indeterminate bar
The working screen polls the job's actual row, so a stalled pipeline shows as stalled instead of animating on through a dead job.
- 04
Gave every job an id and its own working and export screens
Per-job routes replaced a single working screen that showed whichever render started last, which had already caused two producers to collect the wrong file.
- 05
Cleaned up artifacts explicitly, because an audio pipeline fills a disk quickly
Artifacts are listed with their sizes and deleted deliberately. No background job gets the chance to remove the wrong file.
Phase by phase
Phase 1: Extraction
Three Attempts, Kept Side By Side
Getting clean prose out of arbitrary sources took three distinct approaches. We kept all three versioned and live, so their output could be compared on real material.
- Extraction v1/v2/v3
- Comparison Harness
- Source Handling
Phase 2: The Pipeline
Named Stages, Not A Spinner
Split conversion into stages that mean something (extracting, segmenting, synthesizing, encoding), so the interface can say exactly what's happening.
- Stage Model
- FFmpeg Encoding
- Segment Handling
Phase 3: Honest Status
A Job You Can Watch
Built a status endpoint the working screen polls, so progress reflects the pipeline's actual position. No animation timed to look plausible.
- Status Endpoint
- Working Screen
- Stage Progress
Phase 4: Collection and Cleanup
Export, Then Tidy Up
Gave finished jobs an export screen and made deletion explicit, because an audio pipeline that never cleans up is a disk-space incident waiting for a quiet week.
- Export Screen
- File Listing
- Explicit Deletion
Submitting a source for narration: web page, document or pasted text, the extractor and job type, then voice, pronunciation and pacing, all set before the job runs.
Operational results after launch
3
Extraction strategies compared
4
Pipeline stages surfaced
0
Indeterminate spinners
−80%
Producer check-ins per job
Extraction strategies compared and pipeline stages surfaced are counts describing the system. Indeterminate spinners is zero by construction: every long-running surface polls a real status. Producer check-ins per job is the publisher's own estimate of how often someone opened a job to see whether it was still alive, before and after.
Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.
About our collaboration
- 01
Extraction was rebuilt three times and we kept all three versions live, which is how the comparison on real source material was possible at all. The status endpoint came before the working screen, because a progress bar with nothing behind it is worse than no progress bar.
Time and materials, because the extraction work genuinely couldn't be estimated. Nobody would have signed up in advance to three rewrites. Keeping the strategies versioned and live is what made them comparable, and it's the decision the engagement's shape allowed.
What we'd carry into the next one
- 01
A spinner on a six-minute job isn't a loading state. It's a refusal to answer.
A spinner on a six-minute job answers no question a producer has. The only useful signal is which stage it's in and whether that stage is moving.
- 02
When extraction is the hard part, version it and keep the versions comparable. Overwriting them destroys the evidence.
When extraction is the hard part, overwriting each attempt destroys the evidence. Keeping them versioned is what made the comparison possible at all.
- 03
Progress is only useful if the stages are named things a user recognizes.
Named stages work because a producer recognizes them. A percentage only means something if the units behind it are equal, and here they aren't.
- 04
A pipeline that produces files needs a deletion story on day one, well before the first disk alert.
Audio fills a disk faster than anyone plans for, and a cleanup story added after the first alert gets written in a hurry and usually deletes something it shouldn't.
One six-minute job, three progress screens
A spinner can’t tell slow from stuck. A named stage can.
The same job replayed through the old spinner, a percentage that gives each stage a quarter, and the named stages the working screen now reads from the job’s row. Turn on the stall and see which screen notices. Switch tabs, or use the arrow keys once one is focused.
Encoding · chunk 29 of 58
GET /api/jobs/7Q3M/status → "state": "running", "stage": "encoding", "position": 29/58
Illustrative stage durations for one six-minute job (extracting 0:20, segmenting 0:25, synthesizing 3:15, encoding 2:00), chosen to show that the stages differ in length. Not measured. Replayed fast.
From a submitted source to the right file in the right producer’s hands
A render takes minutes, so every step leaves something a person can read: which extractor ran, which stage the job is in and where inside it, and which files exist and how big they are.
- 01 · SourceWeb page, document or pasted textSubmitted as a job with its own id, so it can be linked, reopened on another device or picked up tomorrow.
- 02 · ExtractVersioned extractionReadability over the DOM, Cheerio against known site shapes, or plain text, all kept live. The job records which one produced its text.
- 03 · ConvertSegment, synthesize, encodeNamed stages, each writing its own transition. Long stages report their internal position; encoding runs through FFmpeg.
- 04 · TruthJob row + status endpointThe working screen polls the job's actual row, not a timer. A stall shows as stalled; a failure names its stage and reason.
- 05 · CollectPer-job export + artifactsExport appears only once there is output. Artifacts are listed with sizes and deleted deliberately, never by a background sweep.
Right file, live job, known disk
Job identity, honest status & deliberate cleanup
One id per job, one export per id
Every job has its own working and export routes. They replaced a single screen that showed whichever render started last, which had already led two producers to collect the wrong file.
A dead job looks dead
The status endpoint reads the job's actual row, so a stalled pipeline shows as stalled and stops moving. A failure surfaces the stage it failed in and the reason.
Deletion is a decision
Intermediate WAVs, segment files and encoded output are listed with their sizes and deleted deliberately. There is no background sweep to remove the wrong artifact, and retention is stated per job type.
Turning written content into audio, or running long jobs that need to say where they are? Scope your build in 3 minutes.
Scope your buildNearby engagements
Web PlatformsA writing desk where the manuscript never leaves the machine
A desktop notebook built on Electron, where documents live on disk, the editor is a real rich-text surface, and the AI assistant reads only what the writer hands it.
Book Publishing · 14 weeks
Web PlatformsTwo billing models in one product, and neither one pretending the other doesn't exist
A content platform sold both by subscription and as a lifetime deal, where a redeemed code and a monthly plan resolve to the same entitlement, with no parallel systems.
Media & Publishing · 18 weeks
Web PlatformsThree kinds of publication, two comment streams, and one queue that keeps them civil
A market-news publication with announcements, reporting and commentary as separate content types, member profiles, and a moderation queue that can hold a comment while a person decides.
Media & Publishing · 14 weeks
Let's talk
Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.














