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();
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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}>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -27,12 +28,14 @@ export function useVoiceManagement(apiKey: string | undefined, baseUrl: string |
|
||||||
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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue