Build NaturalReader

KINDAreplaces $13.9/mosaves $166.8/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimePython 3.12 with a small local applicationExtractionA layout-aware PDF extractor, plus format-specific parsersSpeechA local neural engine by default, with system voices as a fallbackLicensingA licence record per installed voice, enforced at exportPlaybackSentence-level scheduling with position tracking

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
LIBRARY_DIRrequired~/Documents/readingSource documents. Read only; extraction never modifies them.
RENDER_DIRrequired~/.local/share/reader/rendersGenerated audio. Every file has a voice and licence reference.
DEFAULT_VOICErequiredlocal-en-mediumMust correspond to an installed voice with a licence record.
BUFFER_SENTENCESrequired3How far ahead to synthesise. More is smoother and wastes work when navigating.
TABLE_POLICYrequiredskip_with_markerWhat happens to tables. Reading a table as a number stream is incomprehensible.
CODE_POLICYrequiredskip_with_markerWhat happens to code blocks. The default exists because punctuation read aloud is unbearable.

The build, in order

  1. Reading order that makes sense aloud

    A two-column PDF is read in the order a human would read it.

    1. Positions and boxes. A text dump has already lost the ordering information.

      Files src/extract.py

    2. This is the whole phase. Test on a real two-column paper.

      Files src/reading_order.pytests/test_order.py

    3. Headers and page numbers spoken on every page make a document unlistenable.

      Files src/furniture.py

    done when · tick each as it passes
    watch 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.
  2. Speech that survives a long document

    An hour-long document plays continuously and resumes exactly where you stopped.

    1. Not the whole document. Time to first sound is what people judge.

      Files src/player.py

    2. Resuming a long document exactly is most of the value.

      Files src/positions.py

    3. One bad sentence should not end an hour of listening.

      Files src/player.py

    done when · tick each as it passes
    watch 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.
  3. Text that is not sentences

    Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately.

    1. A pure function with a fixture list of awkward cases.

      Files src/normalise.pytests/test_normalise.py

    2. Tables, code, formulae, links. Skipping with a marker beats reading nonsense.

      Files src/policy.py

    3. So the listener knows something was there rather than that text was lost.

      Files src/player.py

    done when · tick each as it passes
    watch 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.
  4. Voices, and what their licences allow

    Every voice has a recorded licence and export obeys it.

    1. Export, commercial use, publication. Three separate questions with different answers.

      Files src/licences.py

    2. A file whose provenance is unknown cannot be checked later.

      Files src/render.py

    3. A silent failure teaches the user nothing and looks like a bug.

      Files src/export.pytests/test_licence.py

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more voice ai.