Update TTS providers and story markup

This commit is contained in:
2026-05-20 22:13:31 +02:00
parent b911c40d89
commit 8258ea2321
36 changed files with 1482 additions and 197 deletions
+46 -8
View File
@@ -45,6 +45,8 @@ class SentenceQueueModule extends BaseModule {
'prepareSpeechMetadata',
'preloadAssetsForItem',
'normalizeTtsText',
'getConfiguredTtsGenerationTimeoutMs',
'normalizeTtsGenerationTimeoutMs',
'runTtsPreloadWithTimeout',
'cancelBlockingGeneration',
'cancelGenerationRequests',
@@ -89,19 +91,25 @@ class SentenceQueueModule extends BaseModule {
const persistenceManager = this.getModule('persistence-manager');
if (persistenceManager && typeof persistenceManager.getPreference === 'function') {
this.autoplay = persistenceManager.getPreference('app', 'autoplay', true) !== false;
this.ttsGenerationTimeoutMs = this.getConfiguredTtsGenerationTimeoutMs();
}
this.addEventListener(document, 'preference-updated', (event) => {
const { category, key, value } = event.detail || {};
if (category === 'app' && key === 'autoplay') {
this.autoplay = value !== false;
}
if (category === 'tts' && (key === 'preferred_handler' || key.endsWith('_timeout_ms'))) {
this.ttsGenerationTimeoutMs = this.getConfiguredTtsGenerationTimeoutMs();
}
});
this.addEventListener(document, 'story:input-mode', (event) => {
this.inputMode = ['text', 'choice', 'end'].includes(event.detail) ? event.detail : 'text';
});
this.addEventListener(document, 'ui:command', (event) => {
if (event.detail?.type === 'continue') {
this.lastContinueAt = performance.now();
if (event.detail?.source !== 'display-clear') {
this.lastContinueAt = performance.now();
}
this.cancelBlockingGeneration('user-fast-forward', {
minWaitMs: USER_CANCEL_BLOCKING_WAIT_MIN_MS
});
@@ -305,11 +313,35 @@ class SentenceQueueModule extends BaseModule {
.trim();
}
getConfiguredTtsGenerationTimeoutMs() {
const persistenceManager = this.getModule('persistence-manager');
if (!persistenceManager || typeof persistenceManager.getPreference !== 'function') {
return TTS_GENERATION_TIMEOUT_MS;
}
const preferredHandler = persistenceManager.getPreference('tts', 'preferred_handler', 'none');
const providerTimeout = preferredHandler && preferredHandler !== 'none'
? persistenceManager.getPreference('tts', `${preferredHandler}_timeout_ms`)
: undefined;
const genericTimeout = persistenceManager.getPreference('tts', 'generation_timeout_ms');
return this.normalizeTtsGenerationTimeoutMs(providerTimeout ?? genericTimeout ?? TTS_GENERATION_TIMEOUT_MS);
}
normalizeTtsGenerationTimeoutMs(value) {
const timeout = Number(value);
if (!Number.isFinite(timeout)) {
return TTS_GENERATION_TIMEOUT_MS;
}
return Math.max(1000, Math.min(600000, Math.round(timeout)));
}
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();
const timeoutMs = this.getConfiguredTtsGenerationTimeoutMs();
return new Promise((resolve) => {
let settled = false;
@@ -324,12 +356,12 @@ class SentenceQueueModule extends BaseModule {
const timeoutId = setTimeout(() => {
console.warn('SentenceQueue: TTS generation timed out; continuing without audio', {
sentenceId,
timeoutMs: this.ttsGenerationTimeoutMs,
timeoutMs,
textPreview: text.slice(0, 120)
});
controller.abort('tts-generation-timeout');
finish({ success: false, reason: 'tts_generation_timeout', timedOut: true });
}, this.ttsGenerationTimeoutMs);
}, timeoutMs);
this.generationRequests.set(requestId, {
controller,
@@ -340,7 +372,10 @@ class SentenceQueueModule extends BaseModule {
finish
});
Promise.resolve(ttsFactory.preloadSpeech(text, { signal: controller.signal }))
Promise.resolve(ttsFactory.preloadSpeech(text, {
signal: controller.signal,
ttsInstructions: Array.isArray(context.ttsInstructions) ? context.ttsInstructions : []
}))
.then(result => finish(result || { success: false, reason: 'empty_tts_result' }))
.catch(error => {
if (controller.signal.aborted) {
@@ -426,7 +461,10 @@ class SentenceQueueModule extends BaseModule {
let speedMultiplier = 1.0;
const ttsFactory = this.getModule('tts-factory');
if (ttsFactory) {
speedMultiplier = Number.isFinite(ttsFactory.speed) ? Math.max(0.25, ttsFactory.speed) : 1.0;
const configuredSpeed = Number(ttsFactory.speed);
speedMultiplier = Number.isFinite(configuredSpeed)
? Math.max(0.5, Math.min(2.0, configuredSpeed))
: 1.0;
}
// Calculate estimated duration in milliseconds
@@ -486,6 +524,7 @@ class SentenceQueueModule extends BaseModule {
sentenceId: id,
blockId: metadata.blockId ?? null,
turnId: metadata.turnId ?? null,
ttsInstructions: Array.isArray(metadata.ttsInstructions) ? metadata.ttsInstructions : [],
blocking: true
});
@@ -501,6 +540,7 @@ class SentenceQueueModule extends BaseModule {
paragraphIndex: metadata.paragraphIndex ?? null,
layoutText: metadata.layoutText || text,
glossaryEntries: Array.isArray(metadata.glossaryEntries) ? metadata.glossaryEntries : [],
ttsInstructions: Array.isArray(metadata.ttsInstructions) ? metadata.ttsInstructions : [],
isFirstParagraphInChapter: Boolean(metadata.isFirstParagraphInChapter),
role: metadata.role || (metadata.type === 'heading' ? 'chapter-heading' : 'body'),
dropCap: Boolean(metadata.dropCap),
@@ -753,9 +793,6 @@ class SentenceQueueModule extends BaseModule {
if (this.lastContinueAt >= (sentence.playbackStartedAt || 0)) {
return false;
}
if (this.inputMode === 'choice') {
return false;
}
return this.sentenceQueue.length > 1;
}
@@ -848,6 +885,7 @@ class SentenceQueueModule extends BaseModule {
sentenceId: nextItem.id,
blockId: nextItem.blockId ?? null,
turnId: nextItem.turnId ?? null,
ttsInstructions: Array.isArray(nextItem.ttsInstructions) ? nextItem.ttsInstructions : [],
queueIndex: index,
prefetch: true,
blocking: false