diff --git a/src/components/PDFViewer.tsx b/src/components/PDFViewer.tsx index 7bc5f35..28e7ee9 100644 --- a/src/components/PDFViewer.tsx +++ b/src/components/PDFViewer.tsx @@ -1,6 +1,6 @@ 'use client'; -import { RefObject } from 'react'; +import { RefObject, useCallback } from 'react'; import { Document, Page } from 'react-pdf'; import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/TextLayer.css'; @@ -176,13 +176,9 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) { } const [currentPage, setCurrentPage] = useState(1); - const [pageText, setPageText] = useState(''); - - const handlePageChange = async (pageNumber: number) => { + const handlePageChange = useCallback(async (pageNumber: number) => { if (pageNumber < 1 || pageNumber > (numPages || 1)) return; - - const wasPlaying = isPlaying; - + // Stop current playback and reset states if (isPlaying) { stop(); @@ -247,7 +243,6 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) { fullText += lineText + ' '; } - setPageText(fullText.trim()); setText(fullText.trim()); // Update TTS with current page text // // Wait for text processing before resuming playback @@ -260,7 +255,7 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) { console.error('Error extracting page text:', error); } } - }; + }, [isPlaying, pdfData, pdfDataUrl, numPages, setText, stop]); // Auto-advance to next page when TTS reaches the end useEffect(() => { @@ -270,7 +265,7 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) { }, 500); // Longer delay to ensure states are settled return () => clearTimeout(timer); } - }, [isPlaying, currentIndex, sentences.length, currentPage, numPages]); + }, [isPlaying, currentIndex, sentences.length, currentPage, numPages, handlePageChange]); return (
diff --git a/src/contexts/TTSContext.tsx b/src/contexts/TTSContext.tsx index b5c5e71..e93aa99 100644 --- a/src/contexts/TTSContext.tsx +++ b/src/contexts/TTSContext.tsx @@ -25,7 +25,7 @@ declare global { type AudioContextType = typeof window extends undefined ? never - : (AudioContext | null); + : (AudioContext); interface TTSContextType { isPlaying: boolean; @@ -66,12 +66,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { const [currentText, setCurrentText] = useState(''); const [sentences, setSentences] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); - const [audioContext, setAudioContext] = useState(null); + const [audioContext, setAudioContext] = useState(); const currentRequestRef = useRef(null); const [activeHowl, setActiveHowl] = useState(null); const [audioQueue] = useState([]); const [isProcessing, setIsProcessing] = useState(false); - const isPausingRef = useRef(false); const [speed, setSpeed] = useState(1); const [voice, setVoice] = useState('alloy'); const [availableVoices, setAvailableVoices] = useState([]); @@ -90,20 +89,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { audioCacheRef.current.clear(); }, []); - const togglePlay = useCallback(() => { - setIsPlaying((prev) => { - if (!prev) { - isPausingRef.current = false; - return true; - } else { - isPausingRef.current = true; - abortAudio(); - return false; - } - }); - }, [activeHowl]); - - const abortAudio = useCallback(() => { if (currentRequestRef.current) { currentRequestRef.current.abort(); @@ -116,6 +101,18 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } }, [currentRequestRef, activeHowl]); + const togglePlay = useCallback(() => { + setIsPlaying((prev) => { + if (!prev) { + return true; + } else { + abortAudio(); + return false; + } + }); + }, [abortAudio]); + + const skipForward = useCallback(() => { setIsProcessing(true); @@ -128,7 +125,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { }); setIsProcessing(false); - }, [sentences, activeHowl]); + }, [sentences, abortAudio]); const skipBackward = useCallback(() => { setIsProcessing(true); @@ -143,7 +140,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { setIsProcessing(false); - }, [sentences, activeHowl]); + }, [sentences, abortAudio]); // Initialize OpenAI instance when config loads useEffect(() => { @@ -196,6 +193,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } } } + + return () => { + if (audioContext) { + audioContext.close().catch((error) => { + console.error('Error closing AudioContext:', error); + }); + } + } }, [audioContext]); // Now the MediaSession effect can use these functions @@ -221,10 +226,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { }, [togglePlay, skipForward, skipBackward]); const advance = useCallback(async () => { - if (currentIndex >= sentences.length - 1) { - return; - } - setCurrentIndex((prev) => { const nextIndex = prev + 1; if (nextIndex < sentences.length) { @@ -233,7 +234,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } return prev; }); - }, [currentIndex, sentences]); + }, [sentences]); //new function to return audio buffer with caching const getAudio = useCallback(async (sentence: string): Promise => { @@ -246,6 +247,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { // If not cached, fetch the audio from OpenAI API if (openaiRef.current) { + console.log('Requesting audio for sentence:', sentence); + const response = await openaiRef.current.audio.speech.create({ model: 'tts-1', voice: voice as "alloy", @@ -263,19 +266,21 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } }, [audioContext, voice, speed]); - const processSentence = async (sentence: string, preload: boolean = false): Promise => { + const processSentence = useCallback(async (sentence: string, preload = false): Promise => { + if (isProcessing && !preload) throw new Error('Audio is already being processed'); if (!audioContext || !openaiRef.current) throw new Error('Audio context not initialized'); + + // Only set processing state if not preloading if (!preload) setIsProcessing(true); - + try { const cleanedSentence = preprocessSentenceForAudio(sentence); currentRequestRef.current = new AbortController(); - - console.log('Requesting audio for sentence:', cleanedSentence); - + const audioBuffer = await getAudio(cleanedSentence); - if (!currentRequestRef.current) throw new Error('Request cancelled'); - + if (!currentRequestRef.current) throw new Error('Request was cancelled'); + + return audioBufferToURL(audioBuffer!); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { @@ -285,13 +290,18 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { } finally { currentRequestRef.current = null; } - }; + }, [isProcessing, audioContext, getAudio]); - const playSentenceWithHowl = async (sentence: string) => { + const playSentenceWithHowl = useCallback(async (sentence: string) => { try { const audioUrl = await processSentence(sentence); + if (!audioUrl) { + throw new Error('No audio URL generated'); + } + + // Set processing to false before creating the Howl instance setIsProcessing(false); - + const howl = new Howl({ src: [audioUrl], format: ['wav'], @@ -309,27 +319,29 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { onend: () => { URL.revokeObjectURL(audioUrl); setActiveHowl(null); - if (isPlaying && !isPausingRef.current) { - //advance(); + if (isPlaying) { + advance(); } - isPausingRef.current = false; }, onloaderror: (id, error) => { console.error('Error loading audio:', error); setIsProcessing(false); setActiveHowl(null); URL.revokeObjectURL(audioUrl); - //advance(); + // Don't auto-advance on load error + setIsPlaying(false); }, }); - + setActiveHowl(howl); howl.play(); - + } catch (error) { console.error('Error playing TTS:', error); setActiveHowl(null); setIsProcessing(false); + setIsPlaying(false); // Stop playback on error + if (error instanceof Error && (error.message.includes('Unable to decode audio data') || error.message.includes('EncodingError'))) { @@ -337,48 +349,51 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { advance(); } } - }; + }, [isPlaying, processSentence, advance]); - // Preload adjacent sentences when currentIndex changes - useEffect(() => { - /* - * Preloads the next sentence in the queue to improve playback performance. - * Only preloads the next sentence to reduce API load. - * - * Dependencies: - * - currentIndex: Re-runs when the currentIndex changes - * - sentences: Re-runs when the sentences array changes - */ - const preloadAdjacentSentences = async () => { - try { - // Only preload next sentence to reduce API load - if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) { - await new Promise((resolve) => setTimeout(resolve, 200)); // Add small delay - await processSentence(sentences[currentIndex + 1], true); // True indicates preloading - } - } catch (error) { - console.error('Error preloading adjacent sentences:', error); + const preloadNextAudio = useCallback(async () => { + try { + // Only preload next sentence to reduce API load + if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) { + await new Promise((resolve) => setTimeout(resolve, 200)); // Add small delay + await processSentence(sentences[currentIndex + 1], true); // True indicates preloading } - }; - preloadAdjacentSentences(); - }, [currentIndex, sentences]); + } catch (error) { + console.error('Error preloading next sentence:', error); + } + }, [currentIndex, sentences, audioCacheRef, processSentence]); - // Add this useEffect before the return statement in TTSProvider + const playAudio = useCallback(async () => { + //if (isPlaying && sentences[currentIndex] && !isProcessing) { + await playSentenceWithHowl(sentences[currentIndex]); + //} + }, [sentences, currentIndex, playSentenceWithHowl]); + + // main driver useEffect useEffect(() => { - const playAudio = async () => { - if (isPlaying && sentences[currentIndex] && !isProcessing) { - await playSentenceWithHowl(sentences[currentIndex]); + if (!sentences[currentIndex]) { + return; // Don't proceed if no valid sentence + } + + if (isPlaying && !isProcessing && !activeHowl) { + playAudio(); + if (isPlaying) { // Only preload if still playing + preloadNextAudio().catch(console.error); } + } + + return () => { + abortAudio(); }; - - playAudio(); - }, [isPlaying, currentIndex, sentences, isProcessing]); - - // const playAudio = useCallback(async () => { - // if (isPlaying && sentences[currentIndex] && !isProcessing) { - // await processAndPlaySentence(sentences[currentIndex]); - // } - // }, [isPlaying, sentences, currentIndex, isProcessing]); + }, [ + isPlaying, + isProcessing, + currentIndex, + sentences, + playAudio, + preloadNextAudio, + abortAudio + ]); const stop = useCallback(() => { // Cancel any ongoing request @@ -388,7 +403,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { setCurrentIndex(0); setCurrentText(''); setIsProcessing(false); - }, [activeHowl]); + }, [abortAudio]); const stopAndPlayFromIndex = useCallback((index: number) => { abortAudio(); @@ -398,13 +413,13 @@ export function TTSProvider({ children }: { children: React.ReactNode }) { setCurrentIndex(index); setIsPlaying(true); }, 50); - }, []); + }, [abortAudio]); const setCurrentIndexWithoutPlay = useCallback((index: number) => { abortAudio(); setCurrentIndex(index); - }, [activeHowl]); + }, [abortAudio]); const setSpeedAndRestart = useCallback((newSpeed: number) => { setSpeed(newSpeed);