Build Speechify
KINDAreplaces $29/mosaves $348/yrback to the verdict
Import PDFs, EPUBs and web pages, extract clean readable text, speak it through a local neural text-to-speech server, highlight the sentence being spoken, control speed, and remember where you stopped in every document.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server and the import pipeline.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree, both are open source; the cost is memory and CPU or GPU time
Why The whole point. Kokoro sounds considerably better; Piper is far lighter and runs comfortably on a laptop with no GPU.
Get it Run Kokoro behind its OpenAI-compatible server image, or install Piper and a voice model. Try both before choosing, because the quality difference is large and so is the resource difference.
Verify
A curl to the speech endpoint returns playable audio - have readyfree
Why Kokoro wants materially more than Piper, and a model that swaps produces synthesis slower than real time, which changes the design.
Get it Check available memory before choosing. If the ratio measured in Phase 1 is above one, use the lighter engine.
Verify
The synthesis ratio measured in Phase 1 is below one - have readyfree
Why Phase 2 is only meaningful against real files: a two column academic PDF, an EPUB with chapters, and a web article behind a cookie banner.
Get it A paper from an open archive, a public domain EPUB, and any news article. Keep them as fixtures.
Verify
All three are saved in a fixtures directory - have readyfree
Why Cached sentence audio for a long book adds up quickly, and a full disk mid-playback is an unpleasant failure.
Get it Check free space and set the cache limit in configuration accordingly.
Verify
The configured cache limit is below your free space
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- documents(id, title, source_kind, source_path, imported_at, language) · source_kind is pdf, epub, web or text - blocks(id, document_id, ordinal, kind, text) · kind is heading, paragraph or caption, in reading order - sentences(id, block_id, ordinal, text, char_start, char_end) · the highlight and playback unit - audio_cache(sentence_id, voice, speed, path, duration_ms) · synthesis is slow enough to be worth caching - progress(document_id, sentence_id, updated_at) · one row per document, so resuming is exact - Extraction happens once at import. Playback never reparses the source file.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
TTS_ENDPOINT | required | http://localhost:8880/v1/audio/speech | Your local speech server. The OpenAI-compatible shape is what makes engines swappable. |
TTS_VOICE | required | af_sky | Voice identifier from your engine's voice list. |
TTS_ENGINE | required | kokoro | kokoro or piper. Chosen by the ratio measured in Phase 1. |
PREFETCH_SENTENCES | required | 4 | How far ahead to synthesise. Derived from the synthesis ratio, not guessed. |
AUDIO_CACHE_DIR | required | ./data/audio | Where cached sentence audio is written. Safe to delete. |
AUDIO_CACHE_MAX_GB | required | 5 | Cache ceiling. Oldest entries are evicted first. |
DB_PATH | required | ./data/reader.db | Library, documents, sentences and progress. This is the backup. |
The build, in order
A local voice that speaks a sentence
Text goes in, audio comes out, from a server on your own machine.
Start Kokoro behind its OpenAI-compatible endpoint, or Piper if your machine is modest. Confirm with a curl before writing any application code.
terminalcurl -s http://localhost:8880/v1/audio/speech -H 'Content-Type: application/json' -d '{"model":"kokoro","input":"Hello there.","voice":"af_sky"}' --output test.wavA single synthesize function taking text, voice and speed. Every engine difference lives behind it, which is what makes Piper a fallback rather than a rewrite.
Files
src/tts.tsTime synthesis against the duration of the audio produced, across ten sentences of varying length. Write the ratio in the README.
Files
README.md
done when · tick each as it passeswatch out- If synthesis is slower than real time, streaming sentence by sentence will stutter and you must synthesise ahead. Measure the ratio now rather than discovering it during Phase 4, when the fix is a queue you did not plan for.
Getting clean text out of real files
A PDF, an EPUB and a web page all become the same clean block structure.
pdfjs gives positioned text items. Cluster their x positions per page to detect columns, then order within each column before moving to the next.
Files
src/import/pdf.tsText appearing at the same position on many pages is a header or a footer. Detect by repetition across pages rather than by guessing margins.
Files
src/import/pdf.tsEPUB is structured already, so preserve its heading levels. For web pages, use Readability to get the article, then convert to the same block shape.
Files
src/import/epub.tssrc/import/web.ts
done when · tick each as it passeswatch out- PDF has no concept of reading order. Text is positioned glyphs, and a two column layout extracted naively interleaves the columns into nonsense. Detect columns by clustering horizontal positions, and test on an academic paper rather than a single column report.
Sentences and synchronised highlighting
The highlighted sentence is the sentence you are hearing, all the way through a long document.
Intl.Segmenter with the sentence granularity, storing character start and end so the highlight maps back to the rendered text exactly.
Files
src/segment.tsPlay a sentence, wait for the ended event, then advance. No timers anywhere in the playback loop.
Files
src/player.tsClicking sets the position and starts from there, which is also how you will test that offsets are right.
Files
src/reader.ts
done when · tick each as it passeswatch out- Driving the highlight from a timer and an estimated duration drifts, and by the third chapter the highlight is a paragraph away from the audio. Advance on the audio element's ended event, which cannot drift because it is the audio itself telling you.
Playing ahead so it never stutters
Playback is continuous, and speed changes take effect without a gap.
Keep a few sentences ahead synthesised. The right number is derived from the ratio you measured in Phase 1, not guessed.
Files
src/prefetch.tsThe cache key must include voice and speed or a speed change will replay stale audio.
Files
src/cache.tsAbort in-flight synthesis when the user jumps. Otherwise the queue delivers audio for a position nobody is at any more.
Files
src/prefetch.ts
done when · tick each as it passeswatch out- Changing playback rate on the audio element is instant and free but raises pitch on some engines' output; re-synthesising at a different speed sounds better and costs a delay. Pick one deliberately, and if you use the element's rate, check whether your engine's audio survives it.
Library, progress and export
You can leave mid-chapter and come back a week later exactly where you were.
Documents with progress as a proportion and the last read time. Sorted by recency, because that is how people actually return to reading.
Files
src/library.tsStore the sentence id. On re-import, detect that ids changed and tell the user rather than guessing.
Files
src/progress.tsConcatenate cached sentence audio into one file. Clearing the cache must never touch documents or progress.
Files
src/export.ts
done when · tick each as it passeswatch out- Storing progress as a character offset breaks the moment a document is re-imported and extraction changes slightly. Store the sentence identity, and if re-import produces different sentences, say so rather than silently resuming somewhere wrong.
Sharing it, and the licence questionproduct builder
If anyone else uses it, the voice licences and the copyright position are settled.
Any session mechanism. The point is that an open reader on the internet is a copyright problem waiting to be someone else's.
Files
src/auth.tsA table of model, source and licence terms. Some are permissive, some are personal use only, and the difference matters.
Files
docs/voices.mdOne sentence about personal use next to the export button. Better there than in a document nobody opens.
Files
src/reader.ts
done when · tick each as it passeswatch out- Turning a purchased book into an audio file is fine for you and is redistribution the moment you share it. Similarly, a voice model licensed for personal use only does not become shareable because your application is free. Both questions have to be answered before anyone else connects.
That is the whole plan for Speechify. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- The hosted voice catalogue and its consistency across devices.
- Mobile scanning and optical character recognition capture.
- Cross-device sync of library and position.
- Native mobile applications with background audio.
- Speechify's 1000+ hosted voices and consistent quality across devices
- mobile scanning and polished OCR capture
- cross-device sync and offline native apps
- Google Drive, Dropbox, and OneDrive integrations
- voice typing, AI podcasts, and document chat
- Optical character recognition for scanned PDFs that contain no text layer
- A word-level highlight, which needs timestamps your engine may not provide
- Automatic skipping of reference lists and footnotes in academic papers
Need the files? The project pack on the verdict page hands your agent the whole brief · more reading.