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