Extract TTS extras to hooks
This commit is contained in:
parent
dd7154cccf
commit
055e87e8c1
6 changed files with 145 additions and 117 deletions
|
|
@ -311,7 +311,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get document URL:', error);
|
console.error('Failed to get document URL:', error);
|
||||||
}
|
}
|
||||||
}, [indexedDBService]);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the current document state
|
* Clears the current document state
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,15 @@ import React, {
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { LRUCache } from 'lru-cache'; // Import LRUCache directly
|
|
||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
|
|
||||||
import { useConfig } from '@/contexts/ConfigContext';
|
import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { splitIntoSentences, preprocessSentenceForAudio } from '@/services/nlp';
|
import { splitIntoSentences, preprocessSentenceForAudio } from '@/services/nlp';
|
||||||
import { audioBufferToURL } from '@/services/audio';
|
import { audioBufferToURL } from '@/services/audio';
|
||||||
|
import { useAudioCache } from '@/hooks/useAudioCache';
|
||||||
|
import { useVoiceManagement } from '@/hooks/useVoiceManagement';
|
||||||
|
import { useMediaSession } from '@/hooks/useMediaSession';
|
||||||
|
import { useAudioContext } from '@/hooks/useAudioContext';
|
||||||
|
|
||||||
// Media globals
|
// Media globals
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -38,13 +41,6 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Type definition for AudioContext to handle browser compatibility
|
|
||||||
*/
|
|
||||||
type AudioContextType = typeof window extends undefined
|
|
||||||
? never
|
|
||||||
: (AudioContext);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface defining all available methods and properties in the TTS context
|
* Interface defining all available methods and properties in the TTS context
|
||||||
*/
|
*/
|
||||||
|
|
@ -97,36 +93,25 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
// OpenAI client reference
|
// OpenAI client reference
|
||||||
const openaiRef = useRef<OpenAI | null>(null);
|
const openaiRef = useRef<OpenAI | null>(null);
|
||||||
|
|
||||||
|
// Use custom hooks
|
||||||
|
const audioContext = useAudioContext();
|
||||||
|
const audioCache = useAudioCache(50);
|
||||||
|
const { availableVoices, fetchVoices } = useVoiceManagement(openApiKey, openApiBaseUrl);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State Management
|
* State Management
|
||||||
* - Playback control
|
|
||||||
* - Text and sentence management
|
|
||||||
* - Audio processing
|
|
||||||
* - Voice and speed settings
|
|
||||||
* - Document navigation
|
|
||||||
*/
|
*/
|
||||||
// All existing state declarations and refs stay at the top
|
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [currentText, setCurrentText] = useState('');
|
|
||||||
const [sentences, setSentences] = useState<string[]>([]);
|
const [sentences, setSentences] = useState<string[]>([]);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [audioContext, setAudioContext] = useState<AudioContextType>();
|
|
||||||
const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
|
const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
|
||||||
const [audioQueue] = useState<AudioBuffer[]>([]);
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [speed, setSpeed] = useState(voiceSpeed);
|
const [speed, setSpeed] = useState(voiceSpeed);
|
||||||
const [voice, setVoice] = useState(configVoice);
|
const [voice, setVoice] = useState(configVoice);
|
||||||
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [currDocPage, setCurrDocPage] = useState<number>(1);
|
const [currDocPage, setCurrDocPage] = useState<number>(1);
|
||||||
const [currDocPages, setCurrDocPages] = useState<number>();
|
const [currDocPages, setCurrDocPages] = useState<number>();
|
||||||
const [nextPageLoading, setNextPageLoading] = useState(false);
|
const [nextPageLoading, setNextPageLoading] = useState(false);
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache for storing audio buffers using LRU (Least Recently Used) strategy
|
|
||||||
*/
|
|
||||||
const audioCacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: 50 }));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the current text and splits it into sentences
|
* Sets the current text and splits it into sentences
|
||||||
*
|
*
|
||||||
|
|
@ -134,14 +119,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
const setText = useCallback((text: string) => {
|
const setText = useCallback((text: string) => {
|
||||||
setCurrentText(text);
|
|
||||||
console.log('Setting page text:', text);
|
console.log('Setting page text:', text);
|
||||||
const newSentences = splitIntoSentences(text);
|
const newSentences = splitIntoSentences(text);
|
||||||
setSentences(newSentences);
|
setSentences(newSentences);
|
||||||
|
|
||||||
// Clear audio cache
|
|
||||||
//audioCacheRef.current.clear();
|
|
||||||
|
|
||||||
setNextPageLoading(false);
|
setNextPageLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -230,7 +211,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
return prev;
|
return prev;
|
||||||
});
|
});
|
||||||
}, [sentences, currDocPage, currDocPages, incrementPage]);
|
}, [sentences, currDocPage, currDocPages, incrementPage]);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves forward one sentence in the text
|
* Moves forward one sentence in the text
|
||||||
|
|
@ -274,30 +254,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes OpenAI configuration and fetches available voices
|
* Initializes OpenAI configuration and fetches available voices
|
||||||
*
|
|
||||||
* This effect runs when the config is loaded or API settings change
|
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchVoices = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${openApiBaseUrl}/audio/voices`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${openApiKey}`,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch voices');
|
|
||||||
const data = await response.json();
|
|
||||||
setAvailableVoices(data.voices || []);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching voices:', error);
|
|
||||||
|
|
||||||
// Set available voices to default openai voices
|
|
||||||
// Supported voices are alloy, ash, coral, echo, fable, onyx, nova, sage and shimmer
|
|
||||||
setAvailableVoices(['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer']);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!configIsLoading && openApiKey && openApiBaseUrl) {
|
if (!configIsLoading && openApiKey && openApiBaseUrl) {
|
||||||
openaiRef.current = new OpenAI({
|
openaiRef.current = new OpenAI({
|
||||||
apiKey: openApiKey,
|
apiKey: openApiKey,
|
||||||
|
|
@ -307,49 +265,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
fetchVoices();
|
fetchVoices();
|
||||||
updateVoiceAndSpeed();
|
updateVoiceAndSpeed();
|
||||||
}
|
}
|
||||||
}, [configIsLoading, openApiKey, openApiBaseUrl, updateVoiceAndSpeed]);
|
}, [configIsLoading, openApiKey, openApiBaseUrl, updateVoiceAndSpeed, fetchVoices]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the AudioContext when component mounts
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== 'undefined' && !audioContext) {
|
|
||||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
||||||
if (AudioContextClass) {
|
|
||||||
try {
|
|
||||||
setAudioContext(new AudioContextClass());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to initialize AudioContext:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (audioContext) {
|
|
||||||
audioContext.close().catch((error) => {
|
|
||||||
console.error('Error closing AudioContext:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [audioContext]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up MediaSession API for media controls
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if ('mediaSession' in navigator) {
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
|
||||||
title: 'Text-to-Speech',
|
|
||||||
artist: 'OpenReader WebUI',
|
|
||||||
album: 'Current Document',
|
|
||||||
});
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('play', () => togglePlay());
|
|
||||||
navigator.mediaSession.setActionHandler('pause', () => togglePlay());
|
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', () => skipForward());
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', () => skipBackward());
|
|
||||||
}
|
|
||||||
}, [togglePlay, skipForward, skipBackward]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates and plays audio for the current sentence
|
* Generates and plays audio for the current sentence
|
||||||
|
|
@ -358,7 +274,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
*/
|
*/
|
||||||
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
|
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
|
||||||
// Check if the audio is already cached
|
// Check if the audio is already cached
|
||||||
const cachedAudio = audioCacheRef.current.get(sentence);
|
const cachedAudio = audioCache.get(sentence);
|
||||||
if (cachedAudio) {
|
if (cachedAudio) {
|
||||||
console.log('Using cached audio for sentence:', sentence.substring(0, 20));
|
console.log('Using cached audio for sentence:', sentence.substring(0, 20));
|
||||||
return cachedAudio;
|
return cachedAudio;
|
||||||
|
|
@ -379,11 +295,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
const audioBuffer = await audioContext!.decodeAudioData(arrayBuffer);
|
const audioBuffer = await audioContext!.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
// Cache the audio buffer
|
// Cache the audio buffer
|
||||||
audioCacheRef.current.set(sentence, audioBuffer);
|
audioCache.set(sentence, audioBuffer);
|
||||||
|
|
||||||
return audioBuffer;
|
return audioBuffer;
|
||||||
}
|
}
|
||||||
}, [audioContext, voice, speed]);
|
}, [audioContext, voice, speed, audioCache]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes and plays the current sentence
|
* Processes and plays the current sentence
|
||||||
|
|
@ -467,13 +383,13 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
*/
|
*/
|
||||||
const preloadNextAudio = useCallback(() => {
|
const preloadNextAudio = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) {
|
if (sentences[currentIndex + 1] && !audioCache.has(sentences[currentIndex + 1])) {
|
||||||
processSentence(sentences[currentIndex + 1], true); // True indicates preloading
|
processSentence(sentences[currentIndex + 1], true); // True indicates preloading
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error preloading next sentence:', error);
|
console.error('Error preloading next sentence:', error);
|
||||||
}
|
}
|
||||||
}, [currentIndex, sentences, audioCacheRef, processSentence]);
|
}, [currentIndex, sentences, audioCache, processSentence]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plays the current sentence's audio
|
* Plays the current sentence's audio
|
||||||
|
|
@ -527,7 +443,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
setCurrentText('');
|
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
}, [abortAudio]);
|
}, [abortAudio]);
|
||||||
|
|
||||||
|
|
@ -544,18 +459,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
}, [abortAudio]);
|
}, [abortAudio]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current index without playing
|
|
||||||
*
|
|
||||||
* @param {number} index - The index to set
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
const setCurrentIndexWithoutPlay = useCallback((index: number) => {
|
|
||||||
abortAudio();
|
|
||||||
|
|
||||||
setCurrentIndex(index);
|
|
||||||
}, [abortAudio]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the speed and restarts the playback
|
* Sets the speed and restarts the playback
|
||||||
*
|
*
|
||||||
|
|
@ -566,14 +469,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
setSpeed(newSpeed);
|
setSpeed(newSpeed);
|
||||||
updateConfigKey('voiceSpeed', newSpeed);
|
updateConfigKey('voiceSpeed', newSpeed);
|
||||||
// Clear the audio cache since it contains audio at the old speed
|
// Clear the audio cache since it contains audio at the old speed
|
||||||
audioCacheRef.current.clear();
|
audioCache.clear();
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
abortAudio();
|
abortAudio();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
}
|
}
|
||||||
}, [isPlaying, abortAudio, updateConfigKey]);
|
}, [isPlaying, abortAudio, updateConfigKey, audioCache]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the voice and restarts the playback
|
* Sets the voice and restarts the playback
|
||||||
|
|
@ -585,14 +488,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
setVoice(newVoice);
|
setVoice(newVoice);
|
||||||
updateConfigKey('voice', newVoice);
|
updateConfigKey('voice', newVoice);
|
||||||
// Clear the audio cache since it contains audio with the old voice
|
// Clear the audio cache since it contains audio with the old voice
|
||||||
audioCacheRef.current.clear();
|
audioCache.clear();
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
abortAudio();
|
abortAudio();
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
}
|
}
|
||||||
}, [isPlaying, abortAudio, updateConfigKey]);
|
}, [isPlaying, abortAudio, updateConfigKey, audioCache]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides the TTS context value to child components
|
* Provides the TTS context value to child components
|
||||||
|
|
@ -634,6 +537,13 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
skipToPage,
|
skipToPage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Use media session hook
|
||||||
|
useMediaSession({
|
||||||
|
togglePlay,
|
||||||
|
skipForward,
|
||||||
|
skipBackward,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the TTS context provider with its children
|
* Renders the TTS context provider with its children
|
||||||
*
|
*
|
||||||
|
|
|
||||||
18
src/hooks/useAudioCache.ts
Normal file
18
src/hooks/useAudioCache.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useRef } from 'react';
|
||||||
|
import { LRUCache } from 'lru-cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing audio cache using LRU strategy
|
||||||
|
* @param maxSize Maximum number of items to store in cache
|
||||||
|
* @returns Object containing cache methods
|
||||||
|
*/
|
||||||
|
export function useAudioCache(maxSize = 50) {
|
||||||
|
const cacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: maxSize }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
get: (key: string) => cacheRef.current.get(key),
|
||||||
|
set: (key: string, value: AudioBuffer) => cacheRef.current.set(key, value),
|
||||||
|
has: (key: string) => cacheRef.current.has(key),
|
||||||
|
clear: () => cacheRef.current.clear(),
|
||||||
|
};
|
||||||
|
}
|
||||||
37
src/hooks/useAudioContext.ts
Normal file
37
src/hooks/useAudioContext.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
// Type definition for AudioContext to handle browser compatibility
|
||||||
|
type AudioContextType = typeof window extends undefined
|
||||||
|
? never
|
||||||
|
: (AudioContext);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing AudioContext
|
||||||
|
* @returns AudioContext instance or undefined
|
||||||
|
*/
|
||||||
|
export function useAudioContext() {
|
||||||
|
const [audioContext, setAudioContext] = useState<AudioContextType>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== 'undefined' && !audioContext) {
|
||||||
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (AudioContextClass) {
|
||||||
|
try {
|
||||||
|
setAudioContext(new AudioContextClass());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize AudioContext:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (audioContext) {
|
||||||
|
audioContext.close().catch((error) => {
|
||||||
|
console.error('Error closing AudioContext:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [audioContext]);
|
||||||
|
|
||||||
|
return audioContext;
|
||||||
|
}
|
||||||
28
src/hooks/useMediaSession.ts
Normal file
28
src/hooks/useMediaSession.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface MediaControls {
|
||||||
|
togglePlay: () => void;
|
||||||
|
skipForward: () => void;
|
||||||
|
skipBackward: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing media session controls
|
||||||
|
* @param controls Object containing media control functions
|
||||||
|
*/
|
||||||
|
export function useMediaSession(controls: MediaControls) {
|
||||||
|
useEffect(() => {
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: 'Text-to-Speech',
|
||||||
|
artist: 'OpenReader WebUI',
|
||||||
|
album: 'Current Document',
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => controls.togglePlay());
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => controls.togglePlay());
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', () => controls.skipForward());
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', () => controls.skipBackward());
|
||||||
|
}
|
||||||
|
}, [controls]);
|
||||||
|
}
|
||||||
35
src/hooks/useVoiceManagement.ts
Normal file
35
src/hooks/useVoiceManagement.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook for managing TTS voices
|
||||||
|
* @param apiKey OpenAI API key
|
||||||
|
* @param baseUrl OpenAI API base URL
|
||||||
|
* @returns Object containing available voices and fetch function
|
||||||
|
*/
|
||||||
|
export function useVoiceManagement(apiKey: string | undefined, baseUrl: string | undefined) {
|
||||||
|
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const fetchVoices = useCallback(async () => {
|
||||||
|
if (!apiKey || !baseUrl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/audio/voices`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch voices');
|
||||||
|
const data = await response.json();
|
||||||
|
setAvailableVoices(data.voices || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching voices:', error);
|
||||||
|
// Set available voices to default openai voices
|
||||||
|
setAvailableVoices(DEFAULT_VOICES);
|
||||||
|
}
|
||||||
|
}, [apiKey, baseUrl]);
|
||||||
|
|
||||||
|
return { availableVoices, fetchVoices };
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue