Voice + Speed control
This commit is contained in:
parent
0979f55aec
commit
30f981d616
2 changed files with 91 additions and 3 deletions
|
|
@ -34,7 +34,20 @@ const speedOptions = [
|
|||
|
||||
export default function TTSPlayer() {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const { isPlaying, togglePlay, skipForward, skipBackward, isProcessing, speed, setSpeedAndRestart } = useTTS();
|
||||
const {
|
||||
isPlaying,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
isProcessing,
|
||||
speed,
|
||||
setSpeedAndRestart,
|
||||
voice,
|
||||
setVoiceAndRestart,
|
||||
availableVoices,
|
||||
} = useTTS();
|
||||
|
||||
//console.log(availableVoices);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -92,6 +105,30 @@ export default function TTSPlayer() {
|
|||
</ListboxOptions>
|
||||
</Listbox>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Listbox value={voice} onChange={setVoiceAndRestart}>
|
||||
<ListboxButton className="flex items-center space-x-1 bg-transparent text-foreground text-sm focus:outline-none cursor-pointer hover:bg-offbase rounded pl-2 pr-1 py-1">
|
||||
<span>{voice}</span>
|
||||
<ChevronUpDownIcon className="h-3 w-3" />
|
||||
</ListboxButton>
|
||||
<ListboxOptions className="absolute bottom-full mb-1 w-32 overflow-auto rounded-lg bg-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||
{availableVoices.map((voiceId) => (
|
||||
<ListboxOption
|
||||
key={voiceId}
|
||||
value={voiceId}
|
||||
className={({ active, selected }) =>
|
||||
`relative cursor-pointer select-none py-2 px-3 ${
|
||||
active ? 'bg-offbase' : ''
|
||||
} ${selected ? 'font-medium' : ''}`
|
||||
}
|
||||
>
|
||||
<span>{voiceId}</span>
|
||||
</ListboxOption>
|
||||
))}
|
||||
</ListboxOptions>
|
||||
</Listbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ interface TTSContextType {
|
|||
speed: number;
|
||||
setSpeed: (speed: number) => void;
|
||||
setSpeedAndRestart: (speed: number) => void;
|
||||
voice: string;
|
||||
setVoice: (voice: string) => void;
|
||||
setVoiceAndRestart: (voice: string) => void;
|
||||
availableVoices: string[];
|
||||
}
|
||||
|
||||
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||
|
|
@ -60,6 +64,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const isPausingRef = useRef(false);
|
||||
const [speed, setSpeed] = useState(1);
|
||||
const [voice, setVoice] = useState('alloy');
|
||||
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
||||
|
||||
// Create OpenAI instance
|
||||
const [openai] = useState(
|
||||
|
|
@ -95,6 +101,31 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [audioContext]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch available voices when component mounts
|
||||
const fetchVoices = async () => {
|
||||
try {
|
||||
const response = await fetch(`${openai.baseURL}/audio/voices`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${process.env.NEXT_PUBLIC_OPENAI_API_KEY}`,
|
||||
'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']);
|
||||
}
|
||||
};
|
||||
|
||||
fetchVoices();
|
||||
}, [openai.baseURL]);
|
||||
|
||||
// Text preprocessing function to clean and normalize text
|
||||
const preprocessText = (text: string): string => {
|
||||
return text
|
||||
|
|
@ -164,7 +195,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const startTime = Date.now();
|
||||
const response = await openai.audio.speech.create({
|
||||
model: 'tts-1',
|
||||
voice: 'alloy',
|
||||
voice: voice as "alloy",
|
||||
input: cleanedSentence,
|
||||
speed: speed,
|
||||
});
|
||||
|
|
@ -391,7 +422,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const startTime = Date.now();
|
||||
const response = await openai.audio.speech.create({
|
||||
model: 'tts-1',
|
||||
voice: 'alloy',
|
||||
voice: voice as "alloy",
|
||||
input: sentence,
|
||||
speed: speed,
|
||||
});
|
||||
|
|
@ -494,6 +525,22 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [isPlaying, currentIndex, stop]);
|
||||
|
||||
const setVoiceAndRestart = useCallback((newVoice: string) => {
|
||||
setVoice(newVoice);
|
||||
// Clear the audio cache since it contains audio with the old voice
|
||||
audioCacheRef.current.clear();
|
||||
|
||||
if (isPlaying) {
|
||||
const currentIdx = currentIndex;
|
||||
stop();
|
||||
// Small delay to ensure audio is fully stopped
|
||||
setTimeout(() => {
|
||||
setCurrentIndex(currentIdx);
|
||||
setIsPlaying(true);
|
||||
}, 50);
|
||||
}
|
||||
}, [isPlaying, currentIndex, stop]);
|
||||
|
||||
const value = {
|
||||
isPlaying,
|
||||
currentText,
|
||||
|
|
@ -512,6 +559,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
speed,
|
||||
setSpeed,
|
||||
setSpeedAndRestart,
|
||||
voice,
|
||||
setVoice,
|
||||
setVoiceAndRestart,
|
||||
availableVoices,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in a new issue