Contexts refactor + commenting
This commit is contained in:
parent
8f90455862
commit
dd7154cccf
2 changed files with 297 additions and 113 deletions
|
|
@ -1,3 +1,16 @@
|
|||
/**
|
||||
* PDF Context Provider
|
||||
*
|
||||
* This module provides a React context for managing PDF document functionality.
|
||||
* It handles document loading, text extraction, highlighting, and integration with TTS.
|
||||
*
|
||||
* Key features:
|
||||
* - PDF document management (add/remove/load)
|
||||
* - Text extraction and processing
|
||||
* - Text highlighting and navigation
|
||||
* - Document state management
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import {
|
||||
|
|
@ -23,7 +36,12 @@ import { useTTS } from '@/contexts/TTSContext';
|
|||
// Set worker from public directory
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||
|
||||
// Convert PDF data to PDF data URL
|
||||
/**
|
||||
* Converts PDF binary data to a data URL for display
|
||||
*
|
||||
* @param {Blob} pdfData - The PDF binary data
|
||||
* @returns {Promise<string>} A data URL representing the PDF
|
||||
*/
|
||||
const convertPDFDataToURL = (pdfData: Blob): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
|
@ -33,13 +51,27 @@ const convertPDFDataToURL = (pdfData: Blob): Promise<string> => {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface defining all available methods and properties in the PDF context
|
||||
*/
|
||||
interface PDFContextType {
|
||||
// Documents management
|
||||
documents: PDFDocument[];
|
||||
addDocument: (file: File) => Promise<string>;
|
||||
getDocument: (id: string) => Promise<PDFDocument | undefined>;
|
||||
removeDocument: (id: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
extractTextFromPDF: (pdfURL: string, currDocPage: number) => Promise<string>;
|
||||
|
||||
// Current document state
|
||||
currDocURL: string | undefined;
|
||||
currDocName: string | undefined;
|
||||
currDocPages: number | undefined;
|
||||
currDocPage: number;
|
||||
currDocText: string | undefined;
|
||||
setCurrentDocument: (id: string) => Promise<void>;
|
||||
clearCurrDoc: () => void;
|
||||
|
||||
// PDF functionality
|
||||
onDocumentLoadSuccess: ({ numPages }: { numPages: number }) => void;
|
||||
highlightPattern: (text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => void;
|
||||
clearHighlights: () => void;
|
||||
handleTextClick: (
|
||||
|
|
@ -49,19 +81,24 @@ interface PDFContextType {
|
|||
stopAndPlayFromIndex: (index: number) => void,
|
||||
isProcessing: boolean
|
||||
) => void;
|
||||
onDocumentLoadSuccess: ({ numPages }: { numPages: number }) => void;
|
||||
setCurrentDocument: (id: string) => Promise<void>;
|
||||
currDocURL: string | undefined;
|
||||
currDocName: string | undefined;
|
||||
currDocPages: number | undefined;
|
||||
currDocPage: number;
|
||||
currDocText: string | undefined;
|
||||
clearCurrDoc: () => void;
|
||||
}
|
||||
|
||||
// Create the context
|
||||
const PDFContext = createContext<PDFContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* PDFProvider Component
|
||||
*
|
||||
* Main provider component that manages PDF state and functionality.
|
||||
* Handles document loading, text processing, and integration with TTS.
|
||||
*/
|
||||
export function PDFProvider({ children }: { children: ReactNode }) {
|
||||
/**
|
||||
* State Management
|
||||
* - Document management
|
||||
* - Current document state
|
||||
* - Loading states
|
||||
*/
|
||||
const [documents, setDocuments] = useState<PDFDocument[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
|
@ -78,7 +115,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
const [currDocName, setCurrDocName] = useState<string>();
|
||||
const [currDocText, setCurrDocText] = useState<string>();
|
||||
|
||||
// Load documents from IndexedDB once DB is ready
|
||||
/**
|
||||
* Load documents from IndexedDB when the database is ready
|
||||
*/
|
||||
useEffect(() => {
|
||||
const loadDocuments = async () => {
|
||||
if (!isDBReady) return;
|
||||
|
|
@ -96,7 +135,12 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
loadDocuments();
|
||||
}, [isDBReady]);
|
||||
|
||||
// Add a new document to IndexedDB
|
||||
/**
|
||||
* Adds a new document to IndexedDB
|
||||
*
|
||||
* @param {File} file - The PDF file to add
|
||||
* @returns {Promise<string>} The ID of the added document
|
||||
*/
|
||||
const addDocument = useCallback(async (file: File): Promise<string> => {
|
||||
const id = uuidv4();
|
||||
const newDoc: PDFDocument = {
|
||||
|
|
@ -117,17 +161,11 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Get a document by ID from IndexedDB
|
||||
const getDocument = useCallback(async (id: string): Promise<PDFDocument | undefined> => {
|
||||
try {
|
||||
return await indexedDBService.getDocument(id);
|
||||
} catch (error) {
|
||||
console.error('Failed to get document:', error);
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Remove a document by ID from IndexedDB
|
||||
/**
|
||||
* Removes a document from IndexedDB
|
||||
*
|
||||
* @param {string} id - The ID of the document to remove
|
||||
*/
|
||||
const removeDocument = useCallback(async (id: string): Promise<void> => {
|
||||
try {
|
||||
await indexedDBService.removeDocument(id);
|
||||
|
|
@ -138,12 +176,23 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handles successful document load
|
||||
*
|
||||
* @param {Object} param0 - Object containing numPages
|
||||
*/
|
||||
const onDocumentLoadSuccess = useCallback(({ numPages }: { numPages: number }) => {
|
||||
console.log('Document loaded:', numPages);
|
||||
setCurrDocPages(numPages);
|
||||
}, [setCurrDocPages]);
|
||||
|
||||
// Extract text from a PDF file
|
||||
/**
|
||||
* Extracts text content from a specific page of the PDF
|
||||
*
|
||||
* @param {string} pdfURL - The URL of the PDF
|
||||
* @param {number} currDocPage - The page number to extract
|
||||
* @returns {Promise<string>} The extracted text
|
||||
*/
|
||||
const extractTextFromPDF = useCallback(async (pdfURL: string, currDocPage: number): Promise<string> => {
|
||||
try {
|
||||
const base64Data = pdfURL.split(',')[1];
|
||||
|
|
@ -222,7 +271,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Load curr doc text
|
||||
/**
|
||||
* Loads and processes text from the current document page
|
||||
*/
|
||||
const loadCurrDocText = useCallback(async () => {
|
||||
try {
|
||||
if (!currDocURL) return;
|
||||
|
|
@ -234,17 +285,23 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, [currDocURL, currDocPage, extractTextFromPDF, setTTSText]);
|
||||
|
||||
// Update the current document text when the page changes
|
||||
/**
|
||||
* Updates the current document text when the page changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (currDocURL) {
|
||||
loadCurrDocText();
|
||||
}
|
||||
}, [currDocPage, currDocURL, loadCurrDocText]);
|
||||
|
||||
// Set curr document
|
||||
/**
|
||||
* Sets the current document based on its ID
|
||||
*
|
||||
* @param {string} id - The ID of the document to set
|
||||
*/
|
||||
const setCurrentDocument = useCallback(async (id: string): Promise<void> => {
|
||||
try {
|
||||
const doc = await getDocument(id);
|
||||
const doc = await indexedDBService.getDocument(id);
|
||||
if (doc) {
|
||||
const url = await convertPDFDataToURL(doc.data);
|
||||
setCurrDocName(doc.name);
|
||||
|
|
@ -254,8 +311,11 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
} catch (error) {
|
||||
console.error('Failed to get document URL:', error);
|
||||
}
|
||||
}, [getDocument]);
|
||||
}, [indexedDBService]);
|
||||
|
||||
/**
|
||||
* Clears the current document state
|
||||
*/
|
||||
const clearCurrDoc = useCallback(() => {
|
||||
setCurrDocName(undefined);
|
||||
setCurrDocURL(undefined);
|
||||
|
|
@ -267,7 +327,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
}, [setCurrDocPages, setTTSText]);
|
||||
|
||||
// Clear all highlights in the PDF viewer
|
||||
/**
|
||||
* Removes all text highlights from the PDF viewer
|
||||
*/
|
||||
const clearHighlights = useCallback(() => {
|
||||
const textNodes = document.querySelectorAll('.react-pdf__Page__textContent span');
|
||||
textNodes.forEach((node) => {
|
||||
|
|
@ -277,7 +339,13 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
}, []);
|
||||
|
||||
// Find the best text match using string similarity
|
||||
/**
|
||||
* Finds the best matching text segment using string similarity
|
||||
*
|
||||
* @param {Array} elements - Array of elements and their text content
|
||||
* @param {string} targetText - The text to match against
|
||||
* @param {number} maxCombinedLength - Maximum length of combined text to consider
|
||||
*/
|
||||
const findBestTextMatch = useCallback((
|
||||
elements: Array<{ element: HTMLElement; text: string }>,
|
||||
targetText: string,
|
||||
|
|
@ -320,7 +388,14 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
return bestMatch;
|
||||
}, []);
|
||||
|
||||
// Highlight matching text in the PDF viewer with sliding context window
|
||||
/**
|
||||
* Highlights matching text in the PDF viewer
|
||||
* Uses a sliding context window for improved accuracy
|
||||
*
|
||||
* @param {string} text - The document text
|
||||
* @param {string} pattern - The pattern to highlight
|
||||
* @param {RefObject} containerRef - Reference to the container element
|
||||
*/
|
||||
const highlightPattern = useCallback((text: string, pattern: string, containerRef: React.RefObject<HTMLDivElement>) => {
|
||||
clearHighlights();
|
||||
|
||||
|
|
@ -381,10 +456,19 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
}, [clearHighlights, findBestTextMatch]);
|
||||
|
||||
// Handle text click events in the PDF viewer
|
||||
/**
|
||||
* Handles text click events in the PDF viewer
|
||||
* Integrates with TTS for synchronized playback
|
||||
*
|
||||
* @param {MouseEvent} event - The click event
|
||||
* @param {string} pdfText - The text content of the page
|
||||
* @param {RefObject} containerRef - Reference to the container element
|
||||
* @param {Function} stopAndPlayFromIndex - Function to control TTS playback
|
||||
* @param {boolean} isProcessing - Whether TTS is currently processing
|
||||
*/
|
||||
const handleTextClick = useCallback((
|
||||
event: MouseEvent,
|
||||
pageText: string, // Renamed from pdfText to pageText for clarity
|
||||
pdfText: string, // Renamed from pdfText to pageText for clarity
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
stopAndPlayFromIndex: (index: number) => void,
|
||||
isProcessing: boolean
|
||||
|
|
@ -422,7 +506,7 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
if (bestMatch.rating >= similarityThreshold) {
|
||||
const matchText = bestMatch.text;
|
||||
// Use pageText instead of full PDF text for sentence splitting
|
||||
const sentences = nlp(pageText).sentences().out('array') as string[];
|
||||
const sentences = nlp(pdfText).sentences().out('array') as string[];
|
||||
let bestSentenceMatch = { sentence: '', rating: 0 };
|
||||
|
||||
for (const sentence of sentences) {
|
||||
|
|
@ -436,24 +520,19 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
const sentenceIndex = sentences.findIndex((sentence) => sentence === bestSentenceMatch.sentence);
|
||||
if (sentenceIndex !== -1) {
|
||||
stopAndPlayFromIndex(sentenceIndex);
|
||||
highlightPattern(pageText, bestSentenceMatch.sentence, containerRef);
|
||||
highlightPattern(pdfText, bestSentenceMatch.sentence, containerRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [highlightPattern, findBestTextMatch]);
|
||||
|
||||
// Memoize the context value to prevent unnecessary re-renders
|
||||
// Context value memoization
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
documents,
|
||||
addDocument,
|
||||
getDocument,
|
||||
removeDocument,
|
||||
isLoading,
|
||||
extractTextFromPDF,
|
||||
highlightPattern,
|
||||
clearHighlights,
|
||||
handleTextClick,
|
||||
onDocumentLoadSuccess,
|
||||
setCurrentDocument,
|
||||
currDocURL,
|
||||
|
|
@ -462,17 +541,15 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
currDocPage,
|
||||
currDocText,
|
||||
clearCurrDoc,
|
||||
highlightPattern,
|
||||
clearHighlights,
|
||||
handleTextClick,
|
||||
}),
|
||||
[
|
||||
documents,
|
||||
addDocument,
|
||||
getDocument,
|
||||
removeDocument,
|
||||
isLoading,
|
||||
extractTextFromPDF,
|
||||
highlightPattern,
|
||||
clearHighlights,
|
||||
handleTextClick,
|
||||
onDocumentLoadSuccess,
|
||||
setCurrentDocument,
|
||||
currDocURL,
|
||||
|
|
@ -481,6 +558,9 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
currDocPage,
|
||||
currDocText,
|
||||
clearCurrDoc,
|
||||
highlightPattern,
|
||||
clearHighlights,
|
||||
handleTextClick,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
@ -491,6 +571,13 @@ export function PDFProvider({ children }: { children: ReactNode }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to consume the PDF context
|
||||
* Ensures the context is used within a provider
|
||||
*
|
||||
* @throws {Error} If used outside of PDFProvider
|
||||
* @returns {PDFContextType} The PDF context value
|
||||
*/
|
||||
export function usePDF() {
|
||||
const context = useContext(PDFContext);
|
||||
if (context === undefined) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import React, {
|
|||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import OpenAI from 'openai';
|
||||
import { LRUCache } from 'lru-cache'; // Import LRUCache directly
|
||||
|
|
@ -48,33 +49,28 @@ type AudioContextType = typeof window extends undefined
|
|||
* Interface defining all available methods and properties in the TTS context
|
||||
*/
|
||||
interface TTSContextType {
|
||||
// Playback state
|
||||
isPlaying: boolean;
|
||||
currentText: string;
|
||||
isProcessing: boolean;
|
||||
currentSentence: string;
|
||||
|
||||
// Navigation
|
||||
currDocPage: number;
|
||||
currDocPages: number | undefined;
|
||||
|
||||
// Voice settings
|
||||
availableVoices: string[];
|
||||
|
||||
// Control functions
|
||||
togglePlay: () => void;
|
||||
skipForward: () => void;
|
||||
skipBackward: () => void;
|
||||
setText: (text: string) => void;
|
||||
currentSentence: string;
|
||||
audioQueue: AudioBuffer[];
|
||||
stop: () => void;
|
||||
stopAndPlayFromIndex: (index: number) => void;
|
||||
sentences: string[];
|
||||
isProcessing: boolean;
|
||||
setIsProcessing: (value: boolean) => void;
|
||||
setIsPlaying: (value: boolean) => void;
|
||||
speed: number;
|
||||
setSpeed: (speed: number) => void;
|
||||
setSpeedAndRestart: (speed: number) => void;
|
||||
voice: string;
|
||||
setVoice: (voice: string) => void;
|
||||
setVoiceAndRestart: (voice: string) => void;
|
||||
availableVoices: string[];
|
||||
currentIndex: number;
|
||||
setCurrentIndex: (index: number) => void;
|
||||
currDocPage: number;
|
||||
currDocPages: number | undefined;
|
||||
setText: (text: string) => void;
|
||||
setCurrDocPages: (num: number | undefined) => void;
|
||||
incrementPage: (num?: number) => void;
|
||||
setSpeedAndRestart: (speed: number) => void;
|
||||
setVoiceAndRestart: (voice: string) => void;
|
||||
skipToPage: (page: number) => void;
|
||||
}
|
||||
|
||||
|
|
@ -127,14 +123,15 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
const [nextPageLoading, setNextPageLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* Audio Cache
|
||||
* LRU cache to store processed audio buffers and improve performance
|
||||
* Cache for storing audio buffers using LRU (Least Recently Used) strategy
|
||||
*/
|
||||
const audioCacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: 50 }));
|
||||
|
||||
/**
|
||||
* Text Processing Functions
|
||||
* Handle text input and sentence splitting
|
||||
* Sets the current text and splits it into sentences
|
||||
*
|
||||
* @param {string} text - The text to be processed
|
||||
* @returns {void}
|
||||
*/
|
||||
const setText = useCallback((text: string) => {
|
||||
setCurrentText(text);
|
||||
|
|
@ -148,6 +145,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
setNextPageLoading(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Stops the current audio playback and clears the active Howl instance
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const abortAudio = useCallback(() => {
|
||||
if (activeHowl) {
|
||||
activeHowl.stop();
|
||||
|
|
@ -156,8 +158,9 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [activeHowl]);
|
||||
|
||||
/**
|
||||
* Playback Control Functions
|
||||
* Manage audio playback, navigation, and state
|
||||
* Toggles the playback state between playing and paused
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const togglePlay = useCallback(() => {
|
||||
setIsPlaying((prev) => {
|
||||
|
|
@ -170,6 +173,12 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
});
|
||||
}, [abortAudio]);
|
||||
|
||||
/**
|
||||
* Navigates to a specific page in the document
|
||||
*
|
||||
* @param {number} page - The target page number
|
||||
* @returns {void}
|
||||
*/
|
||||
const skipToPage = useCallback((page: number) => {
|
||||
abortAudio();
|
||||
setIsPlaying(false);
|
||||
|
|
@ -178,11 +187,23 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
setCurrDocPage(page);
|
||||
}, [abortAudio]);
|
||||
|
||||
/**
|
||||
* Changes the current page by a specified amount
|
||||
*
|
||||
* @param {number} [num=1] - The number of pages to increment by
|
||||
* @returns {void}
|
||||
*/
|
||||
const incrementPage = useCallback((num = 1) => {
|
||||
setNextPageLoading(true);
|
||||
setCurrDocPage(currDocPage + num);
|
||||
}, [currDocPage]);
|
||||
|
||||
/**
|
||||
* Moves to the next or previous sentence
|
||||
*
|
||||
* @param {boolean} [backwards=false] - Whether to move backwards
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const advance = useCallback(async (backwards = false) => {
|
||||
setCurrentIndex((prev) => {
|
||||
const nextIndex = prev + (backwards ? -1 : 1);
|
||||
|
|
@ -211,6 +232,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [sentences, currDocPage, currDocPages, incrementPage]);
|
||||
|
||||
|
||||
/**
|
||||
* Moves forward one sentence in the text
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const skipForward = useCallback(() => {
|
||||
setIsProcessing(true);
|
||||
|
||||
|
|
@ -221,6 +247,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
setIsProcessing(false);
|
||||
}, [abortAudio, advance]);
|
||||
|
||||
/**
|
||||
* Moves backward one sentence in the text
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const skipBackward = useCallback(() => {
|
||||
setIsProcessing(true);
|
||||
|
||||
|
|
@ -232,10 +263,20 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [abortAudio, advance]);
|
||||
|
||||
/**
|
||||
* Audio Processing Functions
|
||||
* Handle audio generation, caching, and playback
|
||||
* Updates the voice and speed settings from the configuration
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const updateVoiceAndSpeed = useCallback(() => {
|
||||
setVoice(configVoice);
|
||||
setSpeed(voiceSpeed);
|
||||
}, [configVoice, voiceSpeed]);
|
||||
|
||||
/**
|
||||
* Initializes OpenAI configuration and fetches available voices
|
||||
*
|
||||
* This effect runs when the config is loaded or API settings change
|
||||
*/
|
||||
// Initialize OpenAI instance when config loads
|
||||
useEffect(() => {
|
||||
const fetchVoices = async () => {
|
||||
try {
|
||||
|
|
@ -264,19 +305,14 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
dangerouslyAllowBrowser: true,
|
||||
});
|
||||
fetchVoices();
|
||||
updateVoiceAndSpeed();
|
||||
}
|
||||
}, [configIsLoading, openApiKey, openApiBaseUrl]);
|
||||
}, [configIsLoading, openApiKey, openApiBaseUrl, updateVoiceAndSpeed]);
|
||||
|
||||
// Initialize AudioContext
|
||||
/**
|
||||
* Initializes the AudioContext when component mounts
|
||||
*/
|
||||
useEffect(() => {
|
||||
/*
|
||||
* Initializes the AudioContext for text-to-speech playback.
|
||||
* Creates a new AudioContext instance if one doesn't exist.
|
||||
* Only runs on the client side to avoid SSR issues.
|
||||
*
|
||||
* Dependencies:
|
||||
* - audioContext: Re-runs if the audioContext is null or changes
|
||||
*/
|
||||
if (typeof window !== 'undefined' && !audioContext) {
|
||||
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContextClass) {
|
||||
|
|
@ -297,7 +333,9 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [audioContext]);
|
||||
|
||||
// Set up MediaSession API
|
||||
/**
|
||||
* Sets up MediaSession API for media controls
|
||||
*/
|
||||
useEffect(() => {
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
|
|
@ -313,7 +351,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [togglePlay, skipForward, skipBackward]);
|
||||
|
||||
//new function to return audio buffer with caching
|
||||
/**
|
||||
* Generates and plays audio for the current sentence
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const getAudio = useCallback(async (sentence: string): Promise<AudioBuffer | undefined> => {
|
||||
// Check if the audio is already cached
|
||||
const cachedAudio = audioCacheRef.current.get(sentence);
|
||||
|
|
@ -343,6 +385,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [audioContext, voice, speed]);
|
||||
|
||||
/**
|
||||
* Processes and plays the current sentence
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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');
|
||||
|
|
@ -356,6 +403,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
return audioBufferToURL(audioBuffer!);
|
||||
}, [isProcessing, audioContext, getAudio]);
|
||||
|
||||
/**
|
||||
* Plays the current sentence with Howl
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const playSentenceWithHowl = useCallback(async (sentence: string) => {
|
||||
try {
|
||||
const audioUrl = await processSentence(sentence);
|
||||
|
|
@ -408,6 +460,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [isPlaying, processSentence, advance]);
|
||||
|
||||
/**
|
||||
* Preloads the next sentence's audio
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const preloadNextAudio = useCallback(() => {
|
||||
try {
|
||||
if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) {
|
||||
|
|
@ -418,6 +475,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [currentIndex, sentences, audioCacheRef, processSentence]);
|
||||
|
||||
/**
|
||||
* Plays the current sentence's audio
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const playAudio = useCallback(async () => {
|
||||
await playSentenceWithHowl(sentences[currentIndex]);
|
||||
}, [sentences, currentIndex, playSentenceWithHowl]);
|
||||
|
|
@ -454,6 +516,11 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
abortAudio
|
||||
]);
|
||||
|
||||
/**
|
||||
* Stops the current audio playback
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const stop = useCallback(() => {
|
||||
// Cancel any ongoing request
|
||||
abortAudio();
|
||||
|
|
@ -464,6 +531,12 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
setIsProcessing(false);
|
||||
}, [abortAudio]);
|
||||
|
||||
/**
|
||||
* Stops the current audio playback and starts playing from a specified index
|
||||
*
|
||||
* @param {number} index - The index to start playing from
|
||||
* @returns {void}
|
||||
*/
|
||||
const stopAndPlayFromIndex = useCallback((index: number) => {
|
||||
abortAudio();
|
||||
|
||||
|
|
@ -471,12 +544,24 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
setIsPlaying(true);
|
||||
}, [abortAudio]);
|
||||
|
||||
/**
|
||||
* Sets the current index without playing
|
||||
*
|
||||
* @param {number} index - The index to set
|
||||
* @returns {void}
|
||||
*/
|
||||
const setCurrentIndexWithoutPlay = useCallback((index: number) => {
|
||||
abortAudio();
|
||||
|
||||
setCurrentIndex(index);
|
||||
}, [abortAudio]);
|
||||
|
||||
/**
|
||||
* Sets the speed and restarts the playback
|
||||
*
|
||||
* @param {number} newSpeed - The new speed to set
|
||||
* @returns {void}
|
||||
*/
|
||||
const setSpeedAndRestart = useCallback((newSpeed: number) => {
|
||||
setSpeed(newSpeed);
|
||||
updateConfigKey('voiceSpeed', newSpeed);
|
||||
|
|
@ -490,6 +575,12 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}
|
||||
}, [isPlaying, abortAudio, updateConfigKey]);
|
||||
|
||||
/**
|
||||
* Sets the voice and restarts the playback
|
||||
*
|
||||
* @param {string} newVoice - The new voice to set
|
||||
* @returns {void}
|
||||
*/
|
||||
const setVoiceAndRestart = useCallback((newVoice: string) => {
|
||||
setVoice(newVoice);
|
||||
updateConfigKey('voice', newVoice);
|
||||
|
|
@ -504,45 +595,51 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [isPlaying, abortAudio, updateConfigKey]);
|
||||
|
||||
/**
|
||||
* Context Value
|
||||
* Aggregate all functions and state to be provided to consumers
|
||||
* Provides the TTS context value to child components
|
||||
*/
|
||||
const value = {
|
||||
const value = useMemo(() => ({
|
||||
isPlaying,
|
||||
currentText,
|
||||
isProcessing,
|
||||
currentSentence: sentences[currentIndex] || '',
|
||||
currDocPage,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
setText,
|
||||
currentSentence: sentences[currentIndex] || '',
|
||||
audioQueue,
|
||||
stop,
|
||||
setCurrentIndex: setCurrentIndexWithoutPlay,
|
||||
stopAndPlayFromIndex,
|
||||
sentences,
|
||||
isProcessing,
|
||||
setIsProcessing,
|
||||
setIsPlaying,
|
||||
speed,
|
||||
setSpeed,
|
||||
setText,
|
||||
setCurrDocPages,
|
||||
setSpeedAndRestart,
|
||||
voice,
|
||||
setVoice,
|
||||
setVoiceAndRestart,
|
||||
availableVoices,
|
||||
skipToPage,
|
||||
}), [
|
||||
isPlaying,
|
||||
isProcessing,
|
||||
sentences,
|
||||
currentIndex,
|
||||
currDocPage,
|
||||
currDocPages,
|
||||
availableVoices,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
stop,
|
||||
stopAndPlayFromIndex,
|
||||
setText,
|
||||
setCurrDocPages,
|
||||
incrementPage,
|
||||
setSpeedAndRestart,
|
||||
setVoiceAndRestart,
|
||||
skipToPage,
|
||||
};
|
||||
|
||||
// Render provider with value
|
||||
if (configIsLoading) {
|
||||
return null;
|
||||
}
|
||||
]);
|
||||
|
||||
/**
|
||||
* Renders the TTS context provider with its children
|
||||
*
|
||||
* @param {ReactNode} children - Child components to be wrapped
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
return (
|
||||
<TTSContext.Provider value={value}>
|
||||
{children}
|
||||
|
|
|
|||
Loading…
Reference in a new issue