Vibecode NaturalReader
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NaturalReader, read user-provided text and documents aloud with a small set of licensed or local voices. The hard boundary is voice catalog, ocr, document formats, mobile apps, and commercial licensing options, plus models, compute, rights, and safety operations.
You are building a lean indie version of NaturalReader. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # NaturalReader · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a small local application | the work is document parsing and orchestration | | Extraction | A layout-aware PDF extractor, plus format-specific parsers | a PDF stores positioned glyphs, not a reading order | | Speech | A local neural engine by default, with system voices as a fallback | text going to a service is a decision, and documents can be private | | Licensing | A licence record per installed voice, enforced at export | the restriction is on the generated audio, not only on the software | | Playback | Sentence-level scheduling with position tracking | resuming exactly where you stopped is what makes long documents usable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **A layout-aware PDF library** · free - 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 - [ ] **A local speech engine, and its licence** · free 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 - [ ] **Five awkward documents** · free - 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 - [ ] **What you will do with the audio** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for NaturalReader, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · NaturalReader The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a reader that speaks documents aloud in the right order with a voice you are licensed to use to replace NaturalReader. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Neural voices are licensed, not bought, and the licence usually restricts what you may do with the audio rather than only with the software. Common restrictions include no redistribution of generated audio, no commercial use, no use in a published work such as an audiobook or a video, and no using the output to train another model. So record a licence against every voice you install, record which voice produced every audio file, and block export where the licence forbids it. Someone using this to read articles to themselves is fine under almost any licence. The same person exporting a file and publishing it may not be, and they will not check. Making the licence a field the build enforces is the difference between a tool that respects the terms and one that quietly breaks them on the user's behalf. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a small local application · the work is document parsing and orchestration - Extraction: A layout-aware PDF extractor, plus format-specific parsers · a PDF stores positioned glyphs, not a reading order - Speech: A local neural engine by default, with system voices as a fallback · text going to a service is a decision, and documents can be private - Licensing: A licence record per installed voice, enforced at export · the restriction is on the generated audio, not only on the software - Playback: Sentence-level scheduling with position tracking · resuming exactly where you stopped is what makes long documents usable ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading order that makes sense aloud Build: Extract text with layout information and reconstruct a reading order: columns in sequence, headings before their body, captions and footnotes separated, and running headers and page numbers excluded entirely. Trap: 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. Done when: A two-column academic PDF reads in the correct order, verified by listening to a full page. Running headers and page numbers are excluded rather than spoken every page. Footnotes are separated and offered rather than interrupting a sentence. A scanned PDF with no text layer is reported as such rather than read as silence. Do not build yet: No speech, voices or licensing. ### Phase 2 · Speech that survives a long document Build: Synthesise sentence by sentence with a small buffer ahead of playback. Track position at sentence granularity and persist it. Handle a sentence that fails to synthesise without ending the session. Trap: 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. Done when: Playback starts within a second of pressing play on a long document. Position persists and resumes exactly, tested by quitting mid-sentence. A sentence that fails to synthesise is skipped with a report rather than ending playback. Navigating discards the buffer rather than playing stale audio. Do not build yet: No voices, licensing or export yet. ### Phase 3 · Text that is not sentences Build: Normalise numbers, dates, currencies and abbreviations for speech. Decide explicitly what happens with tables, code blocks, formulae and links: read, summarise or skip, per kind and configurable. Trap: 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. Done when: Numbers, dates and currencies are spoken in a natural form, tested against a list of awkward cases. A table is handled by the configured policy rather than read as a number stream. A code block is skipped with a spoken marker by default. An abbreviation ambiguous between a word and letters is handled consistently. Do not build yet: No voices or licensing yet. ### Phase 4 · Voices, and what their licences allow Build: Install voices with a recorded licence covering export, commercial use and publication. Record the voice on every render. Block export where the licence forbids it, naming the restriction rather than failing silently. Trap: 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. Done when: Every installed voice has a licence record with export, commercial and publication flags. A render without a voice reference cannot be created, verified by attempting it. Export is blocked for a voice whose licence forbids it, with the specific restriction named. Changing a licence record re-evaluates what may be exported. Do not build yet: Export and library come last. ### Phase 5 · A library worth coming back to Build: A library with progress per document, search across extracted text, and export to audio files where the licence allows. Handle a document changing on disk after extraction. Trap: 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. Done when: A changed source file is detected and re-extracted, with the reading position preserved by matching text. Search across extracted documents finds a phrase and plays from it. Export produces audio files only where permitted and names the voice in the metadata. Deleting a document removes its blocks, renders and position. Do not build yet: Nothing after this. ### Out of scope (and why) - Optical character recognition for scanned documents, which is a separate tool with its own accuracy questions. - Commercial-quality voices, which are the product being licensed and cannot be reproduced. - Browser and mobile reading, which is how most people actually use a tool like this. - Voice cloning of any kind, which raises consent questions well beyond a reading tool. ### README must contain - That a PDF has no reading order and how columns and page furniture are handled - That every voice carries a licence record and export is blocked where it forbids - That every render stores which voice produced it, so provenance is answerable - That tables and code follow a configured policy rather than being read as text - That a changed source file is re-extracted and the reading position matched by text ===== AGENTS.md ===== # Agent instructions · NaturalReader indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a small local application, A layout-aware PDF extractor, plus format-specific parsers, A local neural engine by default, with system voices as a fallback, A licence record per installed voice, enforced at export, Sentence-level scheduling with position tracking. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · NaturalReader 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading order that makes sense aloud A two-column PDF is read in the order a human would read it. ### Steps 1. Extract with layout, not as a text dump Positions and boxes. A text dump has already lost the ordering information. Files: `src/extract.py` 2. Detect columns and order blocks This is the whole phase. Test on a real two-column paper. Files: `src/reading_order.py`, `tests/test_order.py` 3. Drop repeating page furniture Headers and page numbers spoken on every page make a document unlistenable. Files: `src/furniture.py` ### Done when - [ ] A two-column PDF reads in the correct order - [ ] Running headers and page numbers are excluded - [ ] Footnotes are separated rather than interrupting - [ ] A scanned PDF is reported as having no text layer ### 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. ## Phase 2 · Speech that survives a long document An hour-long document plays continuously and resumes exactly where you stopped. ### Steps 1. Buffer a short way ahead Not the whole document. Time to first sound is what people judge. Files: `src/player.py` 2. Persist position per document Resuming a long document exactly is most of the value. Files: `src/positions.py` 3. Handle synthesis failures gracefully One bad sentence should not end an hour of listening. Files: `src/player.py` ### Done when - [ ] Playback starts within a second - [ ] Position resumes exactly after quitting - [ ] A failed sentence is skipped with a report - [ ] Navigating discards stale buffered audio ### 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. ## Phase 3 · Text that is not sentences Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately. ### Steps 1. Normalise numbers and abbreviations A pure function with a fixture list of awkward cases. Files: `src/normalise.py`, `tests/test_normalise.py` 2. Decide a policy per block kind Tables, code, formulae, links. Skipping with a marker beats reading nonsense. Files: `src/policy.py` 3. Speak a marker when skipping So the listener knows something was there rather than that text was lost. Files: `src/player.py` ### Done when - [ ] Numbers, dates and currencies are spoken naturally - [ ] Tables follow the configured policy - [ ] Code blocks are skipped with a marker - [ ] Ambiguous abbreviations are handled consistently ### 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. ## Phase 4 · Voices, and what their licences allow Every voice has a recorded licence and export obeys it. ### Steps 1. Record a licence per voice Export, commercial use, publication. Three separate questions with different answers. Files: `src/licences.py` 2. Stamp the voice on every render A file whose provenance is unknown cannot be checked later. Files: `src/render.py` 3. Block export by licence, naming the restriction A silent failure teaches the user nothing and looks like a bug. Files: `src/export.py`, `tests/test_licence.py` ### Done when - [ ] Every voice has a licence record with three flags - [ ] A render without a voice reference cannot be created - [ ] Export is blocked with the restriction named - [ ] Changing a licence re-evaluates export permission ### 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. ## Not in this build - Optical character recognition for scans. - Commercial-quality licensed voices. - Browser and mobile reading. - Voice cloning of any kind. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Source documents. Read only; extraction never modifies them. LIBRARY_DIR=~/Documents/reading # Required. Generated audio. Every file has a voice and licence reference. RENDER_DIR=~/.local/share/reader/renders # Required. Must correspond to an installed voice with a licence record. DEFAULT_VOICE=local-en-medium # Required. How far ahead to synthesise. More is smoother and wastes work when navigating. BUFFER_SENTENCES=3 # Required. What happens to tables. Reading a table as a number stream is incomprehensible. TABLE_POLICY=skip_with_marker # Required. What happens to code blocks. The default exists because punctuation read aloud is unbearable. CODE_POLICY=skip_with_marker
You are building a lean indie version of NaturalReader. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # NaturalReader · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a small local application | the work is document parsing and orchestration | | Extraction | A layout-aware PDF extractor, plus format-specific parsers | a PDF stores positioned glyphs, not a reading order | | Speech | A local neural engine by default, with system voices as a fallback | text going to a service is a decision, and documents can be private | | Licensing | A licence record per installed voice, enforced at export | the restriction is on the generated audio, not only on the software | | Playback | Sentence-level scheduling with position tracking | resuming exactly where you stopped is what makes long documents usable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **A layout-aware PDF library** · free - 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 - [ ] **A local speech engine, and its licence** · free 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 - [ ] **Five awkward documents** · free - 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 - [ ] **What you will do with the audio** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for NaturalReader, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · NaturalReader The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a reader that speaks documents aloud in the right order with a voice you are licensed to use to replace NaturalReader. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Neural voices are licensed, not bought, and the licence usually restricts what you may do with the audio rather than only with the software. Common restrictions include no redistribution of generated audio, no commercial use, no use in a published work such as an audiobook or a video, and no using the output to train another model. So record a licence against every voice you install, record which voice produced every audio file, and block export where the licence forbids it. Someone using this to read articles to themselves is fine under almost any licence. The same person exporting a file and publishing it may not be, and they will not check. Making the licence a field the build enforces is the difference between a tool that respects the terms and one that quietly breaks them on the user's behalf. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a small local application · the work is document parsing and orchestration - Extraction: A layout-aware PDF extractor, plus format-specific parsers · a PDF stores positioned glyphs, not a reading order - Speech: A local neural engine by default, with system voices as a fallback · text going to a service is a decision, and documents can be private - Licensing: A licence record per installed voice, enforced at export · the restriction is on the generated audio, not only on the software - Playback: Sentence-level scheduling with position tracking · resuming exactly where you stopped is what makes long documents usable ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading order that makes sense aloud Build: Extract text with layout information and reconstruct a reading order: columns in sequence, headings before their body, captions and footnotes separated, and running headers and page numbers excluded entirely. Trap: 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. Done when: A two-column academic PDF reads in the correct order, verified by listening to a full page. Running headers and page numbers are excluded rather than spoken every page. Footnotes are separated and offered rather than interrupting a sentence. A scanned PDF with no text layer is reported as such rather than read as silence. Do not build yet: No speech, voices or licensing. ### Phase 2 · Speech that survives a long document Build: Synthesise sentence by sentence with a small buffer ahead of playback. Track position at sentence granularity and persist it. Handle a sentence that fails to synthesise without ending the session. Trap: 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. Done when: Playback starts within a second of pressing play on a long document. Position persists and resumes exactly, tested by quitting mid-sentence. A sentence that fails to synthesise is skipped with a report rather than ending playback. Navigating discards the buffer rather than playing stale audio. Do not build yet: No voices, licensing or export yet. ### Phase 3 · Text that is not sentences Build: Normalise numbers, dates, currencies and abbreviations for speech. Decide explicitly what happens with tables, code blocks, formulae and links: read, summarise or skip, per kind and configurable. Trap: 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. Done when: Numbers, dates and currencies are spoken in a natural form, tested against a list of awkward cases. A table is handled by the configured policy rather than read as a number stream. A code block is skipped with a spoken marker by default. An abbreviation ambiguous between a word and letters is handled consistently. Do not build yet: No voices or licensing yet. ### Phase 4 · Voices, and what their licences allow Build: Install voices with a recorded licence covering export, commercial use and publication. Record the voice on every render. Block export where the licence forbids it, naming the restriction rather than failing silently. Trap: 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. Done when: Every installed voice has a licence record with export, commercial and publication flags. A render without a voice reference cannot be created, verified by attempting it. Export is blocked for a voice whose licence forbids it, with the specific restriction named. Changing a licence record re-evaluates what may be exported. Do not build yet: Export and library come last. ### Phase 5 · A library worth coming back to Build: A library with progress per document, search across extracted text, and export to audio files where the licence allows. Handle a document changing on disk after extraction. Trap: 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. Done when: A changed source file is detected and re-extracted, with the reading position preserved by matching text. Search across extracted documents finds a phrase and plays from it. Export produces audio files only where permitted and names the voice in the metadata. Deleting a document removes its blocks, renders and position. Do not build yet: Nothing after this. ### Out of scope (and why) - Optical character recognition for scanned documents, which is a separate tool with its own accuracy questions. - Commercial-quality voices, which are the product being licensed and cannot be reproduced. - Browser and mobile reading, which is how most people actually use a tool like this. - Voice cloning of any kind, which raises consent questions well beyond a reading tool. ### README must contain - That a PDF has no reading order and how columns and page furniture are handled - That every voice carries a licence record and export is blocked where it forbids - That every render stores which voice produced it, so provenance is answerable - That tables and code follow a configured policy rather than being read as text - That a changed source file is re-extracted and the reading position matched by text ===== AGENTS.md ===== # Agent instructions · NaturalReader indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a small local application, A layout-aware PDF extractor, plus format-specific parsers, A local neural engine by default, with system voices as a fallback, A licence record per installed voice, enforced at export, Sentence-level scheduling with position tracking. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · NaturalReader 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading order that makes sense aloud A two-column PDF is read in the order a human would read it. ### Steps 1. Extract with layout, not as a text dump Positions and boxes. A text dump has already lost the ordering information. Files: `src/extract.py` 2. Detect columns and order blocks This is the whole phase. Test on a real two-column paper. Files: `src/reading_order.py`, `tests/test_order.py` 3. Drop repeating page furniture Headers and page numbers spoken on every page make a document unlistenable. Files: `src/furniture.py` ### Done when - [ ] A two-column PDF reads in the correct order - [ ] Running headers and page numbers are excluded - [ ] Footnotes are separated rather than interrupting - [ ] A scanned PDF is reported as having no text layer ### 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. ## Phase 2 · Speech that survives a long document An hour-long document plays continuously and resumes exactly where you stopped. ### Steps 1. Buffer a short way ahead Not the whole document. Time to first sound is what people judge. Files: `src/player.py` 2. Persist position per document Resuming a long document exactly is most of the value. Files: `src/positions.py` 3. Handle synthesis failures gracefully One bad sentence should not end an hour of listening. Files: `src/player.py` ### Done when - [ ] Playback starts within a second - [ ] Position resumes exactly after quitting - [ ] A failed sentence is skipped with a report - [ ] Navigating discards stale buffered audio ### 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. ## Phase 3 · Text that is not sentences Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately. ### Steps 1. Normalise numbers and abbreviations A pure function with a fixture list of awkward cases. Files: `src/normalise.py`, `tests/test_normalise.py` 2. Decide a policy per block kind Tables, code, formulae, links. Skipping with a marker beats reading nonsense. Files: `src/policy.py` 3. Speak a marker when skipping So the listener knows something was there rather than that text was lost. Files: `src/player.py` ### Done when - [ ] Numbers, dates and currencies are spoken naturally - [ ] Tables follow the configured policy - [ ] Code blocks are skipped with a marker - [ ] Ambiguous abbreviations are handled consistently ### 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. ## Phase 4 · Voices, and what their licences allow Every voice has a recorded licence and export obeys it. ### Steps 1. Record a licence per voice Export, commercial use, publication. Three separate questions with different answers. Files: `src/licences.py` 2. Stamp the voice on every render A file whose provenance is unknown cannot be checked later. Files: `src/render.py` 3. Block export by licence, naming the restriction A silent failure teaches the user nothing and looks like a bug. Files: `src/export.py`, `tests/test_licence.py` ### Done when - [ ] Every voice has a licence record with three flags - [ ] A render without a voice reference cannot be created - [ ] Export is blocked with the restriction named - [ ] Changing a licence re-evaluates export permission ### 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. ## Not in this build - Optical character recognition for scans. - Commercial-quality licensed voices. - Browser and mobile reading. - Voice cloning of any kind. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Source documents. Read only; extraction never modifies them. LIBRARY_DIR=~/Documents/reading # Required. Generated audio. Every file has a voice and licence reference. RENDER_DIR=~/.local/share/reader/renders # Required. Must correspond to an installed voice with a licence record. DEFAULT_VOICE=local-en-medium # Required. How far ahead to synthesise. More is smoother and wastes work when navigating. BUFFER_SENTENCES=3 # Required. What happens to tables. Reading a table as a number stream is incomprehensible. TABLE_POLICY=skip_with_marker # Required. What happens to code blocks. The default exists because punctuation read aloud is unbearable. CODE_POLICY=skip_with_marker
You are building a production product version of NaturalReader. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # NaturalReader · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NaturalReader, read user-provided text and documents aloud with a small set of licensed or local voices. The hard boundary is voice catalog, ocr, document formats, mobile apps, and commercial licensing options, plus models, compute, rights, and safety operations. ## Product outcome A reader that gets the reading order right in documents never designed to be read aloud, and knows which voice made every file and what that voice's licence permits. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local TTS model - ffmpeg - GPU recommended - voices the user has rights and consent to use ## Explicit non-goals for v1 - 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 ## Success criteria - A two-column PDF is confirmed to read in the correct order - Page furniture is confirmed excluded - A render without a voice reference is confirmed impossible - Export is confirmed blocked with the restriction named - A changed source is confirmed re-extracted with position preserved ===== BRIEF.md ===== # Build brief · NaturalReader The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a reader that speaks documents aloud in the right order with a voice you are licensed to use to replace NaturalReader. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Neural voices are licensed, not bought, and the licence usually restricts what you may do with the audio rather than only with the software. Common restrictions include no redistribution of generated audio, no commercial use, no use in a published work such as an audiobook or a video, and no using the output to train another model. So record a licence against every voice you install, record which voice produced every audio file, and block export where the licence forbids it. Someone using this to read articles to themselves is fine under almost any licence. The same person exporting a file and publishing it may not be, and they will not check. Making the licence a field the build enforces is the difference between a tool that respects the terms and one that quietly breaks them on the user's behalf. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a small local application · the work is document parsing and orchestration - Extraction: A layout-aware PDF extractor, plus format-specific parsers · a PDF stores positioned glyphs, not a reading order - Speech: A local neural engine by default, with system voices as a fallback · text going to a service is a decision, and documents can be private - Licensing: A licence record per installed voice, enforced at export · the restriction is on the generated audio, not only on the software - Playback: Sentence-level scheduling with position tracking · resuming exactly where you stopped is what makes long documents usable ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading order that makes sense aloud Build: Extract text with layout information and reconstruct a reading order: columns in sequence, headings before their body, captions and footnotes separated, and running headers and page numbers excluded entirely. Trap: 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. Done when: A two-column academic PDF reads in the correct order, verified by listening to a full page. Running headers and page numbers are excluded rather than spoken every page. Footnotes are separated and offered rather than interrupting a sentence. A scanned PDF with no text layer is reported as such rather than read as silence. Do not build yet: No speech, voices or licensing. ### Phase 2 · Speech that survives a long document Build: Synthesise sentence by sentence with a small buffer ahead of playback. Track position at sentence granularity and persist it. Handle a sentence that fails to synthesise without ending the session. Trap: 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. Done when: Playback starts within a second of pressing play on a long document. Position persists and resumes exactly, tested by quitting mid-sentence. A sentence that fails to synthesise is skipped with a report rather than ending playback. Navigating discards the buffer rather than playing stale audio. Do not build yet: No voices, licensing or export yet. ### Phase 3 · Text that is not sentences Build: Normalise numbers, dates, currencies and abbreviations for speech. Decide explicitly what happens with tables, code blocks, formulae and links: read, summarise or skip, per kind and configurable. Trap: 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. Done when: Numbers, dates and currencies are spoken in a natural form, tested against a list of awkward cases. A table is handled by the configured policy rather than read as a number stream. A code block is skipped with a spoken marker by default. An abbreviation ambiguous between a word and letters is handled consistently. Do not build yet: No voices or licensing yet. ### Phase 4 · Voices, and what their licences allow Build: Install voices with a recorded licence covering export, commercial use and publication. Record the voice on every render. Block export where the licence forbids it, naming the restriction rather than failing silently. Trap: 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. Done when: Every installed voice has a licence record with export, commercial and publication flags. A render without a voice reference cannot be created, verified by attempting it. Export is blocked for a voice whose licence forbids it, with the specific restriction named. Changing a licence record re-evaluates what may be exported. Do not build yet: Export and library come last. ### Phase 5 · A library worth coming back to Build: A library with progress per document, search across extracted text, and export to audio files where the licence allows. Handle a document changing on disk after extraction. Trap: 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. Done when: A changed source file is detected and re-extracted, with the reading position preserved by matching text. Search across extracted documents finds a phrase and plays from it. Export produces audio files only where permitted and names the voice in the metadata. Deleting a document removes its blocks, renders and position. Do not build yet: Nothing after this. ### Out of scope (and why) - Optical character recognition for scanned documents, which is a separate tool with its own accuracy questions. - Commercial-quality voices, which are the product being licensed and cannot be reproduced. - Browser and mobile reading, which is how most people actually use a tool like this. - Voice cloning of any kind, which raises consent questions well beyond a reading tool. ### README must contain - That a PDF has no reading order and how columns and page furniture are handled - That every voice carries a licence record and export is blocked where it forbids - That every render stores which voice produced it, so provenance is answerable - That tables and code follow a configured policy rather than being read as text - That a changed source file is re-extracted and the reading position matched by text ===== ARCHITECTURE.md ===== # Architecture · NaturalReader ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a small local application | the work is document parsing and orchestration | | Extraction | A layout-aware PDF extractor, plus format-specific parsers | a PDF stores positioned glyphs, not a reading order | | Speech | A local neural engine by default, with system voices as a fallback | text going to a service is a decision, and documents can be private | | Licensing | A licence record per installed voice, enforced at export | the restriction is on the generated audio, not only on the software | | Playback | Sentence-level scheduling with position tracking | resuming exactly where you stopped is what makes long documents usable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extraction | Layout-aware text and reading order. | Any library exposing coordinates. A plain text extractor cannot do this. | | Normalisation | Numbers, abbreviations and block policies. | Any implementation, provided policies are per block kind rather than global. | | Playback | Buffered synthesis and position tracking. | Any engine, provided synthesis stays ahead of playback rather than preceding it entirely. | | Licensing | Voice licences and export enforcement. | Not swappable for a note in the README. It is enforced at the export path. | | Library | Source hashing, re-extraction and position matching. | Any store, provided a changed source is detected rather than served stale. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DIR` · required · Source documents. Read only; extraction never modifies them. - `RENDER_DIR` · required · Generated audio. Every file has a voice and licence reference. - `DEFAULT_VOICE` · required · Must correspond to an installed voice with a licence record. - `BUFFER_SENTENCES` · required · How far ahead to synthesise. More is smoother and wastes work when navigating. - `TABLE_POLICY` · required · What happens to tables. Reading a table as a number stream is incomprehensible. - `CODE_POLICY` · required · What happens to code blocks. The default exists because punctuation read aloud is unbearable. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · NaturalReader product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with a small local application, A layout-aware PDF extractor, plus format-specific parsers, A local neural engine by default, with system voices as a fallback, A licence record per installed voice, enforced at export, Sentence-level scheduling with position tracking. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · NaturalReader Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading order that makes sense aloud A two-column PDF is read in the order a human would read it. ### Steps 1. Extract with layout, not as a text dump Positions and boxes. A text dump has already lost the ordering information. Files: `src/extract.py` 2. Detect columns and order blocks This is the whole phase. Test on a real two-column paper. Files: `src/reading_order.py`, `tests/test_order.py` 3. Drop repeating page furniture Headers and page numbers spoken on every page make a document unlistenable. Files: `src/furniture.py` ### Done when - [ ] A two-column PDF reads in the correct order - [ ] Running headers and page numbers are excluded - [ ] Footnotes are separated rather than interrupting - [ ] A scanned PDF is reported as having no text layer ### 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. ## M2 · Speech that survives a long document An hour-long document plays continuously and resumes exactly where you stopped. ### Steps 1. Buffer a short way ahead Not the whole document. Time to first sound is what people judge. Files: `src/player.py` 2. Persist position per document Resuming a long document exactly is most of the value. Files: `src/positions.py` 3. Handle synthesis failures gracefully One bad sentence should not end an hour of listening. Files: `src/player.py` ### Done when - [ ] Playback starts within a second - [ ] Position resumes exactly after quitting - [ ] A failed sentence is skipped with a report - [ ] Navigating discards stale buffered audio ### 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. ## M3 · Text that is not sentences Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately. ### Steps 1. Normalise numbers and abbreviations A pure function with a fixture list of awkward cases. Files: `src/normalise.py`, `tests/test_normalise.py` 2. Decide a policy per block kind Tables, code, formulae, links. Skipping with a marker beats reading nonsense. Files: `src/policy.py` 3. Speak a marker when skipping So the listener knows something was there rather than that text was lost. Files: `src/player.py` ### Done when - [ ] Numbers, dates and currencies are spoken naturally - [ ] Tables follow the configured policy - [ ] Code blocks are skipped with a marker - [ ] Ambiguous abbreviations are handled consistently ### 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. ## M4 · Voices, and what their licences allow Every voice has a recorded licence and export obeys it. ### Steps 1. Record a licence per voice Export, commercial use, publication. Three separate questions with different answers. Files: `src/licences.py` 2. Stamp the voice on every render A file whose provenance is unknown cannot be checked later. Files: `src/render.py` 3. Block export by licence, naming the restriction A silent failure teaches the user nothing and looks like a bug. Files: `src/export.py`, `tests/test_licence.py` ### Done when - [ ] Every voice has a licence record with three flags - [ ] A render without a voice reference cannot be created - [ ] Export is blocked with the restriction named - [ ] Changing a licence re-evaluates export permission ### 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. ## M5 · A library worth coming back to (production only) Documents accumulate, resume where you left them, and export where permitted. ### Steps 1. Hash sources and re-extract on change Reading a stale extraction aloud is confusing and hard to diagnose. Files: `src/library.py` 2. Preserve position by matching text An offset into a changed document points somewhere arbitrary. Files: `src/positions.py` 3. Export with voice metadata So the file itself carries the provenance the licence question needs. Files: `src/export.py` ### Done when - [ ] A changed source is re-extracted with position preserved - [ ] Search finds a phrase and plays from it - [ ] Export names the voice in file metadata - [ ] Deleting removes blocks, renders and position ### Watch 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. ===== OPERATIONS.md ===== # Operations · NaturalReader ## Backup Source documents belong to you already. Renders are regenerable and large, so back up the library metadata and positions rather than the audio. ## Restore Restore and confirm voice and licence records survived, since a render whose licence reference is lost cannot be checked and should be treated as unexportable. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool, the check that matters is that no render exists without a voice reference, which is an invariant worth asserting in the suite rather than watching at runtime. ## Incident checklist Audio exported under a licence that forbade it is the failure this build exists to prevent, and the recovery is the render table, which says exactly which files came from which voice. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A two-column PDF is confirmed to read in the correct order - [ ] Page furniture is confirmed excluded - [ ] A render without a voice reference is confirmed impossible - [ ] Export is confirmed blocked with the restriction named - [ ] A changed source is confirmed re-extracted with position preserved ## Launch constraint Do not market omitted NaturalReader capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Source documents. Read only; extraction never modifies them. LIBRARY_DIR=~/Documents/reading # Required. Generated audio. Every file has a voice and licence reference. RENDER_DIR=~/.local/share/reader/renders # Required. Must correspond to an installed voice with a licence record. DEFAULT_VOICE=local-en-medium # Required. How far ahead to synthesise. More is smoother and wastes work when navigating. BUFFER_SENTENCES=3 # Required. What happens to tables. Reading a table as a number stream is incomprehensible. TABLE_POLICY=skip_with_marker # Required. What happens to code blocks. The default exists because punctuation read aloud is unbearable. CODE_POLICY=skip_with_marker
# NaturalReader · indie build 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: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a small local application | the work is document parsing and orchestration | | Extraction | A layout-aware PDF extractor, plus format-specific parsers | a PDF stores positioned glyphs, not a reading order | | Speech | A local neural engine by default, with system voices as a fallback | text going to a service is a decision, and documents can be private | | Licensing | A licence record per installed voice, enforced at export | the restriction is on the generated audio, not only on the software | | Playback | Sentence-level scheduling with position tracking | resuming exactly where you stopped is what makes long documents usable | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - 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 - [ ] **A layout-aware PDF library** · free - 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 - [ ] **A local speech engine, and its licence** · free 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 - [ ] **Five awkward documents** · free - 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 - [ ] **What you will do with the audio** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for NaturalReader, and the README should say so rather than pretend.
# Build brief · NaturalReader The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a reader that speaks documents aloud in the right order with a voice you are licensed to use to replace NaturalReader. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Neural voices are licensed, not bought, and the licence usually restricts what you may do with the audio rather than only with the software. Common restrictions include no redistribution of generated audio, no commercial use, no use in a published work such as an audiobook or a video, and no using the output to train another model. So record a licence against every voice you install, record which voice produced every audio file, and block export where the licence forbids it. Someone using this to read articles to themselves is fine under almost any licence. The same person exporting a file and publishing it may not be, and they will not check. Making the licence a field the build enforces is the difference between a tool that respects the terms and one that quietly breaks them on the user's behalf. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a small local application · the work is document parsing and orchestration - Extraction: A layout-aware PDF extractor, plus format-specific parsers · a PDF stores positioned glyphs, not a reading order - Speech: A local neural engine by default, with system voices as a fallback · text going to a service is a decision, and documents can be private - Licensing: A licence record per installed voice, enforced at export · the restriction is on the generated audio, not only on the software - Playback: Sentence-level scheduling with position tracking · resuming exactly where you stopped is what makes long documents usable ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading order that makes sense aloud Build: Extract text with layout information and reconstruct a reading order: columns in sequence, headings before their body, captions and footnotes separated, and running headers and page numbers excluded entirely. Trap: 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. Done when: A two-column academic PDF reads in the correct order, verified by listening to a full page. Running headers and page numbers are excluded rather than spoken every page. Footnotes are separated and offered rather than interrupting a sentence. A scanned PDF with no text layer is reported as such rather than read as silence. Do not build yet: No speech, voices or licensing. ### Phase 2 · Speech that survives a long document Build: Synthesise sentence by sentence with a small buffer ahead of playback. Track position at sentence granularity and persist it. Handle a sentence that fails to synthesise without ending the session. Trap: 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. Done when: Playback starts within a second of pressing play on a long document. Position persists and resumes exactly, tested by quitting mid-sentence. A sentence that fails to synthesise is skipped with a report rather than ending playback. Navigating discards the buffer rather than playing stale audio. Do not build yet: No voices, licensing or export yet. ### Phase 3 · Text that is not sentences Build: Normalise numbers, dates, currencies and abbreviations for speech. Decide explicitly what happens with tables, code blocks, formulae and links: read, summarise or skip, per kind and configurable. Trap: 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. Done when: Numbers, dates and currencies are spoken in a natural form, tested against a list of awkward cases. A table is handled by the configured policy rather than read as a number stream. A code block is skipped with a spoken marker by default. An abbreviation ambiguous between a word and letters is handled consistently. Do not build yet: No voices or licensing yet. ### Phase 4 · Voices, and what their licences allow Build: Install voices with a recorded licence covering export, commercial use and publication. Record the voice on every render. Block export where the licence forbids it, naming the restriction rather than failing silently. Trap: 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. Done when: Every installed voice has a licence record with export, commercial and publication flags. A render without a voice reference cannot be created, verified by attempting it. Export is blocked for a voice whose licence forbids it, with the specific restriction named. Changing a licence record re-evaluates what may be exported. Do not build yet: Export and library come last. ### Phase 5 · A library worth coming back to Build: A library with progress per document, search across extracted text, and export to audio files where the licence allows. Handle a document changing on disk after extraction. Trap: 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. Done when: A changed source file is detected and re-extracted, with the reading position preserved by matching text. Search across extracted documents finds a phrase and plays from it. Export produces audio files only where permitted and names the voice in the metadata. Deleting a document removes its blocks, renders and position. Do not build yet: Nothing after this. ### Out of scope (and why) - Optical character recognition for scanned documents, which is a separate tool with its own accuracy questions. - Commercial-quality voices, which are the product being licensed and cannot be reproduced. - Browser and mobile reading, which is how most people actually use a tool like this. - Voice cloning of any kind, which raises consent questions well beyond a reading tool. ### README must contain - That a PDF has no reading order and how columns and page furniture are handled - That every voice carries a licence record and export is blocked where it forbids - That every render stores which voice produced it, so provenance is answerable - That tables and code follow a configured policy rather than being read as text - That a changed source file is re-extracted and the reading position matched by text
# Agent instructions · NaturalReader indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a small local application, A layout-aware PDF extractor, plus format-specific parsers, A local neural engine by default, with system voices as a fallback, A licence record per installed voice, enforced at export, Sentence-level scheduling with position tracking. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · NaturalReader 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading order that makes sense aloud A two-column PDF is read in the order a human would read it. ### Steps 1. Extract with layout, not as a text dump Positions and boxes. A text dump has already lost the ordering information. Files: `src/extract.py` 2. Detect columns and order blocks This is the whole phase. Test on a real two-column paper. Files: `src/reading_order.py`, `tests/test_order.py` 3. Drop repeating page furniture Headers and page numbers spoken on every page make a document unlistenable. Files: `src/furniture.py` ### Done when - [ ] A two-column PDF reads in the correct order - [ ] Running headers and page numbers are excluded - [ ] Footnotes are separated rather than interrupting - [ ] A scanned PDF is reported as having no text layer ### 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. ## Phase 2 · Speech that survives a long document An hour-long document plays continuously and resumes exactly where you stopped. ### Steps 1. Buffer a short way ahead Not the whole document. Time to first sound is what people judge. Files: `src/player.py` 2. Persist position per document Resuming a long document exactly is most of the value. Files: `src/positions.py` 3. Handle synthesis failures gracefully One bad sentence should not end an hour of listening. Files: `src/player.py` ### Done when - [ ] Playback starts within a second - [ ] Position resumes exactly after quitting - [ ] A failed sentence is skipped with a report - [ ] Navigating discards stale buffered audio ### 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. ## Phase 3 · Text that is not sentences Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately. ### Steps 1. Normalise numbers and abbreviations A pure function with a fixture list of awkward cases. Files: `src/normalise.py`, `tests/test_normalise.py` 2. Decide a policy per block kind Tables, code, formulae, links. Skipping with a marker beats reading nonsense. Files: `src/policy.py` 3. Speak a marker when skipping So the listener knows something was there rather than that text was lost. Files: `src/player.py` ### Done when - [ ] Numbers, dates and currencies are spoken naturally - [ ] Tables follow the configured policy - [ ] Code blocks are skipped with a marker - [ ] Ambiguous abbreviations are handled consistently ### 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. ## Phase 4 · Voices, and what their licences allow Every voice has a recorded licence and export obeys it. ### Steps 1. Record a licence per voice Export, commercial use, publication. Three separate questions with different answers. Files: `src/licences.py` 2. Stamp the voice on every render A file whose provenance is unknown cannot be checked later. Files: `src/render.py` 3. Block export by licence, naming the restriction A silent failure teaches the user nothing and looks like a bug. Files: `src/export.py`, `tests/test_licence.py` ### Done when - [ ] Every voice has a licence record with three flags - [ ] A render without a voice reference cannot be created - [ ] Export is blocked with the restriction named - [ ] Changing a licence re-evaluates export permission ### 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. ## Not in this build - Optical character recognition for scans. - Commercial-quality licensed voices. - Browser and mobile reading. - Voice cloning of any kind. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Source documents. Read only; extraction never modifies them. LIBRARY_DIR=~/Documents/reading # Required. Generated audio. Every file has a voice and licence reference. RENDER_DIR=~/.local/share/reader/renders # Required. Must correspond to an installed voice with a licence record. DEFAULT_VOICE=local-en-medium # Required. How far ahead to synthesise. More is smoother and wastes work when navigating. BUFFER_SENTENCES=3 # Required. What happens to tables. Reading a table as a number stream is incomprehensible. TABLE_POLICY=skip_with_marker # Required. What happens to code blocks. The default exists because punctuation read aloud is unbearable. CODE_POLICY=skip_with_marker
# NaturalReader · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For NaturalReader, read user-provided text and documents aloud with a small set of licensed or local voices. The hard boundary is voice catalog, ocr, document formats, mobile apps, and commercial licensing options, plus models, compute, rights, and safety operations. ## Product outcome A reader that gets the reading order right in documents never designed to be read aloud, and knows which voice made every file and what that voice's licence permits. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local TTS model - ffmpeg - GPU recommended - voices the user has rights and consent to use ## Explicit non-goals for v1 - 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 ## Success criteria - A two-column PDF is confirmed to read in the correct order - Page furniture is confirmed excluded - A render without a voice reference is confirmed impossible - Export is confirmed blocked with the restriction named - A changed source is confirmed re-extracted with position preserved
# Build brief · NaturalReader The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a reader that speaks documents aloud in the right order with a voice you are licensed to use to replace NaturalReader. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Neural voices are licensed, not bought, and the licence usually restricts what you may do with the audio rather than only with the software. Common restrictions include no redistribution of generated audio, no commercial use, no use in a published work such as an audiobook or a video, and no using the output to train another model. So record a licence against every voice you install, record which voice produced every audio file, and block export where the licence forbids it. Someone using this to read articles to themselves is fine under almost any licence. The same person exporting a file and publishing it may not be, and they will not check. Making the licence a field the build enforces is the difference between a tool that respects the terms and one that quietly breaks them on the user's behalf. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a small local application · the work is document parsing and orchestration - Extraction: A layout-aware PDF extractor, plus format-specific parsers · a PDF stores positioned glyphs, not a reading order - Speech: A local neural engine by default, with system voices as a fallback · text going to a service is a decision, and documents can be private - Licensing: A licence record per installed voice, enforced at export · the restriction is on the generated audio, not only on the software - Playback: Sentence-level scheduling with position tracking · resuming exactly where you stopped is what makes long documents usable ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading order that makes sense aloud Build: Extract text with layout information and reconstruct a reading order: columns in sequence, headings before their body, captions and footnotes separated, and running headers and page numbers excluded entirely. Trap: 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. Done when: A two-column academic PDF reads in the correct order, verified by listening to a full page. Running headers and page numbers are excluded rather than spoken every page. Footnotes are separated and offered rather than interrupting a sentence. A scanned PDF with no text layer is reported as such rather than read as silence. Do not build yet: No speech, voices or licensing. ### Phase 2 · Speech that survives a long document Build: Synthesise sentence by sentence with a small buffer ahead of playback. Track position at sentence granularity and persist it. Handle a sentence that fails to synthesise without ending the session. Trap: 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. Done when: Playback starts within a second of pressing play on a long document. Position persists and resumes exactly, tested by quitting mid-sentence. A sentence that fails to synthesise is skipped with a report rather than ending playback. Navigating discards the buffer rather than playing stale audio. Do not build yet: No voices, licensing or export yet. ### Phase 3 · Text that is not sentences Build: Normalise numbers, dates, currencies and abbreviations for speech. Decide explicitly what happens with tables, code blocks, formulae and links: read, summarise or skip, per kind and configurable. Trap: 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. Done when: Numbers, dates and currencies are spoken in a natural form, tested against a list of awkward cases. A table is handled by the configured policy rather than read as a number stream. A code block is skipped with a spoken marker by default. An abbreviation ambiguous between a word and letters is handled consistently. Do not build yet: No voices or licensing yet. ### Phase 4 · Voices, and what their licences allow Build: Install voices with a recorded licence covering export, commercial use and publication. Record the voice on every render. Block export where the licence forbids it, naming the restriction rather than failing silently. Trap: 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. Done when: Every installed voice has a licence record with export, commercial and publication flags. A render without a voice reference cannot be created, verified by attempting it. Export is blocked for a voice whose licence forbids it, with the specific restriction named. Changing a licence record re-evaluates what may be exported. Do not build yet: Export and library come last. ### Phase 5 · A library worth coming back to Build: A library with progress per document, search across extracted text, and export to audio files where the licence allows. Handle a document changing on disk after extraction. Trap: 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. Done when: A changed source file is detected and re-extracted, with the reading position preserved by matching text. Search across extracted documents finds a phrase and plays from it. Export produces audio files only where permitted and names the voice in the metadata. Deleting a document removes its blocks, renders and position. Do not build yet: Nothing after this. ### Out of scope (and why) - Optical character recognition for scanned documents, which is a separate tool with its own accuracy questions. - Commercial-quality voices, which are the product being licensed and cannot be reproduced. - Browser and mobile reading, which is how most people actually use a tool like this. - Voice cloning of any kind, which raises consent questions well beyond a reading tool. ### README must contain - That a PDF has no reading order and how columns and page furniture are handled - That every voice carries a licence record and export is blocked where it forbids - That every render stores which voice produced it, so provenance is answerable - That tables and code follow a configured policy rather than being read as text - That a changed source file is re-extracted and the reading position matched by text
# Architecture · NaturalReader ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a small local application | the work is document parsing and orchestration | | Extraction | A layout-aware PDF extractor, plus format-specific parsers | a PDF stores positioned glyphs, not a reading order | | Speech | A local neural engine by default, with system voices as a fallback | text going to a service is a decision, and documents can be private | | Licensing | A licence record per installed voice, enforced at export | the restriction is on the generated audio, not only on the software | | Playback | Sentence-level scheduling with position tracking | resuming exactly where you stopped is what makes long documents usable | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extraction | Layout-aware text and reading order. | Any library exposing coordinates. A plain text extractor cannot do this. | | Normalisation | Numbers, abbreviations and block policies. | Any implementation, provided policies are per block kind rather than global. | | Playback | Buffered synthesis and position tracking. | Any engine, provided synthesis stays ahead of playback rather than preceding it entirely. | | Licensing | Voice licences and export enforcement. | Not swappable for a note in the README. It is enforced at the export path. | | Library | Source hashing, re-extraction and position matching. | Any store, provided a changed source is detected rather than served stale. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_DIR` · required · Source documents. Read only; extraction never modifies them. - `RENDER_DIR` · required · Generated audio. Every file has a voice and licence reference. - `DEFAULT_VOICE` · required · Must correspond to an installed voice with a licence record. - `BUFFER_SENTENCES` · required · How far ahead to synthesise. More is smoother and wastes work when navigating. - `TABLE_POLICY` · required · What happens to tables. Reading a table as a number stream is incomprehensible. - `CODE_POLICY` · required · What happens to code blocks. The default exists because punctuation read aloud is unbearable. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · NaturalReader product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with a small local application, A layout-aware PDF extractor, plus format-specific parsers, A local neural engine by default, with system voices as a fallback, A licence record per installed voice, enforced at export, Sentence-level scheduling with position tracking. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · NaturalReader Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading order that makes sense aloud A two-column PDF is read in the order a human would read it. ### Steps 1. Extract with layout, not as a text dump Positions and boxes. A text dump has already lost the ordering information. Files: `src/extract.py` 2. Detect columns and order blocks This is the whole phase. Test on a real two-column paper. Files: `src/reading_order.py`, `tests/test_order.py` 3. Drop repeating page furniture Headers and page numbers spoken on every page make a document unlistenable. Files: `src/furniture.py` ### Done when - [ ] A two-column PDF reads in the correct order - [ ] Running headers and page numbers are excluded - [ ] Footnotes are separated rather than interrupting - [ ] A scanned PDF is reported as having no text layer ### 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. ## M2 · Speech that survives a long document An hour-long document plays continuously and resumes exactly where you stopped. ### Steps 1. Buffer a short way ahead Not the whole document. Time to first sound is what people judge. Files: `src/player.py` 2. Persist position per document Resuming a long document exactly is most of the value. Files: `src/positions.py` 3. Handle synthesis failures gracefully One bad sentence should not end an hour of listening. Files: `src/player.py` ### Done when - [ ] Playback starts within a second - [ ] Position resumes exactly after quitting - [ ] A failed sentence is skipped with a report - [ ] Navigating discards stale buffered audio ### 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. ## M3 · Text that is not sentences Numbers, abbreviations, tables and code are spoken sensibly or skipped deliberately. ### Steps 1. Normalise numbers and abbreviations A pure function with a fixture list of awkward cases. Files: `src/normalise.py`, `tests/test_normalise.py` 2. Decide a policy per block kind Tables, code, formulae, links. Skipping with a marker beats reading nonsense. Files: `src/policy.py` 3. Speak a marker when skipping So the listener knows something was there rather than that text was lost. Files: `src/player.py` ### Done when - [ ] Numbers, dates and currencies are spoken naturally - [ ] Tables follow the configured policy - [ ] Code blocks are skipped with a marker - [ ] Ambiguous abbreviations are handled consistently ### 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. ## M4 · Voices, and what their licences allow Every voice has a recorded licence and export obeys it. ### Steps 1. Record a licence per voice Export, commercial use, publication. Three separate questions with different answers. Files: `src/licences.py` 2. Stamp the voice on every render A file whose provenance is unknown cannot be checked later. Files: `src/render.py` 3. Block export by licence, naming the restriction A silent failure teaches the user nothing and looks like a bug. Files: `src/export.py`, `tests/test_licence.py` ### Done when - [ ] Every voice has a licence record with three flags - [ ] A render without a voice reference cannot be created - [ ] Export is blocked with the restriction named - [ ] Changing a licence re-evaluates export permission ### 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. ## M5 · A library worth coming back to (production only) Documents accumulate, resume where you left them, and export where permitted. ### Steps 1. Hash sources and re-extract on change Reading a stale extraction aloud is confusing and hard to diagnose. Files: `src/library.py` 2. Preserve position by matching text An offset into a changed document points somewhere arbitrary. Files: `src/positions.py` 3. Export with voice metadata So the file itself carries the provenance the licence question needs. Files: `src/export.py` ### Done when - [ ] A changed source is re-extracted with position preserved - [ ] Search finds a phrase and plays from it - [ ] Export names the voice in file metadata - [ ] Deleting removes blocks, renders and position ### Watch 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.
# Operations · NaturalReader ## Backup Source documents belong to you already. Renders are regenerable and large, so back up the library metadata and positions rather than the audio. ## Restore Restore and confirm voice and licence records survived, since a render whose licence reference is lost cannot be checked and should be treated as unexportable. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool, the check that matters is that no render exists without a voice reference, which is an invariant worth asserting in the suite rather than watching at runtime. ## Incident checklist Audio exported under a licence that forbade it is the failure this build exists to prevent, and the recovery is the render table, which says exactly which files came from which voice. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A two-column PDF is confirmed to read in the correct order - [ ] Page furniture is confirmed excluded - [ ] A render without a voice reference is confirmed impossible - [ ] Export is confirmed blocked with the restriction named - [ ] A changed source is confirmed re-extracted with position preserved ## Launch constraint Do not market omitted NaturalReader capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Source documents. Read only; extraction never modifies them. LIBRARY_DIR=~/Documents/reading # Required. Generated audio. Every file has a voice and licence reference. RENDER_DIR=~/.local/share/reader/renders # Required. Must correspond to an installed voice with a licence record. DEFAULT_VOICE=local-en-medium # Required. How far ahead to synthesise. More is smoother and wastes work when navigating. BUFFER_SENTENCES=3 # Required. What happens to tables. Reading a table as a number stream is incomprehensible. TABLE_POLICY=skip_with_marker # Required. What happens to code blocks. The default exists because punctuation read aloud is unbearable. CODE_POLICY=skip_with_marker
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for NaturalReader because customers pay for output quality, production speed, licensed voices, consent workflows, and a provider that carries the operational risk. The recurring cost buys model licensing, consent records, impersonation risk, watermarking, GPU queues, media storage, abuse response, and rapid model changes, not just the visible interface.
xvoice catalog, OCR, document formats, mobile apps, and commercial licensing options
xfrontier voice or avatar model
xlicensed voice catalog
xreal-time rendering fleet
xmoderation, consent verification, and enterprise rights
Don't feel like building it? These folks already made it free.
all 4 free alternatives to NaturalReader →· no votes, no pay-to-list · just what's real
NaturalReader pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | Unlimited system Free Voices; 20,000 Lite-voice characters/day; 4,000 shared Plus/Cloned characters/day; no MP3 conversion or OCR. |
| lite | $13.90/user | $6.58/user | Unlimited Lite listening; 1,000,000 Lite-voice MP3 characters/month; no Plus/Pro voices or voice cloning. |
| plus | $20.90/user | $9.92/user | 500,000 shared Plus/Cloned listening characters/day; 1,000,000 Lite MP3 characters/month + 1,000,000 shared Plus/Cloned MP3 characters/month; clone up to 2 voices. |
| pro | $25.90/user | $13.25/user | 500,000 shared Plus/Cloned/Pro listening characters/day; 1,000,000 Lite MP3 characters/month + 1,000,000 shared premium MP3 characters/month; clone up to 2 voices. |
| lite edu | — | $49.92 | Annual packages: 50 users $599; 100 $700; 150 $800; 200 $900; 250 $1,000; 500 $1,800; 1,000 $2,500; 1,500 $2,900; 2,000 $3,300. |
| plus edu | — | $24.92 | Annual packages: 5 users $299; 10 $499; 20 $899; 30 $1,200; 40 $1,400; 50 $1,500; above 50 starts at $25/user/year. |
| site license | — | $275 | Minimum 2,000 enrolled users; starts at $3,300/year. |
free tierunlimited system voices; 20,000 Lite-voice characters/day; 4,000 Plus/Cloned characters/day; no MP3 or OCR
billingindividual plans are monthly + annual; EDU and Site License plans are annual-only
hidden costsAudio made under Personal/EDU plans is licensed for personal use only; commercial rights require the separately billed Commercial AI Voice Generator. App Store/Google Play prices, taxes, and regional fees can differ.
verified 2026-08-12 · source ↗
Vibecode NaturalReader
Kinda. The core of NaturalReader is buildable in a weekend with the prompt on this page, but there are real gaps: voice catalog, OCR, document formats, mobile apps, and commercial licensing options, frontier voice or avatar model. Read the honest list above before committing.
How much does NaturalReader cost?
NaturalReader costs about $13.9/month (Lite, checked 2026-08-12), which is $166.8 per year.
What do I lose by replacing NaturalReader?
Honestly: 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. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to NaturalReader?
Yes: Koodo Reader (Reads nearly every ebook format aloud; the fancy hosted voice is paid, the local and system voices are not.) Readest (A book-and-PDF reader with built-in narration and no premium-voice bait-and-switch.) Thorium Reader (A free desktop reader built by accessibility people: ebooks, PDFs and text-to-speech, with no account or ad slot.) All 4 curated free alternatives are at vibecodeit.com/naturalreader/alternatives. The prompt is for when you want it exactly your way.