Add custom voice input fallback for non-standard TTS providers like DeepInfra

This commit is contained in:
kirkins 2025-11-09 01:09:07 -05:00
parent d2477dc9d8
commit ed7df4b205
5 changed files with 46 additions and 8 deletions

View file

@ -21,10 +21,10 @@ export async function GET(req: NextRequest) {
} }
const data = await response.json(); 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) { } catch (error) {
console.error('Error fetching voices:', error); console.error('Error fetching voices:', error);
// Return default voices on error // Return default voices on error with failed flag
return NextResponse.json({ voices: DEFAULT_VOICES }); return NextResponse.json({ voices: DEFAULT_VOICES, failed: true });
} }
} }

View file

@ -27,6 +27,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
setAudioPlayerSpeedAndRestart, setAudioPlayerSpeedAndRestart,
setVoiceAndRestart, setVoiceAndRestart,
availableVoices, availableVoices,
voiceApiFailed,
skipToLocation, skipToLocation,
} = useTTS(); } = useTTS();
@ -77,7 +78,7 @@ export default function TTSPlayer({ currentPage, numPages }: {
</Button> </Button>
{/* Voice control */} {/* Voice control */}
<VoicesControl availableVoices={availableVoices} setVoiceAndRestart={setVoiceAndRestart} /> <VoicesControl availableVoices={availableVoices} setVoiceAndRestart={setVoiceAndRestart} voiceApiFailed={voiceApiFailed} />
</div> </div>
</div> </div>
); );

View file

@ -1,5 +1,6 @@
'use client'; 'use client';
import { useState } from 'react';
import { import {
Listbox, Listbox,
ListboxButton, ListboxButton,
@ -9,15 +10,45 @@ import {
import { ChevronUpDownIcon } from '@/components/icons/Icons'; import { ChevronUpDownIcon } from '@/components/icons/Icons';
import { useConfig } from '@/contexts/ConfigContext'; import { useConfig } from '@/contexts/ConfigContext';
export const VoicesControl = ({ availableVoices, setVoiceAndRestart }: { export const VoicesControl = ({ availableVoices, setVoiceAndRestart, voiceApiFailed }: {
availableVoices: string[]; availableVoices: string[];
setVoiceAndRestart: (voice: string) => void; setVoiceAndRestart: (voice: string) => void;
voiceApiFailed: boolean;
}) => { }) => {
const { voice: configVoice } = useConfig(); const { voice: configVoice } = useConfig();
const [customVoice, setCustomVoice] = useState(configVoice);
// Use configVoice as the source of truth // Use configVoice as the source of truth
const currentVoice = configVoice; 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 ( return (
<div className="relative"> <div className="relative">
<Listbox value={currentVoice} onChange={setVoiceAndRestart}> <Listbox value={currentVoice} onChange={setVoiceAndRestart}>

View file

@ -63,6 +63,7 @@ interface TTSContextType {
// Voice settings // Voice settings
availableVoices: string[]; availableVoices: string[];
voiceApiFailed: boolean;
// Control functions // Control functions
togglePlay: () => void; togglePlay: () => void;
@ -110,7 +111,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
// Remove OpenAI client reference as it's no longer needed // Remove OpenAI client reference as it's no longer needed
const audioContext = useAudioContext(); const audioContext = useAudioContext();
const audioCache = useAudioCache(25); const audioCache = useAudioCache(25);
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl); const { availableVoices, fetchVoices, voiceApiFailed } = useVoiceManagement(openApiKey, openApiBaseUrl);
// Add ref for location change handler // Add ref for location change handler
const locationChangeHandlerRef = useRef<((location: string | number) => void) | null>(null); const locationChangeHandlerRef = useRef<((location: string | number) => void) | null>(null);
@ -868,6 +869,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
currDocPageNumber, currDocPageNumber,
currDocPages, currDocPages,
availableVoices, availableVoices,
voiceApiFailed,
togglePlay, togglePlay,
skipForward, skipForward,
skipBackward, skipBackward,
@ -892,6 +894,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
currDocPageNumber, currDocPageNumber,
currDocPages, currDocPages,
availableVoices, availableVoices,
voiceApiFailed,
togglePlay, togglePlay,
skipForward, skipForward,
skipBackward, skipBackward,

View file

@ -12,6 +12,7 @@ const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova'
*/ */
export function useVoiceManagement(apiKey: string | undefined, baseUrl: string | undefined) { export function useVoiceManagement(apiKey: string | undefined, baseUrl: string | undefined) {
const [availableVoices, setAvailableVoices] = useState<string[]>([]); const [availableVoices, setAvailableVoices] = useState<string[]>([]);
const [voiceApiFailed, setVoiceApiFailed] = useState(false);
const fetchVoices = useCallback(async () => { const fetchVoices = useCallback(async () => {
try { try {
@ -23,16 +24,18 @@ export function useVoiceManagement(apiKey: string | undefined, baseUrl: string |
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}); });
if (!response.ok) throw new Error('Failed to fetch voices'); if (!response.ok) throw new Error('Failed to fetch voices');
const data = await response.json(); const data = await response.json();
setAvailableVoices(data.voices || DEFAULT_VOICES); setAvailableVoices(data.voices || DEFAULT_VOICES);
setVoiceApiFailed(data.failed || false);
} catch (error) { } catch (error) {
console.error('Error fetching voices:', error); console.error('Error fetching voices:', error);
// Set available voices to default openai voices // Set available voices to default openai voices
setAvailableVoices(DEFAULT_VOICES); setAvailableVoices(DEFAULT_VOICES);
setVoiceApiFailed(true);
} }
}, [apiKey, baseUrl]); }, [apiKey, baseUrl]);
return { availableVoices, fetchVoices }; return { availableVoices, fetchVoices, voiceApiFailed };
} }