Build NaturalReader
KINDAreplaces $13.9/mosaves $166.8/yrback to the verdict
A text-to-speech reader whose two real problems are getting the reading order right in documents that were never designed to be read aloud, and keeping track of which voice may be used for what.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs extraction, normalisation and playback orchestration.
Get it python.org or your package manager, then a virtual environment.
Verify
python3 --version prints 3.12 or higher - installfree
Why Reading order is reconstructed from positions and boxes, which a plain text extractor has already discarded.
Get it A library that exposes per-block coordinates rather than only a text dump.
Verify
It returns text blocks with bounding boxes for a two-column paper - installfree for open models; commercial voices are licensed per use
Why Phase 4 depends on there being a licence to record, and reading private documents through a service is a poor default.
Get it An open neural engine you can run locally, plus the system voices as a fallback. Read the licence before installing, not after.
Verify
A sentence synthesises offline and the licence text is saved locally - have readyfree
Why Reading order only breaks on real material: a two-column paper, a document with footnotes, one with tables, one with code, and a scan with no text layer.
Get it Collect from documents you already have. All five failure modes are common.
Verify
All five are present and each exercises a different extraction problem - decidefree
Why The restrictions differ sharply between listening privately, exporting a file and publishing it, and the licence you need depends on the answer.
Get it Decide before choosing voices, since a voice suitable for private listening may forbid the publication you had in mind.
Verify
A written note states the intended use and the licences chosen to permit it
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, path, kind, title, extracted_at, page_count) - blocks(id, document_id, ordinal, kind, text, page, bbox_json) · kind is heading, body, caption, footnote, table or header_footer - voices(id, name, engine, licence_id, installed_at) - licences(id, name, allows_export, allows_commercial, allows_publication, source_url, recorded_at) - renders(id, document_id, block_range, voice_id, path, at) · which voice made which file, so a licence check is possible later - positions(document_id, block_id, offset_ms, at) · where you stopped, per document - A render without a voice and licence reference is not created, because a file whose provenance is unknown cannot be checked.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
LIBRARY_DIR | required | ~/Documents/reading | Source documents. Read only; extraction never modifies them. |
RENDER_DIR | required | ~/.local/share/reader/renders | Generated audio. Every file has a voice and licence reference. |
DEFAULT_VOICE | required | local-en-medium | Must correspond to an installed voice with a licence record. |
BUFFER_SENTENCES | required | 3 | How far ahead to synthesise. More is smoother and wastes work when navigating. |
TABLE_POLICY | required | skip_with_marker | What happens to tables. Reading a table as a number stream is incomprehensible. |
CODE_POLICY | required | skip_with_marker | What happens to code blocks. The default exists because punctuation read aloud is unbearable. |
The build, in order
Reading order that makes sense aloud
A two-column PDF is read in the order a human would read it.
Positions and boxes. A text dump has already lost the ordering information.
Files
src/extract.pyThis is the whole phase. Test on a real two-column paper.
Files
src/reading_order.pytests/test_order.pyHeaders and page numbers spoken on every page make a document unlistenable.
Files
src/furniture.py
done when · tick each as it passeswatch out- A PDF has no reading order at all, only positioned glyphs, so naive extraction reads a two-column paper by crossing both columns on every line and produces confident nonsense. Detect columns from the layout, order blocks within them, and drop repeating page furniture before any of it reaches the speech engine.
Speech that survives a long document
An hour-long document plays continuously and resumes exactly where you stopped.
Not the whole document. Time to first sound is what people judge.
Files
src/player.pyResuming a long document exactly is most of the value.
Files
src/positions.pyOne bad sentence should not end an hour of listening.
Files
src/player.py
done when · tick each as it passeswatch out- Synthesising a whole document before playing means a minute of silence before anything starts and a large amount of wasted work when the user stops after two paragraphs. Synthesise a short way ahead of playback and discard the buffer when the user navigates.
Text that is not sentences
Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately.
A pure function with a fixture list of awkward cases.
Files
src/normalise.pytests/test_normalise.pyTables, code, formulae, links. Skipping with a marker beats reading nonsense.
Files
src/policy.pySo the listener knows something was there rather than that text was lost.
Files
src/player.py
done when · tick each as it passeswatch out- Passing a table to a speech engine unchanged produces a stream of numbers with no structure that is completely incomprehensible, and passing a code block produces punctuation read aloud for a minute. Decide per block kind, and defaulting to skipping with a spoken marker is better than reading nonsense.
Voices, and what their licences allow
Every voice has a recorded licence and export obeys it.
Export, commercial use, publication. Three separate questions with different answers.
Files
src/licences.pyA file whose provenance is unknown cannot be checked later.
Files
src/render.pyA silent failure teaches the user nothing and looks like a bug.
Files
src/export.pytests/test_licence.py
done when · tick each as it passeswatch out- Exporting an audio file with no record of which voice produced it makes the licence question unanswerable later, including for the user who genuinely wants to comply. Store the voice and licence on every render, and refuse to create a render that has neither.
A library worth coming back toproduct builder
Documents accumulate, resume where you left them, and export where permitted.
Reading a stale extraction aloud is confusing and hard to diagnose.
Files
src/library.pyAn offset into a changed document points somewhere arbitrary.
Files
src/positions.pySo the file itself carries the provenance the licence question needs.
Files
src/export.py
done when · tick each as it passeswatch out- Reusing a cached extraction after the source file has changed reads the old version aloud while the user has the new one open, which is confusing and hard to diagnose. Store a hash of the source and re-extract when it differs, preserving the reading position by matching text rather than by offset.
That is the whole plan for NaturalReader. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Optical character recognition for scans.
- Commercial-quality licensed voices.
- Browser and mobile reading.
- Voice cloning of any kind.
- voice catalog, OCR, document formats, mobile apps, and commercial licensing options
- frontier voice or avatar model
- licensed voice catalog
- real-time rendering fleet
- moderation, consent verification, and enterprise rights
- Word highlighting synchronised with speech, which helps a lot of readers
- Speed and pitch per document, since a paper and a novel want different settings
- A queue across documents, once positions are reliable
Need the files? The project pack on the verdict page hands your agent the whole brief · more voice ai.