docs(intro): clarify open-source status and multilingual support in introduction
Update introduction documentation and README to consistently refer to OpenReader as "open-source" and highlight its multilingual capabilities. Refine language segmentation example in multilingual sample text and expand test coverage for language settings. Adjust TTS segment manifest to normalize language tags, improve document language update logic, and fix provider/model compatibility checks for instructions and language input. Enhance reliability of language support detection for TTS generation and Replicate provider integration.
This commit is contained in:
parent
4b226858b1
commit
379e5082f0
10 changed files with 25 additions and 17 deletions
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
# 📄🔊 OpenReader
|
||||
|
||||
OpenReader is an open source, self-host-friendly text-to-speech document reader built with Next.js for **EPUB, PDF, TXT, MD, and DOCX** with multilingual, synchronized read-along playback.
|
||||
OpenReader is an open-source, self-host-friendly text-to-speech document reader built with Next.js for **EPUB, PDF, TXT, MD, and DOCX** with multilingual, synchronized read-along playback.
|
||||
|
||||
> Previously named **OpenReader-WebUI**.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ title: Introduction
|
|||
slug: /
|
||||
---
|
||||
|
||||
OpenReader is an open source text-to-speech document reader built with Next.js. It provides a multilingual read-along experience with narration for **EPUB, PDF, TXT, MD, and DOCX documents**.
|
||||
OpenReader is an open-source text-to-speech document reader built with Next.js. It provides a multilingual read-along experience with narration for **EPUB, PDF, TXT, MD, and DOCX documents**.
|
||||
|
||||
> Previously named **OpenReader-WebUI**.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
TTSSegmentsManifestResponse,
|
||||
} from '@/types/client';
|
||||
import { isTtsProviderType } from '@/lib/shared/tts-provider-catalog';
|
||||
import { normalizeLanguageTag } from '@/lib/shared/language';
|
||||
import { resolveEffectiveProviderType } from '@/lib/shared/tts-provider-policy';
|
||||
import { resolveSegmentAudioUrls } from '@/lib/server/tts/segment-audio-urls';
|
||||
import { createRequestLogger } from '@/lib/server/logger';
|
||||
|
|
@ -71,7 +72,7 @@ function parseSettingsValue(value: unknown): TTSSegmentSettings | null {
|
|||
const nativeSpeed = Number.isFinite(Number(speedSource)) ? Number(speedSource) : 1;
|
||||
const instructionsSource = rec.ttsInstructions ?? rec.instructions;
|
||||
const ttsInstructions = typeof instructionsSource === 'string' ? instructionsSource : '';
|
||||
const language = typeof rec.language === 'string' ? rec.language : 'en';
|
||||
const language = typeof rec.language === 'string' ? normalizeLanguageTag(rec.language, 'en') : 'en';
|
||||
|
||||
if (!providerRef || !providerType || !ttsModel || !voice) return null;
|
||||
return { providerRef, providerType, ttsModel, voice, nativeSpeed, ttsInstructions, language };
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import toast from 'react-hot-toast';
|
||||
import { DotsHorizontalIcon, PlusIcon } from '@/components/icons/Icons';
|
||||
import { providerSupportsCustomModel, resolveProviderModels, type TtsModelDefinition, type TtsProviderId } from '@/lib/shared/tts-provider-catalog';
|
||||
import { providerSupportsCustomModel, resolveProviderModels, type TtsModelDefinition, type TtsProviderId, type TtsProviderType } from '@/lib/shared/tts-provider-catalog';
|
||||
import { defaultBaseUrlForProviderType, defaultModelForProviderType, resolveTtsProviderModelPolicy } from '@/lib/shared/tts-provider-policy';
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -327,9 +327,9 @@ export function AdminProvidersPanel() {
|
|||
? 'custom'
|
||||
: modelDefinitions[0]?.id ?? '';
|
||||
const selectedModelDefinition = modelDefinitions.find((model) => model.id === selectedModelId);
|
||||
const modelSupportsInstructions = useCallback((model: string) => resolveTtsProviderModelPolicy({
|
||||
const modelSupportsInstructions = useCallback((model: string, providerType: TtsProviderType = form.providerType) => resolveTtsProviderModelPolicy({
|
||||
providerRef: form.slug,
|
||||
providerType: form.providerType,
|
||||
providerType,
|
||||
model,
|
||||
}).supportsInstructions, [form.slug, form.providerType]);
|
||||
const baseUrlPlaceholder = form.providerType === 'custom-openai'
|
||||
|
|
@ -425,7 +425,7 @@ export function AdminProvidersPanel() {
|
|||
providerType: opt.value,
|
||||
baseUrl: opt.value === 'custom-openai' ? form.baseUrl : '',
|
||||
defaultModel: nextModel,
|
||||
defaultInstructions: modelSupportsInstructions(nextModel) ? form.defaultInstructions : '',
|
||||
defaultInstructions: modelSupportsInstructions(nextModel, opt.value) ? form.defaultInstructions : '',
|
||||
});
|
||||
setCustomModelInput('');
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
activeReaderType,
|
||||
clearSegmentCaches,
|
||||
resolvedLanguage,
|
||||
setDocumentLanguage,
|
||||
} = useTTS();
|
||||
const {
|
||||
providerRef,
|
||||
|
|
@ -421,7 +422,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
updateConfigKey('voiceSpeed', Number.isFinite(Number(settings.nativeSpeed)) ? Number(settings.nativeSpeed) : 1),
|
||||
updateConfigKey('ttsInstructions', settings.ttsInstructions || ''),
|
||||
]);
|
||||
}, [updateConfigKey]);
|
||||
if (settings.language) setDocumentLanguage(settings.language);
|
||||
}, [updateConfigKey, setDocumentLanguage]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
didAutoScrollOnOpenRef.current = false;
|
||||
|
|
|
|||
|
|
@ -31,19 +31,22 @@ export function useDocumentLanguage(documentId: string | undefined): {
|
|||
|
||||
const updateLanguage = useCallback(async (language: string): Promise<void> => {
|
||||
if (!documentId) return;
|
||||
const next = mergeDocumentSettings(DEFAULT_DOCUMENT_SETTINGS, {
|
||||
...settings,
|
||||
let next = DEFAULT_DOCUMENT_SETTINGS;
|
||||
setSettings((prev) => {
|
||||
next = mergeDocumentSettings(DEFAULT_DOCUMENT_SETTINGS, {
|
||||
...prev,
|
||||
schemaVersion: 1,
|
||||
language,
|
||||
});
|
||||
setSettings(next);
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
const response = await putDocumentSettings(documentId, next);
|
||||
setSettings(mergeDocumentSettings(DEFAULT_DOCUMENT_SETTINGS, response.settings));
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist document language:', error);
|
||||
}
|
||||
}, [documentId, settings]);
|
||||
}, [documentId]);
|
||||
|
||||
return {
|
||||
language: settings.language ?? 'auto',
|
||||
|
|
|
|||
|
|
@ -644,7 +644,7 @@ async function runProviderRequest(
|
|||
}
|
||||
|
||||
if (request.provider !== 'openai' && request.language) {
|
||||
const supportKey = `${request.baseUrl || ''}|${request.model as string}`;
|
||||
const supportKey = `${request.provider}|${request.baseUrl || ''}|${request.model as string}|${request.language}`;
|
||||
if (!openAiCompatibleLanguageUnsupported.has(supportKey)) {
|
||||
try {
|
||||
return await fetchTTSBufferWithRetry(
|
||||
|
|
|
|||
|
|
@ -311,7 +311,7 @@ export async function resolveReplicateLanguageInput({
|
|||
model,
|
||||
apiKey = '',
|
||||
}: ResolveVoicesOptions): Promise<ReplicateLanguageInput | null> {
|
||||
if (provider !== 'replicate' || !apiKey) return null;
|
||||
if (provider !== 'replicate' || REPLICATE_BUILT_IN_MODELS.has(model) || !apiKey) return null;
|
||||
|
||||
const cached = replicateLanguageInputCache.get(model);
|
||||
if (cached) return cached;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ English
|
|||
OpenReader should split this sentence correctly. This is the second sentence.
|
||||
|
||||
French
|
||||
OpenReader doit correctement segmenter cette phrase. Voici la deuxieme phrase.
|
||||
OpenReader doit correctement segmenter cette phrase. Voici la deuxième phrase.
|
||||
|
||||
Spanish
|
||||
OpenReader debe segmentar correctamente esta frase. Esta es la segunda frase.
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ describe('coerceAudiobookGenerationSettings', () => {
|
|||
postSpeed: 1,
|
||||
format: 'mp3',
|
||||
ttsInstructions: '',
|
||||
language: 'en',
|
||||
},
|
||||
restrictUserApiKeys: true,
|
||||
fallbackProviderRef: 'shared-openai',
|
||||
|
|
@ -113,5 +114,6 @@ describe('coerceAudiobookGenerationSettings', () => {
|
|||
expect(settings.providerType).toBe('openai');
|
||||
expect(settings.ttsModel).toBe('gpt-4o-mini-tts');
|
||||
expect(settings.ttsInstructions).toBe('Speak with warmth.');
|
||||
expect(settings.language).toBe('en');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue