diff --git a/src/app/api/tts/voices/route.ts b/src/app/api/tts/voices/route.ts index 75947cd..5f5fa0e 100644 --- a/src/app/api/tts/voices/route.ts +++ b/src/app/api/tts/voices/route.ts @@ -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 }); } } \ No newline at end of file diff --git a/src/components/player/TTSPlayer.tsx b/src/components/player/TTSPlayer.tsx index 07bf938..7c6c016 100644 --- a/src/components/player/TTSPlayer.tsx +++ b/src/components/player/TTSPlayer.tsx @@ -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 }: { {/* Voice control */} - + ); diff --git a/src/components/player/VoicesControl.tsx b/src/components/player/VoicesControl.tsx index 92fec6c..68aa5f4 100644 --- a/src/components/player/VoicesControl.tsx +++ b/src/components/player/VoicesControl.tsx @@ -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 ( +
+ 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" + /> +
+ ); + } + return (
diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index 28ef4ef..fc9028c 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -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, diff --git a/src/hooks/audio/useVoiceManagement.ts b/src/hooks/audio/useVoiceManagement.ts index 42d2585..64c6f75 100644 --- a/src/hooks/audio/useVoiceManagement.ts +++ b/src/hooks/audio/useVoiceManagement.ts @@ -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([]); + 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 }; }