feat: Implement sequential lookahead preloading for multiple upcoming TTS sentences.

This commit is contained in:
Richard R 2026-02-22 03:31:03 -07:00
parent 6e442e8dba
commit 49c5b794ea

View file

@ -1519,13 +1519,20 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
}, [activeHowl, isPlaying, currentSentenceAlignment]); }, [activeHowl, isPlaying, currentSentenceAlignment]);
/** /**
* Preloads the next sentence's audio * Preloads upcoming sentences sequentially.
* As soon as one preload finishes, the next one starts (up to lookahead window).
*/ */
const preloadNextAudio = useCallback(async () => { const preloadNextAudio = useCallback(() => {
const PRELOAD_LOOKAHEAD = 3;
if (isAtLimit) return; if (isAtLimit) return;
try {
const nextSentence = sentences[currentIndex + 1]; const preloadFromOffset = (offset: number) => {
if (nextSentence) { if (offset > PRELOAD_LOOKAHEAD) return;
const sentenceIndex = currentIndex + offset;
const nextSentence = sentences[sentenceIndex];
if (!nextSentence) return;
const nextKey = buildCacheKey( const nextKey = buildCacheKey(
nextSentence, nextSentence,
voice, voice,
@ -1534,9 +1541,25 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
ttsModel, ttsModel,
); );
if (!audioCache.has(nextKey) && !preloadRequests.current.has(nextSentence)) { if (audioCache.has(nextKey)) {
// Start preloading but don't wait for it to complete preloadFromOffset(offset + 1);
processSentence(nextSentence, true).catch(error => { return;
}
const pending = preloadRequests.current.get(nextSentence);
if (pending) {
void pending
.finally(() => {
preloadFromOffset(offset + 1);
})
.catch(() => {
// Prevent unhandled rejections from the chained preload progression.
});
return;
}
void processSentence(nextSentence, true)
.catch((error) => {
const status = (() => { const status = (() => {
if (typeof error === 'object' && error !== null && 'status' in error) { if (typeof error === 'object' && error !== null && 'status' in error) {
const maybe = (error as { status?: unknown }).status; const maybe = (error as { status?: unknown }).status;
@ -1551,13 +1574,18 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
} }
return undefined; return undefined;
})(); })();
// Ignore quota errors during preload // Ignore quota errors during preload.
if (!(status === 429 && code === 'USER_DAILY_QUOTA_EXCEEDED')) { if (!(status === 429 && code === 'USER_DAILY_QUOTA_EXCEEDED')) {
console.error('Error preloading next sentence:', error); console.error(`Error preloading sentence at offset ${offset}:`, error);
} }
})
.finally(() => {
preloadFromOffset(offset + 1);
}); });
} };
}
try {
preloadFromOffset(1);
} catch (error) { } catch (error) {
console.error('Error initiating preload:', error); console.error('Error initiating preload:', error);
} }
@ -1576,7 +1604,7 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
// Start playing current sentence // Start playing current sentence
playAudio(); playAudio();
// Start preloading next sentence in parallel // Start background lookahead preloading for upcoming sentences.
preloadNextAudio(); preloadNextAudio();
return () => { return () => {