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();
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 });
}
}

View file

@ -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>
);

View file

@ -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}>

View file

@ -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,

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) {
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 };
}