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:
parent
874e5ef359
commit
3a21f2a5f5
24 changed files with 166 additions and 241 deletions
48
.env.example
48
.env.example
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
:::
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'))) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue