Add custom voice input fallback for non-standard TTS providers like DeepInfra
This commit is contained in:
parent
d2477dc9d8
commit
ed7df4b205
5 changed files with 46 additions and 8 deletions
|
|
@ -21,10 +21,10 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json({ voices: data.voices || DEFAULT_VOICES });
|
||||
return NextResponse.json({ voices: data.voices || DEFAULT_VOICES, failed: false });
|
||||
} catch (error) {
|
||||
console.error('Error fetching voices:', error);
|
||||
// Return default voices on error
|
||||
return NextResponse.json({ voices: DEFAULT_VOICES });
|
||||
// Return default voices on error with failed flag
|
||||
return NextResponse.json({ voices: DEFAULT_VOICES, failed: true });
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
|||
setAudioPlayerSpeedAndRestart,
|
||||
setVoiceAndRestart,
|
||||
availableVoices,
|
||||
voiceApiFailed,
|
||||
skipToLocation,
|
||||
} = useTTS();
|
||||
|
||||
|
|
@ -77,7 +78,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
|
|||
</Button>
|
||||
|
||||
{/* Voice control */}
|
||||
<VoicesControl availableVoices={availableVoices} setVoiceAndRestart={setVoiceAndRestart} />
|
||||
<VoicesControl availableVoices={availableVoices} setVoiceAndRestart={setVoiceAndRestart} voiceApiFailed={voiceApiFailed} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Listbox,
|
||||
ListboxButton,
|
||||
|
|
@ -9,15 +10,45 @@ import {
|
|||
import { ChevronUpDownIcon } from '@/components/icons/Icons';
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
|
||||
export const VoicesControl = ({ availableVoices, setVoiceAndRestart }: {
|
||||
export const VoicesControl = ({ availableVoices, setVoiceAndRestart, voiceApiFailed }: {
|
||||
availableVoices: string[];
|
||||
setVoiceAndRestart: (voice: string) => void;
|
||||
voiceApiFailed: boolean;
|
||||
}) => {
|
||||
const { voice: configVoice } = useConfig();
|
||||
const [customVoice, setCustomVoice] = useState(configVoice);
|
||||
|
||||
// Use configVoice as the source of truth
|
||||
const currentVoice = configVoice;
|
||||
|
||||
// Show text input only if API failed
|
||||
if (voiceApiFailed) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
value={customVoice}
|
||||
onChange={(e) => setCustomVoice(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && customVoice.trim()) {
|
||||
setVoiceAndRestart(customVoice.trim());
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (customVoice.trim() && customVoice !== configVoice) {
|
||||
setVoiceAndRestart(customVoice.trim());
|
||||
} else {
|
||||
setCustomVoice(configVoice);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter voice"
|
||||
className="bg-transparent text-foreground text-xs sm:text-sm focus:outline-none border border-accent rounded px-1.5 sm:px-2 py-0.5 sm:py-1 w-24 sm:w-28"
|
||||
title="Voice API unavailable - enter custom voice"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Listbox value={currentVoice} onChange={setVoiceAndRestart}>
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ interface TTSContextType {
|
|||
|
||||
// Voice settings
|
||||
availableVoices: string[];
|
||||
voiceApiFailed: boolean;
|
||||
|
||||
// Control functions
|
||||
togglePlay: () => void;
|
||||
|
|
@ -110,7 +111,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
// Remove OpenAI client reference as it's no longer needed
|
||||
const audioContext = useAudioContext();
|
||||
const audioCache = useAudioCache(25);
|
||||
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl);
|
||||
const { availableVoices, fetchVoices, voiceApiFailed } = useVoiceManagement(openApiKey, openApiBaseUrl);
|
||||
|
||||
// Add ref for location change handler
|
||||
const locationChangeHandlerRef = useRef<((location: string | number) => void) | null>(null);
|
||||
|
|
@ -868,6 +869,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
currDocPageNumber,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
voiceApiFailed,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
|
|
@ -892,6 +894,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
currDocPageNumber,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
voiceApiFailed,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova'
|
|||
*/
|
||||
export function useVoiceManagement(apiKey: string | undefined, baseUrl: string | undefined) {
|
||||
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
||||
const [voiceApiFailed, setVoiceApiFailed] = useState(false);
|
||||
|
||||
const fetchVoices = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -23,16 +24,18 @@ export function useVoiceManagement(apiKey: string | undefined, baseUrl: string |
|
|||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch voices');
|
||||
const data = await response.json();
|
||||
setAvailableVoices(data.voices || DEFAULT_VOICES);
|
||||
setVoiceApiFailed(data.failed || false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching voices:', error);
|
||||
// Set available voices to default openai voices
|
||||
setAvailableVoices(DEFAULT_VOICES);
|
||||
setVoiceApiFailed(true);
|
||||
}
|
||||
}, [apiKey, baseUrl]);
|
||||
|
||||
return { availableVoices, fetchVoices };
|
||||
return { availableVoices, fetchVoices, voiceApiFailed };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue