Voice + Speed control

This commit is contained in:
Richard Roberson 2025-01-20 19:06:41 -07:00
parent 0979f55aec
commit 30f981d616
2 changed files with 91 additions and 3 deletions

View file

@ -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>
);

View file

@ -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 (