feat: Implement sequential lookahead preloading for multiple upcoming TTS sentences.
This commit is contained in:
parent
6e442e8dba
commit
49c5b794ea
1 changed files with 64 additions and 36 deletions
|
|
@ -1519,45 +1519,73 @@ 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];
|
|
||||||
if (nextSentence) {
|
|
||||||
const nextKey = buildCacheKey(
|
|
||||||
nextSentence,
|
|
||||||
voice,
|
|
||||||
speed,
|
|
||||||
configTTSProvider,
|
|
||||||
ttsModel,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!audioCache.has(nextKey) && !preloadRequests.current.has(nextSentence)) {
|
const preloadFromOffset = (offset: number) => {
|
||||||
// Start preloading but don't wait for it to complete
|
if (offset > PRELOAD_LOOKAHEAD) return;
|
||||||
processSentence(nextSentence, true).catch(error => {
|
|
||||||
const status = (() => {
|
const sentenceIndex = currentIndex + offset;
|
||||||
if (typeof error === 'object' && error !== null && 'status' in error) {
|
const nextSentence = sentences[sentenceIndex];
|
||||||
const maybe = (error as { status?: unknown }).status;
|
if (!nextSentence) return;
|
||||||
return typeof maybe === 'number' ? maybe : undefined;
|
|
||||||
}
|
const nextKey = buildCacheKey(
|
||||||
return undefined;
|
nextSentence,
|
||||||
})();
|
voice,
|
||||||
const code = (() => {
|
speed,
|
||||||
if (typeof error === 'object' && error !== null && 'code' in error) {
|
configTTSProvider,
|
||||||
const maybe = (error as { code?: unknown }).code;
|
ttsModel,
|
||||||
return typeof maybe === 'string' ? maybe : undefined;
|
);
|
||||||
}
|
|
||||||
return undefined;
|
if (audioCache.has(nextKey)) {
|
||||||
})();
|
preloadFromOffset(offset + 1);
|
||||||
// Ignore quota errors during preload
|
return;
|
||||||
if (!(status === 429 && code === 'USER_DAILY_QUOTA_EXCEEDED')) {
|
|
||||||
console.error('Error preloading next sentence:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = (() => {
|
||||||
|
if (typeof error === 'object' && error !== null && 'status' in error) {
|
||||||
|
const maybe = (error as { status?: unknown }).status;
|
||||||
|
return typeof maybe === 'number' ? maybe : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
|
const code = (() => {
|
||||||
|
if (typeof error === 'object' && error !== null && 'code' in error) {
|
||||||
|
const maybe = (error as { code?: unknown }).code;
|
||||||
|
return typeof maybe === 'string' ? maybe : undefined;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})();
|
||||||
|
// Ignore quota errors during preload.
|
||||||
|
if (!(status === 429 && code === 'USER_DAILY_QUOTA_EXCEEDED')) {
|
||||||
|
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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue