| import { useEffect, useRef, useCallback, useState, useMemo } from "react"; |
| const DEFAULT_VOICE_CONFIG = { |
| voiceURI: "", |
| rate: 1.0, |
| pitch: 1.0, |
| volume: 1.0, |
| enabled: true, |
| }; |
| |
| |
| |
| |
| export function useTTSPlayback({ isActive, isPlaying, currentScene, currentSceneIndex, scenes, onSceneNarrationEnd, }) { |
| const svc = useMemo(() => window.SpeechService, []); |
| const [ttsEnabled, setTtsEnabledState] = useState(() => { |
| return svc?.isTTSEnabled?.() ?? true; |
| }); |
| const [isSpeaking, setIsSpeaking] = useState(false); |
| const [isPaused, setIsPaused] = useState(false); |
| const [isSupported, setIsSupported] = useState(false); |
| const [voices, setVoices] = useState([]); |
| const [voiceConfig, setVoiceConfigState] = useState(() => svc?.getVoiceConfig?.() ?? DEFAULT_VOICE_CONFIG); |
| const lastSpokenSceneRef = useRef(-1); |
| const speakingPromiseRef = useRef(null); |
| |
| const onSceneNarrationEndRef = useRef(onSceneNarrationEnd); |
| useEffect(() => { |
| onSceneNarrationEndRef.current = onSceneNarrationEnd; |
| }, [onSceneNarrationEnd]); |
| |
| useEffect(() => { |
| if (!svc) |
| return; |
| setIsSupported(svc.isSynthesisSupported ?? false); |
| setTtsEnabledState(svc.isTTSEnabled?.() ?? true); |
| setVoiceConfigState(svc.getVoiceConfig?.() ?? DEFAULT_VOICE_CONFIG); |
| const loadVoices = () => { |
| const availableVoices = svc.getVoices?.() ?? []; |
| setVoices(availableVoices); |
| }; |
| loadVoices(); |
| |
| if (window.speechSynthesis?.onvoiceschanged !== undefined) { |
| window.speechSynthesis.onvoiceschanged = loadVoices; |
| } |
| }, [svc]); |
| |
| const setTTSEnabled = useCallback((enabled) => { |
| setTtsEnabledState(enabled); |
| if (svc?.setTTSEnabled) { |
| svc.setTTSEnabled(enabled); |
| } |
| if (!enabled) { |
| svc?.stopSpeaking?.(); |
| setIsSpeaking(false); |
| setIsPaused(false); |
| } |
| }, [svc]); |
| |
| const setVoiceConfig = useCallback((config) => { |
| const newConfig = { ...voiceConfig, ...config }; |
| setVoiceConfigState(newConfig); |
| if (svc?.setVoiceConfig) { |
| svc.setVoiceConfig(config); |
| } |
| }, [svc, voiceConfig]); |
| |
| const speakCurrentScene = useCallback(async () => { |
| if (!svc || !currentScene?.narration || !ttsEnabled) { |
| return false; |
| } |
| |
| svc.stopSpeaking?.(); |
| setIsSpeaking(true); |
| setIsPaused(false); |
| try { |
| const success = await svc.speak(currentScene.narration, { |
| onStart: () => { |
| setIsSpeaking(true); |
| setIsPaused(false); |
| }, |
| onEnd: () => { |
| setIsSpeaking(false); |
| setIsPaused(false); |
| |
| onSceneNarrationEndRef.current?.(); |
| }, |
| onError: (error) => { |
| console.warn("[TTS] Speech error:", error); |
| setIsSpeaking(false); |
| setIsPaused(false); |
| }, |
| }); |
| return success; |
| } |
| catch (error) { |
| console.warn("[TTS] Failed to speak:", error); |
| setIsSpeaking(false); |
| return false; |
| } |
| }, [svc, currentScene?.narration, ttsEnabled]); |
| |
| const stopSpeaking = useCallback(() => { |
| svc?.stopSpeaking?.(); |
| setIsSpeaking(false); |
| setIsPaused(false); |
| }, [svc]); |
| |
| const pauseSpeaking = useCallback(() => { |
| svc?.pauseSpeaking?.(); |
| setIsPaused(true); |
| }, [svc]); |
| |
| const resumeSpeaking = useCallback(() => { |
| svc?.resumeSpeaking?.(); |
| setIsPaused(false); |
| }, [svc]); |
| |
| useEffect(() => { |
| if (!isActive || !isPlaying || !ttsEnabled || !svc) { |
| return; |
| } |
| |
| if (currentSceneIndex === lastSpokenSceneRef.current) { |
| return; |
| } |
| if (!currentScene?.narration) { |
| lastSpokenSceneRef.current = currentSceneIndex; |
| return; |
| } |
| console.log(`[TTS] Scene ${currentSceneIndex + 1} changed, speaking narration...`); |
| lastSpokenSceneRef.current = currentSceneIndex; |
| |
| speakingPromiseRef.current = speakCurrentScene(); |
| }, [isActive, isPlaying, ttsEnabled, currentSceneIndex, currentScene?.narration, speakCurrentScene, svc]); |
| |
| useEffect(() => { |
| if (!svc || !isActive) |
| return; |
| if (isPlaying && isPaused) { |
| |
| resumeSpeaking(); |
| } |
| else if (!isPlaying && isSpeaking && !isPaused) { |
| |
| pauseSpeaking(); |
| } |
| }, [isPlaying, isActive, isSpeaking, isPaused, pauseSpeaking, resumeSpeaking, svc]); |
| |
| useEffect(() => { |
| if (!isActive && svc) { |
| svc.stopSpeaking?.(); |
| setIsSpeaking(false); |
| setIsPaused(false); |
| lastSpokenSceneRef.current = -1; |
| } |
| }, [isActive, svc]); |
| |
| useEffect(() => { |
| if (scenes.length === 0) { |
| lastSpokenSceneRef.current = -1; |
| } |
| }, [scenes.length]); |
| return { |
| ttsEnabled, |
| setTTSEnabled, |
| isSpeaking, |
| isPaused, |
| isSupported, |
| voices, |
| voiceConfig, |
| setVoiceConfig, |
| speakCurrentScene, |
| stopSpeaking, |
| pauseSpeaking, |
| resumeSpeaking, |
| }; |
| } |
| export default useTTSPlayback; |
|
|