diff --git a/package.json b/package.json index eceb5e4..ac8fdf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openreader", - "version": "v2.1.0", + "version": "2.1.1", "private": true, "scripts": { "dev": "node scripts/openreader-entrypoint.mjs -- pnpm dev:raw", diff --git a/src/app/api/tts/voices/route.ts b/src/app/api/tts/voices/route.ts index 221f1b7..31f1155 100644 --- a/src/app/api/tts/voices/route.ts +++ b/src/app/api/tts/voices/route.ts @@ -1,96 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { isKokoroModel } from '@/lib/shared/kokoro'; import { auth } from '@/lib/server/auth/auth'; - -const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']; -const GPT4O_MINI_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer']; -const CUSTOM_OPENAI_VOICES = ['af_sarah', 'af_bella', 'af_nicole', 'am_adam', 'am_michael', 'bf_emma', 'bf_isabella', 'bm_george', 'bm_lewis']; - -const KOKORO_VOICES = [ - 'af_alloy', 'af_aoede', 'af_bella', 'af_heart', 'af_jessica', 'af_kore', 'af_nicole', 'af_nova', - 'af_river', 'af_sarah', 'af_sky', 'am_adam', 'am_echo', 'am_eric', 'am_fenrir', 'am_liam', - 'am_michael', 'am_onyx', 'am_puck', 'am_santa', 'bf_alice', 'bf_emma', 'bf_isabella', 'bf_lily', - 'bm_daniel', 'bm_fable', 'bm_george', 'bm_lewis', 'ef_dora', 'em_alex', 'em_santa', 'ff_siwis', - 'hf_alpha', 'hf_beta', 'hm_omega', 'hm_psi', 'if_sara', 'im_nicola', 'jf_alpha', 'jf_gongitsune', - 'jf_nezumi', 'jf_tebukuro', 'jm_kumo', 'pf_dora', 'pm_alex', 'pm_santa', 'zf_xiaobei', 'zf_xiaoni', - 'zf_xiaoxiao', 'zf_xiaoyi', 'zm_yunjian', 'zm_yunxi', 'zm_yunxia', 'zm_yunyang' -]; - -const ORPHEUS_VOICES = ['tara', 'leah', 'jess', 'leo', 'dan', 'mia', 'zac']; - -const SESAME_VOICES = ['conversational_a', 'conversational_b', 'read_speech_a', 'read_speech_b', 'read_speech_c', 'read_speech_d', 'none']; - -function getDefaultVoices(provider: string, model: string): string[] { - // For OpenAI provider - if (provider === 'openai') { - if (model === 'gpt-4o-mini-tts') { - return GPT4O_MINI_VOICES; - } - return OPENAI_VOICES; - } - - // For Custom OpenAI-Like provider - if (provider === 'custom-openai') { - // If using Kokoro-FastAPI (model string contains 'kokoro'), expose full Kokoro voices - if (isKokoroModel(model)) { - return KOKORO_VOICES; - } - return CUSTOM_OPENAI_VOICES; - } - - // For Deepinfra provider - model-specific voices - if (provider === 'deepinfra') { - if (model === 'hexgrad/Kokoro-82M') { - return KOKORO_VOICES; - } - if (model === 'canopylabs/orpheus-3b-0.1-ft') { - return ORPHEUS_VOICES; - } - if (model === 'sesame/csm-1b') { - return SESAME_VOICES; - } - // For ResembleAI/chatterbox and Zyphra models, return special values - if (model === 'ResembleAI/chatterbox') { - return ['None']; - } - if (model === 'Zyphra/Zonos-v0.1-hybrid' || model === 'Zyphra/Zonos-v0.1-transformer') { - return ['random']; - } - // Default Deepinfra voices - return CUSTOM_OPENAI_VOICES; - } - - // Default fallback - return OPENAI_VOICES; -} - -async function fetchDeepinfraVoices(apiKey: string): Promise { - try { - const response = await fetch('https://api.deepinfra.com/v1/voices', { - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - }, - }); - - if (!response.ok) { - throw new Error('Failed to fetch Deepinfra voices'); - } - - const data = await response.json(); - - // Extract voice names from the response, excluding preset voices - if (data.voices && Array.isArray(data.voices)) { - return data.voices - .filter((voice: { user_id?: string }) => voice.user_id !== 'preset') - .map((voice: { name: string }) => voice.name); - } - return []; - } catch (error) { - console.error('Error fetching Deepinfra voices:', error); - return []; - } -} +import { getDefaultVoices, resolveVoices } from '@/lib/shared/tts-provider-catalog'; export async function GET(req: NextRequest) { try { @@ -104,58 +14,15 @@ export async function GET(req: NextRequest) { const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE; const provider = req.headers.get('x-tts-provider') || 'openai'; const model = req.headers.get('x-tts-model') || 'tts-1'; - - // For OpenAI provider, use default voices (no API call needed) - if (provider === 'openai') { - return NextResponse.json({ voices: getDefaultVoices(provider, model) }); - } - - // For Deepinfra provider with specific models that need API fetching - if (provider === 'deepinfra') { - const needsApiFetch = model === 'ResembleAI/chatterbox' || - model === 'Zyphra/Zonos-v0.1-hybrid' || - model === 'Zyphra/Zonos-v0.1-transformer'; - - if (needsApiFetch) { - const apiVoices = await fetchDeepinfraVoices(openApiKey); - // Combine default voice with fetched voices - const defaultVoice = getDefaultVoices(provider, model); - if (apiVoices.length > 0) { - return NextResponse.json({ voices: [...defaultVoice, ...apiVoices] }); - } - } - - // For other Deepinfra models, return static defaults - return NextResponse.json({ voices: getDefaultVoices(provider, model) }); - } - - // For Custom OpenAI-Like provider, try to fetch voices from custom endpoint - if (provider === 'custom-openai') { - try { - const response = await fetch(`${openApiBaseUrl}/audio/voices`, { - headers: { - 'Authorization': `Bearer ${openApiKey}`, - 'Content-Type': 'application/json', - }, - }); - - if (response.ok) { - const data = await response.json(); - if (data.voices) { - return NextResponse.json({ voices: data.voices }); - } - } - } catch { - console.log('Custom endpoint does not support voices, using defaults'); - } - - // Fallback to default voices if API call fails - return NextResponse.json({ voices: getDefaultVoices(provider, model) }); - } - - // Default fallback - return NextResponse.json({ voices: getDefaultVoices(provider, model) }); + const voices = await resolveVoices({ + provider, + model, + apiKey: openApiKey, + baseUrl: openApiBaseUrl, + }); + return NextResponse.json({ voices }); } catch (error) { + // This catch mainly guards auth/session access failures; voice resolution itself falls back internally. console.error('Error in voices endpoint:', error); const provider = req.headers.get('x-tts-provider') || 'openai'; const model = req.headers.get('x-tts-model') || 'tts-1'; diff --git a/src/components/AudiobookExportModal.tsx b/src/components/AudiobookExportModal.tsx index b7341f4..52838ca 100644 --- a/src/components/AudiobookExportModal.tsx +++ b/src/components/AudiobookExportModal.tsx @@ -11,6 +11,7 @@ import { LoadingSpinner } from '@/components/Spinner'; import { useConfig } from '@/contexts/ConfigContext'; import { useTTS } from '@/contexts/TTSContext'; import { VoicesControlBase } from '@/components/player/VoicesControlBase'; +import { supportsTtsInstructions } from '@/lib/shared/tts-provider-catalog'; import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts'; import { getAudiobookStatus, @@ -115,7 +116,7 @@ export function AudiobookExportModal({ nativeSpeed, postSpeed, format, - ttsInstructions: ttsModel === 'gpt-4o-mini-tts' ? ttsInstructions : undefined, + ttsInstructions: supportsTtsInstructions(ttsModel) ? ttsInstructions : undefined, }; }, [savedSettings, audiobookVoice, configVoice, availableVoices, ttsProvider, ttsModel, ttsInstructions, nativeSpeed, postSpeed, format]); diff --git a/src/components/PrivacyModal.tsx b/src/components/PrivacyModal.tsx index 1ea6746..5909cbc 100644 --- a/src/components/PrivacyModal.tsx +++ b/src/components/PrivacyModal.tsx @@ -96,7 +96,7 @@ export function PrivacyModal({ onAccept }: PrivacyModalProps) { leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - +
- {modelValue === 'gpt-4o-mini-tts' && ( + {supportsTtsInstructions(modelValue) && (