Merge branch 'staging' into update-git-workflows

This commit is contained in:
Wolfsblvt
2025-03-13 16:43:23 +01:00
30 changed files with 823 additions and 266 deletions

View File

@@ -75,6 +75,7 @@ module.exports = {
'plugins/**',
'**/*.min.js',
'public/scripts/extensions/quick-reply/lib/**',
'public/scripts/extensions/tts/lib/**',
],
rules: {
'no-unused-vars': ['error', { args: 'none' }],

1
.github/readme.md vendored
View File

@@ -393,6 +393,7 @@ GNU Affero General Public License for more details.**
* Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background)
* Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand)
* kokoro-js library by [@hexgrad](https://github.com/hexgrad) (Apache-2.0 License)
## Top Contributors

31
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@iconfu/svg-inject": "^1.2.3",
"@mozilla/readability": "^0.5.0",
"@mozilla/readability": "^0.6.0",
"@popperjs/core": "^2.11.8",
"@zeldafan0225/ai_horde": "^5.2.0",
"archiver": "^7.0.1",
@@ -95,11 +95,11 @@
"@types/jquery": "^3.5.32",
"@types/jquery-cropper": "^1.0.4",
"@types/jquery.transit": "^0.9.33",
"@types/jqueryui": "^1.12.23",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.16",
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.12",
"@types/node": "^18.19.78",
"@types/node": "^18.19.80",
"@types/node-persist": "^3.1.8",
"@types/png-chunk-text": "^1.0.3",
"@types/png-chunks-encode": "^1.0.2",
@@ -915,9 +915,9 @@
"license": "MIT"
},
"node_modules/@mozilla/readability": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz",
"integrity": "sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14.0.0"
@@ -1285,9 +1285,9 @@
}
},
"node_modules/@types/jqueryui": {
"version": "1.12.23",
"resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz",
"integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==",
"version": "1.12.24",
"resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.24.tgz",
"integrity": "sha512-E2sGULwzMhg4kAeOV+gYcXjg988RuPkviWCt09jLe6GGK9sHM7dTqS8H7JMuUWoZQBucIBzBAgM5o/ezKUFkeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1354,9 +1354,9 @@
}
},
"node_modules/@types/node": {
"version": "18.19.78",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz",
"integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==",
"version": "18.19.80",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz",
"integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
@@ -2136,9 +2136,10 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",

View File

@@ -4,7 +4,7 @@
"@agnai/sentencepiece-js": "^1.1.1",
"@agnai/web-tokenizers": "^0.1.3",
"@iconfu/svg-inject": "^1.2.3",
"@mozilla/readability": "^0.5.0",
"@mozilla/readability": "^0.6.0",
"@popperjs/core": "^2.11.8",
"@zeldafan0225/ai_horde": "^5.2.0",
"archiver": "^7.0.1",
@@ -125,11 +125,11 @@
"@types/jquery": "^3.5.32",
"@types/jquery-cropper": "^1.0.4",
"@types/jquery.transit": "^0.9.33",
"@types/jqueryui": "^1.12.23",
"@types/jqueryui": "^1.12.24",
"@types/lodash": "^4.17.16",
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.12",
"@types/node": "^18.19.78",
"@types/node": "^18.19.80",
"@types/node-persist": "^3.1.8",
"@types/png-chunk-text": "^1.0.3",
"@types/png-chunks-encode": "^1.0.2",

25
public/global.d.ts vendored
View File

@@ -1,18 +1,21 @@
import libs from './lib';
import getContext from './scripts/st-context';
// Global namespace modules
declare var ai;
declare var pdfjsLib;
declare var ePub;
declare var SillyTavern: {
getContext(): typeof getContext;
llm: any;
libs: typeof libs;
};
declare global {
// Global namespace modules
interface Window {
ai: any;
}
declare var pdfjsLib;
declare var ePub;
declare var SillyTavern: {
getContext(): typeof getContext;
llm: any;
libs: typeof libs;
};
// Jquery plugins
interface JQuery {
nanogallery2(options?: any): JQuery;

View File

@@ -5382,6 +5382,7 @@
<option value="2" data-i18n="Manual">Manual</option>
<option value="0" data-i18n="Natural order">Natural order</option>
<option value="1" data-i18n="List order">List order</option>
<option value="3" data-i18n="Pooled order">Pooled order</option>
</select>
</div>
<div class="flex1 flexGap5">

View File

@@ -14,7 +14,8 @@
"**/.git/**",
"lib/**",
"**/*.min.js",
"scripts/extensions/quick-reply/lib/**"
"scripts/extensions/quick-reply/lib/**",
"scripts/extensions/tts/lib/**"
],
"typeAcquisition": {
"include": []

View File

@@ -1400,6 +1400,7 @@ export async function selectCharacterById(id) {
} else {
//if clicked on character that was already selected
selected_button = 'character_edit';
await unshallowCharacter(this_chid);
select_selected_character(this_chid);
}
}
@@ -3372,8 +3373,8 @@ class StreamingProcessor {
}
if (this.type !== 'impersonate') {
await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId);
await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
} else {
await eventSource.emit(event_types.IMPERSONATE_READY, text);
}
@@ -3402,8 +3403,8 @@ class StreamingProcessor {
const noEmitTypes = ['swipe', 'impersonate', 'continue'];
if (!noEmitTypes.includes(this.type)) {
eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId);
eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId);
eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
}
}
@@ -3939,7 +3940,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
let message = chatItem.mes;
let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
let options = { isPrompt: true, depth: (coreChat.length - index - 1) };
let options = { isPrompt: true, depth: (coreChat.length - index - (isContinue ? 2 : 1)) };
let regexedMessage = getRegexedString(message, regexType, options);
regexedMessage = await appendFileContent(chatItem, regexedMessage);
@@ -3957,7 +3958,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
const promptReasoning = new PromptReasoning();
for (let i = coreChat.length - 1; i >= 0; i--) {
const depth = coreChat.length - i - 1;
const depth = coreChat.length - i - (isContinue ? 2 : 1);
const isPrefix = isContinue && i === coreChat.length - 1;
coreChat[i] = {
...coreChat[i],
@@ -6014,9 +6015,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
addOneMessage(chat[chat_id], { type: 'swipe' });
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
} else {
chat[chat.length - 1]['mes'] = getMessage;
}
@@ -6037,9 +6038,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
addOneMessage(chat[chat_id], { type: 'swipe' });
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
} else if (type === 'appendFinal') {
oldMessage = chat[chat.length - 1]['mes'];
console.debug('Trying to appendFinal.');
@@ -6057,9 +6058,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
}
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
addOneMessage(chat[chat_id], { type: 'swipe' });
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
} else {
console.debug('entering chat update routine for non-swipe post');
@@ -6099,9 +6100,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
saveImageToMessage(img, chat[chat.length - 1]);
const chat_id = (chat.length - 1);
!fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
!fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
addOneMessage(chat[chat_id]);
!fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
!fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
}
const item = chat[chat.length - 1];
@@ -6849,8 +6850,8 @@ async function getChatResult() {
if (chat.length === 1) {
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, 'first_message');
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, 'first_message');
}
}
@@ -8728,10 +8729,10 @@ async function createOrEditCharacter(e) {
if (shouldRegenerateMessage) {
chat.splice(0, chat.length, message);
const messageId = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'first_message');
await clearChat();
await printMessages();
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'first_message');
await saveChatConditional();
}
} catch (error) {

View File

@@ -130,9 +130,10 @@ function getConverter(type) {
* @param {number} start Starting message ID
* @param {number} end Ending message ID (inclusive)
* @param {boolean} unhide If true, unhide the messages instead.
* @param {string} nameFitler Optional name filter
* @returns {Promise<void>}
*/
export async function hideChatMessageRange(start, end, unhide) {
export async function hideChatMessageRange(start, end, unhide, nameFitler = null) {
if (isNaN(start)) return;
if (!end) end = start;
const hide = !unhide;
@@ -140,6 +141,7 @@ export async function hideChatMessageRange(start, end, unhide) {
for (let messageId = start; messageId <= end; messageId++) {
const message = chat[messageId];
if (!message) continue;
if (nameFitler && message.name !== nameFitler) continue;
message.is_system = hide;
@@ -1506,7 +1508,7 @@ jQuery(function () {
embedMessageFile(messageId, messageBlock);
});
$(document).on('click', '.editor_maximize', function () {
$(document).on('click', '.editor_maximize', async function () {
const broId = $(this).attr('data-for');
const bro = $(`#${broId}`);
const contentEditable = bro.is('[contenteditable]');
@@ -1525,6 +1527,7 @@ jQuery(function () {
textarea.value = String(contentEditable ? bro[0].innerText : bro.val());
textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
bro.hasClass('monospace') && textarea.classList.add('monospace');
bro.hasClass('mdHotkeys') && textarea.classList.add('mdHotkeys');
textarea.addEventListener('input', function () {
if (contentEditable) {
bro[0].innerText = textarea.value;
@@ -1565,7 +1568,7 @@ jQuery(function () {
});
}
callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
await callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
});
$(document).on('click', 'body.documentstyle .mes .mes_text', function () {

View File

@@ -9,6 +9,8 @@ import { QuickReplySettings } from './src/QuickReplySettings.js';
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
import { ButtonUi } from './src/ui/ButtonUi.js';
import { SettingsUi } from './src/ui/SettingsUi.js';
import { debounceAsync } from '../../utils.js';
export { debounceAsync };
@@ -17,32 +19,6 @@ const _VERBOSE = true;
export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {Function} func The function to debounce.
* @param {Number} [timeout=300] The timeout in milliseconds.
* @returns {Function} The debounced function.
*/
export function debounceAsync(func, timeout = 300) {
let timer;
/**@type {Promise}*/
let debouncePromise;
/**@type {Function}*/
let debounceResolver;
return (...args) => {
clearTimeout(timer);
if (!debouncePromise) {
debouncePromise = new Promise(resolve => {
debounceResolver = resolve;
});
}
timer = setTimeout(() => {
debounceResolver(func.apply(this, args));
debouncePromise = null;
}, timeout);
return debouncePromise;
};
}
const defaultConfig = {

View File

@@ -110,14 +110,14 @@
</div>
<div class="flex-container wide100p marginTop5">
<div class="flex1 flex-container flexNoGap">
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. When blank / 'Unlimited' or -1, also affect message to continue on Continue.">
<span data-i18n="Min Depth">Min Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>
<input name="min_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
<input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
</div>
<div class="flex1 flex-container flexNoGap">
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. Max must be greater than Min for regex to apply.">
<span data-i18n="Max Depth">Max Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>

View File

@@ -103,8 +103,8 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
}
// Check if the depth is within the min/max depth
if (typeof depth === 'number' && depth >= 0) {
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {
if (typeof depth === 'number') {
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= -1 && depth < script.minDepth) {
console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is less than minDepth ${script.minDepth}`);
return;
}
@@ -139,7 +139,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
}
const getRegexString = () => {
switch(Number(regexScript.substituteRegex)) {
switch (Number(regexScript.substituteRegex)) {
case substitute_find_regex.NONE:
return regexScript.findRegex;
case substitute_find_regex.RAW:

View File

@@ -3739,9 +3739,9 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
};
context.chat.push(message);
const messageId = context.chat.length - 1;
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
context.addOneMessage(message);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
await context.saveChat();
}

View File

@@ -27,6 +27,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { GoogleTranslateTtsProvider } from './google-translate.js';
import { KokoroTtsProvider } from './kokoro.js';
const UPDATE_INTERVAL = 1000;
const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -94,6 +95,7 @@ const ttsProviders = {
'Google Translate': GoogleTranslateTtsProvider,
GSVI: GSVITtsProvider,
'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
Kokoro: KokoroTtsProvider,
Novel: NovelTtsProvider,
OpenAI: OpenAITtsProvider,
'OpenAI Compatible': OpenAICompatibleTtsProvider,
@@ -716,6 +718,9 @@ async function loadTtsProvider(provider) {
}
function onTtsProviderChange() {
if (typeof ttsProvider?.dispose === 'function') {
ttsProvider.dispose();
}
const ttsProviderSelection = $('#tts_provider').val();
extension_settings.tts.currentProvider = ttsProviderSelection;
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');

View File

@@ -0,0 +1,113 @@
// kokoro-worker.js
/** @type {import('./lib/kokoro.web.js').KokoroTTS} */
let tts = null;
/** @type {boolean} */
let ready = false;
/** @type {string[]} */
let voices = [];
// Handle messages from the main thread
self.onmessage = async function(e) {
const { action, data } = e.data;
switch (action) {
case 'initialize':
try {
const result = await initializeTts(data);
self.postMessage({
action: 'initialized',
success: result,
voices,
});
} catch (error) {
self.postMessage({
action: 'initialized',
success: false,
error: error.message,
});
}
break;
case 'generateTts':
try {
const audioBlob = await generateTts(data.text, data.voice, data.speakingRate);
const blobUrl = URL.createObjectURL(audioBlob);
self.postMessage({
action: 'generatedTts',
success: true,
blobUrl,
requestId: data.requestId,
});
} catch (error) {
self.postMessage({
action: 'generatedTts',
success: false,
error: error.message,
requestId: data.requestId,
});
}
break;
case 'checkReady':
self.postMessage({ action: 'readyStatus', ready });
break;
}
};
// Initialize the TTS engine
async function initializeTts(settings) {
try {
const { KokoroTTS } = await import('./lib/kokoro.web.js');
console.log('Worker: Initializing Kokoro TTS with settings:', {
modelId: settings.modelId,
dtype: settings.dtype,
device: settings.device,
});
// Create TTS instance
tts = await KokoroTTS.from_pretrained(settings.modelId, {
dtype: settings.dtype,
device: settings.device,
});
// Get available voices
voices = Object.keys(tts.voices);
// Check if generate method exists
if (typeof tts.generate !== 'function') {
throw new Error('TTS instance does not have generate method');
}
console.log('Worker: TTS initialized successfully');
ready = true;
return true;
} catch (error) {
console.error('Worker: Kokoro TTS initialization failed:', error);
ready = false;
throw error;
}
}
// Generate TTS audio
async function generateTts(text, voiceId, speakingRate) {
if (!ready || !tts) {
throw new Error('TTS engine not initialized');
}
if (text.trim().length === 0) {
throw new Error('Empty text');
}
try {
const audio = await tts.generate(text, {
voice: voiceId,
speed: speakingRate || 1.0,
});
return audio.toBlob();
} catch (error) {
console.error('Worker: TTS generation failed:', error);
throw error;
}
}

View File

@@ -0,0 +1,352 @@
import { debounce_timeout } from '../../constants.js';
import { debounceAsync, splitRecursive } from '../../utils.js';
import { getPreviewString, saveTtsProviderSettings } from './index.js';
export class KokoroTtsProvider {
constructor() {
this.settings = {
modelId: 'onnx-community/Kokoro-82M-v1.0-ONNX',
dtype: 'q8',
device: 'wasm',
voiceMap: {},
defaultVoice: 'af_heart',
speakingRate: 1.0,
};
this.ready = false;
this.voices = [
'af_heart',
'af_alloy',
'af_aoede',
'af_bella',
'af_jessica',
'af_kore',
'af_nicole',
'af_nova',
'af_river',
'af_sarah',
'af_sky',
'am_adam',
'am_echo',
'am_eric',
'am_fenrir',
'am_liam',
'am_michael',
'am_onyx',
'am_puck',
'am_santa',
'bf_emma',
'bf_isabella',
'bm_george',
'bm_lewis',
'bf_alice',
'bf_lily',
'bm_daniel',
'bm_fable',
];
this.worker = null;
this.separator = ' ... ... ... ';
this.pendingRequests = new Map();
this.nextRequestId = 1;
// Update display values immediately but only reinitialize TTS after a delay
this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed);
}
/**
* Perform any text processing before passing to TTS engine.
* @param {string} text Input text
* @returns {string} Processed text
*/
processText(text) {
// TILDE!
text = text.replace(/~/g, '.');
return text;
}
async loadSettings(settings) {
if (settings.modelId !== undefined) this.settings.modelId = settings.modelId;
if (settings.dtype !== undefined) this.settings.dtype = settings.dtype;
if (settings.device !== undefined) this.settings.device = settings.device;
if (settings.voiceMap !== undefined) this.settings.voiceMap = settings.voiceMap;
if (settings.defaultVoice !== undefined) this.settings.defaultVoice = settings.defaultVoice;
if (settings.speakingRate !== undefined) this.settings.speakingRate = settings.speakingRate;
$('#kokoro_model_id').val(this.settings.modelId).on('input', this.onSettingsChange.bind(this));
$('#kokoro_dtype').val(this.settings.dtype).on('change', this.onSettingsChange.bind(this));
$('#kokoro_device').val(this.settings.device).on('change', this.onSettingsChange.bind(this));
$('#kokoro_speaking_rate').val(this.settings.speakingRate).on('input', this.onSettingsChange.bind(this));
$('#kokoro_speaking_rate_output').text(this.settings.speakingRate + 'x');
}
initializeWorker() {
return new Promise((resolve, reject) => {
try {
// Terminate the existing worker if it exists
if (this.worker) {
this.worker.terminate();
$('#kokoro_status_text').text('Initializing...').removeAttr('style');
}
// Create a new worker
this.worker = new Worker(new URL('./kokoro-worker.js', import.meta.url), { type: 'module' });
// Set up message handling
this.worker.onmessage = this.handleWorkerMessage.bind(this);
// Initialize the worker with the current settings
this.worker.postMessage({
action: 'initialize',
data: {
modelId: this.settings.modelId,
dtype: this.settings.dtype,
device: this.settings.device,
},
});
// Create a promise that will resolve when initialization completes
const initPromise = new Promise((initResolve, initReject) => {
const timeoutId = setTimeout(() => {
initReject(new Error('Worker initialization timed out'));
}, 600000); // 600 second timeout
this.pendingRequests.set('initialization', {
resolve: (result) => {
clearTimeout(timeoutId);
initResolve(result);
},
reject: (error) => {
clearTimeout(timeoutId);
initReject(error);
},
});
});
// Resolve the outer promise when initialization completes
initPromise.then(success => {
this.ready = success;
this.updateStatusDisplay();
resolve(success);
}).catch(error => {
console.error('Worker initialization failed:', error);
this.ready = false;
this.updateStatusDisplay();
reject(error);
});
} catch (error) {
console.error('Failed to create worker:', error);
this.ready = false;
this.updateStatusDisplay();
reject(error);
}
});
}
handleWorkerMessage(event) {
const { action, success, ready, error, requestId, blobUrl } = event.data;
switch (action) {
case 'initialized': {
const initRequest = this.pendingRequests.get('initialization');
if (initRequest) {
if (success) {
initRequest.resolve(true);
} else {
initRequest.reject(new Error(error || 'Initialization failed'));
}
this.pendingRequests.delete('initialization');
}
} break;
case 'generatedTts': {
const request = this.pendingRequests.get(requestId);
if (request) {
if (success) {
fetch(blobUrl).then(response => response.blob()).then(audioBlob => {
// Clean up the blob URL
URL.revokeObjectURL(blobUrl);
request.resolve(new Response(audioBlob, {
headers: {
'Content-Type': 'audio/wav',
},
}));
}).catch(error => {
request.reject(new Error('Failed to fetch TTS audio blob: ' + error));
});
} else {
request.reject(new Error(error || 'TTS generation failed'));
}
this.pendingRequests.delete(requestId);
}
} break;
case 'readyStatus':
this.ready = ready;
this.updateStatusDisplay();
break;
}
}
updateStatusDisplay() {
const statusText = this.ready ? 'Ready' : 'Failed';
const statusColor = this.ready ? 'green' : 'red';
$('#kokoro_status_text').text(statusText).css('color', statusColor);
}
async checkReady() {
if (!this.worker) {
return await this.initializeWorker();
}
this.worker.postMessage({ action: 'checkReady' });
return this.ready;
}
async onRefreshClick() {
return await this.initializeWorker();
}
get settingsHtml() {
return `
<div class="kokoro_tts_settings">
<label for="kokoro_model_id">Model ID:</label>
<input id="kokoro_model_id" type="text" class="text_pole" value="${this.settings.modelId}" />
<label for="kokoro_dtype">Data Type:</label>
<select id="kokoro_dtype" class="text_pole">
<option value="q8" ${this.settings.dtype === 'q8' ? 'selected' : ''}>q8 (Recommended)</option>
<option value="fp32" ${this.settings.dtype === 'fp32' ? 'selected' : ''}>fp32 (High Precision)</option>
<option value="fp16" ${this.settings.dtype === 'fp16' ? 'selected' : ''}>fp16</option>
<option value="q4" ${this.settings.dtype === 'q4' ? 'selected' : ''}>q4 (Low Memory)</option>
<option value="q4f16" ${this.settings.dtype === 'q4f16' ? 'selected' : ''}>q4f16</option>
</select>
<label for="kokoro_device">Device:</label>
<select id="kokoro_device" class="text_pole">
<option value="wasm" ${this.settings.device === 'wasm' ? 'selected' : ''}>WebAssembly (CPU)</option>
<option value="webgpu" ${this.settings.device === 'webgpu' ? 'selected' : ''}>WebGPU (GPU Acceleration)</option>
</select>
<label for="kokoro_speaking_rate">Speaking Rate: <span id="kokoro_speaking_rate_output">${this.settings.speakingRate}x</span></label>
<input id="kokoro_speaking_rate" type="range" value="${this.settings.speakingRate}" min="0.5" max="2.0" step="0.1" />
<hr>
<div>
Status: <span id="kokoro_status_text">Initializing...</span>
</div>
</div>
`;
}
async onSettingsChange() {
this.settings.modelId = $('#kokoro_model_id').val().toString();
this.settings.dtype = $('#kokoro_dtype').val().toString();
this.settings.device = $('#kokoro_device').val().toString();
this.settings.speakingRate = parseFloat($('#kokoro_speaking_rate').val().toString());
// Update UI display
$('#kokoro_speaking_rate_output').text(this.settings.speakingRate + 'x');
// Reinitialize TTS engine with debounce
this.initTtsDebounced();
saveTtsProviderSettings();
}
async fetchTtsVoiceObjects() {
if (!this.ready) {
await this.checkReady();
}
return this.voices.map(voice => ({
name: voice,
voice_id: voice,
preview_url: null,
lang: voice.startsWith('b') ? 'en-GB' : 'en-US',
}));
}
async previewTtsVoice(voiceId) {
if (!this.ready) {
await this.checkReady();
}
const voice = this.getVoice(voiceId);
const previewText = getPreviewString(voice.lang);
for await (const response of this.generateTts(previewText, voiceId)) {
const audio = await response.blob();
const url = URL.createObjectURL(audio);
await new Promise(resolve => {
const audioElement = new Audio();
audioElement.src = url;
audioElement.play();
audioElement.onended = () => resolve();
});
URL.revokeObjectURL(url);
}
}
getVoiceDisplayName(voiceId) {
return voiceId;
}
getVoice(voiceName) {
const defaultVoice = this.settings.defaultVoice || 'af_heart';
const actualVoiceName = this.voices.includes(voiceName) ? voiceName : defaultVoice;
return {
name: actualVoiceName,
voice_id: actualVoiceName,
preview_url: null,
lang: actualVoiceName.startsWith('b') ? 'en-GB' : 'en-US',
};
}
/**
* Generate TTS audio for the given text using the specified voice.
* @param {string} text Text to generate
* @param {string} voiceId Voice ID
* @returns {AsyncGenerator<Response>} Audio response generator
*/
async* generateTts(text, voiceId) {
if (!this.ready || !this.worker) {
console.log('TTS not ready, initializing...');
await this.initializeWorker();
}
if (!this.ready || !this.worker) {
throw new Error('Failed to initialize TTS engine');
}
if (text.trim().length === 0) {
throw new Error('Empty text');
}
const voice = this.getVoice(voiceId);
const requestId = this.nextRequestId++;
const chunkSize = 400;
const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
for (const chunk of chunks) {
yield await new Promise((resolve, reject) => {
// Store the promise callbacks
this.pendingRequests.set(requestId, { resolve, reject });
// Send the request to the worker
this.worker.postMessage({
action: 'generateTts',
data: {
text: chunk,
voice: voice.voice_id,
speakingRate: this.settings.speakingRate || 1.0,
requestId,
},
});
});
}
}
dispose() {
// Clean up the worker when the provider is disposed
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}

View File

@@ -0,0 +1,8 @@
# kokoro-js
* Author: hexgrad
* NPM: <https://www.npmjs.com/package/kokoro-js>
* Version: 1.2.0
* License: Apache-2.0
Last updated: 2025-03-10

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,7 @@ Exported for use in extension index.js, and added to providers list in index.js
1. previewTtsVoice()
2. separator field
3. processText(text)
4. dispose()
# Requirement Descriptions
### generateTts(text, voiceId)
@@ -74,3 +75,7 @@ Defines the string of characters used to introduce separation between between th
### processText(text)
Optional.
A function applied to the input text before passing it to the TTS generator. Can be async.
### dispose()
Optional.
Function to handle cleanup of provider resources when the provider is switched.

View File

@@ -114,6 +114,7 @@ export const group_activation_strategy = {
NATURAL: 0,
LIST: 1,
MANUAL: 2,
POOLED: 3,
};
export const group_generation_mode = {
@@ -245,9 +246,9 @@ export async function getGroupChat(groupId, reload = false) {
}
chat.push(mes);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message');
addOneMessage(mes);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message');
}
}
await saveGroupChat(groupId, false);
@@ -880,6 +881,9 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
else if (activationStrategy === group_activation_strategy.LIST) {
activatedMembers = activateListOrder(enabledMembers);
}
else if (activationStrategy === group_activation_strategy.POOLED) {
activatedMembers = activatePooledOrder(enabledMembers, lastMessage);
}
else if (activationStrategy === group_activation_strategy.MANUAL && !isUserInput) {
activatedMembers = shuffle(enabledMembers).slice(0, 1).map(x => characters.findIndex(y => y.avatar === x)).filter(x => x !== -1);
}
@@ -1020,6 +1024,48 @@ function activateListOrder(members) {
return memberIds;
}
/**
* Activate group members based on the last message.
* @param {string[]} members List of member avatars
* @param {Object} lastMessage Last message
* @returns {number[]} List of character ids
*/
function activatePooledOrder(members, lastMessage) {
/** @type {string} */
let activatedMember = null;
/** @type {string[]} */
const spokenSinceUser = [];
for (const message of chat.slice().reverse()) {
if (message.is_user) {
break;
}
if (message.is_system || message.extra?.type === system_message_types.NARRATOR) {
continue;
}
if (message.original_avatar) {
spokenSinceUser.push(message.original_avatar);
}
}
const haveNotSpoken = members.filter(x => !spokenSinceUser.includes(x));
if (haveNotSpoken.length) {
activatedMember = haveNotSpoken[Math.floor(Math.random() * haveNotSpoken.length)];
}
if (activatedMember === null) {
const lastMessageAvatar = members.length > 1 && lastMessage && !lastMessage.is_user && lastMessage.original_avatar;
const randomPool = lastMessageAvatar ? members.filter(x => x !== lastMessage.original_avatar) : members;
activatedMember = randomPool[Math.floor(Math.random() * randomPool.length)];
}
const memberId = characters.findIndex(y => y.avatar === activatedMember);
return memberId !== -1 ? [memberId] : [];
}
function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) {
let activatedMembers = [];

View File

@@ -23,6 +23,7 @@ export const names_behavior_types = {
const controls = [
{ id: 'instruct_enabled', property: 'enabled', isCheckbox: true },
{ id: 'instruct_wrap', property: 'wrap', isCheckbox: true },
{ id: 'instruct_macro', property: 'macro', isCheckbox: true },
{ id: 'instruct_system_sequence_prefix', property: 'system_sequence_prefix', isCheckbox: false },
{ id: 'instruct_system_sequence_suffix', property: 'system_sequence_suffix', isCheckbox: false },
{ id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false },

View File

@@ -71,7 +71,7 @@ export {
export const MAX_CONTEXT_DEFAULT = 8192;
export const MAX_RESPONSE_DEFAULT = 2048;
const MAX_CONTEXT_UNLOCKED = 200 * 1024;
const MAX_RESPONSE_UNLOCKED = 16 * 1024;
const MAX_RESPONSE_UNLOCKED = 32 * 1024;
const unlockedMaxContextStep = 512;
const maxContextMin = 512;
const maxContextStep = 64;
@@ -492,7 +492,7 @@ function switchSwipeNumAllMessages() {
var originalSliderValues = [];
async function switchLabMode() {
async function switchLabMode({ noReset = false } = {}) {
/* if (power_user.enableZenSliders && power_user.enableLabMode) {
toastr.warning("Can't start Lab Mode while Zen Sliders are active")
@@ -522,12 +522,15 @@ async function switchLabMode() {
$('#labModeWarning').removeClass('displayNone');
//$("#advanced-ai-config-block input[type='range']").hide()
$('#amount_gen_counter').attr('min', '1')
.attr('max', '99999')
.attr('step', '1');
$('#amount_gen').attr('min', '1')
.attr('max', '99999')
.attr('step', '1');
} else {
} else if (!noReset) {
//re apply the original sliders values to each input
originalSliderValues.forEach(function (slider) {
$('#' + slider.id)
@@ -539,9 +542,8 @@ async function switchLabMode() {
$('#advanced-ai-config-block input[type=\'range\']').show();
$('#labModeWarning').addClass('displayNone');
$('#amount_gen').attr('min', '16')
.attr('max', '2048')
.attr('step', '1');
// To set the correct amount_gen back, we just call the function calculating it correctly
switchMaxContextSize();
}
}
@@ -1538,7 +1540,7 @@ async function loadPowerUserSettings(settings, data) {
$('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
$('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
$('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
$('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input');
$('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input', { fromInit: true });
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop('checked', true);
$(`#chat_display option[value=${power_user.chat_display}]`).attr('selected', true).trigger('change');
$('#chat_width_slider').val(power_user.chat_width);
@@ -3577,7 +3579,7 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#enableLabMode').on('input', function () {
$('#enableLabMode').on('input', function (event, { fromInit = false } = {}) {
const value = !!$(this).prop('checked');
if (power_user.enableZenSliders === true && value === true) {
//disallow Lab Mode if ZenSliders are active
@@ -3587,7 +3589,7 @@ $(document).ready(() => {
}
power_user.enableLabMode = value;
switchLabMode();
switchLabMode({ noReset: fromInit });
saveSettingsDebounced();
});

View File

@@ -1075,7 +1075,7 @@ export function removeReasoningFromString(str) {
* @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/
function parseReasoningFromString(str, { strict = true } = {}) {
export function parseReasoningFromString(str, { strict = true } = {}) {
// Both prefix and suffix must be defined
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
return null;

View File

@@ -667,11 +667,21 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'hide',
callback: hideMessageCallback,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'only hide messages from a certain character or persona',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.messageNames,
isRequired: false,
acceptsMultiple: false,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'message index (starts with 0) or range',
description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
isRequired: true,
isRequired: false,
enumProvider: commonEnumProviders.messages(),
}),
],
@@ -680,11 +690,21 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'unhide',
callback: unhideMessageCallback,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'only unhide messages from a certain character or persona',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.messageNames,
isRequired: false,
acceptsMultiple: false,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'message index (starts with 0) or range',
description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
isRequired: true,
isRequired: false,
enumProvider: commonEnumProviders.messages(),
}),
],
@@ -3034,37 +3054,29 @@ async function askCharacter(args, text) {
return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
}
async function hideMessageCallback(_, arg) {
if (!arg) {
console.warn('WARN: No argument provided for /hide command');
return '';
}
const range = stringToRange(arg, 0, chat.length - 1);
async function hideMessageCallback(args, value) {
const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
if (!range) {
console.warn(`WARN: Invalid range provided for /hide command: ${arg}`);
console.warn(`WARN: Invalid range provided for /hide command: ${value}`);
return '';
}
await hideChatMessageRange(range.start, range.end, false);
const nameFilter = String(args.name ?? '').trim();
await hideChatMessageRange(range.start, range.end, false, nameFilter);
return '';
}
async function unhideMessageCallback(_, arg) {
if (!arg) {
console.warn('WARN: No argument provided for /unhide command');
return '';
}
const range = stringToRange(arg, 0, chat.length - 1);
async function unhideMessageCallback(args, value) {
const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
if (!range) {
console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`);
console.warn(`WARN: Invalid range provided for /unhide command: ${value}`);
return '';
}
await hideChatMessageRange(range.start, range.end, true);
const nameFilter = String(args.name ?? '').trim();
await hideChatMessageRange(range.start, range.end, true, nameFilter);
return '';
}
@@ -3616,14 +3628,14 @@ export async function sendMessageAs(args, text) {
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
await saveChatConditional();
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt);
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt, 'command');
await reloadCurrentChat();
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt, 'command');
} else {
chat.push(message);
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'command');
addOneMessage(message);
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'command');
await saveChatConditional();
}

View File

@@ -3,6 +3,7 @@ import { extension_settings } from '../extensions.js';
import { getGroupMembers, groups } from '../group-chats.js';
import { power_user } from '../power-user.js';
import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js';
import { world_names } from '../world-info.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -251,14 +252,29 @@ export const commonEnumProviders = {
* @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names
* @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]}
*/
messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_, scope) => {
messages: ({ allowIdAfter = false, allowVars = false } = {}) => (executor, scope) => {
const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || '';
return [
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)),
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)).filter(value => !nameFilter || value.description.startsWith(`${nameFilter}:`)),
...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '')] : [],
...allowVars ? commonEnumProviders.variables('all')(_, scope) : [],
...allowVars ? commonEnumProviders.variables('all')(executor, scope) : [],
];
},
/**
* All names used in the current chat.
*
* @returns {SlashCommandEnumValue[]}
*/
messageNames: () => chat
.map(message => ({
name: message.name,
icon: message.is_user ? enumIcons.user : enumIcons.assistant,
}))
.filter(onlyUniqueJson)
.sort((a, b) => sortIgnoreCaseAndAccents(a.name, b.name))
.map(name => new SlashCommandEnumValue(name.name, null, null, name.icon)),
/**
* All existing worlds / lorebooks
*

View File

@@ -48,6 +48,7 @@ import {
printMessages,
clearChat,
unshallowCharacter,
deleteLastMessage,
} from '../script.js';
import {
extension_settings,
@@ -79,7 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { ChatCompletionService, TextCompletionService } from './custom-request.js';
import { updateReasoningUI } from './reasoning.js';
import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
export function getContext() {
return {
@@ -106,6 +107,7 @@ export function getContext() {
eventSource,
eventTypes: event_types,
addOneMessage,
deleteLastMessage,
generate: Generate,
sendStreamingRequest,
sendGenerationRequest,
@@ -147,6 +149,7 @@ export function getContext() {
unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
ToolManager,
registerDebugFunction,
/** @deprecated Use renderExtensionTemplateAsync instead. */
renderExtensionTemplate,
@@ -213,6 +216,7 @@ export function getContext() {
ChatCompletionService,
TextCompletionService,
updateReasoningUI,
parseReasoningFromString,
unshallowCharacter,
unshallowGroupMembers,
};

View File

@@ -1251,7 +1251,7 @@ function onCharacterCreateClick() {
}
function onGroupCreateClick() {
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
$('#groupTagList').empty();
}
export function applyTagsOnCharacterSelect(chid = null) {
@@ -1259,11 +1259,11 @@ export function applyTagsOnCharacterSelect(chid = null) {
if (menu_type === 'create') {
const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get();
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
printTagList($('#tagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
printTagList($('#tagList'), { forEntityOrKey: undefined, tags: currentTags, tagOptions: { removable: true } });
return;
}
chid = chid ?? Number(this_chid);
chid = chid ?? (this_chid !== undefined ? Number(this_chid) : undefined);
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
}
@@ -1272,11 +1272,11 @@ export function applyTagsOnGroupSelect(groupId = null) {
if (menu_type === 'group_create') {
const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get();
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
printTagList($('#groupTagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
printTagList($('#groupTagList'), { forEntityOrKey: undefined, tags: currentTags, tagOptions: { removable: true } });
return;
}
groupId = groupId ?? Number(selected_group);
groupId = groupId ?? (selected_group ? Number(selected_group) : undefined);
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
}

View File

@@ -6,7 +6,7 @@ import {
} from '../lib.js';
import { getContext } from './extensions.js';
import { characters, getRequestHeaders, this_chid } from '../script.js';
import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines, power_user } from './power-user.js';
import { debounce_timeout } from './constants.js';
@@ -14,7 +14,7 @@ import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { getTagsList } from './tags.js';
import { groups, selected_group } from './group-chats.js';
import { getCurrentLocale } from './i18n.js';
import { getCurrentLocale, t } from './i18n.js';
/**
* Pagination status string template.
@@ -196,6 +196,17 @@ export function onlyUnique(value, index, array) {
return array.indexOf(value) === index;
}
/**
* Determines if a value is unique in an array of objects.
* @param {any} value Current value.
* @param {number} index Current index.
* @param {any[]} array The array being processed.
* @returns {boolean} True if the value is unique, false otherwise.
*/
export function onlyUniqueJson(value, index, array) {
return array.map(v => JSON.stringify(v)).indexOf(JSON.stringify(value)) === index;
}
/**
* Removes the first occurrence of a specified item from an array
*
@@ -436,6 +447,33 @@ export function debounce(func, timeout = debounce_timeout.standard) {
return fn;
}
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {Function} func The function to debounce.
* @param {Number} [timeout=300] The timeout in milliseconds.
* @returns {Function} The debounced function.
*/
export function debounceAsync(func, timeout = debounce_timeout.standard) {
let timer;
/**@type {Promise}*/
let debouncePromise;
/**@type {Function}*/
let debounceResolver;
return (...args) => {
clearTimeout(timer);
if (!debouncePromise) {
debouncePromise = new Promise(resolve => {
debounceResolver = resolve;
});
}
timer = setTimeout(() => {
debounceResolver(func.apply(this, args));
debouncePromise = null;
}, timeout);
return debouncePromise;
};
}
/**
* Cancels a scheduled debounced function.
* Does nothing if the function is not debounced or not scheduled.
@@ -1772,8 +1810,9 @@ export function runAfterAnimation(control, callback, timeout = 500) {
*
* @param {string} a - The first string to compare.
* @param {string} b - The second string to compare.
* @param {(a:string,b:string)=>boolean} comparisonFunction - The function to use for the comparison.
* @returns {*} - The result of the comparison.
* @param {(a:string,b:string)=>T} comparisonFunction - The function to use for the comparison.
* @returns {T} - The result of the comparison.
* @template T
*/
export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) {
if (!a || !b) return comparisonFunction(a, b); // Return the comparison result if either string is empty
@@ -1810,6 +1849,16 @@ export function equalsIgnoreCaseAndAccents(a, b) {
return compareIgnoreCaseAndAccents(a, b, (a, b) => a === b);
}
/**
* Performs a case-insensitive and accent-insensitive sort.
* @param {string} a - The first string to compare
* @param {string} b - The second string to compare
* @returns {number} -1 if a < b, 1 if a > b, 0 if a === b
*/
export function sortIgnoreCaseAndAccents(a, b) {
return compareIgnoreCaseAndAccents(a, b, (a, b) => a?.localeCompare(b));
}
/**
* @typedef {object} Select2Option The option object for select2 controls
* @property {string} id - The unique ID inside this select
@@ -2169,6 +2218,48 @@ export async function showFontAwesomePicker(customList = null) {
return null;
}
/**
* Finds a persona by name, with optional filtering and precedence for avatars
* @param {object} [options={}] - The options for the search
* @param {string?} [options.name=null] - The name to search for
* @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
* @param {boolean} [options.preferCurrentPersona=true] - Whether to prefer the current persona(s)
* @param {boolean} [options.quiet=false] - Whether to suppress warnings
* @returns {PersonaViewModel} The persona object
* @typedef {object} PersonaViewModel
* @property {string} avatar - The avatar of the persona
* @property {string} name - The name of the persona
*/
export function findPersona({ name = null, allowAvatar = true, insensitive = true, preferCurrentPersona = true, quiet = false } = {}) {
/** @type {PersonaViewModel[]} */
const personas = Object.entries(power_user.personas).map(([avatar, name]) => ({ avatar, name }));
const matches = (/** @type {PersonaViewModel} */ persona) => !name || (allowAvatar && persona.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(persona.name, name) : persona.name === name);
// If we have a current persona and prefer it, return that if it matches
const currentPersona = personas.find(a => a.avatar === user_avatar);
if (preferCurrentPersona && currentPersona && matches(currentPersona)) {
return currentPersona;
}
// If allowAvatar is true, search by avatar first
if (allowAvatar && name) {
const personaByAvatar = personas.find(a => a.avatar === name);
if (personaByAvatar && matches(personaByAvatar)) {
return personaByAvatar;
}
}
// Search for matching personas by name
const matchingPersonas = personas.filter(a => matches(a));
if (matchingPersonas.length > 1) {
if (!quiet) toastr.warning(t`Multiple personas found for given conditions.`);
else console.warn(t`Multiple personas found for given conditions. Returning the first match.`);
}
return matchingPersonas[0] || null;
}
/**
* Finds a character by name, with optional filtering and precedence for avatars
* @param {object} [options={}] - The options for the search
@@ -2201,8 +2292,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
if (preferCurrentChar) {
const preferredCharSearch = currentChars.filter(matches);
if (preferredCharSearch.length > 1) {
if (!quiet) toastr.warning('Multiple characters found for given conditions.');
else console.warn('Multiple characters found for given conditions. Returning the first match.');
if (!quiet) toastr.warning(t`Multiple characters found for given conditions.`);
else console.warn(t`Multiple characters found for given conditions. Returning the first match.`);
}
if (preferredCharSearch.length) {
return preferredCharSearch[0];

View File

@@ -3542,6 +3542,10 @@ export async function deleteWorldInfo(worldInfoName) {
return false;
}
if (worldInfoCache.has(worldInfoName)) {
worldInfoCache.delete(worldInfoName);
}
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1);

165
tests/package-lock.json generated
View File

@@ -28,12 +28,13 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.24.7",
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
@@ -214,18 +215,18 @@
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -241,109 +242,26 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz",
"integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.24.7",
"@babel/types": "^7.24.7"
"@babel/template": "^7.26.9",
"@babel/types": "^7.26.10"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
"integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.26.10"
},
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -529,14 +447,14 @@
}
},
"node_modules/@babel/template": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
"version": "7.26.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7"
"@babel/code-frame": "^7.26.2",
"@babel/parser": "^7.26.9",
"@babel/types": "^7.26.9"
},
"engines": {
"node": ">=6.9.0"
@@ -564,14 +482,13 @@
}
},
"node_modules/@babel/types": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
"integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
"version": "7.26.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.24.7",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
"@babel/helper-string-parser": "^7.25.9",
"@babel/helper-validator-identifier": "^7.25.9"
},
"engines": {
"node": ">=6.9.0"
@@ -1653,9 +1570,10 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -5604,15 +5522,6 @@
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"license": "BSD-3-Clause"
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",