@@ -89,9 +89,9 @@ const TTSContext = createContext<TTSContextType | undefined>(undefined);
8989 */
9090export function TTSProvider ( { children } : { children : ReactNode } ) {
9191 // Configuration context consumption
92- const {
93- apiKey : openApiKey ,
94- baseUrl : openApiBaseUrl ,
92+ const {
93+ apiKey : openApiKey ,
94+ baseUrl : openApiBaseUrl ,
9595 isLoading : configIsLoading ,
9696 voiceSpeed,
9797 voice : configVoice ,
@@ -139,6 +139,8 @@ export function TTSProvider({ children }: { children: ReactNode }) {
139139
140140 // Track pending preload requests
141141 const preloadRequests = useRef < Map < string , Promise < string > > > ( new Map ( ) ) ;
142+ // Track active abort controllers for TTS requests
143+ const activeAbortControllers = useRef < Set < AbortController > > ( new Set ( ) ) ;
142144
143145 //console.log('page:', currDocPage, 'pages:', currDocPages);
144146
@@ -177,7 +179,15 @@ export function TTSProvider({ children }: { children: ReactNode }) {
177179 activeHowl . unload ( ) ; // Ensure Howl instance is fully cleaned up
178180 setActiveHowl ( null ) ;
179181 }
182+
180183 if ( clearPending ) {
184+ // Abort all active TTS requests
185+ console . log ( 'Aborting active TTS requests' ) ;
186+ activeAbortControllers . current . forEach ( controller => {
187+ controller . abort ( ) ;
188+ } ) ;
189+ activeAbortControllers . current . clear ( ) ;
190+ // Clear any pending preload requests
181191 preloadRequests . current . clear ( ) ;
182192 }
183193 } , [ activeHowl ] ) ;
@@ -189,13 +199,13 @@ export function TTSProvider({ children }: { children: ReactNode }) {
189199 * @param {string | number } location - The target location to navigate to
190200 * @param {boolean } keepPlaying - Whether to maintain playback state
191201 */
192- const skipToLocation = useCallback ( ( location : string | number ) => {
202+ const skipToLocation = useCallback ( ( location : string | number ) => {
193203 // Reset state for new content in correct order
194204 abortAudio ( ) ;
195205 setCurrentIndex ( 0 ) ;
196206 setSentences ( [ ] ) ;
197207 setCurrDocPage ( location ) ;
198-
208+
199209 } , [ abortAudio ] ) ;
200210
201211 /**
@@ -205,29 +215,29 @@ export function TTSProvider({ children }: { children: ReactNode }) {
205215 */
206216 const advance = useCallback ( async ( backwards = false ) => {
207217 const nextIndex = currentIndex + ( backwards ? - 1 : 1 ) ;
208-
218+
209219 // Handle within current page bounds
210220 if ( nextIndex < sentences . length && nextIndex >= 0 ) {
211221 setCurrentIndex ( nextIndex ) ;
212222 return ;
213223 }
214-
224+
215225 // For EPUB documents, always try to advance to next/prev section
216226 if ( isEPUB && locationChangeHandlerRef . current ) {
217227 locationChangeHandlerRef . current ( nextIndex >= sentences . length ? 'next' : 'prev' ) ;
218228 return ;
219229 }
220-
230+
221231 // For PDFs and other documents, check page bounds
222232 if ( ! isEPUB ) {
223233 // Handle next/previous page transitions
224- if ( ( nextIndex >= sentences . length && currDocPageNumber < currDocPages ! ) ||
225- ( nextIndex < 0 && currDocPageNumber > 1 ) ) {
234+ if ( ( nextIndex >= sentences . length && currDocPageNumber < currDocPages ! ) ||
235+ ( nextIndex < 0 && currDocPageNumber > 1 ) ) {
226236 // Pass wasPlaying to maintain playback state during page turn
227237 skipToLocation ( currDocPageNumber + ( nextIndex >= sentences . length ? 1 : - 1 ) ) ;
228238 return ;
229239 }
230-
240+
231241 // Handle end of document (PDF only)
232242 if ( nextIndex >= sentences . length && currDocPageNumber >= currDocPages ! ) {
233243 setIsPlaying ( false ) ;
@@ -248,7 +258,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
248258
249259 // Use advance to handle navigation for both EPUB and PDF
250260 advance ( ) ;
251-
261+
252262 toast . success ( isEPUB ? 'Skipping blank section' : `Skipping blank page ${ currDocPageNumber } ` , {
253263 id : isEPUB ? `epub-section-skip` : `page-${ currDocPageNumber } ` ,
254264 iconTheme : {
@@ -274,13 +284,13 @@ export function TTSProvider({ children }: { children: ReactNode }) {
274284 const setText = useCallback ( ( text : string , shouldPause = false ) => {
275285 // Check for blank section first
276286 if ( handleBlankSection ( text ) ) return ;
277-
287+
278288 // Keep track of previous state and pause playback
279289 const wasPlaying = isPlaying ;
280290 setIsPlaying ( false ) ;
281291 abortAudio ( true ) ; // Clear pending requests since text is changing
282292 setIsProcessing ( true ) ; // Set processing state before text processing starts
283-
293+
284294 console . log ( 'Setting text:' , text ) ;
285295 processTextToSentences ( text )
286296 . then ( newSentences => {
@@ -396,6 +406,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
396406 try {
397407 console . log ( 'Requesting audio for sentence:' , sentence ) ;
398408
409+ // Create an AbortController for this request
410+ const controller = new AbortController ( ) ;
411+ activeAbortControllers . current . add ( controller ) ;
412+
399413 const response = await fetch ( '/api/tts' , {
400414 method : 'POST' ,
401415 headers : {
@@ -408,8 +422,12 @@ export function TTSProvider({ children }: { children: ReactNode }) {
408422 voice : voice ,
409423 speed : speed ,
410424 } ) ,
425+ signal : controller . signal ,
411426 } ) ;
412427
428+ // Remove the controller once the request is complete
429+ activeAbortControllers . current . delete ( controller ) ;
430+
413431 if ( ! response . ok ) {
414432 throw new Error ( 'Failed to generate audio' ) ;
415433 }
@@ -422,6 +440,12 @@ export function TTSProvider({ children }: { children: ReactNode }) {
422440
423441 return arrayBuffer ;
424442 } catch ( error ) {
443+ // Check if this was an abort error
444+ if ( error instanceof Error && error . name === 'AbortError' ) {
445+ console . log ( 'TTS request aborted:' , sentence . substring ( 0 , 20 ) ) ;
446+ return ;
447+ }
448+
425449 setIsPlaying ( false ) ;
426450 toast . error ( 'Failed to generate audio. Server not responding.' , {
427451 id : 'tts-api-error' ,
@@ -444,7 +468,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
444468 */
445469 const processSentence = useCallback ( async ( sentence : string , preload = false ) : Promise < string > => {
446470 if ( ! audioContext ) throw new Error ( 'Audio context not initialized' ) ;
447-
471+
448472 // Check if there's a pending preload request for this sentence
449473 const pendingRequest = preloadRequests . current . get ( sentence ) ;
450474 if ( pendingRequest ) {
@@ -459,7 +483,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
459483
460484 // Only set processing state if not preloading
461485 if ( ! preload ) setIsProcessing ( true ) ;
462-
486+
463487 // Create the audio processing promise
464488 const processPromise = ( async ( ) => {
465489 try {
@@ -500,7 +524,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
500524 if ( ! audioUrl ) {
501525 throw new Error ( 'No audio URL generated' ) ;
502526 }
503-
527+
504528 // Force unload any previous Howl instance to free up resources
505529 if ( activeHowl ) {
506530 activeHowl . unload ( ) ;
@@ -532,7 +556,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
532556 }
533557 } ,
534558 onloaderror : ( id , error ) => {
535- console . error ( 'Error loading audio:' , error ) ;
559+ console . warn ( 'Error loading audio:' , error ) ;
536560 setIsProcessing ( false ) ;
537561 setActiveHowl ( null ) ;
538562 URL . revokeObjectURL ( audioUrl ) ;
@@ -546,15 +570,15 @@ export function TTSProvider({ children }: { children: ReactNode }) {
546570 howl . unload ( ) ; // Ensure cleanup on stop
547571 }
548572 } ) ;
549-
573+
550574 setActiveHowl ( howl ) ;
551575 howl . play ( ) ;
552-
576+
553577 } catch ( error ) {
554578 console . error ( 'Error playing TTS:' , error ) ;
555579 setActiveHowl ( null ) ;
556580 setIsProcessing ( false ) ;
557-
581+
558582 toast . error ( 'Failed to process audio. Skipping problematic sentence.' , {
559583 id : 'tts-processing-error' ,
560584 style : {
@@ -563,7 +587,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
563587 } ,
564588 duration : 3000 ,
565589 } ) ;
566-
590+
567591 advance ( ) ; // Skip problematic sentence
568592 }
569593 } , [ isPlaying , processSentence , advance , activeHowl ] ) ;
@@ -604,10 +628,10 @@ export function TTSProvider({ children }: { children: ReactNode }) {
604628
605629 // Start playing current sentence
606630 playAudio ( ) ;
607-
631+
608632 // Start preloading next sentence in parallel
609633 preloadNextAudio ( ) ;
610-
634+
611635 return ( ) => {
612636 // Only abort if we're actually stopping playback
613637 if ( ! isPlaying ) {
@@ -648,7 +672,7 @@ export function TTSProvider({ children }: { children: ReactNode }) {
648672 */
649673 const stopAndPlayFromIndex = useCallback ( ( index : number ) => {
650674 abortAudio ( ) ;
651-
675+
652676 setCurrentIndex ( index ) ;
653677 setIsPlaying ( true ) ;
654678 } , [ abortAudio ] ) ;
@@ -660,19 +684,19 @@ export function TTSProvider({ children }: { children: ReactNode }) {
660684 */
661685 const setSpeedAndRestart = useCallback ( ( newSpeed : number ) => {
662686 const wasPlaying = isPlaying ;
663-
687+
664688 // Set a flag to prevent double audio requests during config update
665689 setIsProcessing ( true ) ;
666-
690+
667691 // First stop any current playback
668692 setIsPlaying ( false ) ;
669693 abortAudio ( true ) ; // Clear pending requests since speed changed
670694 setActiveHowl ( null ) ;
671-
695+
672696 // Update speed, clear cache, and config
673697 setSpeed ( newSpeed ) ;
674698 audioCache . clear ( ) ;
675-
699+
676700 // Update config after state changes
677701 updateConfigKey ( 'voiceSpeed' , newSpeed ) . then ( ( ) => {
678702 setIsProcessing ( false ) ;
@@ -690,19 +714,19 @@ export function TTSProvider({ children }: { children: ReactNode }) {
690714 */
691715 const setVoiceAndRestart = useCallback ( ( newVoice : string ) => {
692716 const wasPlaying = isPlaying ;
693-
717+
694718 // Set a flag to prevent double audio requests during config update
695719 setIsProcessing ( true ) ;
696-
720+
697721 // First stop any current playback
698722 setIsPlaying ( false ) ;
699723 abortAudio ( true ) ; // Clear pending requests since voice changed
700724 setActiveHowl ( null ) ;
701-
725+
702726 // Update voice, clear cache, and config
703727 setVoice ( newVoice ) ;
704728 audioCache . clear ( ) ;
705-
729+
706730 // Update config after state changes
707731 updateConfigKey ( 'voice' , newVoice ) . then ( ( ) => {
708732 setIsProcessing ( false ) ;
0 commit comments