diff --git a/public/script.js b/public/script.js index ab94faf21..b5fadcdc1 100644 --- a/public/script.js +++ b/public/script.js @@ -6093,6 +6093,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes, return { type, getMessage }; } +export function syncCurrentSwipeInfoExtras() { + if (!chat.length) { + return; + } + const currentMessage = chat[chat.length - 1]; + if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') { + const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id]; + if (swipeInfo && typeof swipeInfo === 'object') { + swipeInfo.extra = structuredClone(currentMessage.extra); + } + } +} + function saveImageToMessage(img, mes) { if (mes && img.image) { if (!mes.extra || typeof mes.extra !== 'object') { @@ -8524,6 +8537,9 @@ function swipe_left() { // when we swipe left..but no generation. streamingProcessor.onStopStreaming(); } + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); + const swipe_duration = 120; const swipe_range = '700px'; chat[chat.length - 1]['swipe_id']--; @@ -8659,6 +8675,9 @@ const swipe_right = () => { return unblockGeneration(); } + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); + const swipe_duration = 200; const swipe_range = 700; //console.log(swipe_range); diff --git a/public/scripts/authors-note.js b/public/scripts/authors-note.js index 2bf0b254d..326de966a 100644 --- a/public/scripts/authors-note.js +++ b/public/scripts/authors-note.js @@ -566,7 +566,7 @@ export function initAuthorsNote() { namedArgumentList: [], unnamedArgumentList: [ new SlashCommandArgument( - 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'], + 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'], ), ], helpString: ` diff --git a/public/scripts/extensions/connection-manager/index.js b/public/scripts/extensions/connection-manager/index.js index e9d0dd2fe..f31e4b495 100644 --- a/public/scripts/extensions/connection-manager/index.js +++ b/public/scripts/extensions/connection-manager/index.js @@ -30,6 +30,7 @@ const CC_COMMANDS = [ 'api-url', 'model', 'proxy', + 'stop-strings', ]; const TC_COMMANDS = [ @@ -43,6 +44,7 @@ const TC_COMMANDS = [ 'context', 'instruct-state', 'tokenizer', + 'stop-strings', ]; const FANCY_NAMES = { @@ -57,6 +59,7 @@ const FANCY_NAMES = { 'instruct': 'Instruct Template', 'context': 'Context Template', 'tokenizer': 'Tokenizer', + 'stop-strings': 'Custom Stopping Strings', }; /** @@ -138,6 +141,7 @@ const profilesProvider = () => [ * @property {string} [context] Context Template * @property {string} [instruct-state] Instruct Mode * @property {string} [tokenizer] Tokenizer + * @property {string} [stop-strings] Custom Stopping Strings * @property {string[]} [exclude] Commands to exclude */ diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 4b8978422..a01dc5479 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js'; export { talkingAnimation }; const UPDATE_INTERVAL = 1000; +const wrapper = new ModuleWorkerWrapper(moduleWorker); let voiceMapEntries = []; let voiceMap = {}; // {charName:voiceid, charName2:voiceid2} @@ -120,7 +121,7 @@ async function onNarrateOneMessage() { } resetTtsPlayback(); - ttsJobQueue.push(message); + processAndQueueTtsMessage(message); moduleWorker(); } @@ -147,7 +148,7 @@ async function onNarrateText(args, text) { } resetTtsPlayback(); - ttsJobQueue.push({ mes: text, name: name }); + processAndQueueTtsMessage({ mes: text, name: name }); await moduleWorker(); // Return back to the chat voices @@ -220,6 +221,36 @@ function isTtsProcessing() { return processing; } +/** + * Splits a message into lines and adds each non-empty line to the TTS job queue. + * @param {Object} message - The message object to be processed. + * @param {string} message.mes - The text of the message to be split into lines. + * @param {string} message.name - The name associated with the message. + * @returns {void} + */ +function processAndQueueTtsMessage(message) { + if (!extension_settings.tts.narrate_by_paragraphs) { + ttsJobQueue.push(message); + return; + } + + const lines = message.mes.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.length === 0) { + continue; + } + + ttsJobQueue.push( + Object.assign({}, message, { + mes: line, + }), + ); + } +} + function debugTtsPlayback() { console.log(JSON.stringify( { @@ -350,7 +381,7 @@ function onAudioControlClicked() { talkingAnimation(false); } else { // Default play behavior if not processing or playing is to play the last message. - ttsJobQueue.push(context.chat[context.chat.length - 1]); + processAndQueueTtsMessage(context.chat[context.chat.length - 1]); } updateUiAudioPlayState(); } @@ -376,6 +407,7 @@ function completeCurrentAudioJob() { currentAudioJob = null; talkingAnimation(false); //stop lip animation // updateUiPlayState(); + wrapper.update(); } /** @@ -466,7 +498,7 @@ async function processTtsQueue() { } if (extension_settings.tts.skip_tags) { - text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim(); + text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim(); } if (!extension_settings.tts.pass_asterisks) { @@ -569,6 +601,7 @@ function loadSettings() { $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only); $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation); $('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation); + $('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs); $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); $('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user); $('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks); @@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() { saveSettingsDebounced(); } +function onNarrateByParagraphsClick() { + extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked'); + saveSettingsDebounced(); +} + function onNarrateDialoguesClick() { extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked'); @@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) { lastChatId = context.chatId; console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`); - ttsJobQueue.push(message); + + if (extension_settings.tts.periodic_auto_generation) { + ttsJobQueue.push(message); + } else { + processAndQueueTtsMessage(message); + } } async function onMessageDeleted() { @@ -1156,6 +1199,7 @@ jQuery(async function () { $('#tts_pass_asterisks').on('click', onPassAsterisksClick); $('#tts_auto_generation').on('click', onAutoGenerationClick); $('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick); + $('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick); $('#tts_narrate_user').on('click', onNarrateUserClick); $('#playback_rate').on('input', function () { @@ -1177,7 +1221,6 @@ jQuery(async function () { loadSettings(); // Depends on Extension Controls and loadTtsProvider loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies addAudioControl(); // Depends on Extension Controls - const wrapper = new ModuleWorkerWrapper(moduleWorker); setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback); eventSource.on(event_types.CHAT_CHANGED, onChatChanged); diff --git a/public/scripts/extensions/tts/settings.html b/public/scripts/extensions/tts/settings.html index dc005a406..5fb5b6895 100644 --- a/public/scripts/extensions/tts/settings.html +++ b/public/scripts/extensions/tts/settings.html @@ -30,6 +30,10 @@ Narrate by paragraphs (when streaming) + + + Narrate by paragraphs (when not streaming) + Only narrate "quotes" diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 2a74f8722..637fdbffb 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -4072,4 +4072,45 @@ $(document).ready(() => { ], helpString: 'activates a movingUI preset by name', })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'stop-strings', + aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'], + helpString: ` + + Sets a list of custom stopping strings. Gets the list if no value is provided. + + + Examples: + + + Value must be a JSON-serialized array: /stop-strings ["goodbye", "farewell"] + Pipe characters must be escaped with a backslash: /stop-strings ["left\\|right"] + + `, + returns: ARGUMENT_TYPE.LIST, + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'list of strings', + typeList: [ARGUMENT_TYPE.LIST], + acceptsMultiple: false, + isRequired: false, + }), + ], + callback: (_, value) => { + if (String(value ?? '').trim()) { + const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value); + if (!parsedValue || !Array.isArray(parsedValue)) { + throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.'); + } + parsedValue.forEach((item, index) => { + parsedValue[index] = String(item); + }); + power_user.custom_stopping_strings = JSON.stringify(parsedValue); + $('#custom_stopping_strings').val(power_user.custom_stopping_strings); + saveSettingsDebounced(); + } + + return power_user.custom_stopping_strings; + }, + })); }); diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 0bf7bee90..888276f8e 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -147,7 +147,7 @@ function registerReasoningSlashCommands() { }), ], callback: async (args, value) => { - const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1; + const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1; const message = chat[messageId]; if (!message?.extra) { return ''; diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 39b3db71e..531a8f51f 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -42,6 +42,7 @@ import { showMoreMessages, stopGeneration, substituteParams, + syncCurrentSwipeInfoExtras, system_avatar, system_message_types, this_chid, @@ -2814,8 +2815,11 @@ async function addSwipeCallback(args, value) { const newSwipeId = lastMessage.swipes.length - 1; if (isTrueBoolean(args.switch)) { + // Make sure ad-hoc changes to extras are saved before swiping away + syncCurrentSwipeInfoExtras(); lastMessage.swipe_id = newSwipeId; lastMessage.mes = lastMessage.swipes[newSwipeId]; + lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {}); } await saveChatConditional();
/stop-strings ["goodbye", "farewell"]
/stop-strings ["left\\|right"]