File size: 6,337 Bytes
921d377
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import { useEffect, useRef, useCallback, useState, useMemo } from "react";
const DEFAULT_VOICE_CONFIG = {
    voiceURI: "",
    rate: 1.0,
    pitch: 1.0,
    volume: 1.0,
    enabled: true,
};
/**
 * Hook for integrating TTS playback with TV Mode
 * Automatically speaks scene narration when scenes change
 */
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);
    // Use ref to always call the latest callback (avoids stale closure issues)
    const onSceneNarrationEndRef = useRef(onSceneNarrationEnd);
    useEffect(() => {
        onSceneNarrationEndRef.current = onSceneNarrationEnd;
    }, [onSceneNarrationEnd]);
    // Initialize and load voices
    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();
        // Voices might load asynchronously
        if (window.speechSynthesis?.onvoiceschanged !== undefined) {
            window.speechSynthesis.onvoiceschanged = loadVoices;
        }
    }, [svc]);
    // Set TTS enabled
    const setTTSEnabled = useCallback((enabled) => {
        setTtsEnabledState(enabled);
        if (svc?.setTTSEnabled) {
            svc.setTTSEnabled(enabled);
        }
        if (!enabled) {
            svc?.stopSpeaking?.();
            setIsSpeaking(false);
            setIsPaused(false);
        }
    }, [svc]);
    // Update voice config
    const setVoiceConfig = useCallback((config) => {
        const newConfig = { ...voiceConfig, ...config };
        setVoiceConfigState(newConfig);
        if (svc?.setVoiceConfig) {
            svc.setVoiceConfig(config);
        }
    }, [svc, voiceConfig]);
    // Speak the current scene's narration
    const speakCurrentScene = useCallback(async () => {
        if (!svc || !currentScene?.narration || !ttsEnabled) {
            return false;
        }
        // Stop any ongoing speech
        svc.stopSpeaking?.();
        setIsSpeaking(true);
        setIsPaused(false);
        try {
            const success = await svc.speak(currentScene.narration, {
                onStart: () => {
                    setIsSpeaking(true);
                    setIsPaused(false);
                },
                onEnd: () => {
                    setIsSpeaking(false);
                    setIsPaused(false);
                    // Use ref to always call the latest callback (avoids stale closure)
                    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]);
    // Stop speaking
    const stopSpeaking = useCallback(() => {
        svc?.stopSpeaking?.();
        setIsSpeaking(false);
        setIsPaused(false);
    }, [svc]);
    // Pause speaking
    const pauseSpeaking = useCallback(() => {
        svc?.pauseSpeaking?.();
        setIsPaused(true);
    }, [svc]);
    // Resume speaking
    const resumeSpeaking = useCallback(() => {
        svc?.resumeSpeaking?.();
        setIsPaused(false);
    }, [svc]);
    // Auto-speak when scene changes
    useEffect(() => {
        if (!isActive || !isPlaying || !ttsEnabled || !svc) {
            return;
        }
        // Only speak if we haven't spoken this scene yet
        if (currentSceneIndex === lastSpokenSceneRef.current) {
            return;
        }
        if (!currentScene?.narration) {
            lastSpokenSceneRef.current = currentSceneIndex;
            return;
        }
        console.log(`[TTS] Scene ${currentSceneIndex + 1} changed, speaking narration...`);
        lastSpokenSceneRef.current = currentSceneIndex;
        // Speak the scene narration
        speakingPromiseRef.current = speakCurrentScene();
    }, [isActive, isPlaying, ttsEnabled, currentSceneIndex, currentScene?.narration, speakCurrentScene, svc]);
    // Sync with play/pause state
    useEffect(() => {
        if (!svc || !isActive)
            return;
        if (isPlaying && isPaused) {
            // TV Mode resumed, resume TTS
            resumeSpeaking();
        }
        else if (!isPlaying && isSpeaking && !isPaused) {
            // TV Mode paused, pause TTS
            pauseSpeaking();
        }
    }, [isPlaying, isActive, isSpeaking, isPaused, pauseSpeaking, resumeSpeaking, svc]);
    // Stop TTS when TV Mode exits
    useEffect(() => {
        if (!isActive && svc) {
            svc.stopSpeaking?.();
            setIsSpeaking(false);
            setIsPaused(false);
            lastSpokenSceneRef.current = -1;
        }
    }, [isActive, svc]);
    // Reset spoken scene tracking when scenes change significantly
    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;