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';
|
'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">
|
||||||
|
|
|
||||||
|
|
@ -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,19 +266,21 @@ 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) {
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
|
@ -285,13 +290,18 @@ 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({
|
||||||
src: [audioUrl],
|
src: [audioUrl],
|
||||||
format: ['wav'],
|
format: ['wav'],
|
||||||
|
|
@ -309,27 +319,29 @@ 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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setActiveHowl(howl);
|
setActiveHowl(howl);
|
||||||
howl.play();
|
howl.play();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue