Model or dataset
richardr1126/openreader avatar
richardr1126/openreader

Generate ahead, highlight word by word, export the same cache

An open-source read-along document reader server with high-quality TTS options, synchronized highlighting, and audiobook export for EPUB, PDF, DOCX, TXT, and MD.

540 stars73 forksTypeScriptMIT

At a glance

What is it?
OpenReader is a self-hostable Next.js read-along server that turns EPUB, PDF, TXT, MD and DOCX into narrated playback with word-level highlighting, backed by a separate compute worker and a choice of six text-to-speech providers.
Who is it for?
OpenReader is worth the setup when you read long documents aloud and want the highlight to follow the voice, because the sync comes from aligning generated audio with Whisper rather than from estimating a speaking rate, and because the same cached audio feeds playback, seeking and audiobook export instead of being rendered three times. It does not suit a quick skim of a PDF, and it does not suit readers who want a library manager more than a narrator.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One durable timeline, generated ahead of wherever you are listening

The design decision that shapes everything else is that audio is generated in the background into a single durable document timeline, and playback starts as soon as the first segment exists. You are not waiting for a book to finish rendering, and you are not waiting for the page you happen to be on, because the worker keeps generating ahead of the playhead into one timeline rather than per-page files. Cached audio is then reused across seeks, reloads and exports, which is why jumping to a later chapter is not a fresh synthesis request. The same cache is what the audiobook export is assembled from, so the M4B or MP3 you export contains the segments you already listened to rather than a second pass with a different voice or a different provider state. Background generation also means a long document is a background job you can walk away from, and the progress is visible in the interface rather than implied by a spinner.

Two different models do the sync: layout geometry and Whisper alignment

Word-by-word highlighting is not one feature, it is two, and they apply to different formats. PDFs get layout-aware parsing through PP-DocLayoutV3 running on ONNX, which does structured block detection, stitches content across page boundaries, and produces the geometry the highlighter uses to know where a word sits on the page. That step is what stops a two-column paper or a figure caption from being read in the wrong order. On top of that, the alignment of spoken words to audio comes from ONNX Whisper alignment, delivered through the compute worker control plane, which is backed by NATS JetStream. The distinction matters when something looks wrong: a reading-order or placement problem points at the layout stage, while a highlight that drifts a second or two behind the voice points at the alignment stage. They are separate models in a separate worker, so fixing one does not disturb the other.

Five compose entry points and a script that decommissions v4

The root package is private and marked 5.1.0, matching the newest release, and its scripts are the clearest map of how many shapes this thing runs in. Five of them start Docker with different example files: a plain compose file, a full one, and three local variants that differ in whether they slim or build locally. Alongside them sit the scripts that describe the architecture rather than the product. check:compute-boundary runs a script whose name is the whole design principle, since the compute worker is meant to stay separate from the Next.js server. build:bundle-guard checks the server bundle. test:compute runs vitest against a dedicated project for the worker, and test:e2e runs Playwright. Then there is migrate-decommission, which delegates to a v4 decommission routine in the bootstrap package, shipped inside a release line that is now v5.

The TTS provider is seeded on first boot, then the admin UI owns it

The environment file makes a promise it then takes back, and reading it carefully saves an afternoon. API_BASE points at a local OpenAI-compatible endpoint on port 8880 and API_MODEL_NAME is set to kokoro, with the comment recommending Kokoro-FastAPI. Then the note: on first boot the server seeds those values into a shared provider called default-openai, stored in the database and encrypted at rest, and after that the admin interface is authoritative and changing the environment variables has no effect. You may remove them once the seed has run. Two consequences follow. Auth has to be configured before that seed path works, which couples your first-boot order to your auth setup. And if you edit API_BASE expecting a different voice server to take over, nothing will happen until you change it in the admin interface, which is the behaviour most people would call a bug until they read this paragraph.

Three sign-in doors, and anonymous sessions are off

Authentication is not a toggle here, it is a set of decisions. AUTH_SECRET is required, generated with openssl rand base64 32, and has to stay stable across restarts or every session you have is invalidated. BASE_URL is the externally facing address, and the comment tells you to set it to a LAN address if you want other devices on the network to reach the app. AUTH_TRUSTED_ORIGINS adds origins on top of BASE_URL, which is always trusted. Three sign-in paths sit on top of that: GitHub with a client id and secret, a generic OIDC provider with its own discovery URL, provider id, name and scopes, aimed at things like Pocket ID, Authelia, Authentik or Keycloak, where the discovery URL must be https except on localhost; and a first administrator credential account that only exists on a fresh instance, created before the first boot with a password of at least 16 characters. Anonymous sessions exist as an option and default to false.

SeaweedFS is pinned to 4.18 because 4.19 broke uploads

One line in the Dockerfile explains more about this project than any feature list. The SeaweedFS builder stage is pinned to 4.18 with the comment that CI observed upload regressions on 4.19. That is a real constraint on your deployment rather than a stylistic choice, since the storage layer for library files is embedded SeaweedFS or anything S3-compatible, and the image ships a known-good weed binary with its licence text fetched from the upstream repository. The same file extracts a nats-server binary from the 2.11 alpine image for an embedded single-container worker mode, which is what lets the whole thing run in one container when you do not want a separate worker. Two more build details matter for hosting: the app build runs with a placeholder AUTH_SECRET and a localhost BASE_URL, and after the workspace deploy step the native modules are rebuilt, which is what gets better-sqlite3 and ffmpeg-static working on the target image.

Audiobooks are assembled from the cache, not re-synthesized

Export exists because the cache already holds the answer. The worker assembles M4B or MP3 from the same playback cache used for read-along, so the exported file matches what you heard, down to the voice and provider settings in force at generation time. Two format choices with different consequences: M4B is the container audiobooks and players expect when they carry chapter metadata, MP3 is the one that plays anywhere. On the input side the reader handles five formats, EPUB, PDF, TXT, MD and DOCX, with DOCX converted by the worker rather than parsed in the request path, and the library itself generates PDF and EPUB previews so a folder of mixed documents is browsable without opening each one. Language is chosen per document, and which languages are actually available is decided by the configured provider and voice rather than by the interface, so a document language with no matching voice fails late.

A v5 directory at the top level, and an old name still in circulation

Structural signals suggest a codebase mid-migration. There is a v5 directory sitting at the repository root next to src, alongside packages, which is where the workspace packages live: bootstrap, compute-worker, database, runtime-config and tts among them. empty-module.ts sits at the root too, the kind of placeholder that exists to satisfy a bundler or a workspace resolution rather than to hold code. vercel.json is committed, so serverless deployment is a supported target and not an aspiration, and the docs site under docs-site has its own set of scripts including versioned documentation. The naming history is still visible too: the project was previously called OpenReader-WebUI, and both names turn up in badges and search results, so if you arrive looking for the old name you are looking at the same repository. Version numbering moved fast, with v4.4.0 in July, v5.0.0 on September 17 and v5.1.0 on September 30, 2026.

Editorial conclusion

OpenReader is worth the setup when you read long documents aloud and want the highlight to follow the voice, because the sync comes from aligning generated audio with Whisper rather than from estimating a speaking rate, and because the same cached audio feeds playback, seeking and audiobook export instead of being rendered three times. It does not suit a quick skim of a PDF, and it does not suit readers who want a library manager more than a narrator. Four things to verify before you commit. Which speech backend you will run, since the local options are separate OpenAI-compatible servers and each language depends on the provider and voice you configure. Whether your first boot order is right, because the environment file seeds a default provider that then overrides those variables for the life of the database. Whether you need a separate compute worker or the single-container embedded mode. And whether auth suits your household, since anonymous sessions are off by default and the first administrator has to be created during setup with a password of at least 16 characters.

Frequently asked questions

Which file formats can OpenReader read?

Five readable formats: EPUB, PDF, TXT, MD and DOCX, where DOCX is converted by the compute worker rather than parsed in the request path. The library also generates PDF and EPUB previews so mixed folders stay browsable.

Which TTS providers does OpenReader support?

Self-hosted OpenAI-compatible servers including Kokoro-FastAPI, KittenTTS-FastAPI and Orpheus-FastAPI, or cloud APIs from OpenAI, Replicate and DeepInfRA. Which languages are available depends on the provider and voice you configure.

How do I run OpenReader with Docker Compose?

The root package exposes five compose scripts against files under examples/docker, including plain, full and local slim and full variants, and the image is built for amd64 and arm64 with automatic startup migrations. A separate NATS JetStream compute worker can also be deployed on its own.

How does OpenReader highlight words as they are spoken?

Alignment comes from ONNX Whisper through the compute worker control plane, which is backed by NATS JetStream, so the highlight follows the generated audio rather than an estimated speaking rate. PDFs add a separate layout pass with PP-DocLayoutV3 for block detection, cross-page stitching and geometry.

Can OpenReader export an audiobook?

Yes, in M4B or MP3. The worker assembles the file from the same playback cache used for read-along, so the export matches the audio already generated instead of re-synthesizing it.

Does OpenReader need a database?

It supports SQLite or Postgres through its database package, and object storage can be an embedded SeaweedFS instance or any S3-compatible service. Startup migrations run automatically, so a fresh volume is upgraded in place.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. richardr1126/openreader on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/richardr1126-openreader.svg)](https://hysenlabs.com/projects/richardr1126-openreader)