diff --git a/.env.example b/.env.example index 03d69a5..3b38e23 100644 --- a/.env.example +++ b/.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 diff --git a/README.md b/README.md index 63a99ec..73dfdc9 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs-site/docs/configure/admin-panel.md b/docs-site/docs/configure/admin-panel.md index e00d207..a887d4b 100644 --- a/docs-site/docs/configure/admin-panel.md +++ b/docs-site/docs/configure/admin-panel.md @@ -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. diff --git a/docs-site/docs/configure/auth.md b/docs-site/docs/configure/auth.md index 643823d..34e5053 100644 --- a/docs-site/docs/configure/auth.md +++ b/docs-site/docs/configure/auth.md @@ -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. diff --git a/docs-site/docs/configure/tts-providers.md b/docs-site/docs/configure/tts-providers.md index 7a03e5d..1fa5961 100644 --- a/docs-site/docs/configure/tts-providers.md +++ b/docs-site/docs/configure/tts-providers.md @@ -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 diff --git a/docs-site/docs/deploy/local-development.md b/docs-site/docs/deploy/local-development.md index 1bc2183..7ab46c5 100644 --- a/docs-site/docs/deploy/local-development.md +++ b/docs-site/docs/deploy/local-development.md @@ -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`. @@ -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= # Optional when you need multiple local origins: @@ -184,7 +184,7 @@ AUTH_SECRET= # 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= # 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 :::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). diff --git a/docs-site/docs/deploy/vercel-deployment.md b/docs-site/docs/deploy/vercel-deployment.md index 10fe73a..e6053fd 100644 --- a/docs-site/docs/deploy/vercel-deployment.md +++ b/docs-site/docs/deploy/vercel-deployment.md @@ -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. diff --git a/docs-site/docs/docker-quick-start.md b/docs-site/docs/docker-quick-start.md index 6af01e8..933cc29 100644 --- a/docs-site/docs/docker-quick-start.md +++ b/docs-site/docs/docker-quick-start.md @@ -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. ::: diff --git a/docs-site/docs/reference/environment-variables.md b/docs-site/docs/reference/environment-variables.md index 9c58793..f04133b 100644 --- a/docs-site/docs/reference/environment-variables.md +++ b/docs-site/docs/reference/environment-variables.md @@ -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. diff --git a/docs-site/versioned_docs/version-v3.0.0/reference/environment-variables.md b/docs-site/versioned_docs/version-v3.0.0/reference/environment-variables.md index ef79788..781afbb 100644 --- a/docs-site/versioned_docs/version-v3.0.0/reference/environment-variables.md +++ b/docs-site/versioned_docs/version-v3.0.0/reference/environment-variables.md @@ -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 diff --git a/next.config.ts b/next.config.ts index b35c2d0..0773b73 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package.json b/package.json index 63197cf..115a93e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/fetch-models.mjs b/scripts/fetch-models.mjs deleted file mode 100644 index edf56c2..0000000 --- a/scripts/fetch-models.mjs +++ /dev/null @@ -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}`); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1ebd8c7..1315dbe 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 ( diff --git a/src/contexts/RuntimeConfigContext.tsx b/src/contexts/RuntimeConfigContext.tsx index e1e3991..1870b8e 100644 --- a/src/contexts/RuntimeConfigContext.tsx +++ b/src/contexts/RuntimeConfigContext.tsx @@ -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; + __RUNTIME_CONFIG__?: Partial; } } 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 }; } diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 8bbc362..d96d23e 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -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 diff --git a/src/lib/server/admin/seed.ts b/src/lib/server/admin/seed.ts index 18e3896..7cbc2cc 100644 --- a/src/lib/server/admin/seed.ts +++ b/src/lib/server/admin/seed.ts @@ -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 diff --git a/src/lib/server/admin/settings.ts b/src/lib/server/admin/settings.ts index 598d46f..eb2a77f 100644 --- a/src/lib/server/admin/settings.ts +++ b/src/lib/server/admin/settings.ts @@ -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>; diff --git a/src/lib/server/compute/index.ts b/src/lib/server/compute/index.ts index b469a55..8b211b2 100644 --- a/src/lib/server/compute/index.ts +++ b/src/lib/server/compute/index.ts @@ -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(); } diff --git a/src/lib/server/compute/mode.ts b/src/lib/server/compute/mode.ts index 170d6c9..5ebd349 100644 --- a/src/lib/server/compute/mode.ts +++ b/src/lib/server/compute/mode.ts @@ -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'; diff --git a/src/lib/server/compute/none.ts b/src/lib/server/compute/none.ts index a2f6695..64202e8 100644 --- a/src/lib/server/compute/none.ts +++ b/src/lib/server/compute/none.ts @@ -7,11 +7,11 @@ export class NoneComputeBackend implements ComputeBackend { async alignWords(input: WhisperAlignInput): Promise { 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 { 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'); } } diff --git a/src/lib/server/pdf-layout/ensureModel.ts b/src/lib/server/pdf-layout/ensureModel.ts index e39dd62..affe4b9 100644 --- a/src/lib/server/pdf-layout/ensureModel.ts +++ b/src/lib/server/pdf-layout/ensureModel.ts @@ -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 { 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 { 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'))) { diff --git a/src/lib/server/whisper/ensureModel.ts b/src/lib/server/whisper/ensureModel.ts index 6d1ae77..53b1d1b 100644 --- a/src/lib/server/whisper/ensureModel.ts +++ b/src/lib/server/whisper/ensureModel.ts @@ -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 = { 'config.json': `${BASE_MODEL_URL}/config.json`, @@ -33,22 +50,6 @@ const DEFAULT_URLS: Record = { 'onnx/decoder_with_past_model_int8.onnx': `${BASE_MODEL_URL}/onnx/decoder_with_past_model_int8.onnx`, }; -const ENV_URL_OVERRIDES: Record = { - '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(work: () => Promise): () => Promi } async function ensureModelInternal(): Promise { + 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, diff --git a/src/types/config.ts b/src/types/config.ts index 03881c3..58bb388 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -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