refactor(config): migrate compute and runtime env vars to new naming scheme

- Replace `OPENREADER_COMPUTE_MODE` and related variables with `COMPUTE_MODE`
- Replace `OPENREADER_*` PDF/Whisper model URLs with `PDF_LAYOUT_MODEL_BASE_URL` and `WHISPER_MODEL_BASE_URL`
- Remove legacy `NEXT_PUBLIC_*` runtime config seeds in favor of `RUNTIME_SEED_*`
- Update documentation, code, and environment references to match new variable names
- Remove deprecated `scripts/fetch-models.mjs` and related npm script
- Update runtime config SSR injection from `window.__OPENREADER_RUNTIME_CONFIG__` to `window.__RUNTIME_CONFIG__`
- Adjust Next.js config to use new compute mode env var and optimize output file tracing for ONNX dependencies

BREAKING CHANGE: Environment variable names for compute mode, model URLs, and runtime config seeding have changed. Update `.env` files and deployment configs to use `COMPUTE_MODE`, `PDF_LAYOUT_MODEL_BASE_URL`, `WHISPER_MODEL_BASE_URL`, and `RUNTIME_SEED_*` as appropriate. Legacy `OPENREADER_*` and `NEXT_PUBLIC_*` variables are no longer supported.
This commit is contained in:
Richard R 2026-05-19 13:27:07 -06:00
parent 874e5ef359
commit 3a21f2a5f5
24 changed files with 166 additions and 241 deletions

View file

@ -81,31 +81,15 @@ IMPORT_LIBRARY_DIRS=
# local = run compute in-process (default)
# none = disable both capabilities (good for preview/serverless)
# worker = reserved for future external worker mode (not implemented in v1)
OPENREADER_COMPUTE_MODE=local
# OPENREADER_COMPUTE_WORKER_URL=
# OPENREADER_COMPUTE_WORKER_TOKEN=
COMPUTE_MODE=local
# COMPUTE_WORKER_URL=
# COMPUTE_WORKER_TOKEN=
# Optional overrides for Whisper ONNX artifacts
# Defaults target: onnx-community/whisper-base_timestamped int8
# OPENREADER_WHISPER_MODEL_CONFIG_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/config.json
# OPENREADER_WHISPER_MODEL_GENERATION_CONFIG_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/generation_config.json
# OPENREADER_WHISPER_MODEL_TOKENIZER_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/tokenizer.json
# OPENREADER_WHISPER_MODEL_TOKENIZER_CONFIG_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/tokenizer_config.json
# OPENREADER_WHISPER_MODEL_MERGES_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/merges.txt
# OPENREADER_WHISPER_MODEL_VOCAB_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/vocab.json
# OPENREADER_WHISPER_MODEL_NORMALIZER_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/normalizer.json
# OPENREADER_WHISPER_MODEL_ADDED_TOKENS_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/added_tokens.json
# OPENREADER_WHISPER_MODEL_PREPROCESSOR_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/preprocessor_config.json
# OPENREADER_WHISPER_MODEL_SPECIAL_TOKENS_MAP_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/special_tokens_map.json
# OPENREADER_WHISPER_MODEL_ENCODER_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/onnx/encoder_model_int8.onnx
# OPENREADER_WHISPER_MODEL_DECODER_MERGED_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/onnx/decoder_model_merged_int8.onnx
# OPENREADER_WHISPER_MODEL_DECODER_WITH_PAST_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main/onnx/decoder_with_past_model_int8.onnx
# Optional Whisper ONNX base URL override (must contain all expected files)
# WHISPER_MODEL_BASE_URL=https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main
# Optional overrides for PDF layout model artifacts
# OPENREADER_PDF_LAYOUT_MODEL_URL=https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx
# OPENREADER_PDF_LAYOUT_MODEL_DATA_URL=https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx.data
# OPENREADER_PDF_LAYOUT_CONFIG_URL=https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/config.json
# OPENREADER_PDF_LAYOUT_PREPROCESSOR_URL=https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/preprocessor_config.json
# Optional PDF layout ONNX base URL override (must contain all expected files)
# PDF_LAYOUT_MODEL_BASE_URL=https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main
# (Optional) Override ffmpeg binary path used for audiobook processing
FFMPEG_BIN=
@ -113,12 +97,12 @@ FFMPEG_BIN=
# (Optional) Client feature flags — seeded into the admin-managed runtime
# config on first boot, then ignored. Edit values from Settings → Admin →
# Site features instead of redeploying. SSR-injected so they take effect
# without rebuilding (unlike the old NEXT_PUBLIC_* build-time pattern).
# NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=true
# NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=true
# NEXT_PUBLIC_ENABLE_TTS_PROVIDERS_TAB=true
# NEXT_PUBLIC_ENABLE_USER_SIGNUPS=true
# NEXT_PUBLIC_RESTRICT_USER_API_KEYS=true
# NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=custom-openai
# NEXT_PUBLIC_CHANGELOG_FEED_URL=https://docs.openreader.richardr.dev/changelog/manifest.json
# NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true
# without rebuilding (unlike the old build-time public-env pattern).
# RUNTIME_SEED_ENABLE_DOCX_CONVERSION=true
# RUNTIME_SEED_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=true
# RUNTIME_SEED_ENABLE_TTS_PROVIDERS_TAB=true
# RUNTIME_SEED_ENABLE_USER_SIGNUPS=true
# RUNTIME_SEED_RESTRICT_USER_API_KEYS=true
# RUNTIME_SEED_DEFAULT_TTS_PROVIDER=custom-openai
# RUNTIME_SEED_CHANGELOG_FEED_URL=https://docs.openreader.richardr.dev/changelog/manifest.json
# RUNTIME_SEED_ENABLE_AUDIOBOOK_EXPORT=true

View file

@ -20,7 +20,7 @@ OpenReader is an open source, self-host-friendly text-to-speech document reader
- 🎯 **Multi-provider TTS** with OpenAI-compatible endpoints and cloud providers (Kokoro-FastAPI, KittenTTS-FastAPI, Orpheus-FastAPI or OpenAI, Replicate, DeepInfra).
- 📖 **Read-along playback** for PDF/EPUB with sentence-aware narration.
- ⏱️ **Word-by-word highlighting** via built-in ONNX Whisper alignment in local compute mode (`OPENREADER_COMPUTE_MODE=local`).
- ⏱️ **Word-by-word highlighting** via built-in ONNX Whisper alignment in local compute mode (`COMPUTE_MODE=local`).
- 🧱 **Layout-aware PDF parsing** (PP-DocLayoutV3 ONNX) with structured blocks for cleaner TTS/chaptering.
- 🛜 **Sync + library import** to bring docs across devices and from server-mounted folders.
- 🗂️ **Flexible storage** with embedded SeaweedFS or external S3-compatible backends.

View file

@ -21,7 +21,7 @@ On every session resolution the server compares the user's email against this li
When the logged-in user is an admin, an **Admin** tab appears in **Settings → sidebar** with two sub-tabs:
- **Shared providers** — server-side TTS provider instances visible to all users.
- **Site features** — runtime-editable replacements for what were previously `NEXT_PUBLIC_*` build-time flags.
- **Site features** — runtime-editable replacements for what were previously build-time public env flags.
## Shared TTS providers
@ -78,11 +78,11 @@ Runtime-editable settings, one row per key:
| `enableDocxConversion` | Accept .docx uploads (converted to PDF server-side). |
| `enableDestructiveDeleteActions` | Show "Delete all data" buttons in the Documents tab (auth-disabled mode). |
Word-by-word highlighting and PDF layout parsing capability are controlled by `OPENREADER_COMPUTE_MODE` (server env), not an admin runtime flag.
Word-by-word highlighting and PDF layout parsing capability are controlled by `COMPUTE_MODE` (server env), not an admin runtime flag.
Each row shows a source badge:
- **from env** — the value was migrated from the corresponding `NEXT_PUBLIC_*` env var on first boot. Editing it in the UI flips the source to **admin**.
- **from env** — the value was migrated from the corresponding `RUNTIME_SEED_*` env var on first boot. Editing it in the UI flips the source to **admin**.
- **admin** — explicit admin override. Use **Reset** on the row to clear it back to the env-default state.
- **default** — neither env nor admin set; uses the built-in default.
@ -92,11 +92,11 @@ Turning `restrictUserApiKeys` off allows user-supplied API keys to flow through
## Migrating off env vars
The future-direction goal is to remove `NEXT_PUBLIC_*` / `API_KEY` / `API_BASE` from your `.env` entirely. To do that safely:
The future-direction goal is to remove `RUNTIME_SEED_*` / `API_KEY` / `API_BASE` from your `.env` entirely. To do that safely:
1. Deploy this version with your existing env values in place.
2. Boot the app once. Open Settings → Admin and verify:
- Each `NEXT_PUBLIC_*` setting appears as **from env**.
- Each `RUNTIME_SEED_*` setting appears as **from env**.
- A `default-openai` row exists in **Shared providers** (if you had `API_KEY` set).
3. Remove the env vars from your `.env`.
4. Redeploy. Behavior is unchanged — the DB is now the source of truth.

View file

@ -31,7 +31,7 @@ ADMIN_EMAILS=alice@example.com,bob@example.com
Admins see a new **Admin** tab in **Settings** with two sub-tabs:
- **Shared TTS providers** — server-managed TTS provider instances with encrypted keys, visible to all users.
- **Site features** — runtime overrides for what were previously `NEXT_PUBLIC_*` build-time flags (including account signup availability, default TTS provider/model, audiobook export, etc.).
- **Site features** — runtime overrides for what were previously build-time public env flags (including account signup availability, default TTS provider, audiobook export, etc.).
Admin assignment is reconciled on every session resolution, so removing an email from `ADMIN_EMAILS` demotes the user on next login without a restart. See [Admin Panel](./admin-panel) for the full reference.

View file

@ -11,7 +11,7 @@ OpenReader routes all TTS requests through the Next.js server to an OpenAI-compa
**Environment variables**: `API_KEY` and `API_BASE` exist as a one-shot first-boot seed that auto-creates a `default-openai` admin shared provider. After the first boot they are no longer read by the running app.
:::tip
If you're running a private/self-hosted instance and want per-user BYOK behavior, turn off **Settings → Admin → Site features → Restrict user API keys**. Legacy first-boot seed via `NEXT_PUBLIC_RESTRICT_USER_API_KEYS=false` is still supported for no-admin bootstrap flows.
If you're running a private/self-hosted instance and want per-user BYOK behavior, turn off **Settings → Admin → Site features → Restrict user API keys**. Legacy first-boot seed via `RUNTIME_SEED_RESTRICT_USER_API_KEYS=false` is still supported for no-admin bootstrap flows.
:::
## Providers

View file

@ -118,9 +118,9 @@ sudo apt install -y libreoffice
No extra native Whisper CLI build step is required.
Set `OPENREADER_COMPUTE_MODE=local` to enable built-in ONNX word alignment in-process.
Set `COMPUTE_MODE=local` to enable built-in ONNX word alignment in-process.
If you need mirrors or pinned artifact locations, set `OPENREADER_WHISPER_MODEL_*_URL` overrides in `.env`.
If you need mirrors or pinned artifact locations, set `WHISPER_MODEL_BASE_URL` in `.env`.
</details>
@ -157,7 +157,7 @@ Use one of these `.env` mode templates:
```env
API_BASE=http://host.docker.internal:8880/v1
API_KEY=none
OPENREADER_COMPUTE_MODE=local
COMPUTE_MODE=local
# Leave BASE_URL and AUTH_SECRET unset to keep auth disabled.
# (Admin panel is unavailable without auth.)
# API_BASE/API_KEY seed a shared default provider if you want shared mode.
@ -169,7 +169,7 @@ OPENREADER_COMPUTE_MODE=local
```env
API_BASE=http://host.docker.internal:8880/v1
API_KEY=none
OPENREADER_COMPUTE_MODE=local
COMPUTE_MODE=local
BASE_URL=http://localhost:3003
AUTH_SECRET=<generate-with-openssl-rand-hex-32>
# Optional when you need multiple local origins:
@ -184,7 +184,7 @@ AUTH_SECRET=<generate-with-openssl-rand-hex-32>
# on first boot, then no longer read. Manage them in Settings → Admin afterwards.
API_BASE=http://host.docker.internal:8880/v1
API_KEY=none
OPENREADER_COMPUTE_MODE=local
COMPUTE_MODE=local
BASE_URL=http://localhost:3003
AUTH_SECRET=<generate-with-openssl-rand-hex-32>
# Comma-separated emails to auto-promote to admin on signin.
@ -197,7 +197,7 @@ ADMIN_EMAILS=you@example.com
```env
API_BASE=http://host.docker.internal:8880/v1
API_KEY=none
OPENREADER_COMPUTE_MODE=local
COMPUTE_MODE=local
USE_EMBEDDED_WEED_MINI=false
S3_BUCKET=your-bucket
S3_REGION=us-east-1
@ -212,21 +212,17 @@ S3_SECRET_ACCESS_KEY=your-secret-key
</Tabs>
:::note Env vars vs. admin panel
On first boot, `API_KEY` / `API_BASE` and any `NEXT_PUBLIC_*` flags you've set get auto-seeded into the admin-managed runtime config (DB-backed, keys encrypted at rest). After that, the admin UI is authoritative and editing those env vars no longer changes app behavior. See [Admin Panel](../configure/admin-panel).
On first boot, `API_KEY` / `API_BASE` and any `RUNTIME_SEED_*` flags you've set get auto-seeded into the admin-managed runtime config (DB-backed, keys encrypted at rest). After that, the admin UI is authoritative and editing those env vars no longer changes app behavior. See [Admin Panel](../configure/admin-panel).
:::
:::note User BYOK restriction default
If you want each user to enter personal provider credentials, set `restrictUserApiKeys=false` (from **Settings → Admin** when auth/admin is enabled, or via legacy first-boot seed `NEXT_PUBLIC_RESTRICT_USER_API_KEYS=false` for no-admin bootstrap flows).
If you want each user to enter personal provider credentials, set `restrictUserApiKeys=false` (from **Settings → Admin** when auth/admin is enabled, or via legacy first-boot seed `RUNTIME_SEED_RESTRICT_USER_API_KEYS=false` for no-admin bootstrap flows).
:::
:::info
For all environment variables, see [Environment Variables](../reference/environment-variables).
:::
:::tip Optional model prefetch
To pre-populate the PDF layout ONNX model cache before first PDF parse, run `pnpm fetch-models`.
:::
See [Auth](../configure/auth) for app/auth behavior.
See [Admin Panel](../configure/admin-panel) for the shared-provider and feature-flag management UI.
Storage configuration details are in [Object / Blob Storage](../configure/object-blob-storage).

View file

@ -38,7 +38,7 @@ ADMIN_EMAILS=you@example.com # comma-separated; admins manage TTS + features in
# Heavy compute (recommended on Vercel in v1)
# local = requires native binaries/models in-process
# none = disable ONNX whisper alignment + PDF layout parsing
OPENREADER_COMPUTE_MODE=none
COMPUTE_MODE=none
# First-boot seed for the TTS shared provider (optional; manage in-app afterwards)
API_KEY=your_replicate_key
@ -66,7 +66,7 @@ After the first successful deploy and admin login, open **Settings → Admin** a
## 3. Legacy first-boot seed (optional)
If you must pre-seed site features via environment variables, the legacy `NEXT_PUBLIC_*` seeds are still supported on first boot only. Prefer the admin panel for ongoing management.
If you must pre-seed site features via environment variables, the legacy `RUNTIME_SEED_*` seeds are still supported on first boot only. Prefer the admin panel for ongoing management.
See [Environment Variables](../reference/environment-variables#legacy-first-boot-runtime-seeds-optional) for the complete legacy seed list.
@ -129,4 +129,4 @@ Adjust memory per route if your files are larger or your plan differs.
1. Upload and read a PDF/EPUB document.
2. Confirm sync/blob fetch works across refreshes/devices.
3. Generate at least one audiobook chapter and play/download it.
4. If you run with local compute (`OPENREADER_COMPUTE_MODE=local`) outside Vercel, verify word highlighting timestamps on a TTS run.
4. If you run with local compute (`COMPUTE_MODE=local`) outside Vercel, verify word highlighting timestamps on a TTS run.

View file

@ -113,7 +113,7 @@ What this command enables:
- Set `API_BASE` on first boot to a TTS endpoint the container can reach (`host.docker.internal` works for host-local services). After first boot, manage providers in **Settings → Admin → Shared providers**.
- Auth is enabled only when both `BASE_URL` and `AUTH_SECRET` are set. The admin panel requires auth.
- Set `ADMIN_EMAILS` to your email if you want the **Admin** tab in Settings.
- `restrictUserApiKeys` controls shared-provider-only mode. For per-user BYOK in auth-enabled setups, toggle it off in **Settings → Admin → Site features**. Legacy first-boot seed via `NEXT_PUBLIC_RESTRICT_USER_API_KEYS=false` is still supported.
- `restrictUserApiKeys` controls shared-provider-only mode. For per-user BYOK in auth-enabled setups, toggle it off in **Settings → Admin → Site features**. Legacy first-boot seed via `RUNTIME_SEED_RESTRICT_USER_API_KEYS=false` is still supported.
- Use a `/app/docstore` mount if you want data to survive container/image replacement.
- Startup automatically runs DB/storage migrations via the shared entrypoint.
:::

View file

@ -6,7 +6,7 @@ toc_max_heading_level: 3
This is the single reference page for OpenReader environment variables.
:::note Recommended configuration path
For auth-enabled deployments, use **Settings → Admin** as the primary source of truth for shared TTS providers and site features. Legacy env vars (`API_KEY`, `API_BASE`, and `NEXT_PUBLIC_*`) are optional first-boot seeds only.
For auth-enabled deployments, use **Settings → Admin** as the primary source of truth for shared TTS providers and site features. Legacy env vars (`API_KEY`, `API_BASE`, and `RUNTIME_SEED_*`) are optional first-boot seeds only.
:::
## Quick Reference Table
@ -16,9 +16,9 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
| `ADMIN_EMAILS` | Auth/Admin | empty | Comma-separated emails auto-promoted to admin (requires auth enabled) |
| `API_BASE` | Legacy bootstrap seed | none | Optional first-boot seed into `default-openai`; then manage in Settings → Admin → Shared providers |
| `API_KEY` | Legacy bootstrap seed | none | Optional first-boot seed into `default-openai`; then manage in Settings → Admin → Shared providers |
| `NEXT_PUBLIC_*` runtime seeds | Legacy bootstrap seed | varies | Optional first-boot seeds for site features; then manage in Settings → Admin → Site features |
| `NEXT_PUBLIC_CHANGELOG_FEED_URL` | Legacy bootstrap seed | `https://docs.openreader.richardr.dev/changelog/manifest.json` | Optional first-boot seed for changelog feed URL; then manage in Settings → Admin → Site features |
| `NEXT_PUBLIC_ENABLE_USER_SIGNUPS` | Legacy bootstrap seed | `true` | Optional first-boot seed for whether new accounts can be created; then manage in Settings → Admin → Site features |
| `RUNTIME_SEED_*` runtime seeds | Legacy bootstrap seed | varies | Optional first-boot seeds for site features; then manage in Settings → Admin → Site features |
| `RUNTIME_SEED_CHANGELOG_FEED_URL` | Legacy bootstrap seed | `https://docs.openreader.richardr.dev/changelog/manifest.json` | Optional first-boot seed for changelog feed URL; then manage in Settings → Admin → Site features |
| `RUNTIME_SEED_ENABLE_USER_SIGNUPS` | Legacy bootstrap seed | `true` | Optional first-boot seed for whether new accounts can be created; then manage in Settings → Admin → Site features |
| `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 |
@ -53,14 +53,11 @@ For auth-enabled deployments, use **Settings → Admin** as the primary source o
| `RUN_FS_MIGRATIONS` | Storage migrations | `true` | Set `false` to skip startup filesystem -> S3/DB migration pass |
| `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) |
| `OPENREADER_COMPUTE_MODE` | Heavy compute backend | `local` | Set to `none` to disable ONNX word alignment + PDF layout parsing |
| `OPENREADER_COMPUTE_WORKER_URL` | Heavy compute backend | unset | Reserved for future worker backend mode (`worker`) |
| `OPENREADER_COMPUTE_WORKER_TOKEN` | Heavy compute backend | unset | Reserved for future worker backend mode (`worker`) |
| `OPENREADER_PDF_LAYOUT_MODEL_URL` | PDF layout model | PP-DocLayoutV3 ONNX URL | Override ONNX model URL for `ensureModel()` |
| `OPENREADER_PDF_LAYOUT_MODEL_DATA_URL` | PDF layout model | PP-DocLayoutV3 ONNX data URL | Override ONNX external data URL for `ensureModel()` |
| `OPENREADER_PDF_LAYOUT_CONFIG_URL` | PDF layout model | PP-DocLayoutV3 config URL | Override model config URL for `ensureModel()` |
| `OPENREADER_PDF_LAYOUT_PREPROCESSOR_URL` | PDF layout model | PP-DocLayoutV3 preprocessor URL | Override model preprocessor URL for `ensureModel()` |
| `OPENREADER_WHISPER_MODEL_*_URL` | Whisper ONNX model | onnx-community defaults | Optional per-artifact URL overrides for ONNX whisper-base_timestamped int8 downloads |
| `COMPUTE_MODE` | Heavy compute backend | `local` | Set to `none` to disable ONNX word alignment + PDF layout parsing |
| `COMPUTE_WORKER_URL` | Heavy compute backend | unset | Reserved for future worker backend mode (`worker`) |
| `COMPUTE_WORKER_TOKEN` | Heavy compute backend | unset | Reserved for future worker backend mode (`worker`) |
| `PDF_LAYOUT_MODEL_BASE_URL` | PDF layout model | PP-DocLayoutV3 ONNX base URL | Optional base URL override for `ensureModel()` |
| `WHISPER_MODEL_BASE_URL` | Whisper ONNX model | onnx-community defaults | Optional base URL override for ONNX whisper-base_timestamped int8 downloads |
| `FFMPEG_BIN` | Audio runtime | auto-detected (`ffmpeg-static`) | Override ffmpeg binary path |
@ -353,7 +350,7 @@ Multiple library roots for server library import.
## Audio Tooling and Alignment
### OPENREADER_COMPUTE_MODE
### COMPUTE_MODE
Selects the backend for heavy compute features (ONNX word alignment + PDF layout parsing).
@ -363,69 +360,38 @@ Selects the backend for heavy compute features (ONNX word alignment + PDF layout
- `none`: disable these features cleanly
- `worker` is reserved for a future external worker backend and currently fails fast at startup if selected
### OPENREADER_COMPUTE_WORKER_URL
### COMPUTE_WORKER_URL
Reserved for future external compute worker mode.
- Used only when `OPENREADER_COMPUTE_MODE=worker` (not implemented in v1)
- Used only when `COMPUTE_MODE=worker` (not implemented in v1)
- Leave unset in v1
### OPENREADER_COMPUTE_WORKER_TOKEN
### COMPUTE_WORKER_TOKEN
Reserved bearer token for future external compute worker mode.
- Used only when `OPENREADER_COMPUTE_MODE=worker` (not implemented in v1)
- Used only when `COMPUTE_MODE=worker` (not implemented in v1)
- Leave unset in v1
### OPENREADER_PDF_LAYOUT_MODEL_URL
### PDF_LAYOUT_MODEL_BASE_URL
Override URL for the PP-DocLayoutV3 ONNX model downloaded by `ensureModel()`.
Optional base URL override for PP-DocLayoutV3 artifacts downloaded by `ensureModel()`.
- Default: `https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx`
- Optional for custom mirrors/air-gapped workflows
- Default: `https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main`
- Required files at that base:
- `PP-DocLayoutV3.onnx`
- `PP-DocLayoutV3.onnx.data`
- `config.json`
- `preprocessor_config.json`
### OPENREADER_PDF_LAYOUT_MODEL_DATA_URL
### WHISPER_MODEL_BASE_URL
Override URL for the PP-DocLayoutV3 ONNX external data file downloaded by `ensureModel()`.
Optional base URL override for the built-in ONNX Whisper alignment model downloader.
- Default: `https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx.data`
### OPENREADER_PDF_LAYOUT_CONFIG_URL
Override URL for the PP-DocLayoutV3 `config.json` downloaded by `ensureModel()`.
- Default: `https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/config.json`
### OPENREADER_PDF_LAYOUT_PREPROCESSOR_URL
Override URL for the PP-DocLayoutV3 `preprocessor_config.json` downloaded by `ensureModel()`.
- Default: `https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/preprocessor_config.json`
- You can pre-populate the model cache via `pnpm fetch-models`
### OPENREADER_WHISPER_MODEL_*_URL
Optional per-artifact override URLs for the built-in ONNX Whisper alignment model downloader.
- Default base: `https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main`
- Default: `https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main`
- Default model variant: int8 (`encoder_model_int8.onnx`, `decoder_model_merged_int8.onnx`, `decoder_with_past_model_int8.onnx`)
- Use these when you need mirrors, pinned snapshots, or air-gapped fetch routing.
Supported override vars:
- `OPENREADER_WHISPER_MODEL_CONFIG_URL`
- `OPENREADER_WHISPER_MODEL_GENERATION_CONFIG_URL`
- `OPENREADER_WHISPER_MODEL_TOKENIZER_URL`
- `OPENREADER_WHISPER_MODEL_TOKENIZER_CONFIG_URL`
- `OPENREADER_WHISPER_MODEL_MERGES_URL`
- `OPENREADER_WHISPER_MODEL_VOCAB_URL`
- `OPENREADER_WHISPER_MODEL_NORMALIZER_URL`
- `OPENREADER_WHISPER_MODEL_ADDED_TOKENS_URL`
- `OPENREADER_WHISPER_MODEL_PREPROCESSOR_URL`
- `OPENREADER_WHISPER_MODEL_SPECIAL_TOKENS_MAP_URL`
- `OPENREADER_WHISPER_MODEL_ENCODER_URL`
- `OPENREADER_WHISPER_MODEL_DECODER_MERGED_URL`
- `OPENREADER_WHISPER_MODEL_DECODER_WITH_PAST_URL`
- The base URL must host all expected manifest files under the same relative paths.
### FFMPEG_BIN
@ -438,23 +404,23 @@ Absolute path or executable name for the ffmpeg binary used by audiobook/process
These variables exist only as **first-boot seeds** for the admin-managed runtime config. Prefer changing site features from **Settings → Admin → Site features**. Keep these only when you need bootstrap defaults before the first admin login. See [Admin Panel](../configure/admin-panel) for migration behavior.
The values are SSR-injected via `window.__OPENREADER_RUNTIME_CONFIG__`, so admin edits take effect for all users on the next page load — no rebuild required (unlike the old `NEXT_PUBLIC_*` build-time pattern).
The values are SSR-injected via `window.__RUNTIME_CONFIG__`, so admin edits take effect for all users on the next page load — no rebuild required (unlike the old build-time public env pattern).
### NEXT_PUBLIC_ENABLE_DOCX_CONVERSION
### RUNTIME_SEED_ENABLE_DOCX_CONVERSION
Controls whether the experimental DOCX-to-PDF conversion and upload feature is enabled.
- Default: `true` (enabled)
- Runtime key: `enableDocxConversion`
### NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS
### RUNTIME_SEED_ENABLE_DESTRUCTIVE_DELETE_ACTIONS
Controls whether the "Delete all user docs" and other bulk-delete buttons are shown in Settings.
- Default: `true` (enabled)
- Runtime key: `enableDestructiveDeleteActions`
### NEXT_PUBLIC_ENABLE_TTS_PROVIDERS_TAB
### RUNTIME_SEED_ENABLE_TTS_PROVIDERS_TAB
Controls whether the **TTS Provider** section appears in the user-facing Settings modal.
@ -462,7 +428,7 @@ Controls whether the **TTS Provider** section appears in the user-facing Setting
- Set `false` to hide provider/model/API controls in the per-user Settings modal (the admin panel is unaffected).
- Runtime key: `enableTtsProvidersTab`
### NEXT_PUBLIC_ENABLE_USER_SIGNUPS
### RUNTIME_SEED_ENABLE_USER_SIGNUPS
Controls whether new user accounts can be created.
@ -471,7 +437,7 @@ Controls whether new user accounts can be created.
- Existing users can still sign in.
- Runtime key: `enableUserSignups`
### NEXT_PUBLIC_RESTRICT_USER_API_KEYS
### RUNTIME_SEED_RESTRICT_USER_API_KEYS
Controls whether users can supply personal API keys/base URLs for built-in providers.
@ -480,7 +446,7 @@ Controls whether users can supply personal API keys/base URLs for built-in provi
- When `false`, users can use per-user BYOK credentials for built-in providers.
- Runtime key: `restrictUserApiKeys`
### NEXT_PUBLIC_DEFAULT_TTS_PROVIDER
### RUNTIME_SEED_DEFAULT_TTS_PROVIDER
Sets the default TTS provider for new users.
@ -490,7 +456,7 @@ Sets the default TTS provider for new users.
`showAllProviderModels` is a runtime-only admin setting (no env seed). Configure it in **Settings → Admin → Site features**.
### NEXT_PUBLIC_CHANGELOG_FEED_URL
### RUNTIME_SEED_CHANGELOG_FEED_URL
Sets the changelog manifest URL used by the Settings modal changelog viewer.
@ -499,7 +465,7 @@ Sets the changelog manifest URL used by the Settings modal changelog viewer.
- Runtime key: `changelogFeedUrl`
### NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT
### RUNTIME_SEED_ENABLE_AUDIOBOOK_EXPORT
Controls whether audiobook export UI/actions are shown in the client.

View file

@ -364,7 +364,7 @@ Absolute path or executable name for the ffmpeg binary used by audiobook/process
These variables exist only as **first-boot seeds** for the admin-managed runtime config. Prefer changing site features from **Settings → Admin → Site features**. Keep these only when you need bootstrap defaults before the first admin login. See [Admin Panel](../configure/admin-panel) for migration behavior.
The values are SSR-injected via `window.__OPENREADER_RUNTIME_CONFIG__`, so admin edits take effect for all users on the next page load — no rebuild required (unlike the old `NEXT_PUBLIC_*` build-time pattern).
The values are SSR-injected via `window.__RUNTIME_CONFIG__`, so admin edits take effect for all users on the next page load — no rebuild required (unlike the old `NEXT_PUBLIC_*` build-time pattern).
### NEXT_PUBLIC_ENABLE_DOCX_CONVERSION

View file

@ -14,6 +14,16 @@ const securityHeaders = [
value: 'max-age=63072000; includeSubDomains; preload',
},
];
const computeMode = (process.env.COMPUTE_MODE || 'local').trim().toLowerCase();
const computeDisabled = computeMode === 'none';
const serverExternalPackages = [
'@napi-rs/canvas',
'ffmpeg-static',
'better-sqlite3',
...(computeDisabled ? [] : ['onnxruntime-node', '@huggingface/tokenizers']),
];
const nextConfig: NextConfig = {
async headers() {
return [
@ -29,13 +39,7 @@ const nextConfig: NextConfig = {
canvas: '@napi-rs/canvas',
},
},
serverExternalPackages: [
"@napi-rs/canvas",
"ffmpeg-static",
"better-sqlite3",
"onnxruntime-node",
"@huggingface/tokenizers",
],
serverExternalPackages,
outputFileTracingIncludes: {
'/api/audiobook': [
'./node_modules/ffmpeg-static/ffmpeg',
@ -56,6 +60,16 @@ const nextConfig: NextConfig = {
'./node_modules/pdfjs-dist/legacy/build/pdf.worker.mjs',
],
},
...(computeDisabled
? {
outputFileTracingExcludes: {
'/*': [
'./node_modules/onnxruntime-node/**/*',
'./node_modules/@huggingface/tokenizers/**/*',
],
},
}
: {}),
};
export default nextConfig;

View file

@ -15,7 +15,6 @@
"migrate-fs": "node scripts/migrate-fs-v2.mjs",
"migrate-fs:dry-run": "node scripts/migrate-fs-v2.mjs --dry-run true",
"generate": "node drizzle/scripts/generate.mjs",
"fetch-models": "node scripts/fetch-models.mjs",
"docs:init": "pnpm --dir docs-site install",
"docs:dev": "pnpm --dir docs-site start",
"docs:build": "pnpm --dir docs-site build",

View file

@ -1,51 +0,0 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
const modelDir = path.join(process.cwd(), 'docstore', 'model');
const modelPath = path.join(modelDir, 'PP-DocLayoutV3.onnx');
const modelDataPath = path.join(modelDir, 'PP-DocLayoutV3.onnx.data');
const configPath = path.join(modelDir, 'pp-doclayoutv3.config.json');
const preprocessorPath = path.join(modelDir, 'pp-doclayoutv3.preprocessor_config.json');
const licensePath = path.join(modelDir, 'pp-doclayoutv3.LICENSE.txt');
const staticLicensePath = path.join(process.cwd(), 'src', 'lib', 'server', 'pdf-layout', 'model', 'LICENSE.txt');
const modelUrl = process.env.OPENREADER_PDF_LAYOUT_MODEL_URL
|| 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx';
const modelDataUrl = process.env.OPENREADER_PDF_LAYOUT_MODEL_DATA_URL
|| 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx.data';
const configUrl = process.env.OPENREADER_PDF_LAYOUT_CONFIG_URL
|| 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/config.json';
const preprocessorUrl = process.env.OPENREADER_PDF_LAYOUT_PREPROCESSOR_URL
|| 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/preprocessor_config.json';
await mkdir(modelDir, { recursive: true });
const modelRes = await fetch(modelUrl);
if (!modelRes.ok) {
throw new Error(`Failed to fetch model: ${modelRes.status} ${modelRes.statusText}`);
}
await writeFile(modelPath, new Uint8Array(await modelRes.arrayBuffer()));
const modelDataRes = await fetch(modelDataUrl);
if (!modelDataRes.ok) {
throw new Error(`Failed to fetch model data: ${modelDataRes.status} ${modelDataRes.statusText}`);
}
await writeFile(modelDataPath, new Uint8Array(await modelDataRes.arrayBuffer()));
const configRes = await fetch(configUrl);
if (!configRes.ok) {
throw new Error(`Failed to fetch config: ${configRes.status} ${configRes.statusText}`);
}
await writeFile(configPath, new Uint8Array(await configRes.arrayBuffer()));
const preprocessorRes = await fetch(preprocessorUrl);
if (!preprocessorRes.ok) {
throw new Error(`Failed to fetch preprocessor config: ${preprocessorRes.status} ${preprocessorRes.statusText}`);
}
await writeFile(preprocessorPath, new Uint8Array(await preprocessorRes.arrayBuffer()));
const staticLicense = await import('node:fs/promises').then((m) => m.readFile(staticLicensePath));
await writeFile(licensePath, staticLicense);
console.log(`Saved model to ${modelPath}`);

View file

@ -56,7 +56,7 @@ export default async function RootLayout({ children }: { children: ReactNode })
...runtimeConfig,
appVersion: pkg.version,
};
const runtimeConfigInit = `window.__OPENREADER_RUNTIME_CONFIG__=${jsonEmbedSafe(runtimeConfigWithAppVersion)};`;
const runtimeConfigInit = `window.__RUNTIME_CONFIG__=${jsonEmbedSafe(runtimeConfigWithAppVersion)};`;
return (
<html lang="en" className={figtree.variable} suppressHydrationWarning>

View file

@ -4,8 +4,8 @@ import { createContext, useContext, useMemo, type ReactNode } from 'react';
/**
* Site-wide runtime config resolved at SSR time and injected via
* `window.__OPENREADER_RUNTIME_CONFIG__`. Replaces module-scope reads of
* `process.env.NEXT_PUBLIC_*` so admin edits take effect on the next page
* `window.__RUNTIME_CONFIG__`. Replaces module-scope reads of
* build-time public env flags so admin edits take effect on the next page
* load without a redeploy. Read-only from the client; admin writes go
* through `/api/admin/settings` and trigger a reload.
*
@ -42,13 +42,13 @@ const RUNTIME_DEFAULTS: RuntimeConfig = {
declare global {
// Injected via SSR in `src/app/layout.tsx`. Always defined in the browser.
interface Window {
__OPENREADER_RUNTIME_CONFIG__?: Partial<RuntimeConfig>;
__RUNTIME_CONFIG__?: Partial<RuntimeConfig>;
}
}
function readInjectedConfig(): RuntimeConfig {
if (typeof window === 'undefined') return { ...RUNTIME_DEFAULTS };
const injected = window.__OPENREADER_RUNTIME_CONFIG__;
const injected = window.__RUNTIME_CONFIG__;
if (!injected || typeof injected !== 'object') return { ...RUNTIME_DEFAULTS };
return { ...RUNTIME_DEFAULTS, ...injected };
}

View file

@ -217,7 +217,7 @@ const WARM_AUDIO_CACHE_MAX_ITEMS = 6;
const wordHighlightFeatureEnabled = (() => {
if (typeof window === 'undefined') return true;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const injected = (window as any).__OPENREADER_RUNTIME_CONFIG__;
const injected = (window as any).__RUNTIME_CONFIG__;
if (!injected || typeof injected !== 'object') return true;
return typeof injected.computeAvailable === 'boolean'
? injected.computeAvailable

View file

@ -12,7 +12,7 @@ import {
* Idempotent boot-time seeding for the admin layer. Safe to call multiple
* times. Runs:
*
* 1. `seedRuntimeConfigFromEnv()` for each `NEXT_PUBLIC_*` env var that
* 1. `seedRuntimeConfigFromEnv()` for each `RUNTIME_SEED_*` env var that
* maps to a runtime config key, write the value as `source='env-seed'`.
*
* 2. Default admin provider seed if `admin_providers` is empty AND

View file

@ -4,7 +4,7 @@ import { adminProviders, adminSettings } from '@/db/schema';
import { isAuthEnabled } from '@/lib/server/auth/config';
/**
* Runtime config: site-wide settings that used to live in `NEXT_PUBLIC_*`
* Runtime config: site-wide settings that used to live in build-time env vars.
* env vars. Each key has:
* - a TypeScript value type
* - an env var name used for the first-run seed
@ -75,16 +75,16 @@ function stringValue(defaultValue: string, envVar: string): RuntimeConfigKeyDef<
}
export const RUNTIME_CONFIG_SCHEMA = {
defaultTtsProvider: stringValue('custom-openai', 'NEXT_PUBLIC_DEFAULT_TTS_PROVIDER'),
changelogFeedUrl: stringValue('https://docs.openreader.richardr.dev/changelog/manifest.json', 'NEXT_PUBLIC_CHANGELOG_FEED_URL'),
enableUserSignups: booleanFlag(true, 'NEXT_PUBLIC_ENABLE_USER_SIGNUPS'),
restrictUserApiKeys: booleanFlag(true, 'NEXT_PUBLIC_RESTRICT_USER_API_KEYS'),
defaultTtsProvider: stringValue('custom-openai', 'RUNTIME_SEED_DEFAULT_TTS_PROVIDER'),
changelogFeedUrl: stringValue('https://docs.openreader.richardr.dev/changelog/manifest.json', 'RUNTIME_SEED_CHANGELOG_FEED_URL'),
enableUserSignups: booleanFlag(true, 'RUNTIME_SEED_ENABLE_USER_SIGNUPS'),
restrictUserApiKeys: booleanFlag(true, 'RUNTIME_SEED_RESTRICT_USER_API_KEYS'),
// Historically the env semantics were "true unless explicitly 'false'",
// i.e. the feature defaults to ON.
enableTtsProvidersTab: booleanFlag(true, 'NEXT_PUBLIC_ENABLE_TTS_PROVIDERS_TAB'),
enableAudiobookExport: booleanFlag(true, 'NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT'),
enableDocxConversion: booleanFlag(true, 'NEXT_PUBLIC_ENABLE_DOCX_CONVERSION'),
enableDestructiveDeleteActions: booleanFlag(true, 'NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS'),
enableTtsProvidersTab: booleanFlag(true, 'RUNTIME_SEED_ENABLE_TTS_PROVIDERS_TAB'),
enableAudiobookExport: booleanFlag(true, 'RUNTIME_SEED_ENABLE_AUDIOBOOK_EXPORT'),
enableDocxConversion: booleanFlag(true, 'RUNTIME_SEED_ENABLE_DOCX_CONVERSION'),
enableDestructiveDeleteActions: booleanFlag(true, 'RUNTIME_SEED_ENABLE_DESTRUCTIVE_DELETE_ACTIONS'),
showAllProviderModels: runtimeBoolean(true),
} as const satisfies Record<string, RuntimeConfigKeyDef<unknown>>;

View file

@ -1,5 +1,4 @@
import type { ComputeBackend, ComputeMode } from '@/lib/server/compute/types';
import { LocalComputeBackend } from '@/lib/server/compute/local';
import { NoneComputeBackend } from '@/lib/server/compute/none';
import { isComputeModeAvailable, readComputeMode } from '@/lib/server/compute/mode';
@ -10,9 +9,13 @@ function createBackend(): ComputeBackend {
if (mode === 'none') return new NoneComputeBackend();
if (mode === 'worker') {
throw new Error(
'OPENREADER_COMPUTE_MODE=worker is not implemented yet in v1. Switch to local/none or implement WorkerComputeBackend (v2).',
'COMPUTE_MODE=worker is not implemented yet in v1. Switch to local/none or implement WorkerComputeBackend (v2).',
);
}
// Intentionally lazy-load local compute so COMPUTE_MODE=none builds
// can avoid tracing heavy ONNX dependencies.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { LocalComputeBackend } = require('@/lib/server/compute/local') as typeof import('@/lib/server/compute/local');
return new LocalComputeBackend();
}

View file

@ -1,7 +1,7 @@
import type { ComputeMode } from '@/lib/server/compute/types';
export function readComputeMode(): ComputeMode {
const raw = (process.env.OPENREADER_COMPUTE_MODE || 'local').trim().toLowerCase();
const raw = (process.env.COMPUTE_MODE || 'local').trim().toLowerCase();
if (raw === 'local' || raw === 'none' || raw === 'worker') return raw;
return 'local';
}
@ -9,7 +9,7 @@ export function readComputeMode(): ComputeMode {
export function isComputeModeAvailable(mode: ComputeMode): boolean {
if (mode === 'worker') {
throw new Error(
'OPENREADER_COMPUTE_MODE=worker is not implemented yet in v1. Switch to local/none or implement WorkerComputeBackend (v2).',
'COMPUTE_MODE=worker is not implemented yet in v1. Switch to local/none or implement WorkerComputeBackend (v2).',
);
}
return mode !== 'none';

View file

@ -7,11 +7,11 @@ export class NoneComputeBackend implements ComputeBackend {
async alignWords(input: WhisperAlignInput): Promise<WhisperAlignResult> {
void input;
throw new UnsupportedComputeError('Word alignment is unavailable: OPENREADER_COMPUTE_MODE=none');
throw new UnsupportedComputeError('Word alignment is unavailable: COMPUTE_MODE=none');
}
async parsePdfLayout(input: PdfLayoutInput): Promise<ParsedPdfDocument> {
void input;
throw new UnsupportedComputeError('PDF layout parsing is unavailable: OPENREADER_COMPUTE_MODE=none');
throw new UnsupportedComputeError('PDF layout parsing is unavailable: COMPUTE_MODE=none');
}
}

View file

@ -4,10 +4,8 @@ import { access, mkdir, rename, writeFile, readFile, unlink, copyFile } from 'fs
import { DOCSTORE_DIR } from '@/lib/server/storage/library-mount';
import manifest from '@/lib/server/pdf-layout/model/manifest.json';
const DEFAULT_MODEL_URL = 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx';
const DEFAULT_MODEL_DATA_URL = 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/PP-DocLayoutV3.onnx.data';
const DEFAULT_CONFIG_URL = 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/config.json';
const DEFAULT_PREPROCESSOR_URL = 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main/preprocessor_config.json';
const DEFAULT_MODEL_BASE_URL = 'https://huggingface.co/Bei0001/PP-DocLayoutV3-ONNX/resolve/main';
const PDF_LAYOUT_MODEL_BASE_URL_ENV = 'PDF_LAYOUT_MODEL_BASE_URL';
const MODEL_DIR = path.join(DOCSTORE_DIR, 'model');
export const MODEL_PATH = path.join(MODEL_DIR, 'PP-DocLayoutV3.onnx');
export const MODEL_DATA_PATH = path.join(MODEL_DIR, 'PP-DocLayoutV3.onnx.data');
@ -32,6 +30,10 @@ async function downloadToFile(url: string, outPath: string): Promise<void> {
await writeFile(outPath, bytes);
}
function joinModelUrl(baseUrl: string, relativePath: string): string {
return `${baseUrl.replace(/\/+$/, '')}/${relativePath}`;
}
function manifestEntry(filePath: string): { sha256: string; size: number } | null {
const found = manifest.files.find((entry) => entry.path === filePath);
if (!found || !found.sha256) return null;
@ -83,14 +85,12 @@ async function ensureModelInternal(): Promise<string> {
const modelDataTmpPath = `${MODEL_DATA_PATH}.tmp`;
const configTmpPath = `${MODEL_CONFIG_PATH}.tmp`;
const preprocessorTmpPath = `${MODEL_PREPROCESSOR_PATH}.tmp`;
const modelUrl = process.env.OPENREADER_PDF_LAYOUT_MODEL_URL?.trim()
|| DEFAULT_MODEL_URL;
const modelDataUrl = process.env.OPENREADER_PDF_LAYOUT_MODEL_DATA_URL?.trim()
|| DEFAULT_MODEL_DATA_URL;
const configUrl = process.env.OPENREADER_PDF_LAYOUT_CONFIG_URL?.trim()
|| DEFAULT_CONFIG_URL;
const preprocessorUrl = process.env.OPENREADER_PDF_LAYOUT_PREPROCESSOR_URL?.trim()
|| DEFAULT_PREPROCESSOR_URL;
const modelBaseUrl = process.env[PDF_LAYOUT_MODEL_BASE_URL_ENV]?.trim()
|| DEFAULT_MODEL_BASE_URL;
const modelUrl = joinModelUrl(modelBaseUrl, 'PP-DocLayoutV3.onnx');
const modelDataUrl = joinModelUrl(modelBaseUrl, 'PP-DocLayoutV3.onnx.data');
const configUrl = joinModelUrl(modelBaseUrl, 'config.json');
const preprocessorUrl = joinModelUrl(modelBaseUrl, 'preprocessor_config.json');
await downloadToFile(modelUrl, modelTmpPath);
if (!(await verifyFile(modelTmpPath, 'model.onnx'))) {

View file

@ -16,6 +16,23 @@ export const WHISPER_DECODER_MERGED_MODEL_PATH = path.join(MODEL_DIR, 'onnx', 'd
export const WHISPER_DECODER_WITH_PAST_MODEL_PATH = path.join(MODEL_DIR, 'onnx', 'decoder_with_past_model_int8.onnx');
const BASE_MODEL_URL = 'https://huggingface.co/onnx-community/whisper-base_timestamped/resolve/main';
const WHISPER_MODEL_BASE_URL_ENV = 'WHISPER_MODEL_BASE_URL';
const MODEL_RELATIVE_PATHS: string[] = [
'config.json',
'generation_config.json',
'tokenizer.json',
'tokenizer_config.json',
'merges.txt',
'vocab.json',
'normalizer.json',
'added_tokens.json',
'preprocessor_config.json',
'special_tokens_map.json',
'onnx/encoder_model_int8.onnx',
'onnx/decoder_model_merged_int8.onnx',
'onnx/decoder_with_past_model_int8.onnx',
];
const DEFAULT_URLS: Record<string, string> = {
'config.json': `${BASE_MODEL_URL}/config.json`,
@ -33,22 +50,6 @@ const DEFAULT_URLS: Record<string, string> = {
'onnx/decoder_with_past_model_int8.onnx': `${BASE_MODEL_URL}/onnx/decoder_with_past_model_int8.onnx`,
};
const ENV_URL_OVERRIDES: Record<string, string> = {
'config.json': 'OPENREADER_WHISPER_MODEL_CONFIG_URL',
'generation_config.json': 'OPENREADER_WHISPER_MODEL_GENERATION_CONFIG_URL',
'tokenizer.json': 'OPENREADER_WHISPER_MODEL_TOKENIZER_URL',
'tokenizer_config.json': 'OPENREADER_WHISPER_MODEL_TOKENIZER_CONFIG_URL',
'merges.txt': 'OPENREADER_WHISPER_MODEL_MERGES_URL',
'vocab.json': 'OPENREADER_WHISPER_MODEL_VOCAB_URL',
'normalizer.json': 'OPENREADER_WHISPER_MODEL_NORMALIZER_URL',
'added_tokens.json': 'OPENREADER_WHISPER_MODEL_ADDED_TOKENS_URL',
'preprocessor_config.json': 'OPENREADER_WHISPER_MODEL_PREPROCESSOR_URL',
'special_tokens_map.json': 'OPENREADER_WHISPER_MODEL_SPECIAL_TOKENS_MAP_URL',
'onnx/encoder_model_int8.onnx': 'OPENREADER_WHISPER_MODEL_ENCODER_URL',
'onnx/decoder_model_merged_int8.onnx': 'OPENREADER_WHISPER_MODEL_DECODER_MERGED_URL',
'onnx/decoder_with_past_model_int8.onnx': 'OPENREADER_WHISPER_MODEL_DECODER_WITH_PAST_URL',
};
type ManifestEntry = { path: string; sha256?: string; size?: number };
export interface WhisperArtifactSpec {
@ -82,10 +83,15 @@ function resolvePath(relativePath: string, modelDir: string): string {
return path.join(modelDir, relativePath);
}
function joinModelUrl(baseUrl: string, relativePath: string): string {
return `${baseUrl.replace(/\/+$/, '')}/${relativePath}`;
}
function resolveUrl(relativePath: string): string {
const envKey = ENV_URL_OVERRIDES[relativePath];
const override = envKey ? process.env[envKey]?.trim() : '';
if (override) return override;
const overrideBase = process.env[WHISPER_MODEL_BASE_URL_ENV]?.trim();
if (overrideBase) {
return joinModelUrl(overrideBase, relativePath);
}
const fallback = DEFAULT_URLS[relativePath];
if (!fallback) {
throw new Error(`No default URL configured for Whisper model artifact: ${relativePath}`);
@ -194,6 +200,14 @@ export function createSingleflightRunner<T>(work: () => Promise<T>): () => Promi
}
async function ensureModelInternal(): Promise<string> {
if (process.env[WHISPER_MODEL_BASE_URL_ENV]?.trim()) {
for (const relativePath of MODEL_RELATIVE_PATHS) {
if (!(relativePath in DEFAULT_URLS)) {
throw new Error(`Missing default URL path mapping for Whisper artifact: ${relativePath}`);
}
}
}
const artifacts: WhisperArtifactSpec[] = MODEL_FILES.map((entry) => ({
path: entry.path,
sha256: entry.sha256,

View file

@ -5,13 +5,13 @@ import { defaultModelForProviderType } from '@/lib/shared/tts-provider-policy';
// Runtime config (admin-controlled) is layered on top of the static defaults
// below. We resolve it lazily so this module stays importable from non-React
// contexts (Dexie, server routes). The actual values come from
// `window.__OPENREADER_RUNTIME_CONFIG__` (SSR-injected) on the client, and
// `window.__RUNTIME_CONFIG__` (SSR-injected) on the client, and
// from the built-in defaults during SSR.
function readRuntimeString(key: string, defaultValue: string): string {
if (typeof window === 'undefined') return defaultValue;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const injected = (window as any).__OPENREADER_RUNTIME_CONFIG__;
const injected = (window as any).__RUNTIME_CONFIG__;
if (!injected || typeof injected !== 'object') return defaultValue;
const value = injected[key];
return typeof value === 'string' && value ? value : defaultValue;
@ -80,7 +80,7 @@ export interface AppConfigValues {
/**
* Build defaults lazily so we can read SSR-injected admin overrides
* (`window.__OPENREADER_RUNTIME_CONFIG__`). Modules that need the defaults
* (`window.__RUNTIME_CONFIG__`). Modules that need the defaults
* statically should call `getAppConfigDefaults()` at use time. The exported
* `APP_CONFIG_DEFAULTS` is a Proxy that re-resolves on each access so
* mutations to the runtime config (admin edits) are picked up by anything