Skip TTS for voices explicitly set to disabled (fixes #4970) (#5367) * Skip TTS for voices explicitly set to disabled (fixes #4970) * Always show disabled message in commands and fix restoring voice map UI * Always show a message on manual TTS trigger * Fix null current job on disabled * Adjust type annotation * Force update worker when disabled play is attempted * Treat audio control queue as manual * Update TTS message processing to include manual flag * Don't show toast if was already shown by manual playback --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -1,6 +1,7 @@ | |||
| 1 | import { cancelTtsPlay, eventSource, event_types, getCurrentChatId, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js'; | 1 | import { cancelTtsPlay, eventSource, event_types, getCurrentChatId, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js'; |
| 2 | import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; | 2 | import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique, regexFromString } from '../../utils.js'; | 3 | import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique, regexFromString } from '../../utils.js'; |
| 4 | import { accountStorage } from '../../util/AccountStorage.js'; | ||
| 4 | import { EdgeTtsProvider } from './edge.js'; | 5 | import { EdgeTtsProvider } from './edge.js'; |
| 5 | import { ElevenLabsTtsProvider } from './elevenlabs.js'; | 6 | import { ElevenLabsTtsProvider } from './elevenlabs.js'; |
| 6 | import { SileroTtsProvider } from './silerotts.js'; | 7 | import { SileroTtsProvider } from './silerotts.js'; |
| @@ -165,7 +166,7 @@ async function onNarrateOneMessage() { | |||
| 165 | } | 166 | } |
| 166 | 167 | ||
| 167 | resetTtsPlayback(); | 168 | resetTtsPlayback(); |
| 168 | processAndQueueTtsMessage(message, Number(id)); | 169 | processAndQueueTtsMessage(message, Number(id), { manual: true }); |
| 169 | moduleWorker(); | 170 | moduleWorker(); |
| 170 | } | 171 | } |
| 171 | 172 | ||
| @@ -186,13 +187,20 @@ async function onNarrateText(args, text) { | |||
| 186 | ? voiceMap[DEFAULT_VOICE_MARKER] | 187 | ? voiceMap[DEFAULT_VOICE_MARKER] |
| 187 | : voiceMap[name]; | 188 | : voiceMap[name]; |
| 188 | 189 | ||
| 189 | if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { | 190 | if (voiceMapEntry === DISABLED_VOICE_MARKER) { |
| 191 | toastr.info(`TTS voice for ${name} is disabled.`); | ||
| 192 | await initVoiceMap(false); | ||
| 193 | return; | ||
| 194 | } | ||
| 195 | |||
| 196 | if (!voiceMapEntry) { | ||
| 190 | toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`); | 197 | toastr.info(`Specified voice for ${name} was not found. Check the TTS extension settings.`); |
| 198 | await initVoiceMap(false); | ||
| 191 | return; | 199 | return; |
| 192 | } | 200 | } |
| 193 | 201 | ||
| 194 | resetTtsPlayback(); | 202 | resetTtsPlayback(); |
| 195 | processAndQueueTtsMessage({ mes: text, name: name }); | 203 | processAndQueueTtsMessage({ mes: text, name: name }, null, { manual: true }); |
| 196 | await moduleWorker(); | 204 | await moduleWorker(); |
| 197 | 205 | ||
| 198 | // Return back to the chat voices | 206 | // Return back to the chat voices |
| @@ -245,7 +253,7 @@ function isTtsProcessing() { | |||
| 245 | } | 253 | } |
| 246 | 254 | ||
| 247 | /** | 255 | /** |
| 248 | * @typedef {ChatMessage & { id?: number }} TtsMessage | 256 | * @typedef {ChatMessage & { id?: number, manual?: boolean, segmentText?: string, segmentType?: string }} TtsMessage |
| 249 | */ | 257 | */ |
| 250 | 258 | ||
| 251 | /** | 259 | /** |
| @@ -253,12 +261,15 @@ function isTtsProcessing() { | |||
| 253 | * (if enabled) and adds each part to the TTS job queue. | 261 | * (if enabled) and adds each part to the TTS job queue. |
| 254 | * @param {ChatMessage} message - The message object to be processed. | 262 | * @param {ChatMessage} message - The message object to be processed. |
| 255 | * @param {number|null} [messageId=null] - The chat message index to associate with TTS events. | 263 | * @param {number|null} [messageId=null] - The chat message index to associate with TTS events. |
| 264 | * @param {object} [options={}] - Additional options for processing. | ||
| 265 | * @param {boolean} [options.manual=false] - Whether this TTS job was manually triggered (e.g., from the UI) rather than automatically from a new chat message. | ||
| 256 | * @returns {void} | 266 | * @returns {void} |
| 257 | */ | 267 | */ |
| 258 | function processAndQueueTtsMessage(message, messageId = null) { | 268 | function processAndQueueTtsMessage(message, messageId = null, { manual = false } = {}) { |
| 259 | /** @type {TtsMessage} */ | 269 | /** @type {TtsMessage} */ |
| 260 | const clone = structuredClone(message); | 270 | const clone = structuredClone(message); |
| 261 | clone.id = messageId ?? null; | 271 | clone.id = messageId ?? null; |
| 272 | clone.manual = manual ?? false; | ||
| 262 | 273 | ||
| 263 | if (!extension_settings.tts.narrate_by_paragraphs) { | 274 | if (!extension_settings.tts.narrate_by_paragraphs) { |
| 264 | ttsJobQueue.push(clone); | 275 | ttsJobQueue.push(clone); |
| @@ -408,9 +419,10 @@ function onAudioControlClicked() { | |||
| 408 | // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful | 419 | // Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful |
| 409 | if (!audioElement.paused || isTtsProcessing()) { | 420 | if (!audioElement.paused || isTtsProcessing()) { |
| 410 | resetTtsPlayback(); | 421 | resetTtsPlayback(); |
| 411 | } else { | 422 | } else if (context?.chat?.length > 0) { |
| 412 | // Default play behavior if not processing or playing is to play the last message. | 423 | // Default play behavior if not processing or playing is to play the last message. |
| 413 | processAndQueueTtsMessage(context.chat[context.chat.length - 1]); | 424 | const id = context.chat.length - 1; |
| 425 | processAndQueueTtsMessage(context.chat[id], id, { manual: true }); | ||
| 414 | } | 426 | } |
| 415 | updateUiAudioPlayState(); | 427 | updateUiAudioPlayState(); |
| 416 | } | 428 | } |
| @@ -481,8 +493,10 @@ async function processAudioJobQueue() { | |||
| 481 | // TTS Control // | 493 | // TTS Control // |
| 482 | //################// | 494 | //################// |
| 483 | 495 | ||
| 496 | /** @type {TtsMessage[]} */ | ||
| 484 | const ttsJobQueue = []; | 497 | const ttsJobQueue = []; |
| 485 | let currentTtsJob; // Null if nothing is currently being processed | 498 | /** @type {TtsMessage|null} */ |
| 499 | let currentTtsJob = null; // Null if nothing is currently being processed | ||
| 486 | 500 | ||
| 487 | function completeTtsJob() { | 501 | function completeTtsJob() { |
| 488 | console.info(`Current TTS job for ${currentTtsJob?.name} completed.`); | 502 | console.info(`Current TTS job for ${currentTtsJob?.name} completed.`); |
| @@ -621,7 +635,18 @@ async function processTtsQueue() { | |||
| 621 | 635 | ||
| 622 | const voiceMapEntry = voiceMap[voiceMapKey] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[voiceMapKey]; | 636 | const voiceMapEntry = voiceMap[voiceMapKey] === DEFAULT_VOICE_MARKER ? voiceMap[DEFAULT_VOICE_MARKER] : voiceMap[voiceMapKey]; |
| 623 | 637 | ||
| 624 | if (!voiceMapEntry || voiceMapEntry === DISABLED_VOICE_MARKER) { | 638 | if (voiceMapEntry === DISABLED_VOICE_MARKER) { |
| 639 | const storageKey = `tts_disabled_warned_${char}`; | ||
| 640 | if (!accountStorage.getItem(storageKey) || currentTtsJob.manual) { | ||
| 641 | accountStorage.setItem(storageKey, 'true'); | ||
| 642 | toastr.info(`TTS voice for ${char} is disabled.`); | ||
| 643 | } | ||
| 644 | currentTtsJob = null; | ||
| 645 | setTimeout(() => wrapper.update(), 0); | ||
| 646 | return; | ||
| 647 | } | ||
| 648 | |||
| 649 | if (!voiceMapEntry) { | ||
| 625 | throw `${char} not in voicemap. Configure character in extension settings voice map`; | 650 | throw `${char} not in voicemap. Configure character in extension settings voice map`; |
| 626 | } | 651 | } |
| 627 | 652 | ||
| @@ -724,6 +749,7 @@ async function processTtsQueue() { | |||
| 724 | mes: currentTtsJob.mes, | 749 | mes: currentTtsJob.mes, |
| 725 | extra: currentTtsJob.extra, | 750 | extra: currentTtsJob.extra, |
| 726 | id: currentTtsJob.id, | 751 | id: currentTtsJob.id, |
| 752 | manual: currentTtsJob.manual, | ||
| 727 | }; | 753 | }; |
| 728 | ttsJobQueue.unshift(segmentJob); | 754 | ttsJobQueue.unshift(segmentJob); |
| 729 | } | 755 | } |
| @@ -824,7 +850,7 @@ async function playFullConversation() { | |||
| 824 | 850 | ||
| 825 | context.chat.forEach((msg, i) => { | 851 | context.chat.forEach((msg, i) => { |
| 826 | if (!msg.is_system && msg.mes !== '...' && msg.mes !== '') { | 852 | if (!msg.is_system && msg.mes !== '...' && msg.mes !== '') { |
| 827 | processAndQueueTtsMessage(msg, i); | 853 | processAndQueueTtsMessage(msg, i, { manual: false }); |
| 828 | } | 854 | } |
| 829 | }); | 855 | }); |
| 830 | 856 | ||
| @@ -1164,7 +1190,7 @@ async function onMessageEvent(messageId, lastCharIndex) { | |||
| 1164 | message.id = messageId; | 1190 | message.id = messageId; |
| 1165 | ttsJobQueue.push(message); | 1191 | ttsJobQueue.push(message); |
| 1166 | } else { | 1192 | } else { |
| 1167 | processAndQueueTtsMessage(message, messageId); | 1193 | processAndQueueTtsMessage(message, messageId, { manual: false }); |
| 1168 | } | 1194 | } |
| 1169 | } | 1195 | } |
| 1170 | 1196 | ||