feat(tts): emit events and track messageId for third-party integrations (#5309) * feat(tts): add event signals and messageId tracking for third-party integrations * feat(tts): move events to core, centralize clone and id tracking * Apply review suggestions --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e4ad48944806de61ef87ad22ea5262a4ae3cdcec

Xiangzhe <32761048+xz-dev@users.noreply.github.com>

Signed
2 files changed, +51 -19Showing whitespace changes
public/scripts/events.js+3 -0
@@ -97,6 +97,9 @@ export const event_types = {
9797 WORLDINFO_SCAN_DONE: 'worldinfo_scan_done',
9898 MEDIA_ATTACHMENT_DELETED: 'media_attachment_deleted',
9999 PERSONA_CHANGED: 'persona_changed',
100+ TTS_JOB_STARTED: 'tts_job_started',
101+ TTS_AUDIO_READY: 'tts_audio_ready',
102+ TTS_JOB_COMPLETE: 'tts_job_complete',
100103};
101104
102105export const eventSource = new EventEmitter([event_types.APP_READY, event_types.APP_INITIALIZED]);
public/scripts/extensions/tts/index.js+48 -19
@@ -165,7 +165,7 @@ async function onNarrateOneMessage() {
165165 }
166166
167167 resetTtsPlayback();
168168 processAndQueueTtsMessage(message, Number(id));
169169 moduleWorker();
170170}
171171
@@ -245,17 +245,27 @@ function isTtsProcessing() {
245245}
246246
247247/**
248- * Splits a message into lines and adds each non-empty line to the TTS job queue.
248+ * @typedef {ChatMessage & { id?: number }} TtsMessage
249+ */
250+
251+/**
252+ * Clones a message, attaches the given message ID, then splits by paragraphs
253+ * (if enabled) and adds each part to the TTS job queue.
249254 * @param {ChatMessage} message - The message object to be processed.
255+ * @param {number|null} [messageId=null] - The chat message index to associate with TTS events.
250256 * @returns {void}
251257 */
252258function processAndQueueTtsMessage(message, messageId = null) {
259+ /** @type {TtsMessage} */
260+ const clone = structuredClone(message);
261+ clone.id = messageId ?? null;
262+
253263 if (!extension_settings.tts.narrate_by_paragraphs) {
254264 ttsJobQueue.push(messageclone);
255265 return;
256266 }
257267
258268 const lines = messageclone.mes.split('\n');
259269
260270 for (let i = 0; i < lines.length; i++) {
261271 const line = lines[i];
@@ -265,7 +275,7 @@ function processAndQueueTtsMessage(message) {
265275 }
266276
267277 ttsJobQueue.push(
268278 Object.assign({}, messageclone, {
269279 mes: line,
270280 }),
271281 );
@@ -301,7 +311,7 @@ audioElement.autoplay = true;
301311 * @type AudioJob[] Audio job queue
302312 * @typedef {{audioBlob: Blob | string, char: string}} AudioJob Audio job object
303313 */
304314letconst audioJobQueue = [];
305315/**
306316 * @type AudioJob Current audio job
307317 */
@@ -431,18 +441,24 @@ function completeCurrentAudioJob() {
431441/**
432442 * Accepts an HTTP response containing audio/mpeg data, and puts the data as a Blob() on the queue for playback
433443 * @param {Response} response
444+ * @param {string} char
445+ * @returns {Promise<{audioBlob: Blob|string, mimeType: string}>}
434446 */
435447async function addAudioJob(response, char) {
448+ let audioBlob, mimeType;
436449 if (typeof response === 'string') {
437- audioJobQueue.push({ audioBlob: response, char: char });
450+ audioBlob = response;
451+ mimeType = '';
438452 } else {
439453 const audioDataaudioBlob = await response.blob();
440454 if (!audioDataaudioBlob.type.startsWith('audio/')) {
441455 throw `TTS received HTTP response with invalid data format. Expecting audio/*, got ${audioDataaudioBlob.type}`;
442456 }
443- audioJobQueue.push({ audioBlob: audioData, char: char });
457+ mimeType = audioBlob.type;
444458 }
459+ audioJobQueue.push({ audioBlob, char });
445460 console.debug('Pushed audio job to queue.');
461+ return { audioBlob, mimeType };
446462}
447463
448464async function processAudioJobQueue() {
@@ -465,7 +481,7 @@ async function processAudioJobQueue() {
465481// TTS Control //
466482//################//
467483
468484letconst ttsJobQueue = [];
469485let currentTtsJob; // Null if nothing is currently being processed
470486
471487function completeTtsJob() {
@@ -474,12 +490,18 @@ function completeTtsJob() {
474490}
475491
476492async function tts(text, voiceId, char, voiceMapKey = null) {
493+ const messageId = currentTtsJob?.id ?? null;
494+
495+ await eventSource.emit(event_types.TTS_JOB_STARTED, { messageId, characterName: char, text, voiceId });
496+
477497 async function processResponse(response) {
478498 // RVC injection
479499 if (typeof globalThis.rvcVoiceConversion === 'function' && extension_settings.rvc.enabled)
480500 response = await globalThis.rvcVoiceConversion(response, char, text);
481501
482502 const audioResult = await addAudioJob(response, char);
503+ const eventData = { messageId, characterName: char, text, audio: audioResult.audioBlob, mimeType: audioResult.mimeType };
504+ await eventSource.emit(event_types.TTS_AUDIO_READY, eventData);
483505 }
484506
485507 // voiceMapKey can also include segment qualifiers, e.g. '{char} ("Quotes")'
@@ -494,6 +516,7 @@ async function tts(text, voiceId, char, voiceMapKey = null) {
494516 await processResponse(response);
495517 }
496518
519+ await eventSource.emit(event_types.TTS_JOB_COMPLETE, { messageId, characterName: char });
497520 completeTtsJob();
498521}
499522
@@ -700,6 +723,7 @@ async function processTtsQueue() {
700723 is_user: currentTtsJob.is_user,
701724 mes: currentTtsJob.mes,
702725 extra: currentTtsJob.extra,
726+ id: currentTtsJob.id,
703727 };
704728 ttsJobQueue.unshift(segmentJob);
705729 }
@@ -797,13 +821,16 @@ async function playFullConversation() {
797821 }
798822
799823 const context = getContext();
800- const chat = context.chat.filter(x => !x.is_system && x.mes !== '...' && x.mes !== '');
801824
802- if (chat.length === 0) {
825+ context.chat.forEach((msg, i) => {
803- return toastr.info('No messages to narrate.');
826+ if (!msg.is_system && msg.mes !== '...' && msg.mes !== '') {
827+ processAndQueueTtsMessage(msg, i);
804828 }
829+ });
805830
806- ttsJobQueue = chat;
831+ if (ttsJobQueue.length === 0) {
832+ return toastr.info('No messages to narrate.');
833+ }
807834}
808835
809836globalThis.playFullConversation = playFullConversation;
@@ -1076,6 +1103,7 @@ async function onMessageEvent(messageId, lastCharIndex) {
10761103 }
10771104
10781105 // clone message object, as things go haywire if message object is altered below (it's passed by reference)
1106+ /** @type {TtsMessage} */
10791107 const message = structuredClone(context.chat[messageId]);
10801108 const hashNew = getStringHash(message?.mes ?? '');
10811109
@@ -1133,9 +1161,10 @@ async function onMessageEvent(messageId, lastCharIndex) {
11331161 console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
11341162
11351163 if (extension_settings.tts.periodic_auto_generation && isStreamingEnabled()) {
1164+ message.id = messageId;
11361165 ttsJobQueue.push(message);
11371166 } else {
11381167 processAndQueueTtsMessage(message, messageId);
11391168 }
11401169}
11411170