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() {
|
export default function TTSPlayer() {
|
||||||
const [isVisible, setIsVisible] = useState(true);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -92,6 +105,30 @@ export default function TTSPlayer() {
|
||||||
</ListboxOptions>
|
</ListboxOptions>
|
||||||
</Listbox>
|
</Listbox>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ interface TTSContextType {
|
||||||
speed: number;
|
speed: number;
|
||||||
setSpeed: (speed: number) => void;
|
setSpeed: (speed: number) => void;
|
||||||
setSpeedAndRestart: (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);
|
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 skipTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const isPausingRef = useRef(false);
|
const isPausingRef = useRef(false);
|
||||||
const [speed, setSpeed] = useState(1);
|
const [speed, setSpeed] = useState(1);
|
||||||
|
const [voice, setVoice] = useState('alloy');
|
||||||
|
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
||||||
|
|
||||||
// Create OpenAI instance
|
// Create OpenAI instance
|
||||||
const [openai] = useState(
|
const [openai] = useState(
|
||||||
|
|
@ -95,6 +101,31 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [audioContext]);
|
}, [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
|
// Text preprocessing function to clean and normalize text
|
||||||
const preprocessText = (text: string): string => {
|
const preprocessText = (text: string): string => {
|
||||||
return text
|
return text
|
||||||
|
|
@ -164,7 +195,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const response = await openai.audio.speech.create({
|
const response = await openai.audio.speech.create({
|
||||||
model: 'tts-1',
|
model: 'tts-1',
|
||||||
voice: 'alloy',
|
voice: voice as "alloy",
|
||||||
input: cleanedSentence,
|
input: cleanedSentence,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
});
|
});
|
||||||
|
|
@ -391,7 +422,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const response = await openai.audio.speech.create({
|
const response = await openai.audio.speech.create({
|
||||||
model: 'tts-1',
|
model: 'tts-1',
|
||||||
voice: 'alloy',
|
voice: voice as "alloy",
|
||||||
input: sentence,
|
input: sentence,
|
||||||
speed: speed,
|
speed: speed,
|
||||||
});
|
});
|
||||||
|
|
@ -494,6 +525,22 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
}, [isPlaying, currentIndex, stop]);
|
}, [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 = {
|
const value = {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
currentText,
|
currentText,
|
||||||
|
|
@ -512,6 +559,10 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
speed,
|
speed,
|
||||||
setSpeed,
|
setSpeed,
|
||||||
setSpeedAndRestart,
|
setSpeedAndRestart,
|
||||||
|
voice,
|
||||||
|
setVoice,
|
||||||
|
setVoiceAndRestart,
|
||||||
|
availableVoices,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue