diff --git a/pitch-deck/lib/hooks/usePresenterMode.ts b/pitch-deck/lib/hooks/usePresenterMode.ts index d002cbd..a517115 100644 --- a/pitch-deck/lib/hooks/usePresenterMode.ts +++ b/pitch-deck/lib/hooks/usePresenterMode.ts @@ -60,11 +60,30 @@ export function usePresenterMode({ const stateRef = useRef('idle') const audioRef = useRef(null) const abortRef = useRef(null) + const audioUnlockedRef = useRef(false) // Refs for recursive functions to avoid circular useCallback dependencies const advanceRef = useRef<() => void>(() => {}) const speakAndAdvanceRef = useRef<(text: string, pauseAfter: number, onDone: () => void) => void>(() => {}) + // Unlock browser audio playback — must be called from a user gesture (click) + const unlockAudio = useCallback(() => { + if (audioUnlockedRef.current) return + try { + // Create and play a silent audio to unlock the Audio API + const ctx = new (window.AudioContext || (window as any).webkitAudioContext)() + const buffer = ctx.createBuffer(1, 1, 22050) + const source = ctx.createBufferSource() + source.buffer = buffer + source.connect(ctx.destination) + source.start(0) + audioUnlockedRef.current = true + console.log('Audio playback unlocked') + } catch (e) { + console.warn('Audio unlock failed:', e) + } + }, []) + // Check TTS service availability on mount useEffect(() => { fetch('/api/presenter/tts', { @@ -147,6 +166,7 @@ export function usePresenterMode({ let blobUrl = audioCache.get(key) if (!blobUrl) { + console.log('[TTS] Fetching audio for:', text.slice(0, 50) + '...') const res = await fetch('/api/presenter/tts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -157,8 +177,11 @@ export function usePresenterMode({ if (!res.ok) throw new Error(`TTS error: ${res.status}`) const blob = await res.blob() + console.log('[TTS] Audio received:', blob.size, 'bytes') blobUrl = URL.createObjectURL(blob) audioCache.set(key, blobUrl) + } else { + console.log('[TTS] Cache hit for:', text.slice(0, 50) + '...') } if (controller.signal.aborted) return @@ -167,6 +190,7 @@ export function usePresenterMode({ audioRef.current = audio audio.onended = () => { + console.log('[TTS] Audio playback ended') setIsSpeaking(false) audioRef.current = null if (pauseAfter > 0) { @@ -176,22 +200,21 @@ export function usePresenterMode({ } } - audio.onerror = () => { - console.warn('Audio playback error') + audio.onerror = (e) => { + console.warn('[TTS] Audio playback error:', e) setIsSpeaking(false) audioRef.current = null - // Fallback to timer const wordCount = text.split(/\s+/).length const readingTime = Math.max(wordCount * 150, 2000) timerRef.current = setTimeout(onDone, readingTime + pauseAfter) } await audio.play() + console.log('[TTS] Audio playing') } catch (err: any) { if (err.name === 'AbortError') return - console.warn('TTS fetch error:', err) + console.warn('[TTS] Error:', err.name, err.message) setIsSpeaking(false) - // Fallback to timer const wordCount = text.split(/\s+/).length const readingTime = Math.max(wordCount * 150, 2000) timerRef.current = setTimeout(onDone, readingTime + pauseAfter) @@ -281,6 +304,9 @@ export function usePresenterMode({ }, [language, totalSlides, goToSlide, getScriptForIndex, showParagraph, cancelSpeech]) const start = useCallback(() => { + // Unlock audio playback immediately in user gesture context + unlockAudio() + clearTimer() cancelSpeech() setState('presenting') @@ -292,15 +318,12 @@ export function usePresenterMode({ const para = showParagraph(slideIdx, 0) if (para) { const text = language === 'de' ? para.text_de : para.text_en - // Small delay so state is set before speaking - setTimeout(() => { - speakAndAdvanceRef.current(text, para.pause_after, () => advanceRef.current()) - }, 100) + speakAndAdvanceRef.current(text, para.pause_after, () => advanceRef.current()) } } else { timerRef.current = setTimeout(() => advanceRef.current(), 1000) } - }, [clearTimer, cancelSpeech, language, getScriptForIndex, showParagraph]) + }, [unlockAudio, clearTimer, cancelSpeech, language, getScriptForIndex, showParagraph]) const stop = useCallback(() => { clearTimer() @@ -349,12 +372,13 @@ export function usePresenterMode({ }, [clearTimer, cancelSpeech, totalSlides, goToSlide, language, getScriptForIndex, showParagraph]) const toggle = useCallback(() => { + unlockAudio() if (stateRef.current === 'idle') { start() } else { stop() } - }, [start, stop]) + }, [unlockAudio, start, stop]) // Calculate overall progress const progress = (() => {