feat: Replace NEXT_PUBLIC_NODE_ENV with explicit feature flags for granular control over DOCX conversion, destructive actions, TTS defaults, and DeepInfra model visibility.
This commit is contained in:
parent
eeeceb8e54
commit
8f5e87ed4a
7 changed files with 110 additions and 79 deletions
13
.env.example
13
.env.example
|
|
@ -1,7 +1,3 @@
|
|||
# (Optional) Client feature flags (does not work in Docker containers due to being build-time only)
|
||||
NEXT_PUBLIC_NODE_ENV=development
|
||||
NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=
|
||||
NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=
|
||||
|
||||
# Local / OpenAI TTS API Configuration (default)
|
||||
# Suggest using https://github.com/remsky/Kokoro-FastAPI
|
||||
|
|
@ -74,3 +70,12 @@ WHISPER_CPP_BIN=/whisper.cpp/build/bin/whisper-cli
|
|||
|
||||
# (Optional) Override ffmpeg binary path used for audiobook processing
|
||||
FFMPEG_BIN=
|
||||
|
||||
# (Optional) Client feature flags (does not work in Docker containers due to being build-time only)
|
||||
# NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=true
|
||||
# NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=true
|
||||
# NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=custom-openai
|
||||
# NEXT_PUBLIC_DEFAULT_TTS_MODEL=kokoro
|
||||
# NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=true
|
||||
# NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true
|
||||
# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ This guide covers deploying OpenReader WebUI to Vercel with external Postgres an
|
|||
Recommended production setup (auth enabled):
|
||||
|
||||
```bash
|
||||
API_BASE=https://api.deepinfra.com/v1/openai
|
||||
API_KEY=your_deepinfra_key
|
||||
POSTGRES_URL=postgres://...
|
||||
USE_EMBEDDED_WEED_MINI=false
|
||||
S3_ACCESS_KEY_ID=...
|
||||
|
|
@ -27,34 +29,29 @@ S3_REGION=us-east-1
|
|||
S3_PREFIX=openreader
|
||||
BASE_URL=https://your-app.vercel.app
|
||||
AUTH_SECRET=...
|
||||
NEXT_PUBLIC_NODE_ENV=production
|
||||
# Optional client/runtime feature overrides:
|
||||
# NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=false
|
||||
# NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=true
|
||||
# Optional client/runtime feature defaults:
|
||||
NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=false
|
||||
NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=false
|
||||
NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=deepinfra
|
||||
NEXT_PUBLIC_DEFAULT_TTS_MODEL=hexgrad/Kokoro-82M
|
||||
NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=false
|
||||
NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true
|
||||
NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false
|
||||
# Optional (non-AWS S3-compatible providers):
|
||||
# S3_ENDPOINT=https://...
|
||||
# S3_FORCE_PATH_STYLE=true
|
||||
```
|
||||
|
||||
:::info `NEXT_PUBLIC_*` feature flags
|
||||
- `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=false`: hides audiobook export UI entry points.
|
||||
- `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=true`: enables word-highlight UI and timestamp alignment requests.
|
||||
:::
|
||||
:::info Production Configuration & Feature Flags
|
||||
We recommend setting these defaults for a production-like environment:
|
||||
|
||||
:::warning `NEXT_PUBLIC_NODE_ENV` behavior
|
||||
Use `NEXT_PUBLIC_NODE_ENV=production` on Vercel unless you explicitly want dev-oriented client behavior.
|
||||
|
||||
With `production`:
|
||||
- Footer is shown in the app shell
|
||||
- DOCX upload/conversion option is hidden
|
||||
- Default provider/model behavior is production-oriented
|
||||
- DeepInfra model picker is restricted without an API key
|
||||
- Privacy modal shows hosted-service/operator wording
|
||||
- Dev-only destructive document actions are hidden
|
||||
|
||||
With unset/non-`production`, the inverse dev behavior applies.
|
||||
|
||||
Full details: [Environment Variables](../reference/environment-variables#next_public_node_env).
|
||||
- `NEXT_PUBLIC_ENABLE_DOCX_CONVERSION=false`: Disables DOCX upload (requires external tools anyway)
|
||||
- `NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS=false`: Hides destructive "Delete All" actions
|
||||
- `NEXT_PUBLIC_DEFAULT_TTS_PROVIDER=deepinfra`: Points default TTS to a scalable provider
|
||||
- `NEXT_PUBLIC_DEFAULT_TTS_MODEL=hexgrad/Kokoro-82M`: Uses a high-quality default model
|
||||
- `NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS=false`: Restricts usage to free models if no key is provided
|
||||
- `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT=true`: (Optional) Controls audiobook export UI
|
||||
- `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT=false`: (Optional) Controls word highlighting UI (requires timestamp backend)
|
||||
:::
|
||||
|
||||
:::warning Auth recommendation
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ This is the single reference page for OpenReader WebUI environment variables.
|
|||
|
||||
| Variable | Area | Default | When to set |
|
||||
| --- | --- | --- | --- |
|
||||
| `NEXT_PUBLIC_NODE_ENV` | Runtime mode | treated as `development` unless `production` | Set `production` for production client behavior |
|
||||
| `NEXT_PUBLIC_ENABLE_DOCX_CONVERSION` | Client feature flags | `true` unless set to `false` | Set `false` to hide DOCX support |
|
||||
| `NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS` | Client feature flags | `true` unless set to `false` | Set `false` to hide destructive actions |
|
||||
| `NEXT_PUBLIC_DEFAULT_TTS_PROVIDER` | Client feature flags | `custom-openai` | Override default TTS provider |
|
||||
| `NEXT_PUBLIC_DEFAULT_TTS_MODEL` | Client feature flags | `kokoro` | Override default TTS model |
|
||||
| `NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS` | Client feature flags | `true` unless set to `false` | Set `false` to restrict DeepInfra models |
|
||||
| `NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT` | Client feature flags | `true` unless set to `false` | Set `false` to hide audiobook export UI |
|
||||
| `NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT` | Client feature flags | `false` unless set to `true` | Set `true` to enable word highlight + alignment |
|
||||
| `API_BASE` | TTS provider | none | Point to your OpenAI-compatible TTS base URL |
|
||||
|
|
@ -50,41 +54,7 @@ This is the single reference page for OpenReader WebUI environment variables.
|
|||
| `WHISPER_CPP_BIN` | Word timing | unset | Set to enable `whisper.cpp` timestamps |
|
||||
| `FFMPEG_BIN` | Audio runtime | auto-detected (`ffmpeg-static`) | Override ffmpeg binary path |
|
||||
|
||||
## Client Runtime and Feature Flags
|
||||
|
||||
### NEXT_PUBLIC_NODE_ENV
|
||||
|
||||
Controls development vs production behavior in client/server code paths.
|
||||
|
||||
- Typical values: `development`, `production`
|
||||
- OpenReader `isDev` checks rely on this variable directly
|
||||
- If this is not `production`, OpenReader treats the client as development mode
|
||||
- In deployed environments, set `NEXT_PUBLIC_NODE_ENV=production` explicitly for predictable production behavior
|
||||
- Affects:
|
||||
- Footer visibility in the app shell
|
||||
- DOCX upload/conversion availability in upload UI
|
||||
- Default provider/model behavior for first-run TTS config
|
||||
- DeepInfra model picker restrictions when no API key is set
|
||||
- Dev-only destructive document actions in settings
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT
|
||||
|
||||
Controls whether audiobook export UI/actions are shown in the client.
|
||||
|
||||
- Default behavior: enabled unless explicitly set to `false`
|
||||
- Applies in both development and production
|
||||
- Affects export entry points in PDF/EPUB pages and document settings UI
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT
|
||||
|
||||
Controls word-by-word highlighting UI and timestamp-alignment behavior.
|
||||
|
||||
- Default behavior: disabled unless set to `true`
|
||||
- Applies in both development and production
|
||||
- Requires working timestamp generation (for example `WHISPER_CPP_BIN`)
|
||||
- Affects:
|
||||
- Word-highlight toggles in document settings
|
||||
- Alignment requests during TTS playback
|
||||
|
||||
## TTS Provider and Request Behavior
|
||||
|
||||
|
|
@ -367,3 +337,59 @@ Absolute path or executable name for the ffmpeg binary used by audiobook/process
|
|||
|
||||
- Resolution order: `FFMPEG_BIN` -> `ffmpeg-static`
|
||||
- Example: `/var/task/node_modules/ffmpeg-static/ffmpeg`
|
||||
|
||||
## Client Runtime and Feature Flags
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_DOCX_CONVERSION
|
||||
|
||||
Controls whether the experimental DOCX-to-PDF conversion and upload feature is enabled.
|
||||
|
||||
- Default: `true` (enabled)
|
||||
- Set `false` to hide DOCX support in the upload UI
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS
|
||||
|
||||
Controls whether the "Delete all user docs" and other bulk-delete buttons are shown in Settings.
|
||||
|
||||
- Default: `true` (enabled)
|
||||
- Set `false` to hide destructive actions (recommended for production)
|
||||
|
||||
### NEXT_PUBLIC_DEFAULT_TTS_PROVIDER
|
||||
|
||||
Sets the default TTS provider for new users.
|
||||
|
||||
- Default: `custom-openai`
|
||||
- Example values: `deepinfra`, `openai`, `custom-openai`
|
||||
|
||||
### NEXT_PUBLIC_DEFAULT_TTS_MODEL
|
||||
|
||||
Sets the default TTS model for new users.
|
||||
|
||||
- Default: `kokoro`
|
||||
- Example values: `hexgrad/Kokoro-82M`, `tts-1`
|
||||
|
||||
### NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS
|
||||
|
||||
Controls whether the DeepInfra model list shows all models or just the free tier when no API key is set.
|
||||
|
||||
- Default: `true` (show all)
|
||||
- Set `false` to restrict to free tier models when no API key is provided
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_AUDIOBOOK_EXPORT
|
||||
|
||||
Controls whether audiobook export UI/actions are shown in the client.
|
||||
|
||||
- Default behavior: enabled unless explicitly set to `false`
|
||||
- Applies in both development and production
|
||||
- Affects export entry points in PDF/EPUB pages and document settings UI
|
||||
|
||||
### NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT
|
||||
|
||||
Controls word-by-word highlighting UI and timestamp-alignment behavior.
|
||||
|
||||
- Default behavior: disabled unless set to `true`
|
||||
- Applies in both development and production
|
||||
- Requires working timestamp generation (for example `WHISPER_CPP_BIN`)
|
||||
- Affects:
|
||||
- Word-highlight toggles in document settings
|
||||
- Alignment requests during TTS playback
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ import { UploadIcon } from '@/components/icons/Icons';
|
|||
import { useDocuments } from '@/contexts/DocumentContext';
|
||||
import { uploadDocxAsPdf } from '@/lib/client-documents';
|
||||
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||
const enableDocx = process.env.NEXT_PUBLIC_ENABLE_DOCX_CONVERSION !== 'false';
|
||||
|
||||
|
||||
interface DocumentUploaderProps {
|
||||
className?: string;
|
||||
|
|
@ -14,8 +15,8 @@ interface DocumentUploaderProps {
|
|||
}
|
||||
|
||||
export function DocumentUploader({ className = '', variant = 'default' }: DocumentUploaderProps) {
|
||||
const {
|
||||
addPDFDocument: addPDF,
|
||||
const {
|
||||
addPDFDocument: addPDF,
|
||||
addEPUBDocument: addEPUB,
|
||||
addHTMLDocument: addHTML,
|
||||
refreshDocuments,
|
||||
|
|
@ -38,7 +39,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
|
|||
await addEPUB(file);
|
||||
} else if (file.type === 'text/plain' || file.type === 'text/markdown' || file.name.endsWith('.md')) {
|
||||
await addHTML(file);
|
||||
} else if (isDev && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
} else if (enableDocx && file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
|
||||
// Preserve prior UX: show "Converting DOCX..." state rather than generic uploading.
|
||||
setIsUploading(false);
|
||||
setIsConverting(true);
|
||||
|
|
@ -65,7 +66,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
|
|||
'application/epub+zip': ['.epub'],
|
||||
'text/plain': ['.txt'],
|
||||
'text/markdown': ['.md'],
|
||||
...(isDev ? {
|
||||
...(enableDocx ? {
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx']
|
||||
} : {})
|
||||
},
|
||||
|
|
@ -111,7 +112,7 @@ export function DocumentUploader({ className = '', variant = 'default' }: Docume
|
|||
{isDragActive ? 'Drop your file(s) here' : 'Drop your file(s) here, or click to select'}
|
||||
</p>
|
||||
<p className="text-xs sm:text-sm text-muted">
|
||||
{isDev ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
|
||||
{enableDocx ? 'PDF, EPUB, TXT, MD, or DOCX files are accepted' : 'PDF, EPUB, TXT, or MD files are accepted'}
|
||||
</p>
|
||||
{error && <p className="mt-2 text-sm text-red-500">{error}</p>}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-d
|
|||
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/document-cache';
|
||||
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/document-preview-cache';
|
||||
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||
const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
|
||||
const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';
|
||||
|
||||
|
||||
const themes = THEMES.map(id => ({
|
||||
id,
|
||||
|
|
@ -108,7 +110,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
];
|
||||
case 'deepinfra':
|
||||
// In production without an API key, limit to free tier model
|
||||
if (!isDev && !localApiKey) {
|
||||
if (!showAllDeepInfra && !localApiKey) {
|
||||
return [
|
||||
{ id: 'hexgrad/Kokoro-82M', name: 'hexgrad/Kokoro-82M' }
|
||||
];
|
||||
|
|
@ -561,7 +563,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
type="password"
|
||||
value={localApiKey}
|
||||
onChange={(e) => handleInputChange('apiKey', e.target.value)}
|
||||
placeholder={!isDev && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
|
||||
placeholder={!showAllDeepInfra && localTTSProvider === 'deepinfra' ? "Deepinfra free or use your API key" : "Using environment variable"}
|
||||
className="w-full rounded-lg bg-background py-1.5 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -782,7 +784,7 @@ export function SettingsModal({ className = '' }: { className?: string }) {
|
|||
>
|
||||
Clear cache
|
||||
</Button>
|
||||
{isDev && (
|
||||
{enableDestructiveDelete && (
|
||||
<div className="flex w-full gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ type OpenReaderDB = Dexie & {
|
|||
|
||||
export const db = new Dexie(DB_NAME) as OpenReaderDB;
|
||||
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||
|
||||
|
||||
type DexieOpenStatus = 'opening' | 'opened' | 'blocked' | 'stalled' | 'error';
|
||||
|
||||
|
|
@ -113,8 +113,8 @@ function inferProviderAndBaseUrl(raw: RawConfigMap): { provider: string; baseUrl
|
|||
const cachedBaseUrl = raw.baseUrl;
|
||||
let inferredProvider = raw.ttsProvider || '';
|
||||
|
||||
if (!isDev && !raw.ttsProvider) {
|
||||
inferredProvider = 'deepinfra';
|
||||
if (!raw.ttsProvider) {
|
||||
inferredProvider = process.env.NEXT_PUBLIC_DEFAULT_TTS_PROVIDER || 'custom-openai';
|
||||
} else if (!inferredProvider) {
|
||||
if (cachedBaseUrl) {
|
||||
const baseUrlLower = cachedBaseUrl.toLowerCase();
|
||||
|
|
@ -194,8 +194,8 @@ function buildAppConfigFromRaw(raw: RawConfigMap): AppConfigRow {
|
|||
(provider === 'openai'
|
||||
? 'tts-1'
|
||||
: provider === 'deepinfra'
|
||||
? 'hexgrad/Kokoro-82M'
|
||||
: APP_CONFIG_DEFAULTS.ttsModel),
|
||||
? 'hexgrad/Kokoro-82M'
|
||||
: APP_CONFIG_DEFAULTS.ttsModel),
|
||||
ttsInstructions: raw.ttsInstructions ?? APP_CONFIG_DEFAULTS.ttsInstructions,
|
||||
savedVoices,
|
||||
pdfHighlightEnabled:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { DocumentListState } from '@/types/documents';
|
||||
|
||||
const isDev = process.env.NEXT_PUBLIC_NODE_ENV !== 'production' || process.env.NODE_ENV == null;
|
||||
|
||||
const wordHighlightEnabledByDefault = process.env.NEXT_PUBLIC_ENABLE_WORD_HIGHLIGHT === 'true';
|
||||
|
||||
export type ViewType = 'single' | 'dual' | 'scroll';
|
||||
|
|
@ -48,8 +48,8 @@ export const APP_CONFIG_DEFAULTS: AppConfigValues = {
|
|||
footerMargin: 0,
|
||||
leftMargin: 0,
|
||||
rightMargin: 0,
|
||||
ttsProvider: isDev ? 'custom-openai' : 'deepinfra',
|
||||
ttsModel: isDev ? 'kokoro' : 'hexgrad/Kokoro-82M',
|
||||
ttsProvider: process.env.NEXT_PUBLIC_DEFAULT_TTS_PROVIDER || 'custom-openai',
|
||||
ttsModel: process.env.NEXT_PUBLIC_DEFAULT_TTS_MODEL || 'kokoro',
|
||||
ttsInstructions: '',
|
||||
savedVoices: {},
|
||||
smartSentenceSplitting: true,
|
||||
|
|
|
|||
Loading…
Reference in a new issue