Still refactoring
This commit is contained in:
parent
072d11a60b
commit
f585819881
2 changed files with 99 additions and 89 deletions
|
|
@ -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 (
|
||||
<div ref={containerRef} className="flex flex-col items-center overflow-auto max-h-[calc(100vh-100px)] w-full px-6">
|
||||
|
|
|
|||
|
|
@ -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<string[]>([]);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [audioContext, setAudioContext] = useState<AudioContextType>(null);
|
||||
const [audioContext, setAudioContext] = useState<AudioContextType>();
|
||||
const currentRequestRef = useRef<AbortController | null>(null);
|
||||
const [activeHowl, setActiveHowl] = useState<Howl | null>(null);
|
||||
const [audioQueue] = useState<AudioBuffer[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const isPausingRef = useRef(false);
|
||||
const [speed, setSpeed] = useState(1);
|
||||
const [voice, setVoice] = useState('alloy');
|
||||
const [availableVoices, setAvailableVoices] = useState<string[]>([]);
|
||||
|
|
@ -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<AudioBuffer | undefined> => {
|
||||
|
|
@ -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<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');
|
||||
|
||||
// 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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue