feat(docs): add configurable rate limiting and external docs
Add environment variables for fine-grained control over TTS rate limiting and Better Auth behavior. Move documentation to external Docusaurus site with automated deployment workflows. - TTS rate limiting can now be enabled/disabled via TTS_ENABLE_RATE_LIMIT - Customizable daily limits for anonymous/authenticated users and IP backstops - Better Auth rate limiting can be disabled via DISABLE_AUTH_RATE_LIMIT - Rename library import env vars to IMPORT_LIBRARY_DIRS/DIR - Add docs-site with Docusaurus and GitHub Actions workflows - Update README to reference external documentation
This commit is contained in:
parent
81d249ed52
commit
6d5fb65a72
39 changed files with 14268 additions and 507 deletions
30
.env.example
30
.env.example
|
|
@ -6,26 +6,48 @@ NEXT_PUBLIC_NODE_ENV=development # Not availble in Docker containers (due to bei
|
|||
API_BASE=http://localhost:8880/v1
|
||||
API_KEY=api_key_optional
|
||||
|
||||
# (Optional) TTS request/cache tuning (leave unset to use defaults)
|
||||
# TTS_CACHE_MAX_SIZE_BYTES=268435456 # 256MB
|
||||
# TTS_CACHE_TTL_MS=1800000 # 30 minutes
|
||||
# TTS_MAX_RETRIES=2
|
||||
# TTS_RETRY_INITIAL_MS=250
|
||||
# TTS_RETRY_MAX_MS=2000
|
||||
# TTS_RETRY_BACKOFF=2
|
||||
|
||||
# (Optional) Enable TTS character rate limiting (default is `false`)
|
||||
# TTS_ENABLE_RATE_LIMIT=true
|
||||
# (Optional) TTS per-user daily limits (leave unset to use defaults)
|
||||
# TTS_DAILY_LIMIT_ANONYMOUS=50000
|
||||
# TTS_DAILY_LIMIT_AUTHENTICATED=500000
|
||||
# (Optional) TTS IP backstop daily limits (leave unset to use defaults)
|
||||
# TTS_IP_DAILY_LIMIT_ANONYMOUS=100000
|
||||
# TTS_IP_DAILY_LIMIT_AUTHENTICATED=1000000
|
||||
|
||||
# Path to your local whisper.cpp CLI binary for STT timestamp generation
|
||||
WHISPER_CPP_BIN=/whisper.cpp/build/bin/whisper-cli
|
||||
|
||||
# Auth (recommended for contributors and public instances)
|
||||
# (Optional) Auth is only enabled when **both** AUTH_SECRET and BASE_URL are set
|
||||
# Auth configuration (recommended for contributors and public instances)
|
||||
# (Optional) Auth is only enabled when AUTH_SECRET and BASE_URL are set
|
||||
BASE_URL=http://localhost:3003 # Externally facing URL for this app (set to LAN IP for access from other devices on the network)
|
||||
AUTH_SECRET=some_random_secret_key # Generate with `openssl rand -base64 32`
|
||||
AUTH_TRUSTED_ORIGINS=http://localhost:3003,http://127.0.0.1:3003 # Additional trusted origins (BASE_URL is always trusted)
|
||||
# (Optional) Sign in w/ GitHub Configuration
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
# (Optional) Disable Better Auth built-in rate limiting (useful for testing)
|
||||
DISABLE_AUTH_RATE_LIMIT=
|
||||
|
||||
# Backend DB used for server-side metadata (documents/audiobooks) and, when auth is enabled, auth tables.
|
||||
# Defaults to SQLite at docstore/sqlite3.db when not set.
|
||||
POSTGRES_URL=
|
||||
# (Optional) Skip automatic startup migrations when set to `false` (default: `true`)
|
||||
RUN_DB_MIGRATIONS=
|
||||
|
||||
# Embedded SeaweedFS weed mini config
|
||||
USE_EMBEDDED_WEED_MINI=
|
||||
WEED_MINI_DIR=
|
||||
WEED_MINI_WAIT_SEC=
|
||||
|
||||
# S3 storage config (use with embedded weed mini or external S3-compatible storage)
|
||||
# For embedded weed mini, set explicit keys if you want stable credentials across restarts.
|
||||
S3_ACCESS_KEY_ID=
|
||||
|
|
@ -36,3 +58,7 @@ S3_REGION=
|
|||
S3_ENDPOINT=
|
||||
S3_FORCE_PATH_STYLE=
|
||||
S3_PREFIX=
|
||||
|
||||
# Server library import roots (optional, uses /docstore/library by default)
|
||||
IMPORT_LIBRARY_DIR=
|
||||
IMPORT_LIBRARY_DIRS=
|
||||
|
|
|
|||
37
.github/workflows/docs-check.yml
vendored
Normal file
37
.github/workflows/docs-check.yml
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
name: Docs Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- 'docs-site/**'
|
||||
- '.github/workflows/docs-check.yml'
|
||||
- '.github/workflows/docs-deploy.yml'
|
||||
- 'README.md'
|
||||
- 'package.json'
|
||||
|
||||
jobs:
|
||||
docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
cache-dependency-path: docs-site/pnpm-lock.yaml
|
||||
|
||||
- name: Install docs dependencies
|
||||
run: pnpm --dir docs-site install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm --dir docs-site build
|
||||
61
.github/workflows/docs-deploy.yml
vendored
Normal file
61
.github/workflows/docs-deploy.yml
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
name: Docs Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: docs-pages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
cache-dependency-path: docs-site/pnpm-lock.yaml
|
||||
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Install docs dependencies
|
||||
run: pnpm --dir docs-site install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm --dir docs-site build
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: docs-site/build
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
77
.github/workflows/docs-version-on-tag.yml
vendored
Normal file
77
.github/workflows/docs-version-on-tag.yml
vendored
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
name: Docs Version on Tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
version-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
cache-dependency-path: docs-site/pnpm-lock.yaml
|
||||
|
||||
- name: Install docs dependencies
|
||||
run: pnpm --dir docs-site install --frozen-lockfile
|
||||
|
||||
- name: Snapshot docs version
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f docs-site/versions.json ] && grep -q "\"${VERSION}\"" docs-site/versions.json; then
|
||||
echo "Version ${VERSION} already exists in versions.json; skipping docs:version"
|
||||
else
|
||||
pnpm --dir docs-site docusaurus docs:version "${VERSION}"
|
||||
fi
|
||||
|
||||
- name: Set default docs version
|
||||
env:
|
||||
VERSION: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node -e "const fs=require('fs'); const file='docs-site/docusaurus.config.ts'; const source=fs.readFileSync(file,'utf8'); const updated=source.replace(/lastVersion:\s*'[^']*',/, \"lastVersion: '\" + process.env.VERSION + \"',\"); if (source===updated) { throw new Error('Unable to update lastVersion in docusaurus.config.ts'); } fs.writeFileSync(file, updated);"
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm --dir docs-site build
|
||||
|
||||
- name: Create docs version PR
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
commit-message: docs: snapshot ${{ github.ref_name }}
|
||||
title: docs: snapshot ${{ github.ref_name }}
|
||||
body: |
|
||||
Automated docs version snapshot for `${{ github.ref_name }}`.
|
||||
|
||||
- Ran `docusaurus docs:version`
|
||||
- Updated `lastVersion` in `docs-site/docusaurus.config.ts`
|
||||
branch: docs/version-${{ github.ref_name }}
|
||||
base: main
|
||||
delete-branch: true
|
||||
labels: docs,automation
|
||||
add-paths: |
|
||||
docs-site/docusaurus.config.ts
|
||||
docs-site/versions.json
|
||||
docs-site/versioned_docs/**
|
||||
docs-site/versioned_sidebars/**
|
||||
472
README.md
472
README.md
|
|
@ -1,450 +1,48 @@
|
|||
[](../../stargazers)
|
||||
[](../../network/members)
|
||||
[](../../watchers)
|
||||
[](../../issues)
|
||||
[](../../commits)
|
||||
[](../../releases)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/releases)
|
||||
[](LICENSE)
|
||||
[](https://docs.openreader.richardr.dev/)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/actions/workflows/playwright.yml)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/actions/workflows/docs-check.yml)
|
||||
|
||||
[](../../discussions)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/stargazers)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/network/members)
|
||||
[](https://github.com/richardr1126/OpenReader-WebUI/discussions)
|
||||
|
||||
# 📄🔊 OpenReader WebUI
|
||||
|
||||
OpenReader WebUI is an open source text to speech document reader web app built using Next.js, offering a TTS read along experience with narration for **EPUB, PDF, TXT, MD, and DOCX documents**. It supports multiple TTS providers including OpenAI, Deepinfra, and custom OpenAI-compatible endpoints like [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) and [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
|
||||
OpenReader WebUI is an open source, self-host-friendly text-to-speech document reader built with Next.js for **EPUB, PDF, TXT, MD, and DOCX** with synchronized read-along playback.
|
||||
|
||||
- 🎯 **Multi-Provider TTS Support**
|
||||
- [**Kokoro-FastAPI**](https://github.com/remsky/Kokoro-FastAPI): Supporting multi-voice combinations (like `af_heart+af_bella`)
|
||||
- [**Orpheus-FastAPI**](https://github.com/Lex-au/Orpheus-FastAPI)
|
||||
- **Custom OpenAI-compatible**: Any TTS API with `/v1/audio/voices` and `/v1/audio/speech` endpoints
|
||||
- **Cloud TTS Providers (requiring API keys)**
|
||||
- [**Deepinfra**](https://deepinfra.com/models/text-to-speech): Kokoro-82M + models with support for cloned voices and more
|
||||
- [**OpenAI API ($$)**](https://platform.openai.com/docs/pricing#transcription-and-speech): tts-1, tts-1-hd, and gpt-4o-mini-tts w/ instructions
|
||||
- 🛜 *(Updated)* **Server-side Sync and Storage**
|
||||
- *(New)* **External Library Import** enables importing documents to the browser's storage from a folder mounted on the server
|
||||
- *(Updated)* **Sync documents** between the browser and server to get them on other browsers or devices
|
||||
- 🎧 **Server-side Audiobook Export** in **m4b/mp3**, with resumable, chapter-based export and regeneration
|
||||
- 📖 **Read Along Experience** providing real-time text highlighting during playback (PDF/EPUB)
|
||||
- *(New)* **Word-by-word** highlighting uses word-by-word timestamps generated server-side with [*whisper.cpp*](https://github.com/ggml-org/whisper.cpp) (optional)
|
||||
- 🧠 **Smart Sentence-Aware Narration** merges sentences across pages/chapters for smoother TTS
|
||||
- 🚀 **Optimized Next.js TTS Proxy** with audio caching and optimized repeat playback
|
||||
- 🎨 **Customizable Experience**
|
||||
- 🎨 Multiple app theme options
|
||||
- ⚙️ Various TTS and document handling settings
|
||||
- And more ...
|
||||
> **Get started in the [docs](https://docs.openreader.richardr.dev/)**.
|
||||
|
||||
## 🐳 Docker Quick Start
|
||||
## ✨ Highlights
|
||||
|
||||
### Prerequisites
|
||||
- 🎯 **Multi-provider TTS** with OpenAI-compatible endpoints (OpenAI, DeepInfra, Kokoro, Orpheus, custom).
|
||||
- 📖 **Read-along playback** for PDF/EPUB with sentence-aware narration.
|
||||
- ⏱️ **Word-by-word highlighting** via optional `whisper.cpp` timestamps.
|
||||
- 🛜 **Sync + library import** to bring docs across devices and from server-mounted folders.
|
||||
- 🗂️ **Flexible storage** with embedded SeaweedFS or external S3-compatible backends.
|
||||
- 🎧 **Audiobook export** in `m4b`/`mp3` with resumable chapter processing.
|
||||
- 🐳 **Self-host friendly** with Docker, optional auth, and automatic startup migrations.
|
||||
|
||||
- Recent version of Docker installed on your machine
|
||||
- A TTS API server (Kokoro-FastAPI, Orpheus-FastAPI, Deepinfra, OpenAI, etc.) running and accessible
|
||||
## 🚀 Start Here
|
||||
|
||||
> **Note:** If you have good hardware, you can run [Kokoro-FastAPI with Docker locally](#🗣️-local-kokoro-fastapi-quick-start-cpu-or-gpu) (see below).
|
||||
| Goal | Link |
|
||||
| --- | --- |
|
||||
| Run with Docker | [Docker Quick Start](https://docs.openreader.richardr.dev/getting-started/docker-quick-start) |
|
||||
| Develop locally | [Local Development](https://docs.openreader.richardr.dev/getting-started/local-development) |
|
||||
| Configure auth | [Auth](https://docs.openreader.richardr.dev/guides/configuration) |
|
||||
| Configure SQL database | [SQL Database](https://docs.openreader.richardr.dev/operations/database-and-migrations) |
|
||||
| Configure object storage | [Object / Blob Storage](https://docs.openreader.richardr.dev/guides/storage-and-blob-behavior) |
|
||||
| Configure TTS providers | [TTS Providers](https://docs.openreader.richardr.dev/guides/tts-providers) |
|
||||
| Run Kokoro locally | [Kokoro-FastAPI](https://docs.openreader.richardr.dev/integrations/kokoro-fastapi) |
|
||||
| Get support or contribute | [Support and Contributing](https://docs.openreader.richardr.dev/community/support) |
|
||||
|
||||
### 1. 🐳 Start the Docker container
|
||||
## 🧭 Community
|
||||
|
||||
Minimal (auth disabled, embedded storage is ephemeral, no library import):
|
||||
- Questions and ideas: [GitHub Discussions](https://github.com/richardr1126/OpenReader-WebUI/discussions)
|
||||
- Bug reports: [GitHub Issues](https://github.com/richardr1126/OpenReader-WebUI/issues)
|
||||
- Contributions: open a pull request
|
||||
|
||||
```bash
|
||||
docker run --name openreader-webui \
|
||||
--restart unless-stopped \
|
||||
-p 3003:3003 \
|
||||
-p 8333:8333 \
|
||||
ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
## 📜 License
|
||||
|
||||
Fully featured (persistent storage, embedded SeaweedFS `weed mini` for documents, optional auth):
|
||||
|
||||
```bash
|
||||
docker run --name openreader-webui \
|
||||
--restart unless-stopped \
|
||||
-p 3003:3003 \
|
||||
-p 8333:8333 \
|
||||
-v openreader_docstore:/app/docstore \
|
||||
-v /path/to/your/library:/app/docstore/library:ro \
|
||||
-e API_BASE=http://host.docker.internal:8880/v1 \
|
||||
-e API_KEY=none \
|
||||
-e BASE_URL=http://localhost:3003 \
|
||||
-e AUTH_SECRET=<paste_the_output_of_openssl_here> \
|
||||
ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
|
||||
You can remove the `/app/docstore/library` mount if you don't need server library import.
|
||||
You can remove either `BASE_URL` or `AUTH_SECRET` to keep auth disabled.
|
||||
|
||||
Quick notes:
|
||||
- `API_BASE` should point to your TTS API server's base URL (for local Docker Kokoro, use `http://host.docker.internal:8880/v1`).
|
||||
- Expose `-p 8333:8333` for direct browser access to embedded SeaweedFS presigned URLs.
|
||||
- If port `8333` is not exposed, uploads still work via `/api/documents/blob/upload/fallback`.
|
||||
- To enable auth, set both `BASE_URL` and `AUTH_SECRET`.
|
||||
|
||||
<details>
|
||||
<summary><strong>Docker networking and blob behavior</strong> (Click to expand)</summary>
|
||||
|
||||
**Ports**
|
||||
|
||||
- `3003` serves the OpenReader app and API routes.
|
||||
- `8333` serves embedded SeaweedFS S3 for browser direct blob access.
|
||||
|
||||
**Upload behavior**
|
||||
|
||||
- Primary upload path: browser uploads to presigned URL from `/api/documents/blob/upload/presign`.
|
||||
- Fallback upload path: `/api/documents/blob/upload/fallback` if direct upload fails.
|
||||
- Content serving path: `/api/documents/blob` (server-served bytes/snippets).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Which Docker setup should I use?</strong> (Click to expand)</summary>
|
||||
|
||||
| Goal | Recommended setup |
|
||||
| --- | --- |
|
||||
| Fast local setup, no persistence | Minimal `docker run` example |
|
||||
| Persistent docs/audiobooks and optional auth | Fully featured `docker run` example with `/app/docstore` volume |
|
||||
| Browser direct blob uploads/downloads | Publish both `3003` and `8333` |
|
||||
| Private blob endpoint (no `8333` published) | Keep using app APIs; uploads use fallback proxy when direct presigned upload is unreachable |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Common Docker environment variables</strong> (Click to expand)</summary>
|
||||
|
||||
| Variable | Purpose | Example / Notes |
|
||||
| --- | --- | --- |
|
||||
| `API_BASE` | Default TTS API base URL (server-side) | `http://host.docker.internal:8880/v1` |
|
||||
| `API_KEY` | Default TTS API key | `none` or your provider key |
|
||||
| `BASE_URL` | Enables auth when set with `AUTH_SECRET` | External URL for this app, e.g. `http://localhost:3003` or `https://reader.example.com` |
|
||||
| `AUTH_SECRET` | Enables auth when set with `BASE_URL` | Generate with `openssl rand -base64 32` |
|
||||
| `AUTH_TRUSTED_ORIGINS` | Extra allowed auth request origins | Comma-separated list; leave empty to trust only `BASE_URL` |
|
||||
| `POSTGRES_URL` | Use Postgres for server DB (metadata + auth tables) instead of SQLite | If set, startup migrations target Postgres |
|
||||
| `GITHUB_CLIENT_ID` | Optional GitHub OAuth sign-in | Requires `GITHUB_CLIENT_SECRET` |
|
||||
| `GITHUB_CLIENT_SECRET` | Optional GitHub OAuth sign-in | Requires `GITHUB_CLIENT_ID` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Blob and embedded storage environment variables</strong> (Click to expand)</summary>
|
||||
|
||||
| Variable | Purpose | Example / Notes |
|
||||
| --- | --- | --- |
|
||||
| `USE_EMBEDDED_WEED_MINI` | Start SeaweedFS before app startup | default: `true` when unset in shared entrypoint |
|
||||
| `WEED_MINI_DIR` | Data directory for embedded `weed mini` | default: `docstore/seaweedfs` |
|
||||
| `WEED_MINI_WAIT_SEC` | Startup wait timeout for embedded `weed mini` | default: `20` |
|
||||
| `S3_BUCKET` | S3 bucket used for document blobs | default: `openreader-documents` in embedded mode |
|
||||
| `S3_REGION` | S3 region used by AWS SDK | default: `us-east-1` in embedded mode |
|
||||
| `S3_ENDPOINT` | Custom endpoint for S3-compatible providers | default: `http://<BASE_URL host>:8333` when `BASE_URL` is set, otherwise detected LAN host |
|
||||
| `S3_ACCESS_KEY_ID` | S3 access key | auto-generated in embedded mode if unset |
|
||||
| `S3_SECRET_ACCESS_KEY` | S3 secret key | auto-generated in embedded mode if unset |
|
||||
| `S3_FORCE_PATH_STYLE` | Force path-style addressing | default: `true` in embedded mode |
|
||||
| `S3_PREFIX` | Prefix for stored object keys | default: `openreader` |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Docker volume mounts</strong> (Click to expand)</summary>
|
||||
|
||||
| Mount | Type | Recommended | Purpose | Example |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `/app/docstore` | Docker named volume | Yes (if you want persistence) | Persists embedded SeaweedFS blob data (`docstore/seaweedfs`), SQLite metadata DB (`docstore/sqlite3.db` when `POSTGRES_URL` is unset), audiobook export artifacts, and migration/runtime files | `-v openreader_docstore:/app/docstore` |
|
||||
| `/app/docstore/library` | Bind mount (host folder) | Optional + `:ro` | Read-only source directory for **Server Library Import**; files are imported/copied into browser storage, not modified in place | `-v /path/to/your/library:/app/docstore/library:ro` |
|
||||
|
||||
To import from the mounted library: **Settings → Documents → Server Library Import**
|
||||
|
||||
> **Note:** Every file in the mounted library is imported into the client browser's storage. Keep the library reasonably sized to avoid performance issues.
|
||||
|
||||
</details>
|
||||
|
||||
Visit [http://localhost:3003](http://localhost:3003) to run the app and set your settings.
|
||||
|
||||
### 2. ⚙️ Configure the app settings in the UI
|
||||
|
||||
- Set the TTS Provider and Model in the Settings modal
|
||||
- Set the TTS API Base URL and API Key if needed (more secure to set in env vars)
|
||||
- Select your model's voice from the dropdown (voices try to be fetched from TTS Provider API)
|
||||
|
||||
### 3. ⬆️ Updating Docker Image
|
||||
|
||||
```bash
|
||||
docker stop openreader-webui || true && \
|
||||
docker rm openreader-webui || true && \
|
||||
docker image rm ghcr.io/richardr1126/openreader-webui:latest || true && \
|
||||
docker pull ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
|
||||
### 🗣️ Local Kokoro-FastAPI Quick-start (CPU or GPU)
|
||||
|
||||
You can run the Kokoro TTS API server directly with Docker. **We are not responsible for issues with [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI).** For best performance, use an NVIDIA GPU (for GPU version) or Apple Silicon (for CPU version).
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
**Kokoro-FastAPI (CPU)**
|
||||
|
||||
</summary>
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name kokoro-tts \
|
||||
--restart unless-stopped \
|
||||
-p 8880:8880 \
|
||||
-e ONNX_NUM_THREADS=8 \
|
||||
-e ONNX_INTER_OP_THREADS=4 \
|
||||
-e ONNX_EXECUTION_MODE=parallel \
|
||||
-e ONNX_OPTIMIZATION_LEVEL=all \
|
||||
-e ONNX_MEMORY_PATTERN=true \
|
||||
-e ONNX_ARENA_EXTEND_STRATEGY=kNextPowerOfTwo \
|
||||
-e API_LOG_LEVEL=DEBUG \
|
||||
ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
|
||||
```
|
||||
|
||||
> Adjust environment variables as needed for your hardware and use case.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
|
||||
**Kokoro-FastAPI (GPU)**
|
||||
|
||||
</summary>
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name kokoro-tts \
|
||||
--gpus all \
|
||||
--user 1001:1001 \
|
||||
--restart unless-stopped \
|
||||
-p 8880:8880 \
|
||||
-e USE_GPU=true \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-e API_LOG_LEVEL=DEBUG \
|
||||
ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4
|
||||
```
|
||||
|
||||
> Adjust environment variables as needed for your hardware and use case.
|
||||
|
||||
</details>
|
||||
|
||||
> **⚠️ Important Notes:**
|
||||
>
|
||||
> - For best results, set the `-e API_BASE=` for OpenReader's Docker to `http://kokoro-tts:8880/v1`
|
||||
> - For issues or support, see the [Kokoro-FastAPI repository](https://github.com/remsky/Kokoro-FastAPI).
|
||||
> - The GPU version requires NVIDIA Docker support and works best with NVIDIA GPUs. The CPU version works best on Apple Silicon or modern x86 CPUs.
|
||||
|
||||
## Local Development Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (recommended: use [nvm](https://github.com/nvm-sh/nvm))
|
||||
- pnpm (recommended) or npm
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
- A TTS API server (Kokoro-FastAPI, Orpheus-FastAPI, Deepinfra, OpenAI, etc.) running and accessible
|
||||
- [SeaweedFS](https://github.com/seaweedfs/seaweedfs) `weed` binary (required)
|
||||
|
||||
```bash
|
||||
brew install seaweedfs
|
||||
```
|
||||
|
||||
> **Note:** Verify install with `weed version`.
|
||||
|
||||
#### Optionally required for different features:
|
||||
- [FFmpeg](https://ffmpeg.org) (required for audiobook m4b creation only)
|
||||
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
- [libreoffice](https://www.libreoffice.org) (required for DOCX files)
|
||||
|
||||
```bash
|
||||
brew install libreoffice
|
||||
```
|
||||
|
||||
- [whisper.cpp](https://github.com/ggml-org/whisper.cpp) (optional, required for word-by-word highlighting)
|
||||
|
||||
```bash
|
||||
# clone and build whisper.cpp (no model download needed – OpenReader handles that)
|
||||
git clone https://github.com/ggml-org/whisper.cpp.git
|
||||
cd whisper.cpp
|
||||
cmake -B build
|
||||
cmake --build build -j --config Release
|
||||
|
||||
# point OpenReader to the compiled whisper-cli binary
|
||||
echo WHISPER_CPP_BIN=\"$(pwd)/build/bin/whisper-cli\"
|
||||
```
|
||||
|
||||
> **Note:** The `WHISPER_CPP_BIN` path should be set in your `.env` file for OpenReader to use word-by-word highlighting features.
|
||||
|
||||
### Steps
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/richardr1126/OpenReader-WebUI.git
|
||||
cd OpenReader-WebUI
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
|
||||
With pnpm (recommended):
|
||||
|
||||
```bash
|
||||
pnpm i # or npm i
|
||||
```
|
||||
|
||||
3. Configure the environment:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration settings
|
||||
```
|
||||
|
||||
Auth is recommended for contributors and is enabled when **both** values are set:
|
||||
|
||||
- Set `BASE_URL` to your local URL (default: `http://localhost:3003`)
|
||||
- Generate a `AUTH_SECRET` and paste it into `.env`:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
- (Optional) If you use both localhost and LAN URL, set `AUTH_TRUSTED_ORIGINS` (leave empty to trust only `BASE_URL`):
|
||||
|
||||
```bash
|
||||
AUTH_TRUSTED_ORIGINS=http://localhost:3003,http://192.168.0.116:3003
|
||||
```
|
||||
|
||||
The embedded weed mini object store is enabled by default for local development, and started through the shared entrypoint. The ACCESS_KEY_ID and SECRET_ACCESS_KEY are auto-generated if unset, but for stable credentials across restarts, you can generate and set them in `.env`:
|
||||
- (Optional) Generate `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY` and paste them into `.env` for stable credentials:
|
||||
|
||||
```bash
|
||||
S3_ACCESS_KEY_ID=<`openssl rand -hex 16`>
|
||||
S3_SECRET_ACCESS_KEY=<`openssl rand -hex 32`>
|
||||
```
|
||||
- (Optional) Connect to external S3-compatible storage instead of embedded `weed mini`:
|
||||
|
||||
```bash
|
||||
USE_EMBEDDED_WEED_MINI=false
|
||||
|
||||
# Required
|
||||
S3_BUCKET=your-bucket
|
||||
S3_REGION=us-east-1
|
||||
S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
|
||||
# Optional / provider-specific
|
||||
S3_ENDPOINT=
|
||||
S3_FORCE_PATH_STYLE=
|
||||
S3_PREFIX=openreader
|
||||
```
|
||||
|
||||
Notes:
|
||||
- For AWS S3: usually leave `S3_ENDPOINT` empty and `S3_FORCE_PATH_STYLE` empty/false.
|
||||
- For MinIO/SeaweedFS/R2/B2 S3: set `S3_ENDPOINT` to your endpoint URL and set `S3_FORCE_PATH_STYLE=true` when required by that provider.
|
||||
|
||||
> Notes:
|
||||
> - The base URL for the TTS API should be accessible and relative to the Next.js server
|
||||
>
|
||||
> - To disable auth, remove either `BASE_URL` or `AUTH_SECRET`.
|
||||
>
|
||||
> - If S3 credentials are unset, they are auto-generated per startup.
|
||||
|
||||
4. Run DB migrations:
|
||||
|
||||
- **Production / Docker**: Migrations run automatically on startup via `pnpm start`.
|
||||
- **Development**: When using `pnpm dev`, it needs to be run explicitly:
|
||||
|
||||
```bash
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
> Note: If you set `POSTGRES_URL` in `.env`, migrations will target Postgres instead of local SQLite.
|
||||
>
|
||||
> Generating migrations (contributors):
|
||||
> - `pnpm generate` generates migrations for **both** SQLite and Postgres (requires `POSTGRES_URL`).
|
||||
>
|
||||
> Manual Drizzle Kit runs:
|
||||
> - SQLite migrate: `npx drizzle-kit migrate --config drizzle.config.sqlite.ts`
|
||||
> - Postgres migrate: `npx drizzle-kit migrate --config drizzle.config.pg.ts`
|
||||
> - SQLite generate: `npx drizzle-kit generate --config drizzle.config.sqlite.ts`
|
||||
> - Postgres generate: `npx drizzle-kit generate --config drizzle.config.pg.ts`
|
||||
|
||||
5. Start the development server:
|
||||
|
||||
With pnpm (recommended):
|
||||
|
||||
```bash
|
||||
pnpm dev # or npm run dev
|
||||
```
|
||||
|
||||
or build and run the production server:
|
||||
|
||||
With pnpm:
|
||||
|
||||
```bash
|
||||
pnpm build # or npm run build
|
||||
pnpm start # or npm start
|
||||
```
|
||||
|
||||
Visit [http://localhost:3003](http://localhost:3003) to run the app.
|
||||
|
||||
## 💡 Feature requests
|
||||
|
||||
For feature requests or ideas you have for the project, please use the [Discussions](https://github.com/richardr1126/OpenReader-WebUI/discussions) tab.
|
||||
|
||||
## 🙋♂️ Support and issues
|
||||
|
||||
If you encounter issues, please open an issue on GitHub following the template (which is very light).
|
||||
|
||||
## 👥 Contributing
|
||||
|
||||
Contributions are welcome! Fork the repository and submit a pull request with your changes.
|
||||
|
||||
## ❤️ Acknowledgements
|
||||
|
||||
This project would not be possible without standing on the shoulders of these giants:
|
||||
|
||||
- [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) model
|
||||
- [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI)
|
||||
- [Better Auth](https://www.better-auth.com/)
|
||||
- [SQLite](https://www.sqlite.org/)
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
- [SeaweedFS](https://github.com/seaweedfs/seaweedfs) (`weed mini`)
|
||||
- [whisper.cpp](https://github.com/ggerganov/whisper.cpp)
|
||||
- [ffmpeg](https://ffmpeg.org)
|
||||
- [react-pdf](https://github.com/wojtekmaj/react-pdf) npm package
|
||||
- [react-reader](https://github.com/happyr/react-reader) npm package
|
||||
|
||||
## Docker Supported Architectures
|
||||
|
||||
- linux/amd64 (x86_64)
|
||||
- linux/arm64 (Apple Silicon, Raspberry Pi, SBCs, etc.)
|
||||
|
||||
## Stack
|
||||
|
||||
- **Framework:** [Next.js](https://nextjs.org/) 15 (App Router), [React](https://react.dev/) 19, [TypeScript](https://www.typescriptlang.org/)
|
||||
- **Containerization / Runtime:** [Docker](https://www.docker.com/) (linux/amd64 + linux/arm64), with a shared entrypoint that can bootstrap embedded SeaweedFS before app startup
|
||||
- **Next.js Client:**
|
||||
- **UI:** [Tailwind CSS](https://tailwindcss.com), [Headless UI](https://headlessui.com), [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin)
|
||||
- **Interactions:** `react-dnd`, `react-dropzone`
|
||||
- **Authentication:** [Better Auth](https://www.better-auth.com/) client SDK (`better-auth/react`, anonymous client plugin)
|
||||
- **Local storage/cache:** [Dexie.js](https://dexie.org/) (IndexedDB) for documents, cache, and app settings
|
||||
- **Document rendering:**
|
||||
- **PDF:** [react-pdf](https://github.com/wojtekmaj/react-pdf), [pdf.js](https://mozilla.github.io/pdf.js/)
|
||||
- **EPUB:** [react-reader](https://github.com/gerhardsletten/react-reader), [epubjs](https://github.com/futurepress/epub.js/)
|
||||
- **Markdown/Text:** [react-markdown](https://github.com/remarkjs/react-markdown), [remark-gfm](https://github.com/remarkjs/remark-gfm)
|
||||
- **Text preprocessing / matching:** [compromise](https://github.com/spencermountain/compromise), [cmpstr](https://github.com/remsky/cmpstr)
|
||||
- **Next.js Server:**
|
||||
- **APIs:** Next.js Route Handlers for document sync, blob/content access, migration flows, audiobook export, and TTS/Whisper proxying
|
||||
- **Authentication:** [Better Auth](https://www.better-auth.com/) server handlers/adapters for session and auth routing
|
||||
- **Text preprocessing / NLP utilities:** [compromise](https://github.com/spencermountain/compromise) via shared `lib/nlp` helpers used in server processing paths
|
||||
- **Metadata database:** [Drizzle ORM](https://orm.drizzle.team/), [SQLite](https://www.sqlite.org/) (`better-sqlite3`) by default, optional [PostgreSQL](https://www.postgresql.org/) (`pg`)
|
||||
- **Blob/object storage:** embedded [SeaweedFS](https://github.com/seaweedfs/seaweedfs) (`weed mini`) by default, or external S3-compatible storage via AWS SDK v3 (`@aws-sdk/client-s3`, presigned URLs, upload fallback proxy)
|
||||
- **Audio/processing pipeline:** OpenAI-compatible TTS providers (OpenAI, DeepInfra, Kokoro, Orpheus, custom), [ffmpeg](https://ffmpeg.org/) for audiobook assembly, optional [whisper.cpp](https://github.com/ggerganov/whisper.cpp) for word-level timestamps
|
||||
- **Tooling / Testing:** ESLint, TypeScript, [Playwright](https://playwright.dev/) end-to-end tests, and Drizzle migrations/generation scripts
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
|
|
|
|||
3
docs-site/.gitignore
vendored
Normal file
3
docs-site/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.docusaurus
|
||||
build
|
||||
node_modules
|
||||
134
docs-site/custom.css
Normal file
134
docs-site/custom.css
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
:root {
|
||||
/* OpenReader default light theme colors */
|
||||
--ifm-color-primary: #ef4444;
|
||||
--ifm-color-primary-dark: #dc2626;
|
||||
--ifm-color-primary-darker: #b91c1c;
|
||||
--ifm-color-primary-darkest: #991b1b;
|
||||
--ifm-color-primary-light: #f87171;
|
||||
--ifm-color-primary-lighter: #fca5a5;
|
||||
--ifm-color-primary-lightest: #fecaca;
|
||||
|
||||
--ifm-background-color: #ffffff;
|
||||
--ifm-background-surface-color: #f7fafc;
|
||||
--ifm-navbar-background-color: #f7fafc;
|
||||
--ifm-footer-background-color: #f7fafc;
|
||||
--ifm-font-color-base: #2d3748;
|
||||
--ifm-color-content: #2d3748;
|
||||
--ifm-color-content-secondary: #718096;
|
||||
--ifm-toc-border-color: #e2e8f0;
|
||||
--ifm-hr-background-color: #e2e8f0;
|
||||
--ifm-code-background: #e2e8f0;
|
||||
--ifm-code-font-size: 95%;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
/* OpenReader default dark theme colors */
|
||||
--ifm-color-primary: #f87171;
|
||||
--ifm-color-primary-dark: #ef4444;
|
||||
--ifm-color-primary-darker: #dc2626;
|
||||
--ifm-color-primary-darkest: #b91c1c;
|
||||
--ifm-color-primary-light: #fb8c8c;
|
||||
--ifm-color-primary-lighter: #fca5a5;
|
||||
--ifm-color-primary-lightest: #fecaca;
|
||||
|
||||
--ifm-background-color: #111111;
|
||||
--ifm-background-surface-color: #171717;
|
||||
--ifm-navbar-background-color: #171717;
|
||||
--ifm-footer-background-color: #171717;
|
||||
--ifm-font-color-base: #ededed;
|
||||
--ifm-color-content: #ededed;
|
||||
--ifm-color-content-secondary: #a3a3a3;
|
||||
--ifm-toc-border-color: #343434;
|
||||
--ifm-hr-background-color: #343434;
|
||||
--ifm-code-background: #343434;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
border-bottom: 1px solid var(--ifm-toc-border-color);
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--ifm-toc-border-color);
|
||||
background: var(--ifm-background-surface-color);
|
||||
}
|
||||
|
||||
.footer__title {
|
||||
color: var(--ifm-font-color-base);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.footer__link-item {
|
||||
color: var(--ifm-color-content-secondary);
|
||||
}
|
||||
|
||||
.footer__link-item:hover {
|
||||
color: var(--ifm-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer__copyright {
|
||||
border-top: 1px solid var(--ifm-toc-border-color);
|
||||
color: var(--ifm-color-content-secondary);
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
/* Force override Infima defaults (which use high-specificity selectors). */
|
||||
:root:not(#\#):not(#\#):not(#\#) {
|
||||
--ifm-color-primary: #ef4444 !important;
|
||||
--ifm-color-primary-dark: #dc2626 !important;
|
||||
--ifm-color-primary-darker: #b91c1c !important;
|
||||
--ifm-color-primary-darkest: #991b1b !important;
|
||||
--ifm-color-primary-light: #f87171 !important;
|
||||
--ifm-color-primary-lighter: #fca5a5 !important;
|
||||
--ifm-color-primary-lightest: #fecaca !important;
|
||||
|
||||
--ifm-background-color: #ffffff !important;
|
||||
--ifm-background-surface-color: #f7fafc !important;
|
||||
--ifm-navbar-background-color: #f7fafc !important;
|
||||
--ifm-footer-background-color: #f7fafc !important;
|
||||
--ifm-font-color-base: #2d3748 !important;
|
||||
--ifm-color-content: #2d3748 !important;
|
||||
--ifm-color-content-secondary: #718096 !important;
|
||||
--ifm-toc-border-color: #e2e8f0 !important;
|
||||
--ifm-hr-background-color: #e2e8f0 !important;
|
||||
--ifm-code-background: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
html[data-theme='dark']:not(#\#):not(#\#):not(#\#) {
|
||||
--ifm-color-primary: #f87171 !important;
|
||||
--ifm-color-primary-dark: #ef4444 !important;
|
||||
--ifm-color-primary-darker: #dc2626 !important;
|
||||
--ifm-color-primary-darkest: #b91c1c !important;
|
||||
--ifm-color-primary-light: #fb8c8c !important;
|
||||
--ifm-color-primary-lighter: #fca5a5 !important;
|
||||
--ifm-color-primary-lightest: #fecaca !important;
|
||||
|
||||
--ifm-background-color: #111111 !important;
|
||||
--ifm-background-surface-color: #171717 !important;
|
||||
--ifm-navbar-background-color: #171717 !important;
|
||||
--ifm-footer-background-color: #171717 !important;
|
||||
--ifm-font-color-base: #ededed !important;
|
||||
--ifm-color-content: #ededed !important;
|
||||
--ifm-color-content-secondary: #a3a3a3 !important;
|
||||
--ifm-toc-border-color: #343434 !important;
|
||||
--ifm-hr-background-color: #343434 !important;
|
||||
--ifm-code-background: #343434 !important;
|
||||
}
|
||||
|
||||
/* Search/Ask-AI plugin accents should follow OpenReader accent colors. */
|
||||
:root:not(#\#):not(#\#):not(#\#) {
|
||||
--search-local-highlight-color: var(--ifm-color-primary) !important;
|
||||
--search-local-hit-active-color: var(--ifm-color-primary) !important;
|
||||
--search-local-input-active-border-color: var(--ifm-color-primary) !important;
|
||||
}
|
||||
|
||||
.ask-ai:not(#\#):not(#\#) {
|
||||
--ask-ai-primary: var(--ifm-color-primary) !important;
|
||||
--ask-ai-primary-hover: var(--ifm-color-primary-dark) !important;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] .ask-ai:not(#\#):not(#\#) {
|
||||
--ask-ai-primary: var(--ifm-color-primary) !important;
|
||||
--ask-ai-primary-hover: var(--ifm-color-primary-dark) !important;
|
||||
}
|
||||
16
docs-site/docs/community/acknowledgements.md
Normal file
16
docs-site/docs/community/acknowledgements.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
title: Acknowledgements
|
||||
---
|
||||
|
||||
This project is built with support from the following open-source projects and tools:
|
||||
|
||||
- [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M)
|
||||
- [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI)
|
||||
- [Better Auth](https://www.better-auth.com/)
|
||||
- [SQLite](https://www.sqlite.org/)
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
- [SeaweedFS](https://github.com/seaweedfs/seaweedfs)
|
||||
- [whisper.cpp](https://github.com/ggerganov/whisper.cpp)
|
||||
- [ffmpeg](https://ffmpeg.org)
|
||||
- [react-pdf](https://github.com/wojtekmaj/react-pdf)
|
||||
- [react-reader](https://github.com/happyr/react-reader)
|
||||
7
docs-site/docs/community/license.md
Normal file
7
docs-site/docs/community/license.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: License
|
||||
---
|
||||
|
||||
OpenReader WebUI is licensed under the MIT License.
|
||||
|
||||
- Repository license file: [LICENSE](https://github.com/richardr1126/OpenReader-WebUI/blob/main/LICENSE)
|
||||
19
docs-site/docs/community/support.md
Normal file
19
docs-site/docs/community/support.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: Support and Contributing
|
||||
---
|
||||
|
||||
## Feature requests
|
||||
|
||||
Use [GitHub Discussions](https://github.com/richardr1126/OpenReader-WebUI/discussions) for feature requests and ideas.
|
||||
|
||||
## Issues and support
|
||||
|
||||
If you encounter a bug, open an issue in [GitHub Issues](https://github.com/richardr1126/OpenReader-WebUI/issues).
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome.
|
||||
|
||||
- Fork the repository
|
||||
- Create your branch
|
||||
- Open a pull request with your changes
|
||||
73
docs-site/docs/getting-started/docker-quick-start.md
Normal file
73
docs-site/docs/getting-started/docker-quick-start.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
title: Docker Quick Start
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A recent Docker version installed
|
||||
- A TTS API server that OpenReader can reach (Kokoro-FastAPI, Orpheus-FastAPI, DeepInfra, OpenAI, or equivalent)
|
||||
|
||||
:::note
|
||||
If you have suitable hardware, you can run Kokoro locally with Docker. See [Kokoro-FastAPI](../integrations/kokoro-fastapi).
|
||||
:::
|
||||
|
||||
## 1. Start the Docker container
|
||||
|
||||
Minimal setup (auth disabled, embedded storage ephemeral, no library import):
|
||||
|
||||
```bash
|
||||
docker run --name openreader-webui \
|
||||
--restart unless-stopped \
|
||||
-p 3003:3003 \
|
||||
-p 8333:8333 \
|
||||
ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
|
||||
Fully featured setup (persistent storage, embedded SeaweedFS `weed mini`, optional auth):
|
||||
|
||||
```bash
|
||||
docker run --name openreader-webui \
|
||||
--restart unless-stopped \
|
||||
-p 3003:3003 \
|
||||
-p 8333:8333 \
|
||||
-v openreader_docstore:/app/docstore \
|
||||
-v /path/to/your/library:/app/docstore/library:ro \
|
||||
-e API_BASE=http://host.docker.internal:8880/v1 \
|
||||
-e API_KEY=none \
|
||||
-e BASE_URL=http://localhost:3003 \
|
||||
-e AUTH_SECRET=<paste_the_output_of_openssl_here> \
|
||||
ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
|
||||
You can remove `/app/docstore/library` if you do not need server library import.
|
||||
You can remove either `BASE_URL` or `AUTH_SECRET` to keep auth disabled.
|
||||
|
||||
Quick notes:
|
||||
|
||||
- `API_BASE` should point to your TTS server base URL.
|
||||
- Expose `8333` for direct browser access to embedded SeaweedFS presigned URLs.
|
||||
- If `8333` is not exposed, uploads still work through `/api/documents/blob/upload/fallback`.
|
||||
- To enable auth, set both `BASE_URL` and `AUTH_SECRET`.
|
||||
- DB migrations run automatically during container startup via the shared entrypoint.
|
||||
|
||||
For all environment variables, see [Environment Variables](../guides/environment-variables).
|
||||
For app/auth behavior, see [Auth](../guides/configuration).
|
||||
For database startup and migration behavior, see [SQL Database](../operations/database-and-migrations).
|
||||
For blob behavior and mounts, see [Object / Blob Storage](../guides/storage-and-blob-behavior).
|
||||
|
||||
## 2. Configure settings in the app UI
|
||||
|
||||
- Set TTS provider and model in Settings
|
||||
- Set TTS API base URL and API key if needed
|
||||
- Select the model voice from the voice dropdown
|
||||
|
||||
## 3. Update Docker image
|
||||
|
||||
```bash
|
||||
docker stop openreader-webui || true && \
|
||||
docker rm openreader-webui || true && \
|
||||
docker image rm ghcr.io/richardr1126/openreader-webui:latest || true && \
|
||||
docker pull ghcr.io/richardr1126/openreader-webui:latest
|
||||
```
|
||||
|
||||
Visit [http://localhost:3003](http://localhost:3003) after startup.
|
||||
117
docs-site/docs/getting-started/local-development.md
Normal file
117
docs-site/docs/getting-started/local-development.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
---
|
||||
title: Local Development
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js (recommended with [nvm](https://github.com/nvm-sh/nvm))
|
||||
- `pnpm` (recommended) or `npm`
|
||||
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
- A reachable TTS API server
|
||||
- [SeaweedFS](https://github.com/seaweedfs/seaweedfs) `weed` binary (required)
|
||||
|
||||
```bash
|
||||
brew install seaweedfs
|
||||
```
|
||||
|
||||
Optional, depending on features:
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org) (required for `m4b` audiobook generation)
|
||||
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
- [libreoffice](https://www.libreoffice.org) (required for DOCX conversion)
|
||||
|
||||
```bash
|
||||
brew install libreoffice
|
||||
```
|
||||
|
||||
- [whisper.cpp](https://github.com/ggml-org/whisper.cpp) (optional, for word-by-word highlighting)
|
||||
|
||||
```bash
|
||||
# clone and build whisper.cpp (no model download needed – OpenReader handles that)
|
||||
git clone https://github.com/ggml-org/whisper.cpp.git
|
||||
cd whisper.cpp
|
||||
cmake -B build
|
||||
cmake --build build -j --config Release
|
||||
|
||||
# point OpenReader to the compiled whisper-cli binary
|
||||
echo WHISPER_CPP_BIN="$(pwd)/build/bin/whisper-cli"
|
||||
```
|
||||
|
||||
:::note
|
||||
Set `WHISPER_CPP_BIN` in your `.env` to enable word-by-word highlighting.
|
||||
:::
|
||||
|
||||
## Steps
|
||||
|
||||
1. Clone the repository.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/richardr1126/OpenReader-WebUI.git
|
||||
cd OpenReader-WebUI
|
||||
```
|
||||
|
||||
2. Install dependencies.
|
||||
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
|
||||
3. Configure the environment.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Then edit `.env`.
|
||||
|
||||
Auth is enabled when both are set:
|
||||
|
||||
- `BASE_URL` (for local dev, typically `http://localhost:3003`)
|
||||
- `AUTH_SECRET` (generate with `openssl rand -base64 32`)
|
||||
|
||||
Optional:
|
||||
|
||||
- `AUTH_TRUSTED_ORIGINS=http://localhost:3003,http://192.168.0.116:3003`
|
||||
- Stable S3 credentials via `S3_ACCESS_KEY_ID` and `S3_SECRET_ACCESS_KEY`
|
||||
- External S3 storage by setting `USE_EMBEDDED_WEED_MINI=false` and related S3 vars
|
||||
|
||||
For all environment variables, see [Environment Variables](../guides/environment-variables).
|
||||
For app/auth behavior, see [Auth](../guides/configuration).
|
||||
For storage configuration, see [Object / Blob Storage](../guides/storage-and-blob-behavior).
|
||||
For database mode and migrations, see [SQL Database](../operations/database-and-migrations).
|
||||
|
||||
4. Run DB migrations.
|
||||
|
||||
- Migrations run automatically on startup through the shared entrypoint for both `pnpm dev` and `pnpm start`.
|
||||
- You only need manual migration commands for one-off troubleshooting or explicit migration workflows:
|
||||
|
||||
```bash
|
||||
pnpm migrate
|
||||
```
|
||||
|
||||
:::note
|
||||
If `POSTGRES_URL` is set, migrations target Postgres; otherwise local SQLite is used. To disable automatic startup migrations, set `RUN_DB_MIGRATIONS=false`.
|
||||
:::
|
||||
|
||||
5. Start the app.
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Or build + start production mode:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start
|
||||
```
|
||||
|
||||
Visit [http://localhost:3003](http://localhost:3003).
|
||||
19
docs-site/docs/guides/configuration.md
Normal file
19
docs-site/docs/guides/configuration.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: Auth
|
||||
---
|
||||
|
||||
This page covers application-level configuration for provider access and authentication.
|
||||
|
||||
## Auth behavior
|
||||
|
||||
- Auth is enabled only when both `BASE_URL` and `AUTH_SECRET` are set.
|
||||
- Remove either value to disable auth.
|
||||
- Keep `AUTH_TRUSTED_ORIGINS` empty to trust only `BASE_URL`.
|
||||
|
||||
## Related docs
|
||||
|
||||
- For the complete variable reference: [Environment Variables](./environment-variables)
|
||||
- For TTS character limits and quota behavior: [TTS Rate Limiting](./tts-rate-limiting)
|
||||
- For provider-specific guidance: [TTS Providers](./tts-providers)
|
||||
- For storage/S3/SeaweedFS behavior: [Object / Blob Storage](./storage-and-blob-behavior)
|
||||
- For database mode and migration commands: [SQL Database](../operations/database-and-migrations)
|
||||
280
docs-site/docs/guides/environment-variables.md
Normal file
280
docs-site/docs/guides/environment-variables.md
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
---
|
||||
title: Environment Variables
|
||||
toc_max_heading_level: 3
|
||||
---
|
||||
|
||||
This is the single reference page for OpenReader WebUI environment variables.
|
||||
|
||||
## Quick Reference Table
|
||||
|
||||
| Variable | Area | Default | When to set |
|
||||
| --- | --- | --- | --- |
|
||||
| `NEXT_PUBLIC_NODE_ENV` | Runtime mode | `development` | Set `production` for production builds |
|
||||
| `API_BASE` | TTS provider | none | Point to your OpenAI-compatible TTS base URL |
|
||||
| `API_KEY` | TTS provider | `none` fallback in TTS route | Set when provider requires auth |
|
||||
| `TTS_CACHE_MAX_SIZE_BYTES` | TTS caching | `268435456` (256 MB) | Tune in-memory TTS cache size |
|
||||
| `TTS_CACHE_TTL_MS` | TTS caching | `1800000` (30 min) | Tune in-memory TTS cache TTL |
|
||||
| `TTS_MAX_RETRIES` | TTS retry | `2` | Tune retry attempts for upstream 429/5xx |
|
||||
| `TTS_RETRY_INITIAL_MS` | TTS retry | `250` | Tune initial retry delay |
|
||||
| `TTS_RETRY_MAX_MS` | TTS retry | `2000` | Tune max retry delay |
|
||||
| `TTS_RETRY_BACKOFF` | TTS retry | `2` | Tune exponential backoff factor |
|
||||
| `TTS_ENABLE_RATE_LIMIT` | Rate limiting | `false` | Set `true` to enable TTS per-user/IP daily character limits |
|
||||
| `TTS_DAILY_LIMIT_ANONYMOUS` | Rate limiting | `50000` | Override anonymous per-user daily character limit |
|
||||
| `TTS_DAILY_LIMIT_AUTHENTICATED` | Rate limiting | `500000` | Override authenticated per-user daily character limit |
|
||||
| `TTS_IP_DAILY_LIMIT_ANONYMOUS` | Rate limiting | `100000` | Override anonymous IP backstop daily limit |
|
||||
| `TTS_IP_DAILY_LIMIT_AUTHENTICATED` | Rate limiting | `1000000` | Override authenticated IP backstop daily limit |
|
||||
| `WHISPER_CPP_BIN` | Word timing | unset | Set to enable `whisper.cpp` timestamps |
|
||||
| `BASE_URL` | Auth | unset | Required (with `AUTH_SECRET`) to enable auth |
|
||||
| `AUTH_SECRET` | Auth | unset | Required (with `BASE_URL`) to enable auth |
|
||||
| `AUTH_TRUSTED_ORIGINS` | Auth | empty | Add extra allowed origins |
|
||||
| `GITHUB_CLIENT_ID` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_SECRET` to enable GitHub sign-in |
|
||||
| `GITHUB_CLIENT_SECRET` | Auth/OAuth | unset | Set with `GITHUB_CLIENT_ID` to enable GitHub sign-in |
|
||||
| `DISABLE_AUTH_RATE_LIMIT` | Rate limiting | `false` | Set `true` to disable auth-layer rate limiting |
|
||||
| `POSTGRES_URL` | Database | unset (SQLite mode) | Set to switch metadata/auth DB to Postgres |
|
||||
| `RUN_DB_MIGRATIONS` | Database | `true` | Set `false` to skip startup migrations |
|
||||
| `USE_EMBEDDED_WEED_MINI` | Storage | `true` when unset | Set `false` to use external S3-compatible storage only |
|
||||
| `WEED_MINI_DIR` | Storage | `docstore/seaweedfs` | Override embedded SeaweedFS data directory |
|
||||
| `WEED_MINI_WAIT_SEC` | Storage | `20` | Tune SeaweedFS startup wait timeout |
|
||||
| `S3_ACCESS_KEY_ID` | Storage | auto-generated in embedded mode | Set explicitly for stable/external credentials |
|
||||
| `S3_SECRET_ACCESS_KEY` | Storage | auto-generated in embedded mode | Set explicitly for stable/external credentials |
|
||||
| `S3_BUCKET` | Storage | `openreader-documents` in embedded mode | Required for external S3-compatible storage |
|
||||
| `S3_REGION` | Storage | `us-east-1` in embedded mode | Required for external S3-compatible storage |
|
||||
| `S3_ENDPOINT` | Storage | derived in embedded mode | Set for S3-compatible providers (MinIO/SeaweedFS/R2/etc.) |
|
||||
| `S3_FORCE_PATH_STYLE` | Storage | `true` in embedded mode | Set per provider requirement |
|
||||
| `S3_PREFIX` | Storage | `openreader` | Customize object key prefix |
|
||||
| `IMPORT_LIBRARY_DIR` | Library import | `docstore/library` fallback | Set a single server library root |
|
||||
| `IMPORT_LIBRARY_DIRS` | Library import | unset | Set multiple roots (comma/colon/semicolon separated) |
|
||||
|
||||
## Detailed Reference
|
||||
|
||||
### NEXT_PUBLIC_NODE_ENV
|
||||
|
||||
Controls development vs production behavior in client/server code paths.
|
||||
|
||||
- Typical values: `development`, `production`
|
||||
- In production builds, set `production`
|
||||
|
||||
### API_BASE
|
||||
|
||||
Base URL for OpenAI-compatible TTS API requests.
|
||||
|
||||
- Example: `http://host.docker.internal:8880/v1`
|
||||
- Can be overridden per request from UI settings
|
||||
|
||||
### API_KEY
|
||||
|
||||
Default API key for TTS provider requests.
|
||||
|
||||
- Example: `none` or your provider token
|
||||
- Can be overridden by request headers from app settings
|
||||
|
||||
### TTS_CACHE_MAX_SIZE_BYTES
|
||||
|
||||
Maximum in-memory TTS audio cache size in bytes.
|
||||
|
||||
- Default: `268435456` (256 MB)
|
||||
|
||||
### TTS_CACHE_TTL_MS
|
||||
|
||||
In-memory TTS audio cache TTL in milliseconds.
|
||||
|
||||
- Default: `1800000` (30 minutes)
|
||||
|
||||
### TTS_MAX_RETRIES
|
||||
|
||||
Maximum retries for upstream TTS failures (429/5xx).
|
||||
|
||||
- Default: `2`
|
||||
|
||||
### TTS_RETRY_INITIAL_MS
|
||||
|
||||
Initial retry delay in milliseconds for TTS upstream requests.
|
||||
|
||||
- Default: `250`
|
||||
|
||||
### TTS_RETRY_MAX_MS
|
||||
|
||||
Maximum retry delay in milliseconds.
|
||||
|
||||
- Default: `2000`
|
||||
|
||||
### TTS_RETRY_BACKOFF
|
||||
|
||||
Exponential backoff multiplier between retries.
|
||||
|
||||
- Default: `2`
|
||||
|
||||
### TTS_ENABLE_RATE_LIMIT
|
||||
|
||||
Controls TTS character rate limiting in the TTS API.
|
||||
|
||||
- Default: `false` (TTS char limits disabled)
|
||||
- Set to `true` to enforce `TTS_DAILY_LIMIT_*` and `TTS_IP_DAILY_LIMIT_*`
|
||||
- For behavior details and examples, see [TTS Rate Limiting](./tts-rate-limiting)
|
||||
|
||||
### TTS_DAILY_LIMIT_ANONYMOUS
|
||||
|
||||
Anonymous per-user daily character limit.
|
||||
|
||||
- Default: `50000`
|
||||
|
||||
### TTS_DAILY_LIMIT_AUTHENTICATED
|
||||
|
||||
Authenticated per-user daily character limit.
|
||||
|
||||
- Default: `500000`
|
||||
|
||||
### TTS_IP_DAILY_LIMIT_ANONYMOUS
|
||||
|
||||
Anonymous IP backstop daily character limit.
|
||||
|
||||
- Default: `100000`
|
||||
|
||||
### TTS_IP_DAILY_LIMIT_AUTHENTICATED
|
||||
|
||||
Authenticated IP backstop daily character limit.
|
||||
|
||||
- Default: `1000000`
|
||||
|
||||
### WHISPER_CPP_BIN
|
||||
|
||||
Absolute path to compiled `whisper.cpp` binary for word-level timestamps.
|
||||
|
||||
- Example: `/whisper.cpp/build/bin/whisper-cli`
|
||||
- Required only for optional word-by-word highlighting
|
||||
|
||||
### BASE_URL
|
||||
|
||||
External base URL for this OpenReader instance.
|
||||
|
||||
- Required with `AUTH_SECRET` to enable auth
|
||||
- Example: `http://localhost:3003` or `https://reader.example.com`
|
||||
|
||||
### AUTH_SECRET
|
||||
|
||||
Secret key used by auth/session handling.
|
||||
|
||||
- Required with `BASE_URL` to enable auth
|
||||
- Generate with `openssl rand -base64 32`
|
||||
|
||||
### AUTH_TRUSTED_ORIGINS
|
||||
|
||||
Additional allowed origins for auth requests.
|
||||
|
||||
- Comma-separated list
|
||||
- `BASE_URL` origin is always trusted automatically
|
||||
|
||||
### GITHUB_CLIENT_ID
|
||||
|
||||
GitHub OAuth client ID.
|
||||
|
||||
- Enable only with `GITHUB_CLIENT_SECRET`
|
||||
|
||||
### GITHUB_CLIENT_SECRET
|
||||
|
||||
GitHub OAuth client secret.
|
||||
|
||||
- Enable only with `GITHUB_CLIENT_ID`
|
||||
|
||||
### DISABLE_AUTH_RATE_LIMIT
|
||||
|
||||
Controls Better Auth rate limiting.
|
||||
|
||||
- Default behavior: auth-layer rate limiting enabled
|
||||
- Set to `true` to disable auth-layer rate limiting
|
||||
- This does not affect TTS character rate limiting
|
||||
|
||||
### POSTGRES_URL
|
||||
|
||||
Switches metadata/auth storage from SQLite to Postgres.
|
||||
|
||||
- Unset: SQLite at `docstore/sqlite3.db`
|
||||
- Set: Postgres mode
|
||||
|
||||
### RUN_DB_MIGRATIONS
|
||||
|
||||
Controls startup migration execution in shared entrypoint.
|
||||
|
||||
- Default: `true`
|
||||
- Set `false` to skip automatic startup migrations
|
||||
|
||||
### USE_EMBEDDED_WEED_MINI
|
||||
|
||||
Controls embedded SeaweedFS startup.
|
||||
|
||||
- Default behavior: treated as enabled when unset
|
||||
- Set `false` to rely on external S3-compatible storage
|
||||
|
||||
### WEED_MINI_DIR
|
||||
|
||||
Data directory for embedded SeaweedFS (`weed mini`).
|
||||
|
||||
- Default: `docstore/seaweedfs`
|
||||
|
||||
### WEED_MINI_WAIT_SEC
|
||||
|
||||
Maximum seconds to wait for embedded SeaweedFS startup.
|
||||
|
||||
- Default: `20`
|
||||
|
||||
### S3_ACCESS_KEY_ID
|
||||
|
||||
Access key for S3-compatible storage.
|
||||
|
||||
- Auto-generated in embedded mode if unset
|
||||
- Set explicitly for stable credentials or external providers
|
||||
|
||||
### S3_SECRET_ACCESS_KEY
|
||||
|
||||
Secret key for S3-compatible storage.
|
||||
|
||||
- Auto-generated in embedded mode if unset
|
||||
- Set explicitly for stable credentials or external providers
|
||||
|
||||
### S3_BUCKET
|
||||
|
||||
Bucket name used for document blobs.
|
||||
|
||||
- Default in embedded mode: `openreader-documents`
|
||||
- Required for external S3-compatible storage
|
||||
|
||||
### S3_REGION
|
||||
|
||||
Region used by the S3 client.
|
||||
|
||||
- Default in embedded mode: `us-east-1`
|
||||
|
||||
### S3_ENDPOINT
|
||||
|
||||
Endpoint URL for S3-compatible storage.
|
||||
|
||||
- In embedded mode, defaults to `http://<BASE_URL host>:8333` (or detected host)
|
||||
- For AWS S3, usually leave unset
|
||||
- For MinIO/SeaweedFS/R2/B2-style APIs, typically set explicitly
|
||||
|
||||
### S3_FORCE_PATH_STYLE
|
||||
|
||||
Path-style S3 addressing toggle.
|
||||
|
||||
- Default in embedded mode: `true`
|
||||
- Set according to provider requirements
|
||||
|
||||
### S3_PREFIX
|
||||
|
||||
Prefix prepended to stored object keys.
|
||||
|
||||
- Default: `openreader`
|
||||
|
||||
### IMPORT_LIBRARY_DIR
|
||||
|
||||
Single directory root for server library import.
|
||||
|
||||
- Used when `IMPORT_LIBRARY_DIRS` is unset
|
||||
- Default fallback root: `docstore/library`
|
||||
|
||||
### IMPORT_LIBRARY_DIRS
|
||||
|
||||
Multiple library roots for server library import.
|
||||
|
||||
- Separator: comma, colon, or semicolon
|
||||
- Takes precedence over `IMPORT_LIBRARY_DIR`
|
||||
44
docs-site/docs/guides/storage-and-blob-behavior.md
Normal file
44
docs-site/docs/guides/storage-and-blob-behavior.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
title: Object / Blob Storage
|
||||
---
|
||||
|
||||
This page documents storage backends, blob upload routing, and Docker mount behavior.
|
||||
|
||||
## Storage backends
|
||||
|
||||
- Default: embedded SQLite metadata + embedded SeaweedFS (`weed mini`) blobs
|
||||
- External option: Postgres + external S3-compatible object storage
|
||||
|
||||
Storage variables are documented in [Environment Variables](./environment-variables) under the storage sections.
|
||||
|
||||
## Ports
|
||||
|
||||
- `3003`: OpenReader app and API routes
|
||||
- `8333`: Embedded SeaweedFS S3 endpoint for direct browser blob access
|
||||
|
||||
## Upload behavior
|
||||
|
||||
- Primary path: browser uploads to presigned URL from `/api/documents/blob/upload/presign`
|
||||
- Fallback path: `/api/documents/blob/upload/fallback` if direct upload fails or endpoint is unreachable
|
||||
- Content serving path: `/api/documents/blob`
|
||||
|
||||
## Recommended Docker mounts
|
||||
|
||||
| Mount | Type | Recommended | Purpose | Example |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `/app/docstore` | Docker named volume | Yes (for persistence) | Persists SeaweedFS blob data, SQLite metadata DB, audiobook artifacts, migration/runtime files | `-v openreader_docstore:/app/docstore` |
|
||||
| `/app/docstore/library` | Bind mount | Optional + `:ro` | Read-only source for server library import (files are copied/imported into client storage) | `-v /path/to/your/library:/app/docstore/library:ro` |
|
||||
|
||||
To import from mounted library: **Settings -> Documents -> Server Library Import**.
|
||||
|
||||
:::note
|
||||
Every file in the mounted library is imported into client browser storage. Keep the library reasonably sized.
|
||||
:::
|
||||
|
||||
## Private blob endpoint mode
|
||||
|
||||
If `8333` is not published externally:
|
||||
|
||||
- Document uploads still work through upload fallback proxy
|
||||
- Reads/snippets continue through app API routes
|
||||
- Direct presigned browser upload/download to embedded endpoint is unavailable
|
||||
48
docs-site/docs/guides/tts-providers.md
Normal file
48
docs-site/docs/guides/tts-providers.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
title: TTS Providers
|
||||
---
|
||||
|
||||
OpenReader WebUI supports OpenAI-compatible TTS providers through a common API shape.
|
||||
|
||||
## Supported provider patterns
|
||||
|
||||
- OpenAI API
|
||||
- DeepInfra
|
||||
- Kokoro-FastAPI
|
||||
- Orpheus-FastAPI
|
||||
- Custom OpenAI-compatible endpoints
|
||||
|
||||
## Provider dropdown behavior
|
||||
|
||||
In Settings, the provider dropdown includes:
|
||||
|
||||
- `OpenAI`
|
||||
- `Deepinfra`
|
||||
- `Custom OpenAI-Like` (for Kokoro, Orpheus, and other compatible endpoints)
|
||||
|
||||
`API_BASE` guidance:
|
||||
|
||||
- For `OpenAI` and `Deepinfra`, OpenReader auto-fills the default endpoint.
|
||||
- For `Custom OpenAI-Like`, set `API_BASE` to your server endpoint.
|
||||
- In practice, you usually only set `API_BASE` when using a provider/endpoint that is not directly covered by the built-in dropdown defaults.
|
||||
|
||||
## Custom provider compatibility
|
||||
|
||||
For custom providers, OpenReader expects these endpoints:
|
||||
|
||||
- `GET /v1/audio/voices`
|
||||
- `POST /v1/audio/speech`
|
||||
|
||||
If your provider exposes this interface, it can be used as an OpenAI-compatible TTS backend.
|
||||
|
||||
## Setup flow
|
||||
|
||||
1. Select your provider in the OpenReader Settings modal.
|
||||
2. If using `Custom OpenAI-Like` (or overriding a default), set `API_BASE`.
|
||||
3. Set `API_KEY` if required by your provider.
|
||||
4. Choose model and voice.
|
||||
|
||||
For environment variables, see [Environment Variables](./environment-variables).
|
||||
For TTS quota behavior, see [TTS Rate Limiting](./tts-rate-limiting).
|
||||
For auth behavior, see [Auth](./configuration).
|
||||
For provider-specific integration guides, see [Kokoro-FastAPI](../integrations/kokoro-fastapi), [Orpheus-FastAPI](../integrations/orpheus-fastapi), [Deepinfra](../integrations/deepinfra), [OpenAI](../integrations/openai), and [Custom OpenAI](../integrations/custom-openai).
|
||||
51
docs-site/docs/guides/tts-rate-limiting.md
Normal file
51
docs-site/docs/guides/tts-rate-limiting.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: TTS Rate Limiting
|
||||
---
|
||||
|
||||
This page explains OpenReader's TTS character rate limiting controls.
|
||||
|
||||
## Overview
|
||||
|
||||
- TTS rate limiting is disabled by default.
|
||||
- To enable it, set `TTS_ENABLE_RATE_LIMIT=true`.
|
||||
- Limits are enforced per day in UTC.
|
||||
- Enforcement applies only when auth is enabled.
|
||||
|
||||
## How enforcement works
|
||||
|
||||
When enabled, OpenReader enforces:
|
||||
|
||||
- Per-user daily character limits.
|
||||
- IP backstop daily character limits.
|
||||
- Anonymous device backstop tracking (cookie-based) to reduce limit resets.
|
||||
|
||||
If a request exceeds the active limit, the TTS API returns `429` with reset metadata for the next UTC day.
|
||||
|
||||
## Required auth behavior
|
||||
|
||||
- Auth must be enabled (`BASE_URL` + `AUTH_SECRET`) for TTS char limits to apply.
|
||||
- If auth is disabled, TTS character limits are effectively unlimited.
|
||||
- `DISABLE_AUTH_RATE_LIMIT` only affects Better Auth's own request throttling.
|
||||
- `DISABLE_AUTH_RATE_LIMIT` does not disable TTS character limits.
|
||||
|
||||
## Environment variables
|
||||
|
||||
Enable/disable:
|
||||
|
||||
- `TTS_ENABLE_RATE_LIMIT` (default: `false`)
|
||||
|
||||
Per-user daily limits:
|
||||
|
||||
- `TTS_DAILY_LIMIT_ANONYMOUS` (default: `50000`)
|
||||
- `TTS_DAILY_LIMIT_AUTHENTICATED` (default: `500000`)
|
||||
|
||||
IP backstop daily limits:
|
||||
|
||||
- `TTS_IP_DAILY_LIMIT_ANONYMOUS` (default: `100000`)
|
||||
- `TTS_IP_DAILY_LIMIT_AUTHENTICATED` (default: `1000000`)
|
||||
|
||||
## Related docs
|
||||
|
||||
- Full variable list: [Environment Variables](./environment-variables)
|
||||
- Auth configuration: [Auth](./configuration)
|
||||
- Provider setup: [TTS Providers](./tts-providers)
|
||||
28
docs-site/docs/integrations/custom-openai.md
Normal file
28
docs-site/docs/integrations/custom-openai.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
title: Custom OpenAI
|
||||
---
|
||||
|
||||
Use any custom OpenAI-compatible TTS service with OpenReader.
|
||||
|
||||
Use this integration when your endpoint is not directly covered by built-in dropdown defaults.
|
||||
|
||||
## Compatibility requirements
|
||||
|
||||
Your provider should expose:
|
||||
|
||||
- `GET /v1/audio/voices`
|
||||
- `POST /v1/audio/speech`
|
||||
|
||||
## OpenReader setup
|
||||
|
||||
1. In OpenReader settings, choose provider `Custom OpenAI-Like`.
|
||||
2. Pick model `Kokoro`, `Orpheus`, or `Other` as appropriate.
|
||||
3. Set `API_BASE` to your service base URL.
|
||||
4. Set `API_KEY` if required by your service.
|
||||
5. Choose voice.
|
||||
|
||||
## Notes
|
||||
|
||||
- `API_BASE` is required for this provider path because OpenReader cannot infer your custom host.
|
||||
- If voices do not load, verify the `/v1/audio/voices` response format.
|
||||
- For variable details, see [Environment Variables](../guides/environment-variables).
|
||||
19
docs-site/docs/integrations/deepinfra.md
Normal file
19
docs-site/docs/integrations/deepinfra.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: Deepinfra
|
||||
---
|
||||
|
||||
Use Deepinfra as a hosted OpenAI-compatible TTS provider.
|
||||
|
||||
## OpenReader setup
|
||||
|
||||
1. In OpenReader settings, choose provider `Deepinfra`.
|
||||
2. Leave `API_BASE` unset unless you want to override the default endpoint.
|
||||
- Default value: `https://api.deepinfra.com/v1/openai`
|
||||
3. Set `API_KEY` to your Deepinfra API key if needed.
|
||||
4. Choose model and voice.
|
||||
|
||||
## Notes
|
||||
|
||||
- `Deepinfra` is a built-in provider in the dropdown, so `API_BASE` is usually not required.
|
||||
- Deepinfra supports multiple TTS models, including Kokoro-family options.
|
||||
- For variable details, see [Environment Variables](../guides/environment-variables).
|
||||
49
docs-site/docs/integrations/kokoro-fastapi.md
Normal file
49
docs-site/docs/integrations/kokoro-fastapi.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: Kokoro-FastAPI
|
||||
---
|
||||
|
||||
You can run the Kokoro TTS API server directly with Docker.
|
||||
|
||||
:::warning
|
||||
For Kokoro issues and support, use the upstream repository: [remsky/Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI).
|
||||
:::
|
||||
|
||||
## CPU image
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name kokoro-tts \
|
||||
--restart unless-stopped \
|
||||
-p 8880:8880 \
|
||||
-e ONNX_NUM_THREADS=8 \
|
||||
-e ONNX_INTER_OP_THREADS=4 \
|
||||
-e ONNX_EXECUTION_MODE=parallel \
|
||||
-e ONNX_OPTIMIZATION_LEVEL=all \
|
||||
-e ONNX_MEMORY_PATTERN=true \
|
||||
-e ONNX_ARENA_EXTEND_STRATEGY=kNextPowerOfTwo \
|
||||
-e API_LOG_LEVEL=DEBUG \
|
||||
ghcr.io/remsky/kokoro-fastapi-cpu:v0.2.4
|
||||
```
|
||||
|
||||
## GPU image
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name kokoro-tts \
|
||||
--gpus all \
|
||||
--user 1001:1001 \
|
||||
--restart unless-stopped \
|
||||
-p 8880:8880 \
|
||||
-e USE_GPU=true \
|
||||
-e PYTHONUNBUFFERED=1 \
|
||||
-e API_LOG_LEVEL=DEBUG \
|
||||
ghcr.io/remsky/kokoro-fastapi-gpu:v0.2.4
|
||||
```
|
||||
|
||||
## OpenReader integration notes
|
||||
|
||||
- In OpenReader settings, choose provider `Custom OpenAI-Like` and model `Kokoro`.
|
||||
- Set OpenReader `API_BASE` to your Kokoro endpoint (for Docker Compose, commonly `http://kokoro-tts:8880/v1`).
|
||||
- `API_BASE` is needed here because Kokoro is used via the custom provider path, not a built-in provider endpoint.
|
||||
- GPU mode requires NVIDIA Docker support and is best on NVIDIA hardware.
|
||||
- CPU mode works best on Apple Silicon or modern x86 CPUs.
|
||||
18
docs-site/docs/integrations/openai.md
Normal file
18
docs-site/docs/integrations/openai.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
title: OpenAI
|
||||
---
|
||||
|
||||
Use OpenAI directly as an OpenAI-compatible TTS provider.
|
||||
|
||||
## OpenReader setup
|
||||
|
||||
1. In OpenReader settings, choose provider `OpenAI`.
|
||||
2. Leave `API_BASE` unset unless you want to override the default `https://api.openai.com/v1`.
|
||||
3. Set `API_KEY` to your OpenAI API key.
|
||||
4. Choose model and voice.
|
||||
|
||||
## Notes
|
||||
|
||||
- `OpenAI` is a built-in provider in the dropdown, so `API_BASE` is usually not required.
|
||||
- OpenReader routes TTS calls through its server API.
|
||||
- For variable details, see [Environment Variables](../guides/environment-variables).
|
||||
23
docs-site/docs/integrations/orpheus-fastapi.md
Normal file
23
docs-site/docs/integrations/orpheus-fastapi.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
title: Orpheus-FastAPI
|
||||
---
|
||||
|
||||
Use Orpheus-FastAPI as an OpenAI-compatible TTS backend for OpenReader.
|
||||
|
||||
## Upstream project
|
||||
|
||||
- [Lex-au/Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
|
||||
|
||||
## OpenReader setup
|
||||
|
||||
1. Start your Orpheus-FastAPI server.
|
||||
2. In OpenReader settings, choose provider `Custom OpenAI-Like` and model `Orpheus`.
|
||||
3. Set OpenReader `API_BASE` to your Orpheus base URL (typically ending with `/v1`).
|
||||
4. Set `API_KEY` if your Orpheus deployment requires one.
|
||||
5. Choose voice.
|
||||
|
||||
## Notes
|
||||
|
||||
- `API_BASE` is needed here because Orpheus is configured through the custom provider path.
|
||||
- OpenReader expects OpenAI-compatible audio endpoints.
|
||||
- For variable details, see [Environment Variables](../guides/environment-variables).
|
||||
40
docs-site/docs/intro.md
Normal file
40
docs-site/docs/intro.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
id: intro
|
||||
title: Introduction
|
||||
slug: /
|
||||
---
|
||||
|
||||
OpenReader WebUI is an open source text-to-speech document reader built with Next.js. It provides a read-along experience with narration for **EPUB, PDF, TXT, MD, and DOCX documents**.
|
||||
|
||||
It supports multiple TTS providers including OpenAI, DeepInfra, and custom OpenAI-compatible endpoints such as [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) and [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI).
|
||||
|
||||
## Highlights
|
||||
|
||||
- Multi-provider TTS support
|
||||
- [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) (including multi-voice combinations)
|
||||
- [Orpheus-FastAPI](https://github.com/Lex-au/Orpheus-FastAPI)
|
||||
- Custom OpenAI-compatible endpoints (`/v1/audio/voices` and `/v1/audio/speech`)
|
||||
- Cloud providers such as [DeepInfra](https://deepinfra.com/models/text-to-speech) and [OpenAI API](https://platform.openai.com/docs/pricing#transcription-and-speech)
|
||||
- Server-side sync and storage
|
||||
- External library import from a server-mounted folder
|
||||
- Sync documents between browser and server for multi-device use
|
||||
- Server-side audiobook export in `m4b`/`mp3`, with resumable chapter-based export
|
||||
- Read-along highlighting for PDF and EPUB
|
||||
- Optional word-by-word highlighting using server-side timestamps from [whisper.cpp](https://github.com/ggml-org/whisper.cpp)
|
||||
- Sentence-aware narration that merges across pages/chapters for smoother playback
|
||||
- Optimized Next.js TTS proxy with caching for faster repeat playback
|
||||
- Customizable themes, TTS settings, and document handling
|
||||
|
||||
## Start Here
|
||||
|
||||
- [Docker Quick Start](./getting-started/docker-quick-start)
|
||||
- [Local Development](./getting-started/local-development)
|
||||
- [Environment Variables](./guides/environment-variables)
|
||||
- [Auth](./guides/configuration)
|
||||
- [SQL Database](./operations/database-and-migrations)
|
||||
- [Object / Blob Storage](./guides/storage-and-blob-behavior)
|
||||
- [TTS Providers](./guides/tts-providers)
|
||||
|
||||
## Source Repository
|
||||
|
||||
- GitHub: [richardr1126/OpenReader-WebUI](https://github.com/richardr1126/OpenReader-WebUI)
|
||||
52
docs-site/docs/operations/database-and-migrations.md
Normal file
52
docs-site/docs/operations/database-and-migrations.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
---
|
||||
title: SQL Database
|
||||
---
|
||||
|
||||
This page covers database mode selection and migration behavior for OpenReader WebUI.
|
||||
|
||||
## Database mode
|
||||
|
||||
- Default mode: embedded SQLite at `docstore/sqlite3.db`
|
||||
- External mode: Postgres when `POSTGRES_URL` is set
|
||||
|
||||
## Startup migration behavior
|
||||
|
||||
By default, the shared entrypoint runs DB migrations automatically before app startup in:
|
||||
|
||||
- Docker container startup
|
||||
- `pnpm dev`
|
||||
- `pnpm start`
|
||||
|
||||
To skip automatic startup migrations:
|
||||
|
||||
- Set `RUN_DB_MIGRATIONS=false`
|
||||
|
||||
Database variables are documented in [Environment Variables](../guides/environment-variables).
|
||||
|
||||
## Common project commands
|
||||
|
||||
In most cases, you do not need manual migration commands because startup runs migrations automatically.
|
||||
|
||||
```bash
|
||||
# Run pending migrations (uses Postgres config when POSTGRES_URL is set, otherwise SQLite)
|
||||
pnpm migrate
|
||||
|
||||
# Generate new migration files for both SQLite and Postgres outputs
|
||||
pnpm generate
|
||||
```
|
||||
|
||||
## Manual Drizzle commands (advanced)
|
||||
|
||||
```bash
|
||||
# Migrate SQLite
|
||||
npx drizzle-kit migrate --config drizzle.config.sqlite.ts
|
||||
|
||||
# Migrate Postgres
|
||||
npx drizzle-kit migrate --config drizzle.config.pg.ts
|
||||
|
||||
# Generate SQLite migrations
|
||||
npx drizzle-kit generate --config drizzle.config.sqlite.ts
|
||||
|
||||
# Generate Postgres migrations
|
||||
npx drizzle-kit generate --config drizzle.config.pg.ts
|
||||
```
|
||||
41
docs-site/docs/reference/stack.md
Normal file
41
docs-site/docs/reference/stack.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
title: Stack
|
||||
---
|
||||
|
||||
## Framework
|
||||
|
||||
- [Next.js](https://nextjs.org/) 15 (App Router)
|
||||
- [React](https://react.dev/) 19
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
## Containerization and runtime
|
||||
|
||||
- [Docker](https://www.docker.com/) (linux/amd64 and linux/arm64)
|
||||
- Shared entrypoint that runs DB migrations by default and can bootstrap embedded SeaweedFS before app startup
|
||||
|
||||
## Next.js client
|
||||
|
||||
- UI: [Tailwind CSS](https://tailwindcss.com), [Headless UI](https://headlessui.com), [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin)
|
||||
- Interactions: `react-dnd`, `react-dropzone`
|
||||
- Authentication: [Better Auth](https://www.better-auth.com/) client SDK
|
||||
- Local storage/cache: [Dexie.js](https://dexie.org/) (IndexedDB)
|
||||
- Document rendering:
|
||||
- PDF: [react-pdf](https://github.com/wojtekmaj/react-pdf), [pdf.js](https://mozilla.github.io/pdf.js/)
|
||||
- EPUB: [react-reader](https://github.com/gerhardsletten/react-reader), [epubjs](https://github.com/futurepress/epub.js/)
|
||||
- Markdown/Text: [react-markdown](https://github.com/remarkjs/react-markdown), [remark-gfm](https://github.com/remarkjs/remark-gfm)
|
||||
- Text preprocessing/matching: [compromise](https://github.com/spencermountain/compromise), [cmpstr](https://github.com/remsky/cmpstr)
|
||||
|
||||
## Next.js server
|
||||
|
||||
- APIs: Route Handlers for sync, blob/content access, migrations, audiobook export, TTS/Whisper proxying
|
||||
- Authentication: [Better Auth](https://www.better-auth.com/) server handlers/adapters
|
||||
- Metadata DB: [Drizzle ORM](https://orm.drizzle.team/) with SQLite (`better-sqlite3`) by default and optional Postgres (`pg`)
|
||||
- Blob storage: embedded [SeaweedFS](https://github.com/seaweedfs/seaweedfs) (`weed mini`) by default, or external S3-compatible storage via AWS SDK v3
|
||||
- Audio/processing pipeline: OpenAI-compatible TTS providers, [ffmpeg](https://ffmpeg.org/) for audiobook assembly, optional [whisper.cpp](https://github.com/ggerganov/whisper.cpp) for word timestamps
|
||||
|
||||
## Tooling and testing
|
||||
|
||||
- ESLint
|
||||
- TypeScript
|
||||
- [Playwright](https://playwright.dev/) end-to-end tests
|
||||
- Drizzle migration/generation scripts
|
||||
122
docs-site/docusaurus.config.ts
Normal file
122
docs-site/docusaurus.config.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { themes as prismThemes } from 'prism-react-renderer';
|
||||
import type { Config } from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
|
||||
const config: Config = {
|
||||
title: 'OpenReader WebUI Docs',
|
||||
tagline: 'Docs for OpenReader',
|
||||
|
||||
future: {
|
||||
v4: true,
|
||||
},
|
||||
|
||||
url: 'https://docs.openreader.richardr.dev',
|
||||
baseUrl: '/',
|
||||
|
||||
organizationName: 'richardr1126',
|
||||
projectName: 'OpenReader-WebUI',
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
markdown: {
|
||||
hooks: {
|
||||
onBrokenMarkdownLinks: 'throw',
|
||||
},
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
routeBasePath: '/',
|
||||
sidebarPath: './sidebars.ts',
|
||||
editUrl: 'https://github.com/richardr1126/OpenReader-WebUI/tree/main/docs-site/',
|
||||
// lastVersion: 'current',
|
||||
// versions: {
|
||||
// current: {
|
||||
// label: 'Current',
|
||||
// },
|
||||
// },
|
||||
},
|
||||
blog: false,
|
||||
theme: {
|
||||
customCss: './custom.css',
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
plugins: [
|
||||
[
|
||||
'@easyops-cn/docusaurus-search-local',
|
||||
{
|
||||
indexDocs: true,
|
||||
indexBlog: false,
|
||||
docsRouteBasePath: '/',
|
||||
language: ['en'],
|
||||
hashed: true,
|
||||
explicitSearchResultPath: true,
|
||||
highlightSearchTermsOnTargetPage: true,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
colorMode: {
|
||||
defaultMode: 'light',
|
||||
disableSwitch: false,
|
||||
respectPrefersColorScheme: true,
|
||||
},
|
||||
navbar: {
|
||||
title: 'OpenReader WebUI',
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'tutorialSidebar',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/richardr1126/OpenReader-WebUI',
|
||||
label: 'GitHub',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
links: [
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{ label: 'Support', to: '/community/support' },
|
||||
{ label: 'GitHub Discussions', href: 'https://github.com/richardr1126/OpenReader-WebUI/discussions' },
|
||||
{ label: 'Issues', href: 'https://github.com/richardr1126/OpenReader-WebUI/issues' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Project',
|
||||
items: [
|
||||
{ label: 'GitHub', href: 'https://github.com/richardr1126/OpenReader-WebUI' },
|
||||
{ label: 'Releases', href: 'https://github.com/richardr1126/OpenReader-WebUI/releases' },
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} OpenReader contributors.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
darkTheme: prismThemes.dracula,
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
32
docs-site/package.json
Normal file
32
docs-site/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "openreader-docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "docusaurus start --host 0.0.0.0 --port 3004",
|
||||
"build": "docusaurus build",
|
||||
"serve": "docusaurus serve",
|
||||
"clear": "docusaurus clear",
|
||||
"docusaurus": "docusaurus",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.9.2",
|
||||
"@docusaurus/preset-classic": "^3.9.2",
|
||||
"@easyops-cn/docusaurus-search-local": "^0.54.1",
|
||||
"clsx": "^2.1.1",
|
||||
"prism-react-renderer": "^2.4.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "^3.9.2",
|
||||
"@docusaurus/tsconfig": "^3.9.2",
|
||||
"@docusaurus/types": "^3.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
12621
docs-site/pnpm-lock.yaml
Normal file
12621
docs-site/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
53
docs-site/sidebars.ts
Normal file
53
docs-site/sidebars.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { SidebarsConfig } from '@docusaurus/plugin-content-docs';
|
||||
|
||||
const sidebars: SidebarsConfig = {
|
||||
tutorialSidebar: [
|
||||
'intro',
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Start Here',
|
||||
items: ['getting-started/docker-quick-start', 'getting-started/local-development'],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Reference',
|
||||
items: [
|
||||
'guides/environment-variables',
|
||||
'reference/stack',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Configure',
|
||||
items: [
|
||||
'guides/tts-providers',
|
||||
{
|
||||
type: 'doc',
|
||||
id: 'guides/configuration',
|
||||
label: 'Auth (Reccomended)',
|
||||
},
|
||||
'guides/tts-rate-limiting',
|
||||
'operations/database-and-migrations',
|
||||
'guides/storage-and-blob-behavior',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Integrations',
|
||||
items: [
|
||||
'integrations/kokoro-fastapi',
|
||||
'integrations/orpheus-fastapi',
|
||||
'integrations/deepinfra',
|
||||
'integrations/openai',
|
||||
'integrations/custom-openai',
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'category',
|
||||
label: 'Project',
|
||||
items: ['community/support', 'community/acknowledgements', 'community/license'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default sidebars;
|
||||
1
docs-site/static/CNAME
Normal file
1
docs-site/static/CNAME
Normal file
|
|
@ -0,0 +1 @@
|
|||
docs.openreader.richardr.dev
|
||||
2
docs-site/static/robots.txt
Normal file
2
docs-site/static/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
6
docs-site/tsconfig.json
Normal file
6
docs-site/tsconfig.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "@docusaurus/tsconfig",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "."
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,13 @@
|
|||
"lint": "next lint",
|
||||
"test": "playwright test",
|
||||
"migrate": "node drizzle/scripts/migrate.mjs",
|
||||
"generate": "node drizzle/scripts/generate.mjs"
|
||||
"generate": "node drizzle/scripts/generate.mjs",
|
||||
"docs:init": "pnpm --dir docs-site install",
|
||||
"docs:dev": "pnpm --dir docs-site start",
|
||||
"docs:build": "pnpm --dir docs-site build",
|
||||
"docs:serve": "pnpm --dir docs-site serve",
|
||||
"docs:version": "pnpm --dir docs-site docusaurus docs:version",
|
||||
"docs:clear": "pnpm --dir docs-site clear"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.985.0",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextResponse, type NextRequest } from 'next/server';
|
||||
import { auth } from '@/lib/server/auth';
|
||||
import { rateLimiter, RATE_LIMITS } from '@/lib/server/rate-limiter';
|
||||
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
|
||||
import { headers } from 'next/headers';
|
||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
||||
import { getClientIp } from '@/lib/server/request-ip';
|
||||
|
|
@ -18,6 +18,8 @@ function getUtcResetTimeIso(): string {
|
|||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const ttsRateLimitEnabled = isTtsRateLimitEnabled();
|
||||
|
||||
// If auth is not enabled, return unlimited status
|
||||
if (!isAuthEnabled() || !auth) {
|
||||
const resetTime = getUtcResetTimeIso();
|
||||
|
|
@ -45,8 +47,8 @@ export async function GET(req: NextRequest) {
|
|||
return NextResponse.json({
|
||||
allowed: true,
|
||||
currentCount: 0,
|
||||
limit: RATE_LIMITS.ANONYMOUS,
|
||||
remainingChars: RATE_LIMITS.ANONYMOUS,
|
||||
limit: ttsRateLimitEnabled ? RATE_LIMITS.ANONYMOUS : Number.MAX_SAFE_INTEGER,
|
||||
remainingChars: ttsRateLimitEnabled ? RATE_LIMITS.ANONYMOUS : Number.MAX_SAFE_INTEGER,
|
||||
resetTime,
|
||||
userType: 'unauthenticated',
|
||||
authEnabled: true
|
||||
|
|
@ -56,7 +58,7 @@ export async function GET(req: NextRequest) {
|
|||
const isAnonymous = Boolean(session.user.isAnonymous);
|
||||
|
||||
const ip = getClientIp(req);
|
||||
const device = isAnonymous ? getOrCreateDeviceId(req) : null;
|
||||
const device = isAnonymous && ttsRateLimitEnabled ? getOrCreateDeviceId(req) : null;
|
||||
|
||||
const result = await rateLimiter.getCurrentUsage(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import type { TTSRequestPayload } from '@/types/client';
|
|||
import type { TTSError, TTSAudioBuffer } from '@/types/tts';
|
||||
import { headers } from 'next/headers';
|
||||
import { auth } from '@/lib/server/auth';
|
||||
import { rateLimiter, RATE_LIMITS } from '@/lib/server/rate-limiter';
|
||||
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
|
||||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
||||
import { getClientIp } from '@/lib/server/request-ip';
|
||||
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/device-id';
|
||||
|
|
@ -142,7 +142,7 @@ export async function POST(req: NextRequest) {
|
|||
return NextResponse.json(errorBody, { status: 400 });
|
||||
}
|
||||
|
||||
// Auth and rate limiting check (only when auth is enabled)
|
||||
// Auth and TTS char rate limiting check (only when auth is enabled)
|
||||
let didCreateDeviceIdCookie = false;
|
||||
let deviceIdToSet: string | null = null;
|
||||
|
||||
|
|
@ -159,62 +159,63 @@ export async function POST(req: NextRequest) {
|
|||
}
|
||||
|
||||
const isAnonymous = Boolean(session.user.isAnonymous);
|
||||
const charCount = text.length;
|
||||
|
||||
const ip = getClientIp(req);
|
||||
const device = isAnonymous ? getOrCreateDeviceId(req) : null;
|
||||
if (device?.didCreate) {
|
||||
didCreateDeviceIdCookie = true;
|
||||
deviceIdToSet = device.deviceId;
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
const rateLimitResult = await rateLimiter.checkAndIncrementLimit(
|
||||
{ id: session.user.id, isAnonymous },
|
||||
charCount,
|
||||
{
|
||||
deviceId: device?.deviceId ?? null,
|
||||
ip,
|
||||
if (isTtsRateLimitEnabled()) {
|
||||
const charCount = text.length;
|
||||
const ip = getClientIp(req);
|
||||
const device = isAnonymous ? getOrCreateDeviceId(req) : null;
|
||||
if (device?.didCreate) {
|
||||
didCreateDeviceIdCookie = true;
|
||||
deviceIdToSet = device.deviceId;
|
||||
}
|
||||
);
|
||||
|
||||
if (!rateLimitResult.allowed) {
|
||||
const resetTime = rateLimitResult.resetTime.toISOString();
|
||||
const retryAfterSeconds = Math.max(
|
||||
0,
|
||||
Math.ceil((rateLimitResult.resetTime.getTime() - Date.now()) / 1000)
|
||||
// Check rate limit
|
||||
const rateLimitResult = await rateLimiter.checkAndIncrementLimit(
|
||||
{ id: session.user.id, isAnonymous },
|
||||
charCount,
|
||||
{
|
||||
deviceId: device?.deviceId ?? null,
|
||||
ip,
|
||||
}
|
||||
);
|
||||
|
||||
const problem: ProblemDetails = {
|
||||
type: PROBLEM_TYPES.dailyQuotaExceeded,
|
||||
title: 'Daily quota exceeded',
|
||||
status: 429,
|
||||
detail: 'Daily character limit exceeded',
|
||||
code: 'USER_DAILY_QUOTA_EXCEEDED',
|
||||
currentCount: rateLimitResult.currentCount,
|
||||
limit: rateLimitResult.limit,
|
||||
remainingChars: rateLimitResult.remainingChars,
|
||||
resetTime,
|
||||
userType: isAnonymous ? 'anonymous' : 'authenticated',
|
||||
upgradeHint: isAnonymous
|
||||
? `Sign up to increase your limit from ${formatLimitForHint(RATE_LIMITS.ANONYMOUS)} to ${formatLimitForHint(RATE_LIMITS.AUTHENTICATED)} characters per day`
|
||||
: undefined,
|
||||
instance: req.nextUrl.pathname,
|
||||
};
|
||||
if (!rateLimitResult.allowed) {
|
||||
const resetTime = rateLimitResult.resetTime.toISOString();
|
||||
const retryAfterSeconds = Math.max(
|
||||
0,
|
||||
Math.ceil((rateLimitResult.resetTime.getTime() - Date.now()) / 1000)
|
||||
);
|
||||
|
||||
const response = new NextResponse(JSON.stringify(problem), {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/problem+json',
|
||||
'Retry-After': String(retryAfterSeconds),
|
||||
},
|
||||
});
|
||||
const problem: ProblemDetails = {
|
||||
type: PROBLEM_TYPES.dailyQuotaExceeded,
|
||||
title: 'Daily quota exceeded',
|
||||
status: 429,
|
||||
detail: 'Daily character limit exceeded',
|
||||
code: 'USER_DAILY_QUOTA_EXCEEDED',
|
||||
currentCount: rateLimitResult.currentCount,
|
||||
limit: rateLimitResult.limit,
|
||||
remainingChars: rateLimitResult.remainingChars,
|
||||
resetTime,
|
||||
userType: isAnonymous ? 'anonymous' : 'authenticated',
|
||||
upgradeHint: isAnonymous
|
||||
? `Sign up to increase your limit from ${formatLimitForHint(RATE_LIMITS.ANONYMOUS)} to ${formatLimitForHint(RATE_LIMITS.AUTHENTICATED)} characters per day`
|
||||
: undefined,
|
||||
instance: req.nextUrl.pathname,
|
||||
};
|
||||
|
||||
if (didCreateDeviceIdCookie && deviceIdToSet) {
|
||||
setDeviceIdCookie(response, deviceIdToSet);
|
||||
const response = new NextResponse(JSON.stringify(problem), {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Content-Type': 'application/problem+json',
|
||||
'Retry-After': String(retryAfterSeconds),
|
||||
},
|
||||
});
|
||||
|
||||
if (didCreateDeviceIdCookie && deviceIdToSet) {
|
||||
setDeviceIdCookie(response, deviceIdToSet);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ function getTrustedOrigins(): string[] {
|
|||
return Array.from(origins);
|
||||
}
|
||||
|
||||
function envFlagEnabled(name: string, defaultValue: boolean): boolean {
|
||||
const raw = process.env[name];
|
||||
if (!raw || raw.trim() === '') return defaultValue;
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') return true;
|
||||
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') return false;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const createAuth = () => betterAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
database: drizzleAdapter(db as any, {
|
||||
|
|
@ -65,9 +74,9 @@ const createAuth = () => betterAuth({
|
|||
},
|
||||
},
|
||||
rateLimit: {
|
||||
// Disable rate limiting when running tests to support parallel test workers
|
||||
// In production, better-auth's default rate limiting applies
|
||||
enabled: process.env.DISABLE_AUTH_RATE_LIMIT !== 'true',
|
||||
// Better Auth built-in rate limiting is enabled by default.
|
||||
// Set DISABLE_AUTH_RATE_LIMIT=true to disable it.
|
||||
enabled: !envFlagEnabled('DISABLE_AUTH_RATE_LIMIT', false),
|
||||
},
|
||||
socialProviders: {
|
||||
...(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET && {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
|
|||
export const DEFAULT_LIBRARY_DIR = path.join(DOCSTORE_DIR, 'library');
|
||||
|
||||
export function parseLibraryRoots(): string[] {
|
||||
const raw = process.env.OPENREADER_LIBRARY_DIRS ?? process.env.OPENREADER_LIBRARY_DIR ?? '';
|
||||
const raw = process.env.IMPORT_LIBRARY_DIRS ?? process.env.IMPORT_LIBRARY_DIR ?? '';
|
||||
|
||||
const roots = raw
|
||||
.split(/[,:;]/g)
|
||||
|
|
|
|||
|
|
@ -3,14 +3,40 @@ import { userTtsChars } from '@/db/schema';
|
|||
import { isAuthEnabled } from '@/lib/server/auth-config';
|
||||
import { eq, and, lt, sql } from 'drizzle-orm';
|
||||
|
||||
function readPositiveIntEnv(name: string, fallback: number): number {
|
||||
const raw = process.env[name];
|
||||
if (!raw || raw.trim() === '') return fallback;
|
||||
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
console.warn(`[rate-limiter] Invalid ${name}=${raw}; using default ${fallback}`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function readBooleanEnv(name: string, fallback: boolean): boolean {
|
||||
const raw = process.env[name];
|
||||
if (!raw || raw.trim() === '') return fallback;
|
||||
const normalized = raw.trim().toLowerCase();
|
||||
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') return true;
|
||||
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') return false;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function isTtsRateLimitEnabled(): boolean {
|
||||
return readBooleanEnv('TTS_ENABLE_RATE_LIMIT', false);
|
||||
}
|
||||
|
||||
// Rate limits configuration - character counts per day
|
||||
export const RATE_LIMITS = {
|
||||
ANONYMOUS: 50_000, // 50K characters per day for anonymous users
|
||||
AUTHENTICATED: 500_000, // 500K characters per day for authenticated users
|
||||
ANONYMOUS: readPositiveIntEnv('TTS_DAILY_LIMIT_ANONYMOUS', 50_000),
|
||||
AUTHENTICATED: readPositiveIntEnv('TTS_DAILY_LIMIT_AUTHENTICATED', 500_000),
|
||||
// IP-based backstop limits to make it harder to reset limits by creating new accounts
|
||||
// or clearing storage/cookies
|
||||
IP_ANONYMOUS: Number(process.env.TTS_IP_DAILY_LIMIT_ANONYMOUS || 100_000),
|
||||
IP_AUTHENTICATED: Number(process.env.TTS_IP_DAILY_LIMIT_AUTHENTICATED || 1_000_000),
|
||||
IP_ANONYMOUS: readPositiveIntEnv('TTS_IP_DAILY_LIMIT_ANONYMOUS', 100_000),
|
||||
IP_AUTHENTICATED: readPositiveIntEnv('TTS_IP_DAILY_LIMIT_AUTHENTICATED', 1_000_000),
|
||||
} as const;
|
||||
|
||||
// Helper to ensure DB is strictly typed when we know it exists
|
||||
|
|
@ -126,7 +152,7 @@ export class RateLimiter {
|
|||
* Check if a user can use TTS and increment their char count if allowed
|
||||
*/
|
||||
async checkAndIncrementLimit(user: UserInfo, charCount: number, backstops?: RateLimitBackstops): Promise<RateLimitResult> {
|
||||
if (!isAuthEnabled()) {
|
||||
if (!isAuthEnabled() || !isTtsRateLimitEnabled()) {
|
||||
return {
|
||||
allowed: true,
|
||||
currentCount: 0,
|
||||
|
|
@ -227,7 +253,7 @@ export class RateLimiter {
|
|||
* Get current usage for a user without incrementing
|
||||
*/
|
||||
async getCurrentUsage(user: UserInfo, backstops?: RateLimitBackstops): Promise<RateLimitResult> {
|
||||
if (!isAuthEnabled()) {
|
||||
if (!isAuthEnabled() || !isTtsRateLimitEnabled()) {
|
||||
return {
|
||||
allowed: true,
|
||||
currentCount: 0,
|
||||
|
|
@ -283,7 +309,7 @@ export class RateLimiter {
|
|||
* Transfer char counts when anonymous user creates an account
|
||||
*/
|
||||
async transferAnonymousUsage(anonymousUserId: string, authenticatedUserId: string): Promise<void> {
|
||||
if (!isAuthEnabled()) return;
|
||||
if (!isAuthEnabled() || !isTtsRateLimitEnabled()) return;
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const dateValue = today as unknown as UserTtsCharsDateValue;
|
||||
|
|
|
|||
|
|
@ -23,5 +23,5 @@
|
|||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "docs-site", "docs-site/**"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue