Checkpoint Eibenreith ink architecture
This commit is contained in:
@@ -30,6 +30,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
this.assetPreloadTimeoutMs = ASSET_PRELOAD_TIMEOUT_MS;
|
||||
this.generationRequests = new Map();
|
||||
this.assetPreloadRequests = new Map();
|
||||
this.queueGeneration = 0;
|
||||
|
||||
// Bind methods
|
||||
this.bindMethods([
|
||||
@@ -64,6 +65,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
'getDropCapText',
|
||||
'extractDropCapText',
|
||||
'calculateAnimationTiming',
|
||||
'isCurrentQueueItem',
|
||||
'clear'
|
||||
]);
|
||||
}
|
||||
@@ -170,12 +172,14 @@ class SentenceQueueModule extends BaseModule {
|
||||
|
||||
this.isProcessing = true;
|
||||
const item = this.sentenceQueue[0];
|
||||
const queueGeneration = this.queueGeneration;
|
||||
|
||||
try {
|
||||
if (this.pauseBeforeNextReason) {
|
||||
const reason = this.pauseBeforeNextReason;
|
||||
this.pauseBeforeNextReason = null;
|
||||
await this.waitForManualContinue(reason);
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
}
|
||||
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
@@ -189,10 +193,12 @@ class SentenceQueueModule extends BaseModule {
|
||||
}));
|
||||
|
||||
const sentence = await this.getPreparedSentence(item);
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
|
||||
// Prefetch far enough ahead that media pauses do not block TTS
|
||||
// generation for the next spoken paragraph.
|
||||
this.prefetchAhead();
|
||||
this.prefetchAhead(4, queueGeneration);
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
|
||||
// Notify display handler with complete sentence
|
||||
if (this.onSentenceReadyCallback) {
|
||||
@@ -201,22 +207,28 @@ class SentenceQueueModule extends BaseModule {
|
||||
sentence.playbackStartedAt = performance.now();
|
||||
this.onSentenceReadyCallback(sentence, resolve);
|
||||
});
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
}
|
||||
|
||||
const mediaPauseSeconds = this.getMediaPauseSeconds(sentence);
|
||||
if (mediaPauseSeconds > 0) {
|
||||
await this.waitForSkippableMediaPause(mediaPauseSeconds, sentence.kind, sentence.id);
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
}
|
||||
|
||||
if (this.shouldPauseAfterSentence(sentence)) {
|
||||
await this.waitForManualContinue(sentence.id);
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
}
|
||||
|
||||
// Remove from queue and continue
|
||||
this.sentenceQueue.shift();
|
||||
if (this.sentenceQueue[0] === item) {
|
||||
this.sentenceQueue.shift();
|
||||
}
|
||||
if (item.callback) item.callback({ success: true });
|
||||
|
||||
} catch (error) {
|
||||
if (!this.isCurrentQueueItem(item, queueGeneration)) return;
|
||||
console.error("SentenceQueue: Error processing sentence:", error);
|
||||
const failedItem = this.sentenceQueue.shift();
|
||||
console.warn('SentenceQueue: Dropped failed queue item so playback can continue', {
|
||||
@@ -843,7 +855,11 @@ class SentenceQueueModule extends BaseModule {
|
||||
return this.prepareSentence(item);
|
||||
}
|
||||
|
||||
prefetchAhead(maxLookahead = 4) {
|
||||
isCurrentQueueItem(item, queueGeneration = this.queueGeneration) {
|
||||
return queueGeneration === this.queueGeneration && this.sentenceQueue[0] === item;
|
||||
}
|
||||
|
||||
prefetchAhead(maxLookahead = 4, queueGeneration = this.queueGeneration) {
|
||||
if (this.sentenceQueue.length <= 1) {
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'playing-ready', reason: 'no-prefetch-needed', sentenceId: this.sentenceQueue[0]?.id }
|
||||
@@ -871,11 +887,13 @@ class SentenceQueueModule extends BaseModule {
|
||||
console.log(`Process state: ${state}`, { reason: 'prefetch-start', sentenceId: nextItem.id, queueIndex: index });
|
||||
|
||||
const promise = (async () => {
|
||||
if (queueGeneration !== this.queueGeneration) return null;
|
||||
await this.preloadAssetsForItem(nextItem, {
|
||||
sentenceId: nextItem.id,
|
||||
blocking: false,
|
||||
prefetch: true
|
||||
});
|
||||
if (queueGeneration !== this.queueGeneration) return null;
|
||||
|
||||
if (!this.isSpeechItem(nextItem)) {
|
||||
return null;
|
||||
@@ -892,6 +910,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
});
|
||||
})()
|
||||
.then(() => {
|
||||
if (queueGeneration !== this.queueGeneration) return false;
|
||||
console.log('SentenceQueue: Prefetched queued speech/media', { sentenceId: nextItem.id, queueIndex: index });
|
||||
document.dispatchEvent(new CustomEvent('story:process-state', {
|
||||
detail: { state: 'playing-ready', reason: 'prefetch-complete', sentenceId: nextItem.id, queueIndex: index }
|
||||
@@ -1316,10 +1335,13 @@ class SentenceQueueModule extends BaseModule {
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.queueGeneration += 1;
|
||||
this.sentenceQueue = [];
|
||||
this.isProcessing = false;
|
||||
this.cancelGenerationRequests('sentence-queue-cleared');
|
||||
this.cancelAssetPreloads('sentence-queue-cleared');
|
||||
this.prefetchingSpeech.clear();
|
||||
this.pauseBeforeNextReason = null;
|
||||
document.dispatchEvent(new CustomEvent('tts:queue-empty', {
|
||||
detail: { reason: 'sentence-queue-cleared' }
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user