Document markup and improve choice tags
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
import { BaseModule } from './base-module.js';
|
||||
|
||||
const TTS_GENERATION_TIMEOUT_MS = 60000;
|
||||
|
||||
class SentenceQueueModule extends BaseModule {
|
||||
constructor() {
|
||||
super('sentence-queue', 'Sentence Queue');
|
||||
@@ -22,6 +24,8 @@ class SentenceQueueModule extends BaseModule {
|
||||
this.inputMode = 'text';
|
||||
this.lastContinueAt = 0;
|
||||
this.pauseBeforeNextReason = null;
|
||||
this.ttsGenerationTimeoutMs = TTS_GENERATION_TIMEOUT_MS;
|
||||
this.generationRequests = new Map();
|
||||
|
||||
// Bind methods
|
||||
this.bindMethods([
|
||||
@@ -34,6 +38,11 @@ class SentenceQueueModule extends BaseModule {
|
||||
'getCacheKey',
|
||||
'getPreparedSentence',
|
||||
'prefetchAhead',
|
||||
'prepareSpeechMetadata',
|
||||
'normalizeTtsText',
|
||||
'runTtsPreloadWithTimeout',
|
||||
'cancelBlockingGeneration',
|
||||
'cancelGenerationRequests',
|
||||
'isSpeechItem',
|
||||
'getMediaPauseSeconds',
|
||||
'readFirstFiniteNumber',
|
||||
@@ -86,6 +95,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
this.addEventListener(document, 'ui:command', (event) => {
|
||||
if (event.detail?.type === 'continue') {
|
||||
this.lastContinueAt = performance.now();
|
||||
this.cancelBlockingGeneration('user-fast-forward');
|
||||
}
|
||||
});
|
||||
return true;
|
||||
@@ -200,13 +210,21 @@ class SentenceQueueModule extends BaseModule {
|
||||
* @param {string} text - Text to prepare speech for
|
||||
* @returns {Promise<Object>} - Speech metadata object
|
||||
*/
|
||||
async prepareSpeechMetadata(text) {
|
||||
async prepareSpeechMetadata(text, context = {}) {
|
||||
const ttsFactory = this.getModule('tts-factory');
|
||||
|
||||
if (!ttsFactory) {
|
||||
throw new Error("TTS dependencies not found");
|
||||
}
|
||||
|
||||
const ttsText = this.normalizeTtsText(text);
|
||||
if (!ttsText) {
|
||||
console.warn('SentenceQueue: Empty TTS text after normalization, using estimated silent timing', {
|
||||
sentenceId: context.sentenceId || null
|
||||
});
|
||||
return this.estimateSpeechDuration(text);
|
||||
}
|
||||
|
||||
// Check if TTS is enabled via active handler
|
||||
const activeHandler = ttsFactory.getActiveHandler();
|
||||
const isTtsEnabled = activeHandler !== null;
|
||||
@@ -218,20 +236,28 @@ class SentenceQueueModule extends BaseModule {
|
||||
|
||||
try {
|
||||
// Preload the speech to get metadata
|
||||
const result = await ttsFactory.preloadSpeech(text);
|
||||
const result = await this.runTtsPreloadWithTimeout(ttsFactory, ttsText, context);
|
||||
|
||||
if (!result.success) {
|
||||
console.warn("SentenceQueue: Speech preload failed, using estimated duration");
|
||||
console.warn("SentenceQueue: Speech preload failed, using estimated duration", {
|
||||
reason: result.reason || 'unknown',
|
||||
sentenceId: context.sentenceId || null,
|
||||
textPreview: ttsText.slice(0, 80)
|
||||
});
|
||||
return this.estimateSpeechDuration(text);
|
||||
}
|
||||
|
||||
// Create a speech metadata object
|
||||
return {
|
||||
text: text,
|
||||
text: ttsText,
|
||||
duration: result.duration || this.estimateSpeechDuration(text).duration,
|
||||
handler: ttsFactory.getActiveHandler() ? ttsFactory.getActiveHandler().id : null,
|
||||
audioData: result.audioData || null,
|
||||
play: async () => {
|
||||
return ttsFactory.speak(text);
|
||||
if (result.audioData && typeof ttsFactory.speakPreloaded === 'function') {
|
||||
return ttsFactory.speakPreloaded(result);
|
||||
}
|
||||
return ttsFactory.speak(ttsText);
|
||||
},
|
||||
stop: () => {
|
||||
return ttsFactory.stop();
|
||||
@@ -243,6 +269,94 @@ class SentenceQueueModule extends BaseModule {
|
||||
return this.estimateSpeechDuration(text);
|
||||
}
|
||||
}
|
||||
|
||||
normalizeTtsText(text) {
|
||||
return String(text || '')
|
||||
.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
runTtsPreloadWithTimeout(ttsFactory, text, context = {}) {
|
||||
const sentenceId = context.sentenceId || context.id || `tts-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const requestId = `${sentenceId}:${context.prefetch ? 'prefetch' : 'blocking'}:${Date.now()}`;
|
||||
const controller = new AbortController();
|
||||
const startedAt = performance.now();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (result) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
this.generationRequests.delete(requestId);
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn('SentenceQueue: TTS generation timed out; continuing without audio', {
|
||||
sentenceId,
|
||||
timeoutMs: this.ttsGenerationTimeoutMs,
|
||||
textPreview: text.slice(0, 120)
|
||||
});
|
||||
controller.abort('tts-generation-timeout');
|
||||
finish({ success: false, reason: 'tts_generation_timeout', timedOut: true });
|
||||
}, this.ttsGenerationTimeoutMs);
|
||||
|
||||
this.generationRequests.set(requestId, {
|
||||
controller,
|
||||
sentenceId,
|
||||
blocking: context.blocking !== false,
|
||||
startedAt,
|
||||
textPreview: text.slice(0, 120),
|
||||
finish
|
||||
});
|
||||
|
||||
Promise.resolve(ttsFactory.preloadSpeech(text, { signal: controller.signal }))
|
||||
.then(result => finish(result || { success: false, reason: 'empty_tts_result' }))
|
||||
.catch(error => {
|
||||
if (controller.signal.aborted) {
|
||||
console.warn('SentenceQueue: TTS generation cancelled; continuing without audio', {
|
||||
sentenceId,
|
||||
reason: controller.signal.reason || 'aborted',
|
||||
elapsedMs: Math.round(performance.now() - startedAt)
|
||||
});
|
||||
finish({ success: false, reason: 'tts_generation_aborted', error });
|
||||
} else {
|
||||
console.warn('SentenceQueue: TTS generation failed; continuing without audio', {
|
||||
sentenceId,
|
||||
error
|
||||
});
|
||||
finish({ success: false, reason: 'tts_generation_error', error });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cancelBlockingGeneration(reason = 'cancelled') {
|
||||
this.cancelGenerationRequests(reason, request => request.blocking === true);
|
||||
}
|
||||
|
||||
cancelGenerationRequests(reason = 'cancelled', predicate = () => true) {
|
||||
for (const [requestId, request] of this.generationRequests.entries()) {
|
||||
if (!predicate(request)) continue;
|
||||
console.warn('SentenceQueue: Cancelling TTS generation request', {
|
||||
requestId,
|
||||
sentenceId: request.sentenceId,
|
||||
reason,
|
||||
elapsedMs: Math.round(performance.now() - request.startedAt),
|
||||
textPreview: request.textPreview
|
||||
});
|
||||
try {
|
||||
request.controller.abort(reason);
|
||||
} catch (error) {
|
||||
console.warn('SentenceQueue: Failed to abort TTS generation request', { requestId, error });
|
||||
}
|
||||
if (typeof request.finish === 'function') {
|
||||
request.finish({ success: false, reason: 'tts_generation_cancelled' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate speech duration based on character count
|
||||
@@ -314,7 +428,12 @@ class SentenceQueueModule extends BaseModule {
|
||||
await audioManager.preloadMediaCues(metadata.cueMarkers || []);
|
||||
}
|
||||
|
||||
const ttsData = await this.prepareSpeechMetadata(text);
|
||||
const ttsData = await this.prepareSpeechMetadata(text, {
|
||||
sentenceId: id,
|
||||
blockId: metadata.blockId ?? null,
|
||||
turnId: metadata.turnId ?? null,
|
||||
blocking: true
|
||||
});
|
||||
|
||||
console.log(`SentenceQueue: Prepared speech "${text.substring(0, 50)}..." - TTS duration: ${ttsData.duration}ms`);
|
||||
|
||||
@@ -557,7 +676,14 @@ class SentenceQueueModule extends BaseModule {
|
||||
console.log(`Process state: ${state}`, { reason: 'prefetch-start', sentenceId: nextItem.id, queueIndex: index });
|
||||
|
||||
const promise = (this.isSpeechItem(nextItem)
|
||||
? this.prepareSpeechMetadata(nextItem.text || '')
|
||||
? this.prepareSpeechMetadata(nextItem.text || '', {
|
||||
sentenceId: nextItem.id,
|
||||
blockId: nextItem.blockId ?? null,
|
||||
turnId: nextItem.turnId ?? null,
|
||||
queueIndex: index,
|
||||
prefetch: true,
|
||||
blocking: false
|
||||
})
|
||||
: Promise.resolve(null))
|
||||
.then(() => {
|
||||
console.log('SentenceQueue: Prefetched queued speech/media', { sentenceId: nextItem.id, queueIndex: index });
|
||||
@@ -781,6 +907,7 @@ class SentenceQueueModule extends BaseModule {
|
||||
clear() {
|
||||
this.sentenceQueue = [];
|
||||
this.isProcessing = false;
|
||||
this.cancelGenerationRequests('sentence-queue-cleared');
|
||||
this.prefetchingSpeech.clear();
|
||||
document.dispatchEvent(new CustomEvent('tts:queue-empty', {
|
||||
detail: { reason: 'sentence-queue-cleared' }
|
||||
|
||||
Reference in New Issue
Block a user