Still refactoring

This commit is contained in:
Richard Roberson 2025-01-24 13:47:20 -07:00
parent 072d11a60b
commit f585819881
2 changed files with 99 additions and 89 deletions

View file

@ -1,6 +1,6 @@
'use client'; 'use client';
import { RefObject } from 'react'; import { RefObject, useCallback } from 'react';
import { Document, Page } from 'react-pdf'; import { Document, Page } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css'; import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css'; import 'react-pdf/dist/Page/TextLayer.css';
@ -176,13 +176,9 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
} }
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageText, setPageText] = useState(''); const handlePageChange = useCallback(async (pageNumber: number) => {
const handlePageChange = async (pageNumber: number) => {
if (pageNumber < 1 || pageNumber > (numPages || 1)) return; if (pageNumber < 1 || pageNumber > (numPages || 1)) return;
const wasPlaying = isPlaying;
// Stop current playback and reset states // Stop current playback and reset states
if (isPlaying) { if (isPlaying) {
stop(); stop();
@ -247,7 +243,6 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
fullText += lineText + ' '; fullText += lineText + ' ';
} }
setPageText(fullText.trim());
setText(fullText.trim()); // Update TTS with current page text setText(fullText.trim()); // Update TTS with current page text
// // Wait for text processing before resuming playback // // Wait for text processing before resuming playback
@ -260,7 +255,7 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
console.error('Error extracting page text:', error); console.error('Error extracting page text:', error);
} }
} }
}; }, [isPlaying, pdfData, pdfDataUrl, numPages, setText, stop]);
// Auto-advance to next page when TTS reaches the end // Auto-advance to next page when TTS reaches the end
useEffect(() => { useEffect(() => {
@ -270,7 +265,7 @@ export function PDFViewer({ pdfData, zoomLevel }: PDFViewerProps) {
}, 500); // Longer delay to ensure states are settled }, 500); // Longer delay to ensure states are settled
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isPlaying, currentIndex, sentences.length, currentPage, numPages]); }, [isPlaying, currentIndex, sentences.length, currentPage, numPages, handlePageChange]);
return ( return (
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6"> <div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">

View file

@ -25,7 +25,7 @@ declare global {
type AudioContextType = typeof window extends undefined type AudioContextType = typeof window extends undefined
? never ? never
: (AudioContext | null); : (AudioContext);
interface TTSContextType { interface TTSContextType {
isPlaying: boolean; isPlaying: boolean;
@ -66,12 +66,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
const [currentText, setCurrentText] = useState(''); const [currentText, setCurrentText] = useState('');
const [sentences, setSentences] = useState<string[]>([]); const [sentences, setSentences] = useState<string[]>([]);
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [audioContext, setAudioContext] = useState<AudioContextType>(null); const [audioContext, setAudioContext] = useState<AudioContextType>();
const currentRequestRef = useRef<AbortController | null>(null); const currentRequestRef = useRef<AbortController | null>(null);
const [activeHowl, setActiveHowl] = useState<Howl | null>(null); const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
const [audioQueue] = useState<AudioBuffer[]>([]); const [audioQueue] = useState<AudioBuffer[]>([]);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const isPausingRef = useRef(false);
const [speed, setSpeed] = useState(1); const [speed, setSpeed] = useState(1);
const [voice, setVoice] = useState('alloy'); const [voice, setVoice] = useState('alloy');
const [availableVoices, setAvailableVoices] = useState<string[]>([]); const [availableVoices, setAvailableVoices] = useState<string[]>([]);
@ -90,20 +89,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
audioCacheRef.current.clear(); 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(() => { const abortAudio = useCallback(() => {
if (currentRequestRef.current) { if (currentRequestRef.current) {
currentRequestRef.current.abort(); currentRequestRef.current.abort();
@ -116,6 +101,18 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
} }
}, [currentRequestRef, activeHowl]); }, [currentRequestRef, activeHowl]);
const togglePlay = useCallback(() => {
setIsPlaying((prev) => {
if (!prev) {
return true;
} else {
abortAudio();
return false;
}
});
}, [abortAudio]);
const skipForward = useCallback(() => { const skipForward = useCallback(() => {
setIsProcessing(true); setIsProcessing(true);
@ -128,7 +125,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
}); });
setIsProcessing(false); setIsProcessing(false);
}, [sentences, activeHowl]); }, [sentences, abortAudio]);
const skipBackward = useCallback(() => { const skipBackward = useCallback(() => {
setIsProcessing(true); setIsProcessing(true);
@ -143,7 +140,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setIsProcessing(false); setIsProcessing(false);
}, [sentences, activeHowl]); }, [sentences, abortAudio]);
// Initialize OpenAI instance when config loads // Initialize OpenAI instance when config loads
useEffect(() => { 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]); }, [audioContext]);
// Now the MediaSession effect can use these functions // Now the MediaSession effect can use these functions
@ -221,10 +226,6 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
}, [togglePlay, skipForward, skipBackward]); }, [togglePlay, skipForward, skipBackward]);
const advance = useCallback(async () => { const advance = useCallback(async () => {
if (currentIndex >= sentences.length - 1) {
return;
}
setCurrentIndex((prev) => { setCurrentIndex((prev) => {
const nextIndex = prev + 1; const nextIndex = prev + 1;
if (nextIndex < sentences.length) { if (nextIndex < sentences.length) {
@ -233,7 +234,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
} }
return prev; return prev;
}); });
}, [currentIndex, sentences]); }, [sentences]);
//new function to return audio buffer with caching //new function to return audio buffer with caching
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => { const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
@ -246,6 +247,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
// If not cached, fetch the audio from OpenAI API // If not cached, fetch the audio from OpenAI API
if (openaiRef.current) { if (openaiRef.current) {
console.log('Requesting audio for sentence:', sentence);
const response = await openaiRef.current.audio.speech.create({ const response = await openaiRef.current.audio.speech.create({
model: 'tts-1', model: 'tts-1',
voice: voice as "alloy", voice: voice as "alloy",
@ -263,18 +266,20 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
} }
}, [audioContext, voice, speed]); }, [audioContext, voice, speed]);
const processSentence = async (sentence: string, preload: boolean = false): Promise<string> => { const processSentence = useCallback(async (sentence: string, preload = false): Promise<string> => {
if (isProcessing && !preload) throw new Error('Audio is already being processed');
if (!audioContext || !openaiRef.current) throw new Error('Audio context not initialized'); if (!audioContext || !openaiRef.current) throw new Error('Audio context not initialized');
// Only set processing state if not preloading
if (!preload) setIsProcessing(true); if (!preload) setIsProcessing(true);
try { try {
const cleanedSentence = preprocessSentenceForAudio(sentence); const cleanedSentence = preprocessSentenceForAudio(sentence);
currentRequestRef.current = new AbortController(); currentRequestRef.current = new AbortController();
console.log('Requesting audio for sentence:', cleanedSentence);
const audioBuffer = await getAudio(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!); return audioBufferToURL(audioBuffer!);
} catch (error) { } catch (error) {
@ -285,11 +290,16 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
} finally { } finally {
currentRequestRef.current = null; currentRequestRef.current = null;
} }
}; }, [isProcessing, audioContext, getAudio]);
const playSentenceWithHowl = async (sentence: string) => { const playSentenceWithHowl = useCallback(async (sentence: string) => {
try { try {
const audioUrl = await processSentence(sentence); 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); setIsProcessing(false);
const howl = new Howl({ const howl = new Howl({
@ -309,17 +319,17 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
onend: () => { onend: () => {
URL.revokeObjectURL(audioUrl); URL.revokeObjectURL(audioUrl);
setActiveHowl(null); setActiveHowl(null);
if (isPlaying && !isPausingRef.current) { if (isPlaying) {
//advance(); advance();
} }
isPausingRef.current = false;
}, },
onloaderror: (id, error) => { onloaderror: (id, error) => {
console.error('Error loading audio:', error); console.error('Error loading audio:', error);
setIsProcessing(false); setIsProcessing(false);
setActiveHowl(null); setActiveHowl(null);
URL.revokeObjectURL(audioUrl); URL.revokeObjectURL(audioUrl);
//advance(); // Don't auto-advance on load error
setIsPlaying(false);
}, },
}); });
@ -330,6 +340,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
console.error('Error playing TTS:', error); console.error('Error playing TTS:', error);
setActiveHowl(null); setActiveHowl(null);
setIsProcessing(false); setIsProcessing(false);
setIsPlaying(false); // Stop playback on error
if (error instanceof Error && if (error instanceof Error &&
(error.message.includes('Unable to decode audio data') || (error.message.includes('Unable to decode audio data') ||
error.message.includes('EncodingError'))) { error.message.includes('EncodingError'))) {
@ -337,48 +349,51 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
advance(); advance();
} }
} }
}; }, [isPlaying, processSentence, advance]);
// Preload adjacent sentences when currentIndex changes const preloadNextAudio = useCallback(async () => {
useEffect(() => { try {
/* // Only preload next sentence to reduce API load
* Preloads the next sentence in the queue to improve playback performance. if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) {
* Only preloads the next sentence to reduce API load. await new Promise((resolve) => setTimeout(resolve, 200)); // Add small delay
* await processSentence(sentences[currentIndex + 1], true); // True indicates preloading
* 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);
} }
}; } catch (error) {
preloadAdjacentSentences(); console.error('Error preloading next sentence:', error);
}, [currentIndex, sentences]); }
}, [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(() => { useEffect(() => {
const playAudio = async () => { if (!sentences[currentIndex]) {
if (isPlaying && sentences[currentIndex] && !isProcessing) { return; // Don't proceed if no valid sentence
await playSentenceWithHowl(sentences[currentIndex]); }
if (isPlaying && !isProcessing && !activeHowl) {
playAudio();
if (isPlaying) { // Only preload if still playing
preloadNextAudio().catch(console.error);
} }
}
return () => {
abortAudio();
}; };
}, [
playAudio(); isPlaying,
}, [isPlaying, currentIndex, sentences, isProcessing]); isProcessing,
currentIndex,
// const playAudio = useCallback(async () => { sentences,
// if (isPlaying && sentences[currentIndex] && !isProcessing) { playAudio,
// await processAndPlaySentence(sentences[currentIndex]); preloadNextAudio,
// } abortAudio
// }, [isPlaying, sentences, currentIndex, isProcessing]); ]);
const stop = useCallback(() => { const stop = useCallback(() => {
// Cancel any ongoing request // Cancel any ongoing request
@ -388,7 +403,7 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setCurrentIndex(0); setCurrentIndex(0);
setCurrentText(''); setCurrentText('');
setIsProcessing(false); setIsProcessing(false);
}, [activeHowl]); }, [abortAudio]);
const stopAndPlayFromIndex = useCallback((index: number) => { const stopAndPlayFromIndex = useCallback((index: number) => {
abortAudio(); abortAudio();
@ -398,13 +413,13 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
setCurrentIndex(index); setCurrentIndex(index);
setIsPlaying(true); setIsPlaying(true);
}, 50); }, 50);
}, []); }, [abortAudio]);
const setCurrentIndexWithoutPlay = useCallback((index: number) => { const setCurrentIndexWithoutPlay = useCallback((index: number) => {
abortAudio(); abortAudio();
setCurrentIndex(index); setCurrentIndex(index);
}, [activeHowl]); }, [abortAudio]);
const setSpeedAndRestart = useCallback((newSpeed: number) => { const setSpeedAndRestart = useCallback((newSpeed: number) => {
setSpeed(newSpeed); setSpeed(newSpeed);