Update API settings

This commit is contained in:
Richard Roberson 2025-03-13 21:00:34 -06:00
parent 12b46cc8e3
commit f464b059e4
7 changed files with 260 additions and 178 deletions

View file

@ -17,7 +17,7 @@ OpenReader WebUI is a document reader with Text-to-Speech capabilities, offering
- 💾 **Local-First Architecture**: Uses IndexedDB browser storage for documents - 💾 **Local-First Architecture**: Uses IndexedDB browser storage for documents
- 🛜 **Optional Server-side documents**: Manually upload documents to the next backend for all users to download - 🛜 **Optional Server-side documents**: Manually upload documents to the next backend for all users to download
- 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates - 📖 **Read Along Experience**: Follow along with highlighted text as the TTS narrates
- 📄 **Document formats**: EPUB, PDF, DOCX - 📄 **Document formats**: EPUB, PDF, DOCX (with libreoffice installed)
- 🎧 **Audiobook Creation**: Create and export audiobooks from PDF and ePub files with m4b format - 🎧 **Audiobook Creation**: Create and export audiobooks from PDF and ePub files with m4b format
- 📲 **Mobile Support**: Works on mobile devices, and can be added as a PWA web app - 📲 **Mobile Support**: Works on mobile devices, and can be added as a PWA web app
- 🎨 **Customizable Experience**: - 🎨 **Customizable Experience**:
@ -33,7 +33,7 @@ OpenReader WebUI is a document reader with Text-to-Speech capabilities, offering
- [x] **Get PDFs on iOS 17 and below working 🤞** - [x] **Get PDFs on iOS 17 and below working 🤞**
- [ ] **End-to-end Testing**: More playwright tests (in progress) - [ ] **End-to-end Testing**: More playwright tests (in progress)
- [ ] **More document formats**: .txt, .md - [ ] **More document formats**: .txt, .md
- [ ] **Support more TTS APIs**: ElevenLabs, Ollama, etc. - [ ] **Support more TTS APIs**: ElevenLabs, etc.
- [ ] **Accessibility Improvements** - [ ] **Accessibility Improvements**
## 🐳 Docker Quick Start ## 🐳 Docker Quick Start

View file

@ -6,8 +6,8 @@ export async function POST(req: NextRequest) {
// Get API credentials from headers or fall back to environment variables // Get API credentials from headers or fall back to environment variables
const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none'; const openApiKey = req.headers.get('x-openai-key') || process.env.API_KEY || 'none';
const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE; const openApiBaseUrl = req.headers.get('x-openai-base-url') || process.env.API_BASE;
const { text, voice, speed, format } = await req.json(); const { text, voice, speed, format, model } = await req.json();
console.log('Received TTS request:', text, voice, speed, format); console.log('Received TTS request:', text, voice, speed, format, model);
if (!openApiKey) { if (!openApiKey) {
return NextResponse.json({ error: 'Missing OpenAI API key' }, { status: 401 }); return NextResponse.json({ error: 'Missing OpenAI API key' }, { status: 401 });
@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
// Request audio from OpenAI and pass along the abort signal // Request audio from OpenAI and pass along the abort signal
const response = await openai.audio.speech.create({ const response = await openai.audio.speech.create({
model: 'tts-1', model: model || 'tts-1',
voice: voice as "alloy", voice: voice as "alloy",
input: text, input: text,
speed: speed, speed: speed,

View file

@ -40,7 +40,6 @@ export const metadata: Metadata = {
}, },
}, },
verification: { verification: {
// Add your verification codes if you have them
google: "MJXyTudn1kgQF8EtGD-tsnAWev7Iawso9hEvqeGHB3U", google: "MJXyTudn1kgQF8EtGD-tsnAWev7Iawso9hEvqeGHB3U",
}, },
}; };

View file

@ -1,7 +1,24 @@
'use client'; 'use client';
import { Fragment, useState, useEffect, useCallback } from 'react'; import { Fragment, useState, useEffect, useCallback } from 'react';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Listbox, ListboxButton, ListboxOptions, ListboxOption, Button, Input } from '@headlessui/react'; import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
Listbox,
ListboxButton,
ListboxOptions,
ListboxOption,
Button,
Input,
TabGroup,
TabList,
Tab,
TabPanels,
TabPanel,
} from '@headlessui/react';
import { useTheme } from '@/contexts/ThemeContext'; import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons'; import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
@ -22,10 +39,11 @@ export function SettingsModal() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const { apiKey, baseUrl, updateConfig } = useConfig(); const { apiKey, baseUrl, ttsModel, updateConfig, updateConfigKey } = useConfig();
const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments(); const { refreshPDFs, refreshEPUBs, clearPDFs, clearEPUBs } = useDocuments();
const [localApiKey, setLocalApiKey] = useState(apiKey); const [localApiKey, setLocalApiKey] = useState(apiKey);
const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl); const [localBaseUrl, setLocalBaseUrl] = useState(baseUrl);
const [localTTSModel, setLocalTTSModel] = useState(ttsModel);
const [isSyncing, setIsSyncing] = useState(false); const [isSyncing, setIsSyncing] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const selectedTheme = themes.find(t => t.id === theme) || themes[0]; const selectedTheme = themes.find(t => t.id === theme) || themes[0];
@ -46,7 +64,8 @@ export function SettingsModal() {
checkFirstVist(); checkFirstVist();
setLocalApiKey(apiKey); setLocalApiKey(apiKey);
setLocalBaseUrl(baseUrl); setLocalBaseUrl(baseUrl);
}, [apiKey, baseUrl, checkFirstVist]); setLocalTTSModel(ttsModel);
}, [apiKey, baseUrl, ttsModel, checkFirstVist]);
const handleSync = async () => { const handleSync = async () => {
try { try {
@ -91,14 +110,22 @@ export function SettingsModal() {
setShowClearServerConfirm(false); setShowClearServerConfirm(false);
}; };
const handleInputChange = (type: 'apiKey' | 'baseUrl', value: string) => { const handleInputChange = (type: 'apiKey' | 'baseUrl' | 'ttsModel', value: string) => {
if (type === 'apiKey') { if (type === 'apiKey') {
setLocalApiKey(value === '' ? '' : value); setLocalApiKey(value === '' ? '' : value);
} else { } else if (type === 'baseUrl') {
setLocalBaseUrl(value === '' ? '' : value); setLocalBaseUrl(value === '' ? '' : value);
} else if (type === 'ttsModel') {
setLocalTTSModel(value === '' ? 'tts-1' : value);
} }
}; };
const tabs = [
{ name: 'Appearance', icon: '✨' },
{ name: 'API', icon: '🔑' },
{ name: 'Documents', icon: '📄' }
];
return ( return (
<Button <Button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
@ -140,12 +167,34 @@ export function SettingsModal() {
<DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all"> <DialogPanel className="w-full max-w-md transform rounded-2xl bg-base p-6 text-left align-middle shadow-xl transition-all">
<DialogTitle <DialogTitle
as="h3" as="h3"
className="text-lg font-semibold leading-6 text-foreground" className="text-lg font-semibold leading-6 text-foreground mb-4"
> >
Settings Settings
</DialogTitle> </DialogTitle>
<div className="mt-4">
<div className="relative space-y-4"> <TabGroup>
<TabList className="flex space-x-1 rounded-xl bg-background p-1 mb-4">
{tabs.map((tab) => (
<Tab
key={tab.name}
className={({ selected }) =>
`w-full rounded-lg py-1 text-sm font-medium
ring-accent/60 ring-offset-2 ring-offset-base
${selected
? 'bg-accent text-white shadow'
: 'text-foreground hover:bg-accent/[0.12] hover:text-accent'
}`
}
>
<span className="flex items-center justify-center gap-2">
<span>{tab.icon}</span>
{tab.name}
</span>
</Tab>
))}
</TabList>
<TabPanels className="mt-2">
<TabPanel className="space-y-4 pb-3">
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-foreground">Theme</label> <label className="block text-sm font-medium text-foreground">Theme</label>
<Listbox value={selectedTheme} onChange={(newTheme) => setTheme(newTheme.id)}> <Listbox value={selectedTheme} onChange={(newTheme) => setTheme(newTheme.id)}>
@ -189,7 +238,9 @@ export function SettingsModal() {
</Transition> </Transition>
</Listbox> </Listbox>
</div> </div>
</TabPanel>
<TabPanel className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<label className="block text-sm font-medium text-foreground"> <label className="block text-sm font-medium text-foreground">
OpenAI API Key OpenAI API Key
@ -222,6 +273,58 @@ export function SettingsModal() {
</div> </div>
</div> </div>
<div className="space-y-2">
<label className="block text-sm font-medium text-foreground">
OpenAI TTS Model
{localTTSModel !== 'tts-1' && <span className="ml-2 text-xs text-accent">(Custom model)</span>}
</label>
<div className="flex gap-2">
<Input
type="text"
value={localTTSModel}
onChange={(e) => handleInputChange('ttsModel', e.target.value)}
placeholder="tts-1"
className="w-full rounded-lg bg-background py-2 px-3 text-foreground shadow-sm focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-2">
<Button
type="button"
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
font-medium text-foreground hover:bg-background/90 focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent"
onClick={async () => {
setLocalApiKey('');
setLocalBaseUrl('');
setLocalTTSModel('tts-1');
}}
>
Reset
</Button>
<Button
type="button"
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-white hover:bg-accent/90 focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-background"
onClick={async () => {
await updateConfig({
apiKey: localApiKey || '',
baseUrl: localBaseUrl || '',
});
await updateConfigKey('ttsModel', localTTSModel || 'tts-1');
setIsOpen(false);
}}
>
Done
</Button>
</div>
</TabPanel>
<TabPanel className="space-y-4">
{isDev && <div className="space-y-2"> {isDev && <div className="space-y-2">
<label className="block text-sm font-medium text-foreground">Document Sync</label> <label className="block text-sm font-medium text-foreground">Document Sync</label>
<div className="flex gap-2"> <div className="flex gap-2">
@ -250,7 +353,7 @@ export function SettingsModal() {
</div> </div>
</div>} </div>}
<div className="space-y-2"> <div className="space-y-2 pb-3">
<label className="block text-sm font-medium text-foreground">Bulk Delete</label> <label className="block text-sm font-medium text-foreground">Bulk Delete</label>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
@ -273,40 +376,9 @@ export function SettingsModal() {
</Button>} </Button>}
</div> </div>
</div> </div>
</div> </TabPanel>
</div> </TabPanels>
</TabGroup>
<div className="mt-6 flex justify-end gap-2">
<Button
type="button"
className="inline-flex justify-center rounded-lg bg-background px-3 py-1.5 text-sm
font-medium text-foreground hover:bg-background/90 focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-accent"
onClick={async () => {
setLocalApiKey('');
setLocalBaseUrl('');
}}
>
Reset
</Button>
<Button
type="button"
className="inline-flex justify-center rounded-lg bg-accent px-3 py-1.5 text-sm
font-medium text-white hover:bg-accent/90 focus:outline-none
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2
transform transition-transform duration-200 ease-in-out hover:scale-[1.04] hover:text-background"
onClick={async () => {
await updateConfig({
apiKey: localApiKey || '',
baseUrl: localBaseUrl || '',
});
setIsOpen(false);
}}
>
Done
</Button>
</div>
</DialogPanel> </DialogPanel>
</TransitionChild> </TransitionChild>
</div> </div>

View file

@ -15,11 +15,11 @@ type ConfigValues = {
voice: string; voice: string;
skipBlank: boolean; skipBlank: boolean;
epubTheme: boolean; epubTheme: boolean;
textExtractionMargin: number;
headerMargin: number; headerMargin: number;
footerMargin: number; footerMargin: number;
leftMargin: number; leftMargin: number;
rightMargin: number; rightMargin: number;
ttsModel: string;
}; };
/** Interface defining the configuration context shape and functionality */ /** Interface defining the configuration context shape and functionality */
@ -31,11 +31,11 @@ interface ConfigContextType {
voice: string; voice: string;
skipBlank: boolean; skipBlank: boolean;
epubTheme: boolean; epubTheme: boolean;
textExtractionMargin: number;
headerMargin: number; headerMargin: number;
footerMargin: number; footerMargin: number;
leftMargin: number; leftMargin: number;
rightMargin: number; rightMargin: number;
ttsModel: string;
updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>; updateConfig: (newConfig: Partial<{ apiKey: string; baseUrl: string; viewType: ViewType }>) => Promise<void>;
updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>; updateConfigKey: <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => Promise<void>;
isLoading: boolean; isLoading: boolean;
@ -59,11 +59,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const [voice, setVoice] = useState<string>('af_sarah'); const [voice, setVoice] = useState<string>('af_sarah');
const [skipBlank, setSkipBlank] = useState<boolean>(true); const [skipBlank, setSkipBlank] = useState<boolean>(true);
const [epubTheme, setEpubTheme] = useState<boolean>(false); const [epubTheme, setEpubTheme] = useState<boolean>(false);
const [textExtractionMargin, setTextExtractionMargin] = useState<number>(0.07);
const [headerMargin, setHeaderMargin] = useState<number>(0.07); const [headerMargin, setHeaderMargin] = useState<number>(0.07);
const [footerMargin, setFooterMargin] = useState<number>(0.07); const [footerMargin, setFooterMargin] = useState<number>(0.07);
const [leftMargin, setLeftMargin] = useState<number>(0.07); const [leftMargin, setLeftMargin] = useState<number>(0.07);
const [rightMargin, setRightMargin] = useState<number>(0.07); const [rightMargin, setRightMargin] = useState<number>(0.07);
const [ttsModel, setTTSModel] = useState<string>('tts-1');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isDBReady, setIsDBReady] = useState(false); const [isDBReady, setIsDBReady] = useState(false);
@ -83,11 +83,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
const cachedVoice = await getItem('voice'); const cachedVoice = await getItem('voice');
const cachedSkipBlank = await getItem('skipBlank'); const cachedSkipBlank = await getItem('skipBlank');
const cachedEpubTheme = await getItem('epubTheme'); const cachedEpubTheme = await getItem('epubTheme');
const cachedMargin = await getItem('textExtractionMargin');
const cachedHeaderMargin = await getItem('headerMargin'); const cachedHeaderMargin = await getItem('headerMargin');
const cachedFooterMargin = await getItem('footerMargin'); const cachedFooterMargin = await getItem('footerMargin');
const cachedLeftMargin = await getItem('leftMargin'); const cachedLeftMargin = await getItem('leftMargin');
const cachedRightMargin = await getItem('rightMargin'); const cachedRightMargin = await getItem('rightMargin');
const cachedTTSModel = await getItem('ttsModel');
// Only set API key and base URL if they were explicitly saved by the user // Only set API key and base URL if they were explicitly saved by the user
if (cachedApiKey) { if (cachedApiKey) {
@ -105,11 +105,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
setVoice(cachedVoice || 'af_sarah'); setVoice(cachedVoice || 'af_sarah');
setSkipBlank(cachedSkipBlank === 'false' ? false : true); setSkipBlank(cachedSkipBlank === 'false' ? false : true);
setEpubTheme(cachedEpubTheme === 'true'); setEpubTheme(cachedEpubTheme === 'true');
setTextExtractionMargin(parseFloat(cachedMargin || '0.07'));
setHeaderMargin(parseFloat(cachedHeaderMargin || '0.07')); setHeaderMargin(parseFloat(cachedHeaderMargin || '0.07'));
setFooterMargin(parseFloat(cachedFooterMargin || '0.07')); setFooterMargin(parseFloat(cachedFooterMargin || '0.07'));
setLeftMargin(parseFloat(cachedLeftMargin || '0.07')); setLeftMargin(parseFloat(cachedLeftMargin || '0.07'));
setRightMargin(parseFloat(cachedRightMargin || '0.07')); setRightMargin(parseFloat(cachedRightMargin || '0.07'));
setTTSModel(cachedTTSModel || 'tts-1');
// Only save non-sensitive settings by default // Only save non-sensitive settings by default
if (!cachedViewType) { if (!cachedViewType) {
@ -121,13 +121,13 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
if (cachedEpubTheme === null) { if (cachedEpubTheme === null) {
await setItem('epubTheme', 'false'); await setItem('epubTheme', 'false');
} }
if (cachedMargin === null) {
await setItem('textExtractionMargin', '0.07');
}
if (cachedHeaderMargin === null) await setItem('headerMargin', '0.07'); if (cachedHeaderMargin === null) await setItem('headerMargin', '0.07');
if (cachedFooterMargin === null) await setItem('footerMargin', '0.07'); if (cachedFooterMargin === null) await setItem('footerMargin', '0.07');
if (cachedLeftMargin === null) await setItem('leftMargin', '0.0'); if (cachedLeftMargin === null) await setItem('leftMargin', '0.0');
if (cachedRightMargin === null) await setItem('rightMargin', '0.0'); if (cachedRightMargin === null) await setItem('rightMargin', '0.0');
if (cachedTTSModel === null) {
await setItem('ttsModel', 'tts-1');
}
} catch (error) { } catch (error) {
console.error('Error initializing:', error); console.error('Error initializing:', error);
@ -145,6 +145,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
*/ */
const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => { const updateConfig = async (newConfig: Partial<{ apiKey: string; baseUrl: string }>) => {
try { try {
setIsLoading(true);
if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') { if (newConfig.apiKey !== undefined || newConfig.apiKey !== '') {
// Only save API key to IndexedDB if it's different from env default // Only save API key to IndexedDB if it's different from env default
await setItem('apiKey', newConfig.apiKey!); await setItem('apiKey', newConfig.apiKey!);
@ -169,6 +170,8 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
} catch (error) { } catch (error) {
console.error('Error updating config:', error); console.error('Error updating config:', error);
throw error; throw error;
} finally {
setIsLoading(false);
} }
}; };
@ -179,6 +182,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
*/ */
const updateConfigKey = async <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => { const updateConfigKey = async <K extends keyof ConfigValues>(key: K, value: ConfigValues[K]) => {
try { try {
setIsLoading(true);
await setItem(key, value.toString()); await setItem(key, value.toString());
switch (key) { switch (key) {
case 'apiKey': case 'apiKey':
@ -202,9 +206,6 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
case 'epubTheme': case 'epubTheme':
setEpubTheme(value as boolean); setEpubTheme(value as boolean);
break; break;
case 'textExtractionMargin':
setTextExtractionMargin(value as number);
break;
case 'headerMargin': case 'headerMargin':
setHeaderMargin(value as number); setHeaderMargin(value as number);
break; break;
@ -217,10 +218,15 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
case 'rightMargin': case 'rightMargin':
setRightMargin(value as number); setRightMargin(value as number);
break; break;
case 'ttsModel':
setTTSModel(value as string);
break;
} }
} catch (error) { } catch (error) {
console.error(`Error updating config key ${key}:`, error); console.error(`Error updating config key ${key}:`, error);
throw error; throw error;
} finally {
setIsLoading(false);
} }
}; };
@ -233,11 +239,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
voice, voice,
skipBlank, skipBlank,
epubTheme, epubTheme,
textExtractionMargin,
headerMargin, headerMargin,
footerMargin, footerMargin,
leftMargin, leftMargin,
rightMargin, rightMargin,
ttsModel,
updateConfig, updateConfig,
updateConfigKey, updateConfigKey,
isLoading, isLoading,

View file

@ -97,6 +97,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
isLoading: configIsLoading, isLoading: configIsLoading,
voiceSpeed, voiceSpeed,
voice: configVoice, voice: configVoice,
ttsModel: configTTSModel,
updateConfigKey, updateConfigKey,
skipBlank, skipBlank,
} = useConfig(); } = useConfig();
@ -138,6 +139,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
const [activeHowl, setActiveHowl] = useState<Howl | null>(null); const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
const [speed, setSpeed] = useState(voiceSpeed); const [speed, setSpeed] = useState(voiceSpeed);
const [voice, setVoice] = useState(configVoice); const [voice, setVoice] = useState(configVoice);
const [ttsModel, setTTSModel] = useState(configTTSModel);
// Track pending preload requests // Track pending preload requests
const preloadRequests = useRef<Map<string, Promise<string>>>(new Map()); const preloadRequests = useRef<Map<string, Promise<string>>>(new Map());
@ -386,8 +388,9 @@ export function TTSProvider({ children }: { children: ReactNode }) {
if (!configIsLoading) { if (!configIsLoading) {
fetchVoices(); fetchVoices();
updateVoiceAndSpeed(); updateVoiceAndSpeed();
setTTSModel(configTTSModel);
} }
}, [configIsLoading, openApiKey, openApiBaseUrl, updateVoiceAndSpeed, fetchVoices]); }, [configIsLoading, openApiKey, openApiBaseUrl, updateVoiceAndSpeed, fetchVoices, configTTSModel]);
/** /**
* Generates and plays audio for the current sentence * Generates and plays audio for the current sentence
@ -423,6 +426,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
text: sentence, text: sentence,
voice: voice, voice: voice,
speed: speed, speed: speed,
model: ttsModel
}), }),
signal: controller.signal, signal: controller.signal,
}); });
@ -466,7 +470,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
}); });
throw error; throw error;
} }
}, [voice, speed, audioCache, openApiKey, openApiBaseUrl]); }, [voice, speed, ttsModel, audioCache, openApiKey, openApiBaseUrl]);
/** /**
* Processes and plays the current sentence * Processes and plays the current sentence

View file

@ -75,5 +75,6 @@ export async function setupTest(page: Page) {
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
// Click the "done" button to dismiss the welcome message // Click the "done" button to dismiss the welcome message
await page.getByText('Done').click(); await page.getByRole('tab', { name: '🔑 API' }).click();
await page.getByRole('button', { name: 'Done' }).click();
} }