mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into parser-followup-2
This commit is contained in:
@@ -725,8 +725,14 @@ export function initRossMods() {
|
||||
RA_autoconnect();
|
||||
}
|
||||
|
||||
if (getParsedUA()?.os?.name === 'iOS') {
|
||||
document.body.classList.add('ios');
|
||||
const userAgent = getParsedUA();
|
||||
console.debug('User Agent', userAgent);
|
||||
const isMobileSafari = /iPad|iPhone|iPod/.test(navigator.platform) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
||||
const isDesktopSafari = userAgent?.browser?.name === 'Safari' && userAgent?.platform?.type === 'desktop';
|
||||
const isIOS = userAgent?.os?.name === 'iOS';
|
||||
|
||||
if (isIOS || isMobileSafari || isDesktopSafari) {
|
||||
document.body.classList.add('safari');
|
||||
}
|
||||
|
||||
$('#main_api').change(function () {
|
||||
|
@@ -185,18 +185,19 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const slug = getStringHash(file.name);
|
||||
const fileNamePrefix = `${Date.now()}_${slug}`;
|
||||
const fileBase64 = await getBase64Async(file);
|
||||
let base64Data = fileBase64.split(',')[1];
|
||||
|
||||
// If file is image
|
||||
if (file.type.startsWith('image/')) {
|
||||
const extension = file.type.split('/')[1];
|
||||
const imageUrl = await saveBase64AsFile(base64Data, name2, file.name, extension);
|
||||
const imageUrl = await saveBase64AsFile(base64Data, name2, fileNamePrefix, extension);
|
||||
message.extra.image = imageUrl;
|
||||
message.extra.inline_image = true;
|
||||
} else {
|
||||
const slug = getStringHash(file.name);
|
||||
const uniqueFileName = `${Date.now()}_${slug}.txt`;
|
||||
const uniqueFileName = `${fileNamePrefix}.txt`;
|
||||
|
||||
if (isConvertible(file.type)) {
|
||||
try {
|
||||
@@ -319,12 +320,10 @@ export function hasPendingFileAttachment() {
|
||||
|
||||
/**
|
||||
* Displays file information in the message sending form.
|
||||
* @param {File} file File object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onFileAttach() {
|
||||
const fileInput = document.getElementById('file_form_input');
|
||||
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||
const file = fileInput.files[0];
|
||||
async function onFileAttach(file) {
|
||||
if (!file) return;
|
||||
|
||||
const isValid = await validateFile(file);
|
||||
@@ -419,6 +418,7 @@ function embedMessageFile(messageId, messageBlock) {
|
||||
}
|
||||
|
||||
await populateFileAttachment(message, 'embed_file_input');
|
||||
await eventSource.emit(event_types.MESSAGE_FILE_EMBEDDED, messageId);
|
||||
appendMediaToMessage(message, messageBlock);
|
||||
await saveChatConditional();
|
||||
}
|
||||
@@ -616,6 +616,8 @@ async function deleteMessageImage() {
|
||||
const message = chat[mesId];
|
||||
delete message.extra.image;
|
||||
delete message.extra.inline_image;
|
||||
delete message.extra.title;
|
||||
delete message.extra.append_title;
|
||||
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
||||
mesBlock.find('.mes_img').attr('src', '');
|
||||
await saveChatConditional();
|
||||
@@ -1428,7 +1430,7 @@ jQuery(function () {
|
||||
wrapper.classList.add('flexFlowColumn', 'justifyCenter', 'alignitemscenter');
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = String(bro.val());
|
||||
textarea.classList.add('height100p', 'wide100p');
|
||||
textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
|
||||
bro.hasClass('monospace') && textarea.classList.add('monospace');
|
||||
textarea.addEventListener('input', function () {
|
||||
bro.val(textarea.value).trigger('input');
|
||||
@@ -1503,8 +1505,34 @@ jQuery(function () {
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$('#file_form_input').on('change', onFileAttach);
|
||||
$('#file_form_input').on('change', async () => {
|
||||
const fileInput = document.getElementById('file_form_input');
|
||||
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||
const file = fileInput.files[0];
|
||||
await onFileAttach(file);
|
||||
});
|
||||
$('#file_form').on('reset', function () {
|
||||
$('#file_form').addClass('displayNone');
|
||||
});
|
||||
|
||||
document.getElementById('send_textarea').addEventListener('paste', async function (event) {
|
||||
if (event.clipboardData.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const fileInput = document.getElementById('file_form_input');
|
||||
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||
|
||||
// Workaround for Firefox: Use a DataTransfer object to indirectly set fileInput.files
|
||||
const dataTransfer = new DataTransfer();
|
||||
for (let i = 0; i < event.clipboardData.files.length; i++) {
|
||||
dataTransfer.items.add(event.clipboardData.files[i]);
|
||||
}
|
||||
|
||||
fileInput.files = dataTransfer.files;
|
||||
await onFileAttach(fileInput.files[0]);
|
||||
});
|
||||
});
|
||||
|
@@ -5,6 +5,8 @@
|
||||
export const debounce_timeout = {
|
||||
/** [100 ms] For ultra-fast responses, typically for keypresses or executions that might happen multiple times in a loop or recursion. */
|
||||
quick: 100,
|
||||
/** [200 ms] Slightly slower than quick, but still very responsive. */
|
||||
short: 200,
|
||||
/** [300 ms] Default time for general use, good balance between responsiveness and performance. */
|
||||
standard: 300,
|
||||
/** [1.000 ms] For situations where the function triggers more intensive tasks. */
|
||||
|
@@ -154,7 +154,7 @@ export function initDynamicStyles() {
|
||||
// Process all stylesheets on initial load
|
||||
Array.from(document.styleSheets).forEach(sheet => {
|
||||
try {
|
||||
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href.toLowerCase().includes('scripts/extensions') });
|
||||
applyDynamicFocusStyles(sheet, { fromExtension: sheet.href?.toLowerCase().includes('scripts/extensions') == true });
|
||||
} catch (e) {
|
||||
console.warn('Failed to process stylesheet on initial load:', e);
|
||||
}
|
||||
|
@@ -374,7 +374,7 @@ async function addExtensionsButtonAndMenu() {
|
||||
|
||||
$('html').on('click', function (e) {
|
||||
const clickTarget = $(e.target);
|
||||
const noCloseTargets = ['#sd_gen', '#extensionsMenuButton'];
|
||||
const noCloseTargets = ['#sd_gen', '#extensionsMenuButton', '#roll_dice'];
|
||||
if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) {
|
||||
$(dropdown).fadeOut(animation_duration);
|
||||
}
|
||||
@@ -641,9 +641,16 @@ async function showExtensionsDetails() {
|
||||
action: async () => {
|
||||
requiresReload = true;
|
||||
await autoUpdateExtensions(true);
|
||||
popup.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
await popup.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
},
|
||||
};
|
||||
|
||||
// If we are updating an extension, the "old" popup is still active. We should close that.
|
||||
const oldPopup = Popup.util.popups.find(popup => popup.content.querySelector('.extensions_info'));
|
||||
if (oldPopup) {
|
||||
await oldPopup.complete(POPUP_RESULT.CANCELLED);
|
||||
}
|
||||
|
||||
const popup = new Popup(`<div class="extensions_info">${html}</div>`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true });
|
||||
popupPromise = popup.show();
|
||||
} catch (error) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
|
||||
import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
|
||||
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
@@ -84,12 +84,11 @@ async function setSpinnerIcon() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a captioned message to the chat.
|
||||
* @param {string} caption Caption text
|
||||
* @param {string} image Image URL
|
||||
* Wraps a caption with a message template.
|
||||
* @param {string} caption Raw caption
|
||||
* @returns {Promise<string>} Wrapped caption
|
||||
*/
|
||||
async function sendCaptionedMessage(caption, image) {
|
||||
const context = getContext();
|
||||
async function wrapCaptionTemplate(caption) {
|
||||
let template = extension_settings.caption.template || TEMPLATE_DEFAULT;
|
||||
|
||||
if (!/{{caption}}/i.test(template)) {
|
||||
@@ -101,7 +100,7 @@ async function sendCaptionedMessage(caption, image) {
|
||||
|
||||
if (extension_settings.caption.refine_mode) {
|
||||
messageText = await callPopup(
|
||||
'<h3>Review and edit the generated message:</h3>Press "Cancel" to abort the caption sending.',
|
||||
'<h3>Review and edit the generated caption:</h3>Press "Cancel" to abort the caption sending.',
|
||||
'input',
|
||||
messageText,
|
||||
{ rows: 5, okButton: 'Send' });
|
||||
@@ -111,6 +110,55 @@ async function sendCaptionedMessage(caption, image) {
|
||||
}
|
||||
}
|
||||
|
||||
return messageText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends caption to an existing message.
|
||||
* @param {Object} data Message data
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function captionExistingMessage(data) {
|
||||
if (!(data?.extra?.image)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageData = await fetch(data.extra.image);
|
||||
const blob = await imageData.blob();
|
||||
const type = imageData.headers.get('Content-Type');
|
||||
const file = new File([blob], 'image.png', { type });
|
||||
const caption = await getCaptionForFile(file, null, true);
|
||||
|
||||
if (!caption) {
|
||||
console.warn('Failed to generate a caption for the image.');
|
||||
return;
|
||||
}
|
||||
|
||||
const wrappedCaption = await wrapCaptionTemplate(caption);
|
||||
|
||||
const messageText = String(data.mes).trim();
|
||||
|
||||
if (!messageText) {
|
||||
data.extra.inline_image = false;
|
||||
data.mes = wrappedCaption;
|
||||
data.extra.title = wrappedCaption;
|
||||
}
|
||||
else {
|
||||
data.extra.inline_image = true;
|
||||
data.extra.append_title = true;
|
||||
data.extra.title = wrappedCaption;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a captioned message to the chat.
|
||||
* @param {string} caption Caption text
|
||||
* @param {string} image Image URL
|
||||
*/
|
||||
async function sendCaptionedMessage(caption, image) {
|
||||
const messageText = await wrapCaptionTemplate(caption);
|
||||
|
||||
const context = getContext();
|
||||
const message = {
|
||||
name: context.name1,
|
||||
is_user: true,
|
||||
@@ -356,6 +404,7 @@ jQuery(async function () {
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'koboldcpp' && textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'vllm' && textgenerationwebui_settings.server_urls[textgen_types.VLLM]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
|
||||
extension_settings.caption.source === 'local' ||
|
||||
extension_settings.caption.source === 'horde';
|
||||
@@ -408,7 +457,7 @@ jQuery(async function () {
|
||||
});
|
||||
}
|
||||
async function addSettings() {
|
||||
const html = await renderExtensionTemplateAsync('caption', 'settings');
|
||||
const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });
|
||||
$('#caption_container').append(html);
|
||||
}
|
||||
|
||||
@@ -422,6 +471,7 @@ jQuery(async function () {
|
||||
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
|
||||
$('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
|
||||
$('#caption_prompt_ask').prop('checked', !!(extension_settings.caption.prompt_ask));
|
||||
$('#caption_auto_mode').prop('checked', !!(extension_settings.caption.auto_mode));
|
||||
$('#caption_source').val(extension_settings.caption.source);
|
||||
$('#caption_prompt').val(extension_settings.caption.prompt);
|
||||
$('#caption_template').val(extension_settings.caption.template);
|
||||
@@ -447,6 +497,41 @@ jQuery(async function () {
|
||||
extension_settings.caption.prompt_ask = $('#caption_prompt_ask').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_auto_mode').on('input', () => {
|
||||
extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const onMessageEvent = async (index) => {
|
||||
if (!extension_settings.caption.auto_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = getContext().chat[index];
|
||||
await captionExistingMessage(data);
|
||||
};
|
||||
|
||||
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
|
||||
eventSource.on(event_types.MESSAGE_FILE_EMBEDDED, onMessageEvent);
|
||||
|
||||
$(document).on('click', '.mes_img_caption', async function () {
|
||||
const animationClass = 'fa-fade';
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
const messageImg = messageBlock.find('.mes_img');
|
||||
if (messageImg.hasClass(animationClass)) return;
|
||||
messageImg.addClass(animationClass);
|
||||
try {
|
||||
const index = Number(messageBlock.attr('mesid'));
|
||||
const data = getContext().chat[index];
|
||||
await captionExistingMessage(data);
|
||||
appendMediaToMessage(data, messageBlock, false);
|
||||
await saveChatConditional();
|
||||
} catch(e) {
|
||||
console.error('Message image recaption failed', e);
|
||||
} finally {
|
||||
messageImg.removeClass(animationClass);
|
||||
}
|
||||
});
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||
callback: captionCommandCallback,
|
||||
@@ -482,4 +567,6 @@ jQuery(async function () {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
|
||||
document.body.classList.add('caption');
|
||||
});
|
||||
|
@@ -26,6 +26,7 @@
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
|
||||
<option value="vllm">vLLM</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
@@ -66,6 +67,7 @@
|
||||
<option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
|
||||
<option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -82,14 +84,19 @@
|
||||
</div>
|
||||
<div id="caption_prompt_block">
|
||||
<label for="caption_prompt" data-i18n="Caption Prompt">Caption Prompt</label>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">${PROMPT_DEFAULT}</textarea>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">{{PROMPT_DEFAULT}}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
|
||||
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Ask every time">Ask every time</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="caption_template"><span data-i18n="Message Template">Message Template</span> <small><span data-i18n="(use _space">(use </span> <code>{{caption}}</code> <span data-i18n="macro)">macro)</span></small></label>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">${TEMPLATE_DEFAULT}</textarea>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">{{TEMPLATE_DEFAULT}}</textarea>
|
||||
<label class="checkbox_label" for="caption_auto_mode">
|
||||
<input id="caption_auto_mode" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Automatically caption images">Automatically caption images</span>
|
||||
<i class="fa-solid fa-info-circle" title="Automatically caption images when they are pasted into the chat or attached to messages."></i>
|
||||
</label>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
|
||||
<input id="caption_refine_mode" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Edit captions before saving">Edit captions before saving</span>
|
||||
|
@@ -34,6 +34,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
const isCustom = extension_settings.caption.multimodal_api === 'custom';
|
||||
const isOoba = extension_settings.caption.multimodal_api === 'ooba';
|
||||
const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp';
|
||||
const isVllm = extension_settings.caption.multimodal_api === 'vllm';
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
|
||||
@@ -65,6 +66,14 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
|
||||
}
|
||||
|
||||
if (isVllm) {
|
||||
if (extension_settings.caption.multimodal_model === 'vllm_current') {
|
||||
requestBody.model = textgenerationwebui_settings.vllm_model;
|
||||
}
|
||||
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
|
||||
}
|
||||
|
||||
if (isLlamaCpp) {
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
|
||||
}
|
||||
@@ -151,6 +160,14 @@ function throwIfInvalidModel(useReverseProxy) {
|
||||
throw new Error('KoboldCpp server URL is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM]) {
|
||||
throw new Error('vLLM server URL is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'vllm' && extension_settings.caption.multimodal_model === 'vllm_current' && !textgenerationwebui_settings.vllm_model) {
|
||||
throw new Error('vLLM model is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
|
||||
throw new Error('Custom API URL is not set.');
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ import {
|
||||
systemUserName,
|
||||
hideSwipeButtons,
|
||||
showSwipeButtons,
|
||||
callPopup,
|
||||
getRequestHeaders,
|
||||
event_types,
|
||||
eventSource,
|
||||
@@ -29,10 +28,9 @@ import { getMultimodalCaption } from '../shared.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'sd';
|
||||
@@ -572,7 +570,7 @@ async function onDeleteStyleClick() {
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = await callPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, 'confirm', '', { okButton: 'Delete' });
|
||||
const confirmed = await callGenericPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel' });
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@@ -601,7 +599,7 @@ async function onDeleteStyleClick() {
|
||||
}
|
||||
|
||||
async function onSaveStyleClick() {
|
||||
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
|
||||
const userInput = await callGenericPopup('Enter style name:', POPUP_TYPE.INPUT);
|
||||
|
||||
if (!userInput) {
|
||||
return;
|
||||
@@ -670,7 +668,7 @@ async function refinePrompt(prompt, allowExpand, isNegative = false) {
|
||||
|
||||
if (extension_settings.sd.refine_mode) {
|
||||
const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
|
||||
const refinedPrompt = await callPopup(text + 'Press "Cancel" to abort the image generation.', 'input', prompt.trim(), { rows: 5, okButton: 'Continue' });
|
||||
const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
|
||||
|
||||
if (refinedPrompt) {
|
||||
return refinedPrompt;
|
||||
@@ -2918,25 +2916,25 @@ async function generateComfyImage(prompt, negativePrompt) {
|
||||
const text = await workflowResponse.text();
|
||||
toastr.error(`Failed to load workflow.\n\n${text}`);
|
||||
}
|
||||
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
|
||||
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
|
||||
let workflow = (await workflowResponse.json()).replaceAll('"%prompt%"', JSON.stringify(prompt));
|
||||
workflow = workflow.replaceAll('"%negative_prompt%"', JSON.stringify(negativePrompt));
|
||||
|
||||
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
|
||||
placeholders.forEach(ph => {
|
||||
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
|
||||
workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
|
||||
});
|
||||
(extension_settings.sd.comfy_placeholders ?? []).forEach(ph => {
|
||||
workflow = workflow.replace(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
|
||||
workflow = workflow.replaceAll(`"%${ph.find}%"`, JSON.stringify(substituteParams(ph.replace)));
|
||||
});
|
||||
if (/%user_avatar%/gi.test(workflow)) {
|
||||
const response = await fetch(getUserAvatarUrl());
|
||||
if (response.ok) {
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(avatarBase64));
|
||||
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64));
|
||||
} else {
|
||||
workflow = workflow.replace('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
}
|
||||
}
|
||||
if (/%char_avatar%/gi.test(workflow)) {
|
||||
@@ -2944,9 +2942,9 @@ async function generateComfyImage(prompt, negativePrompt) {
|
||||
if (response.ok) {
|
||||
const avatarBlob = await response.blob();
|
||||
const avatarBase64 = await getBase64Async(avatarBlob);
|
||||
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(avatarBase64));
|
||||
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64));
|
||||
} else {
|
||||
workflow = workflow.replace('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL));
|
||||
}
|
||||
}
|
||||
console.log(`{
|
||||
@@ -2978,7 +2976,7 @@ async function onComfyOpenWorkflowEditorClick() {
|
||||
}),
|
||||
})).json();
|
||||
const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html'));
|
||||
const popupResult = callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save', wide: true, large: true, rows: 1 });
|
||||
const popupResult = callGenericPopup(editorHtml, POPUP_TYPE.CONFIRM, '', { okButton: 'Save', cancelButton: 'Cancel', wide: true, large: true });
|
||||
const checkPlaceholders = () => {
|
||||
workflow = $('#sd_comfy_workflow_editor_workflow').val().toString();
|
||||
$('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function (idx) {
|
||||
@@ -3058,7 +3056,7 @@ async function onComfyOpenWorkflowEditorClick() {
|
||||
}
|
||||
|
||||
async function onComfyNewWorkflowClick() {
|
||||
let name = await callPopup('<h3>Workflow name:</h3>', 'input');
|
||||
let name = await callGenericPopup('Workflow name:', POPUP_TYPE.INPUT);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
@@ -3085,7 +3083,7 @@ async function onComfyNewWorkflowClick() {
|
||||
}
|
||||
|
||||
async function onComfyDeleteWorkflowClick() {
|
||||
const confirm = await callPopup('Delete the workflow? This action is irreversible.', 'confirm');
|
||||
const confirm = await callGenericPopup('Delete the workflow? This action is irreversible.', POPUP_TYPE.CONFIRM, '', { okButton: 'Delete', cancelButton: 'Cancel' });
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { callPopup, getRequestHeaders } from '../../../script.js';
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
export { AzureTtsProvider };
|
||||
@@ -69,13 +70,13 @@ class AzureTtsProvider {
|
||||
const popupText = 'Azure TTS API Key';
|
||||
const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
|
||||
|
||||
const key = await callPopup(popupText, 'input', savedKey);
|
||||
const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedKey);
|
||||
|
||||
if (key == false || key == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSecret(SECRET_KEYS.AZURE_TTS, key);
|
||||
await writeSecret(SECRET_KEYS.AZURE_TTS, String(key));
|
||||
|
||||
toastr.success('API Key saved');
|
||||
$('#azure_tts_key').addClass('success');
|
||||
|
@@ -5,8 +5,8 @@ TODO:
|
||||
*/
|
||||
|
||||
import { doExtrasFetch, extension_settings, getApiUrl, modules } from '../../extensions.js';
|
||||
import { callPopup } from '../../../script.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
|
||||
export { CoquiTtsProvider };
|
||||
|
||||
@@ -246,7 +246,7 @@ class CoquiTtsProvider {
|
||||
}
|
||||
|
||||
// Ask user for voiceId name to save voice
|
||||
const voiceName = await callPopup('<h3>Name of Coqui voice to add to voice select dropdown:</h3>', 'input');
|
||||
const voiceName = await callGenericPopup('Name of Coqui voice to add to voice select dropdown:', POPUP_TYPE.INPUT);
|
||||
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { cancelTtsPlay, eventSource, event_types, getCurrentChatId, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
|
||||
import { EdgeTtsProvider } from './edge.js';
|
||||
@@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js';
|
||||
import { power_user } from '../../power-user.js';
|
||||
import { OpenAITtsProvider } from './openai.js';
|
||||
import { XTTSTtsProvider } from './xtts.js';
|
||||
import { VITSTtsProvider } from './vits.js';
|
||||
import { GSVITtsProvider } from './gsvi.js';
|
||||
import { SBVits2TtsProvider } from './sbvits2.js';
|
||||
import { AllTalkTtsProvider } from './alltalk.js';
|
||||
@@ -34,6 +35,7 @@ let lastMessage = null;
|
||||
let lastMessageHash = null;
|
||||
let periodicMessageGenerationTimer = null;
|
||||
let lastPositionOfParagraphEnd = -1;
|
||||
let currentInitVoiceMapPromise = null;
|
||||
|
||||
const DEFAULT_VOICE_MARKER = '[Default Voice]';
|
||||
const DISABLED_VOICE_MARKER = 'disabled';
|
||||
@@ -83,6 +85,7 @@ const ttsProviders = {
|
||||
ElevenLabs: ElevenLabsTtsProvider,
|
||||
Silero: SileroTtsProvider,
|
||||
XTTSv2: XTTSTtsProvider,
|
||||
VITS: VITSTtsProvider,
|
||||
GSVI: GSVITtsProvider,
|
||||
SBVits2: SBVits2TtsProvider,
|
||||
System: SystemTtsProvider,
|
||||
@@ -1008,9 +1011,39 @@ class VoiceMapEntry {
|
||||
|
||||
/**
|
||||
* Init voiceMapEntries for character select list.
|
||||
* If an initialization is already in progress, it returns the existing Promise instead of starting a new one.
|
||||
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
|
||||
* @returns {Promise} A promise that resolves when the initialization is complete.
|
||||
*/
|
||||
export async function initVoiceMap(unrestricted = false) {
|
||||
// Preventing parallel execution
|
||||
if (currentInitVoiceMapPromise) {
|
||||
return currentInitVoiceMapPromise;
|
||||
}
|
||||
|
||||
currentInitVoiceMapPromise = (async () => {
|
||||
const initialChatId = getCurrentChatId();
|
||||
try {
|
||||
await initVoiceMapInternal(unrestricted);
|
||||
} finally {
|
||||
currentInitVoiceMapPromise = null;
|
||||
}
|
||||
const currentChatId = getCurrentChatId();
|
||||
|
||||
if (initialChatId !== currentChatId) {
|
||||
// Chat changed during initialization, reinitialize
|
||||
await initVoiceMap(unrestricted);
|
||||
}
|
||||
})();
|
||||
|
||||
return currentInitVoiceMapPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init voiceMapEntries for character select list.
|
||||
* @param {boolean} unrestricted - If true, will include all characters in voiceMapEntries, even if they are not in the current chat.
|
||||
*/
|
||||
async function initVoiceMapInternal(unrestricted) {
|
||||
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
|
||||
const enabled = $('#tts_enabled').is(':checked');
|
||||
if (!enabled) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { getRequestHeaders, callPopup } from '../../../script.js';
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
@@ -56,7 +57,7 @@ class NovelTtsProvider {
|
||||
|
||||
// Add a new Novel custom voice to provider
|
||||
async addCustomVoice() {
|
||||
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input');
|
||||
const voiceName = await callGenericPopup('Custom Voice name:', POPUP_TYPE.INPUT);
|
||||
this.settings.customVoices.push(voiceName);
|
||||
this.populateCustomVoices();
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
|
404
public/scripts/extensions/tts/vits.js
Normal file
404
public/scripts/extensions/tts/vits.js
Normal file
@@ -0,0 +1,404 @@
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { VITSTtsProvider };
|
||||
|
||||
class VITSTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings;
|
||||
ready = false;
|
||||
voices = [];
|
||||
separator = '. ';
|
||||
audioElement = document.createElement('audio');
|
||||
|
||||
/**
|
||||
* Perform any text processing before passing to TTS engine.
|
||||
* @param {string} text Input text
|
||||
* @returns {string} Processed text
|
||||
*/
|
||||
processText(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
|
||||
|
||||
languageLabels = {
|
||||
'Auto': 'auto',
|
||||
'Chinese': 'zh',
|
||||
'English': 'en',
|
||||
'Japanese': 'ja',
|
||||
'Korean': 'ko',
|
||||
};
|
||||
|
||||
langKey2LangCode = {
|
||||
'zh': 'zh-CN',
|
||||
'en': 'en-US',
|
||||
'ja': 'ja-JP',
|
||||
'ko': 'ko-KR',
|
||||
};
|
||||
|
||||
modelTypes = {
|
||||
VITS: 'VITS',
|
||||
W2V2_VITS: 'W2V2-VITS',
|
||||
BERT_VITS2: 'BERT-VITS2',
|
||||
};
|
||||
|
||||
defaultSettings = {
|
||||
provider_endpoint: 'http://localhost:23456',
|
||||
format: 'wav',
|
||||
lang: 'auto',
|
||||
length: 1.0,
|
||||
noise: 0.33,
|
||||
noisew: 0.4,
|
||||
segment_size: 50,
|
||||
streaming: false,
|
||||
dim_emotion: 0,
|
||||
sdp_ratio: 0.2,
|
||||
emotion: 0,
|
||||
text_prompt: '',
|
||||
style_text: '',
|
||||
style_weight: 1,
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
<label for="vits_lang">Text Language</label>
|
||||
<select id="vits_lang">`;
|
||||
|
||||
for (let language in this.languageLabels) {
|
||||
if (this.languageLabels[language] == this.settings?.lang) {
|
||||
html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
|
||||
continue;
|
||||
}
|
||||
html += `<option value="${this.languageLabels[language]}">${language}</option>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</select>
|
||||
<label>VITS / W2V2-VITS / Bert-VITS2 Settings:</label><br/>
|
||||
<label for="vits_endpoint">Provider Endpoint:</label>
|
||||
<input id="vits_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
|
||||
<span>Use <a target="_blank" href="https://github.com/Artrajz/vits-simple-api">vits-simple-api</a>.</span><br/>
|
||||
|
||||
<label for="vits_format">Audio format:</label>
|
||||
<select id="vits_format">`;
|
||||
|
||||
for (let format of this.audioFormats) {
|
||||
if (format == this.settings?.format) {
|
||||
html += `<option value="${format}" selected="selected">${format}</option>`;
|
||||
continue;
|
||||
}
|
||||
html += `<option value="${format}">${format}</option>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</select>
|
||||
<label for="vits_length">Audio length: <span id="vits_length_output">${this.defaultSettings.length}</span></label>
|
||||
<input id="vits_length" type="range" value="${this.defaultSettings.length}" min="0.0" max="5" step="0.01" />
|
||||
|
||||
<label for="vits_noise">Noise: <span id="vits_noise_output">${this.defaultSettings.noise}</span></label>
|
||||
<input id="vits_noise" type="range" value="${this.defaultSettings.noise}" min="0.1" max="2" step="0.01" />
|
||||
|
||||
<label for="vits_noisew">SDP noise: <span id="vits_noisew_output">${this.defaultSettings.noisew}</span></label>
|
||||
<input id="vits_noisew" type="range" value="${this.defaultSettings.noisew}" min="0.1" max="2" step="0.01" />
|
||||
|
||||
<label for="vits_segment_size">Segment Size: <span id="vits_segment_size_output">${this.defaultSettings.segment_size}</span></label>
|
||||
<input id="vits_segment_size" type="range" value="${this.defaultSettings.segment_size}" min="0" max="1000" step="1" />
|
||||
|
||||
<label for="vits_streaming" class="checkbox_label">
|
||||
<input id="vits_streaming" type="checkbox" />
|
||||
<span>Streaming</span>
|
||||
</label>
|
||||
|
||||
<label>W2V2-VITS Settings:</label><br/>
|
||||
<label for="vits_dim_emotion">Dimensional emotion:</label>
|
||||
<input id="vits_dim_emotion" type="number" class="text_pole" min="0" max="5457" step="1" value="${this.defaultSettings.dim_emotion}"/>
|
||||
|
||||
<label>BERT-VITS2 Settings:</label><br/>
|
||||
<label for="vits_sdp_ratio">sdp_ratio: <span id="vits_sdp_ratio_output">${this.defaultSettings.sdp_ratio}</span></label>
|
||||
<input id="vits_sdp_ratio" type="range" value="${this.defaultSettings.sdp_ratio}" min="0.0" max="1" step="0.01" />
|
||||
|
||||
<label for="vits_emotion">emotion: <span id="vits_emotion_output">${this.defaultSettings.emotion}</span></label>
|
||||
<input id="vits_emotion" type="range" value="${this.defaultSettings.emotion}" min="0" max="9" step="1" />
|
||||
|
||||
<label for="vits_text_prompt">Text Prompt:</label>
|
||||
<input id="vits_text_prompt" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.text_prompt}"/>
|
||||
|
||||
<label for="vits_style_text">Style text:</label>
|
||||
<input id="vits_style_text" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.style_text}"/>
|
||||
|
||||
<label for="vits_style_weight">Style weight <span id="vits_style_weight_output">${this.defaultSettings.style_weight}</span></label>
|
||||
<input id="vits_style_weight" type="range" value="${this.defaultSettings.style_weight}" min="0" max="1" step="0.01" />
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
// Used when provider settings are updated from UI
|
||||
this.settings.provider_endpoint = $('#vits_endpoint').val();
|
||||
this.settings.lang = $('#vits_lang').val();
|
||||
this.settings.format = $('#vits_format').val();
|
||||
this.settings.dim_emotion = $('#vits_dim_emotion').val();
|
||||
this.settings.text_prompt = $('#vits_text_prompt').val();
|
||||
this.settings.style_text = $('#vits_style_text').val();
|
||||
|
||||
// Update the default TTS settings based on input fields
|
||||
this.settings.length = $('#vits_length').val();
|
||||
this.settings.noise = $('#vits_noise').val();
|
||||
this.settings.noisew = $('#vits_noisew').val();
|
||||
this.settings.segment_size = $('#vits_segment_size').val();
|
||||
this.settings.streaming = $('#vits_streaming').is(':checked');
|
||||
this.settings.sdp_ratio = $('#vits_sdp_ratio').val();
|
||||
this.settings.emotion = $('#vits_emotion').val();
|
||||
this.settings.style_weight = $('#vits_style_weight').val();
|
||||
|
||||
// Update the UI to reflect changes
|
||||
$('#vits_length_output').text(this.settings.length);
|
||||
$('#vits_noise_output').text(this.settings.noise);
|
||||
$('#vits_noisew_output').text(this.settings.noisew);
|
||||
$('#vits_segment_size_output').text(this.settings.segment_size);
|
||||
$('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
|
||||
$('#vits_emotion_output').text(this.settings.emotion);
|
||||
$('#vits_style_weight_output').text(this.settings.style_weight);
|
||||
|
||||
saveTtsProviderSettings();
|
||||
this.changeTTSSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
console.debug(`Ignoring non-user-configurable setting: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial values from the settings
|
||||
$('#vits_endpoint').val(this.settings.provider_endpoint);
|
||||
$('#vits_lang').val(this.settings.lang);
|
||||
$('#vits_format').val(this.settings.format);
|
||||
$('#vits_length').val(this.settings.length);
|
||||
$('#vits_noise').val(this.settings.noise);
|
||||
$('#vits_noisew').val(this.settings.noisew);
|
||||
$('#vits_segment_size').val(this.settings.segment_size);
|
||||
$('#vits_streaming').prop('checked', this.settings.streaming);
|
||||
$('#vits_dim_emotion').val(this.settings.dim_emotion);
|
||||
$('#vits_sdp_ratio').val(this.settings.sdp_ratio);
|
||||
$('#vits_emotion').val(this.settings.emotion);
|
||||
$('#vits_text_prompt').val(this.settings.text_prompt);
|
||||
$('#vits_style_text').val(this.settings.style_text);
|
||||
$('#vits_style_weight').val(this.settings.style_weight);
|
||||
|
||||
// Update the UI to reflect changes
|
||||
$('#vits_length_output').text(this.settings.length);
|
||||
$('#vits_noise_output').text(this.settings.noise);
|
||||
$('#vits_noisew_output').text(this.settings.noisew);
|
||||
$('#vits_segment_size_output').text(this.settings.segment_size);
|
||||
$('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
|
||||
$('#vits_emotion_output').text(this.settings.emotion);
|
||||
$('#vits_style_weight_output').text(this.settings.style_weight);
|
||||
|
||||
// Register input/change event listeners to update settings on user interaction
|
||||
$('#vits_endpoint').on('input', () => { this.onSettingsChange(); });
|
||||
$('#vits_lang').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_format').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_length').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_noise').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_noisew').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_segment_size').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_streaming').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_dim_emotion').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_sdp_ratio').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_emotion').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_text_prompt').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_style_text').on('change', () => { this.onSettingsChange(); });
|
||||
$('#vits_style_weight').on('change', () => { this.onSettingsChange(); });
|
||||
|
||||
await this.checkReady();
|
||||
|
||||
console.info('VITS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady() {
|
||||
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
// TTS Interfaces //
|
||||
//#################//
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
v => v.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
async getVoiceById(voiceId) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
v => v.voice_id == voiceId,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice id ${voiceId} not found`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
const response = await fetch(`${this.settings.provider_endpoint}/voice/speakers`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
|
||||
}
|
||||
const jsonData = await response.json();
|
||||
const voices = [];
|
||||
|
||||
const addVoices = (modelType) => {
|
||||
jsonData[modelType].forEach(voice => {
|
||||
voices.push({
|
||||
name: `[${modelType}] ${voice.name} (${voice.lang})`,
|
||||
voice_id: `${modelType}&${voice.id}`,
|
||||
preview_url: false,
|
||||
lang: voice.lang,
|
||||
});
|
||||
});
|
||||
};
|
||||
for (const key in this.modelTypes) {
|
||||
addVoices(this.modelTypes[key]);
|
||||
}
|
||||
|
||||
this.voices = voices; // Assign to the class property
|
||||
return voices; // Also return this list
|
||||
}
|
||||
|
||||
// Each time a parameter is changed, we change the configuration
|
||||
async changeTTSSettings() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch TTS generation from the API.
|
||||
* @param {string} inputText Text to generate TTS for
|
||||
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
|
||||
* @returns {Promise<Response|string>} Fetch response
|
||||
*/
|
||||
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
|
||||
const streaming = !forceNoStreaming && this.settings.streaming;
|
||||
const [model_type, speaker_id] = voiceId.split('&');
|
||||
const params = new URLSearchParams();
|
||||
params.append('text', inputText);
|
||||
params.append('id', speaker_id);
|
||||
if (streaming) {
|
||||
params.append('streaming', streaming);
|
||||
// Streaming response only supports MP3
|
||||
}
|
||||
else {
|
||||
params.append('format', this.settings.format);
|
||||
}
|
||||
params.append('lang', lang ?? this.settings.lang);
|
||||
params.append('length', this.settings.length);
|
||||
params.append('noise', this.settings.noise);
|
||||
params.append('noisew', this.settings.noisew);
|
||||
params.append('segment_size', this.settings.segment_size);
|
||||
|
||||
if (model_type == this.modelTypes.W2V2_VITS) {
|
||||
params.append('emotion', this.settings.dim_emotion);
|
||||
}
|
||||
else if (model_type == this.modelTypes.BERT_VITS2) {
|
||||
params.append('sdp_ratio', this.settings.sdp_ratio);
|
||||
params.append('emotion', this.settings.emotion);
|
||||
if (this.settings.text_prompt) {
|
||||
params.append('text_prompt', this.settings.text_prompt);
|
||||
}
|
||||
if (this.settings.style_text) {
|
||||
params.append('style_text', this.settings.style_text);
|
||||
params.append('style_weight', this.settings.style_weight);
|
||||
}
|
||||
}
|
||||
|
||||
const url = `${this.settings.provider_endpoint}/voice/${model_type.toLowerCase()}`;
|
||||
|
||||
if (streaming) {
|
||||
return url + `?${params.toString()}`;
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: params,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview TTS for a given voice ID.
|
||||
* @param {string} id Voice ID
|
||||
*/
|
||||
async previewTtsVoice(id) {
|
||||
this.audioElement.pause();
|
||||
this.audioElement.currentTime = 0;
|
||||
const voice = await this.getVoiceById(id);
|
||||
const lang = voice.lang.includes(this.settings.lang) ? this.settings.lang : voice.lang[0];
|
||||
|
||||
let lang_code = this.langKey2LangCode[lang];
|
||||
const text = getPreviewString(lang_code);
|
||||
const response = await this.fetchTtsGeneration(text, id, lang, true);
|
||||
if (typeof response != 'string') {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
const audio = await response.blob();
|
||||
const url = URL.createObjectURL(audio);
|
||||
this.audioElement.src = url;
|
||||
this.audioElement.play();
|
||||
}
|
||||
}
|
||||
|
||||
// Interface not used
|
||||
async fetchTtsFromHistory(history_item_id) {
|
||||
return Promise.resolve(history_item_id);
|
||||
}
|
||||
}
|
@@ -178,8 +178,37 @@ async function loadGroupChat(chatId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function validateGroup(group) {
|
||||
if (!group) return;
|
||||
|
||||
// Validate that all members exist as characters
|
||||
let dirty = false;
|
||||
group.members = group.members.filter(member => {
|
||||
const character = characters.find(x => x.avatar === member || x.name === member);
|
||||
if (!character) {
|
||||
const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
|
||||
toastr.warning(msg, 'Group Validation');
|
||||
console.warn(msg);
|
||||
dirty = true;
|
||||
}
|
||||
return character;
|
||||
});
|
||||
|
||||
if (dirty) {
|
||||
await editGroup(group.id, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGroupChat(groupId, reload = false) {
|
||||
const group = groups.find((x) => x.id === groupId);
|
||||
if (!group) {
|
||||
console.warn('Group not found', groupId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run validation before any loading
|
||||
validateGroup(group);
|
||||
|
||||
const chat_id = group.chat_id;
|
||||
const data = await loadGroupChat(chat_id);
|
||||
let freshChat = false;
|
||||
@@ -197,7 +226,6 @@ export async function getGroupChat(groupId, reload = false) {
|
||||
if (group && Array.isArray(group.members)) {
|
||||
for (let member of group.members) {
|
||||
const character = characters.find(x => x.avatar === member || x.name === member);
|
||||
|
||||
if (!character) {
|
||||
continue;
|
||||
}
|
||||
@@ -219,10 +247,8 @@ export async function getGroupChat(groupId, reload = false) {
|
||||
freshChat = true;
|
||||
}
|
||||
|
||||
if (group) {
|
||||
let metadata = group.chat_metadata ?? {};
|
||||
updateChatMetadata(metadata, true);
|
||||
}
|
||||
let metadata = group.chat_metadata ?? {};
|
||||
updateChatMetadata(metadata, true);
|
||||
|
||||
if (reload) {
|
||||
select_group_chats(groupId, true);
|
||||
@@ -1576,6 +1602,8 @@ export async function openGroupById(groupId) {
|
||||
}
|
||||
|
||||
if (!is_send_press && !is_group_generating) {
|
||||
select_group_chats(groupId);
|
||||
|
||||
if (selected_group !== groupId) {
|
||||
await clearChat();
|
||||
cancelTtsPlay();
|
||||
@@ -1587,8 +1615,6 @@ export async function openGroupById(groupId) {
|
||||
chat.length = 0;
|
||||
await getGroupChat(groupId);
|
||||
}
|
||||
|
||||
select_group_chats(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,71 @@ const langs = await fetch('/locales/lang.json').then(response => response.json()
|
||||
// eslint-disable-next-line prefer-const
|
||||
var localeData = await getLocaleData(localeFile);
|
||||
|
||||
/**
|
||||
* An observer that will check if any new i18n elements are added to the document
|
||||
* @type {MutationObserver}
|
||||
*/
|
||||
const observer = new MutationObserver(mutations => {
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node instanceof Element) {
|
||||
if (node.hasAttribute('data-i18n')) {
|
||||
translateElement(node);
|
||||
}
|
||||
node.querySelectorAll('[data-i18n]').forEach(element => {
|
||||
translateElement(element);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (mutation.attributeName === 'data-i18n' && mutation.target instanceof Element) {
|
||||
translateElement(mutation.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Translates a template string with named arguments
|
||||
*
|
||||
* Uses the template literal with all values replaced by index placeholder for translation key.
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* toastr.warn(t`Tag ${tagName} not found.`);
|
||||
* ```
|
||||
* Should be translated in the translation files as:
|
||||
* ```
|
||||
* Tag ${0} not found. -> Tag ${0} nicht gefunden.
|
||||
* ```
|
||||
*
|
||||
* @param {TemplateStringsArray} strings - Template strings array
|
||||
* @param {...any} values - Values for placeholders in the template string
|
||||
* @returns {string} Translated and formatted string
|
||||
*/
|
||||
export function t(strings, ...values) {
|
||||
let str = strings.reduce((result, string, i) => result + string + (values[i] !== undefined ? `\${${i}}` : ''), '');
|
||||
let translatedStr = translate(str);
|
||||
|
||||
// Replace indexed placeholders with actual values
|
||||
return translatedStr.replace(/\$\{(\d+)\}/g, (match, index) => values[index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given key or text
|
||||
*
|
||||
* If the translation is based on a key, that one is used to find a possible translation in the translation file.
|
||||
* The original text still has to be provided, as that is the default value being returned if no translation is found.
|
||||
*
|
||||
* For in-code text translation on a format string, using the template literal `t` is preferred.
|
||||
*
|
||||
* @param {string} text - The text to translate
|
||||
* @param {string?} key - The key to use for translation. If not provided, text is used as the key.
|
||||
* @returns {string} - The translated text
|
||||
*/
|
||||
export function translate(text, key = null) {
|
||||
const translationKey = key || text;
|
||||
return localeData?.[translationKey] || text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the locale data for the given language.
|
||||
* @param {string} language Language code
|
||||
@@ -40,6 +105,29 @@ function findLang(language) {
|
||||
return supportedLang;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a given element based on its data-i18n attribute.
|
||||
* @param {Element} element The element to translate
|
||||
*/
|
||||
function translateElement(element) {
|
||||
const keys = element.getAttribute('data-i18n').split(';'); // Multi-key entries are ; delimited
|
||||
for (const key of keys) {
|
||||
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
|
||||
if (attributeMatch) { // attribute-tagged key
|
||||
const localizedValue = localeData?.[attributeMatch[2]];
|
||||
if (localizedValue || localizedValue === '') {
|
||||
element.setAttribute(attributeMatch[1], localizedValue);
|
||||
}
|
||||
} else { // No attribute tag, treat as 'text'
|
||||
const localizedValue = localeData?.[key];
|
||||
if (localizedValue || localizedValue === '') {
|
||||
element.textContent = localizedValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function getMissingTranslations() {
|
||||
const missingData = [];
|
||||
|
||||
@@ -103,22 +191,7 @@ export function applyLocale(root = document) {
|
||||
|
||||
//find all the elements with `data-i18n` attribute
|
||||
$root.find('[data-i18n]').each(function () {
|
||||
//read the translation from the language data
|
||||
const keys = $(this).data('i18n').split(';'); // Multi-key entries are ; delimited
|
||||
for (const key of keys) {
|
||||
const attributeMatch = key.match(/\[(\S+)\](.+)/); // [attribute]key
|
||||
if (attributeMatch) { // attribute-tagged key
|
||||
const localizedValue = localeData?.[attributeMatch[2]];
|
||||
if (localizedValue || localizedValue == '') {
|
||||
$(this).attr(attributeMatch[1], localizedValue);
|
||||
}
|
||||
} else { // No attribute tag, treat as 'text'
|
||||
const localizedValue = localeData?.[key];
|
||||
if (localizedValue || localizedValue == '') {
|
||||
$(this).text(localizedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
translateElement(this);
|
||||
});
|
||||
|
||||
if (root !== document) {
|
||||
@@ -126,7 +199,6 @@ export function applyLocale(root = document) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addLanguagesToDropdown() {
|
||||
const uiLanguageSelects = $('#ui_language_select, #onboarding_ui_language_select');
|
||||
for (const langObj of langs) { // Set the value to the language code
|
||||
@@ -159,6 +231,13 @@ export function initLocales() {
|
||||
location.reload();
|
||||
});
|
||||
|
||||
observer.observe(document, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['data-i18n'],
|
||||
});
|
||||
|
||||
registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data in the current locale and dumps the data into the browser console. If the current locale is English, searches all other locales.', getMissingTranslations);
|
||||
registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
|
||||
}
|
||||
|
@@ -1,25 +1,55 @@
|
||||
const ELEMENT_ID = 'loader';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
|
||||
|
||||
/** @type {Popup} */
|
||||
let loaderPopup;
|
||||
|
||||
let preloaderYoinked = false;
|
||||
|
||||
export function showLoader() {
|
||||
const container = $('<div></div>').attr('id', ELEMENT_ID);
|
||||
const loader = $('<div></div>').attr('id', 'load-spinner').addClass('fa-solid fa-gear fa-spin fa-3x');
|
||||
container.append(loader);
|
||||
$('body').append(container);
|
||||
// Two loaders don't make sense. Don't await, we can overlay the old loader while it closes
|
||||
if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED);
|
||||
|
||||
loaderPopup = new Popup(`
|
||||
<div id="loader">
|
||||
<div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div>
|
||||
</div>`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none' });
|
||||
|
||||
// No close button, loaders are not closable
|
||||
loaderPopup.closeButton.style.display = 'none';
|
||||
|
||||
loaderPopup.show();
|
||||
}
|
||||
|
||||
export async function hideLoader() {
|
||||
//Sets up a 2-step animation. Spinner blurs/fades out, and then the loader shadow does the same.
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
$(`#${ELEMENT_ID}`)
|
||||
//only fade out the spinner and replace with login screen
|
||||
.animate({ opacity: 0 }, 300, function () {
|
||||
$(`#${ELEMENT_ID}`).remove();
|
||||
if (!loaderPopup) {
|
||||
console.warn('There is no loader showing to hide');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Spinner blurs/fades out
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
$('#loader').remove();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
// If it's present, we remove it once and then it's gone.
|
||||
yoinkPreloader();
|
||||
|
||||
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
|
||||
loaderPopup = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
'filter': 'blur(15px)',
|
||||
'opacity': '0',
|
||||
});
|
||||
});
|
||||
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
'filter': 'blur(15px)',
|
||||
'opacity': '0',
|
||||
});
|
||||
}
|
||||
|
||||
function yoinkPreloader() {
|
||||
if (preloaderYoinked) return;
|
||||
document.getElementById('preloader').remove();
|
||||
preloaderYoinked = true;
|
||||
}
|
||||
|
@@ -254,7 +254,7 @@ function getCurrentSwipeId() {
|
||||
// For swipe macro, we are accepting using the message that is currently being swiped
|
||||
const mid = getLastMessageId({ exclude_swipe_in_propress: false });
|
||||
const swipeId = chat[mid]?.swipe_id;
|
||||
return swipeId ? swipeId + 1 : null;
|
||||
return swipeId !== null ? swipeId + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,7 +401,7 @@ function timeDiffReplace(input) {
|
||||
const time2 = moment(matchPart2);
|
||||
|
||||
const timeDifference = moment.duration(time1.diff(time2));
|
||||
return timeDifference.humanize();
|
||||
return timeDifference.humanize(true);
|
||||
});
|
||||
|
||||
return output;
|
||||
|
@@ -689,7 +689,7 @@ function formatWorldInfo(value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!oai_settings.wi_format) {
|
||||
if (!oai_settings.wi_format.trim()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { shouldSendOnEnter } from './RossAscends-mods.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
||||
|
||||
@@ -35,6 +36,7 @@ export const POPUP_RESULT = {
|
||||
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
|
||||
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
|
||||
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
|
||||
* @property {'slow'|'fast'|'none'?} [animation='slow'] - Animation speed for the popup (opening, closing, ...)
|
||||
* @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
|
||||
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
|
||||
* @property {CustomPopupInput[]?} [customInputs=null] - Custom inputs to add to the popup. The display below the content and the input box, one by one.
|
||||
@@ -98,7 +100,7 @@ const showPopupHelper = {
|
||||
const result = await popup.show();
|
||||
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
|
||||
return result;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export class Popup {
|
||||
@@ -142,7 +144,7 @@ export class Popup {
|
||||
* @param {string} [inputValue=''] - The initial value of the input field
|
||||
* @param {PopupOptions} [options={}] - Additional options for the popup
|
||||
*/
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, transparent = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, animation = 'fast', defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, customInputs = null, onClosing = null, onClose = null, cropAspect = null, cropImage = null } = {}) {
|
||||
Popup.util.popups.push(this);
|
||||
|
||||
// Make this popup uniquely identifiable
|
||||
@@ -175,6 +177,7 @@ export class Popup {
|
||||
if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
|
||||
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
|
||||
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
|
||||
if (animation) this.dlg.classList.add('popup--animation-' + animation);
|
||||
|
||||
// If custom button captions are provided, we set them beforehand
|
||||
this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
@@ -210,7 +213,7 @@ export class Popup {
|
||||
this.customInputs = customInputs;
|
||||
this.customInputs?.forEach(input => {
|
||||
if (!input.id || !(typeof input.id === 'string')) {
|
||||
console.warn('Given custom input does not have a valid id set')
|
||||
console.warn('Given custom input does not have a valid id set');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -318,20 +321,20 @@ export class Popup {
|
||||
if (String(undefined) === String(resultControl.dataset.result)) return;
|
||||
if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
|
||||
const type = resultControl.dataset.resultEvent || 'click';
|
||||
resultControl.addEventListener(type, () => this.complete(result));
|
||||
resultControl.addEventListener(type, async () => await this.complete(result));
|
||||
});
|
||||
|
||||
// Bind dialog listeners manually, so we can be sure context is preserved
|
||||
const cancelListener = (evt) => {
|
||||
this.complete(POPUP_RESULT.CANCELLED);
|
||||
const cancelListener = async (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
await this.complete(POPUP_RESULT.CANCELLED);
|
||||
window.removeEventListener('cancel', cancelListenerBound);
|
||||
};
|
||||
const cancelListenerBound = cancelListener.bind(this);
|
||||
this.dlg.addEventListener('cancel', cancelListenerBound);
|
||||
|
||||
const keyListener = (evt) => {
|
||||
const keyListener = async (evt) => {
|
||||
switch (evt.key) {
|
||||
case 'Enter': {
|
||||
// CTRL+Enter counts as a closing action, but all other modifiers (ALT, SHIFT) should not trigger this
|
||||
@@ -342,15 +345,23 @@ export class Popup {
|
||||
if (this.dlg != document.activeElement?.closest('.popup'))
|
||||
return;
|
||||
|
||||
// Check if the current focus is a result control. Only should we apply the compelete action
|
||||
// Check if the current focus is a result control. Only should we apply the complete action
|
||||
const resultControl = document.activeElement?.closest('.result-control');
|
||||
if (!resultControl)
|
||||
return;
|
||||
|
||||
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
|
||||
this.complete(result);
|
||||
// Check if we are inside an input type text or a textarea field and send on enter is disabled
|
||||
const textarea = document.activeElement?.closest('textarea');
|
||||
if (textarea instanceof HTMLTextAreaElement && !shouldSendOnEnter())
|
||||
return;
|
||||
const input = document.activeElement?.closest('input[type="text"]');
|
||||
if (input instanceof HTMLInputElement && !shouldSendOnEnter())
|
||||
return;
|
||||
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
const result = Number(document.activeElement.getAttribute('data-result') ?? this.defaultResult);
|
||||
await this.complete(result);
|
||||
window.removeEventListener('keydown', keyListenerBound);
|
||||
|
||||
break;
|
||||
@@ -430,8 +441,10 @@ export class Popup {
|
||||
* - All other will return the result value as provided as `POPUP_RESULT` or a custom number value
|
||||
*
|
||||
* @param {POPUP_RESULT|number} result - The result of the popup (either an existing `POPUP_RESULT` or a custom result value)
|
||||
*
|
||||
* @returns {Promise<string|number|boolean?>} A promise that resolves with the value of the popup when it is completed.
|
||||
*/
|
||||
complete(result) {
|
||||
async complete(result) {
|
||||
// In all cases besides INPUT the popup value should be the result
|
||||
/** @type {POPUP_RESULT|number|boolean|string?} */
|
||||
let value = result;
|
||||
@@ -468,6 +481,8 @@ export class Popup {
|
||||
|
||||
Popup.util.lastResult = { value, result, inputResults: this.inputResults };
|
||||
this.#hide();
|
||||
|
||||
return this.#promise;
|
||||
}
|
||||
completeAffirmative() {
|
||||
return this.complete(POPUP_RESULT.AFFIRMATIVE);
|
||||
@@ -627,8 +642,13 @@ export function fixToastrForDialogs() {
|
||||
// Now another case is if we only have one popup and that is currently closing. In that case the toastr container exists,
|
||||
// but we don't have an open dialog to move it into. It's just inside the existing one that will be gone in milliseconds.
|
||||
// To prevent new toasts from being showing up in there and then vanish in an instant,
|
||||
// we move the toastr back to the main body
|
||||
// we move the toastr back to the main body, or delete if its empty
|
||||
if (!dlg && isAlreadyPresent) {
|
||||
document.body.appendChild(toastContainer);
|
||||
if (!toastContainer.childNodes.length) {
|
||||
toastContainer.remove();
|
||||
} else {
|
||||
document.body.appendChild(toastContainer);
|
||||
toastContainer.classList.add('toast-top-center');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ import { tokenizers } from './tokenizers.js';
|
||||
import { BIAS_CACHE } from './logit-bias.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
import { countOccurrences, debounce, delay, download, getFileText, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
import { countOccurrences, debounce, delay, download, getFileText, getStringHash, isOdd, isTrueBoolean, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||
import { FILTER_TYPES } from './filters.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
@@ -335,6 +335,8 @@ const storage_keys = {
|
||||
compact_input_area: 'compact_input_area',
|
||||
auto_connect_legacy: 'AutoConnectEnabled',
|
||||
auto_load_chat_legacy: 'AutoLoadChatEnabled',
|
||||
|
||||
storyStringValidationCache: 'StoryStringValidationCache',
|
||||
};
|
||||
|
||||
const contextControls = [
|
||||
@@ -2105,6 +2107,9 @@ export function fuzzySearchGroups(searchValue) {
|
||||
*/
|
||||
export function renderStoryString(params) {
|
||||
try {
|
||||
// Validate and log possible warnings/errors
|
||||
validateStoryString(power_user.context.story_string, params);
|
||||
|
||||
// compile the story string template into a function, with no HTML escaping
|
||||
const compiledTemplate = Handlebars.compile(power_user.context.story_string, { noEscape: true });
|
||||
|
||||
@@ -2132,6 +2137,55 @@ export function renderStoryString(params) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the story string for possible warnings or issues
|
||||
*
|
||||
* @param {string} storyString - The story string
|
||||
* @param {Object} params - The story string parameters
|
||||
*/
|
||||
function validateStoryString(storyString, params) {
|
||||
/** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
|
||||
const cache = JSON.parse(localStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
|
||||
|
||||
const hash = getStringHash(storyString);
|
||||
|
||||
// Initialize the cache for the current hash if it doesn't exist
|
||||
if (!cache.hashCache[hash]) {
|
||||
cache.hashCache[hash] = { fieldsWarned: {} };
|
||||
}
|
||||
|
||||
const currentCache = cache.hashCache[hash];
|
||||
const fieldsToWarn = [];
|
||||
|
||||
function validateMissingField(field, fallbackLegacyField = null) {
|
||||
const contains = storyString.includes(`{{${field}}}`) || (!!fallbackLegacyField && storyString.includes(`{{${fallbackLegacyField}}}`));
|
||||
if (!contains && params[field]) {
|
||||
const wasLogged = currentCache.fieldsWarned[field];
|
||||
if (!wasLogged) {
|
||||
fieldsToWarn.push(field);
|
||||
currentCache.fieldsWarned[field] = true;
|
||||
}
|
||||
console.warn(`The story string does not contain {{${field}}}, but it would contain content:\n`, params[field]);
|
||||
}
|
||||
}
|
||||
|
||||
validateMissingField('description');
|
||||
validateMissingField('personality');
|
||||
validateMissingField('persona');
|
||||
validateMissingField('scenario');
|
||||
validateMissingField('system');
|
||||
validateMissingField('wiBefore', 'loreBefore');
|
||||
validateMissingField('wiAfter', 'loreAfter');
|
||||
|
||||
if (fieldsToWarn.length > 0) {
|
||||
const fieldsList = fieldsToWarn.map(field => `{{${field}}}`).join(', ');
|
||||
toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
|
||||
}
|
||||
|
||||
localStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
|
||||
}
|
||||
|
||||
|
||||
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
|
||||
const compareFunc = (first, second) => {
|
||||
const a = first[power_user.sort_field];
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ export class SlashCommandArgument {
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
|
||||
*/
|
||||
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) {
|
||||
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) {
|
||||
this.description = description;
|
||||
this.typeList = types ? Array.isArray(types) ? types : [types] : [];
|
||||
this.isRequired = isRequired ?? false;
|
||||
@@ -93,7 +93,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
* @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
|
||||
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
|
||||
* @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values
|
||||
* @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
|
||||
*/
|
||||
static fromProps(props) {
|
||||
return new SlashCommandNamedArgument(
|
||||
@@ -106,7 +106,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
props.enumList ?? [],
|
||||
props.aliasList ?? [],
|
||||
props.enumProvider ?? null,
|
||||
props.forceEnum ?? true,
|
||||
props.forceEnum ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,9 +123,9 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
|
||||
* @param {string[]} [aliases=[]]
|
||||
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
|
||||
* @param {boolean} [forceEnum=true]
|
||||
* @param {boolean} [forceEnum=false]
|
||||
*/
|
||||
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) {
|
||||
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) {
|
||||
super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
|
||||
this.name = name;
|
||||
this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
|
||||
|
@@ -160,7 +160,7 @@ async function* parseStreamData(json) {
|
||||
return;
|
||||
}
|
||||
// llama.cpp?
|
||||
else if (typeof json.content === 'string' && json.content.length > 0) {
|
||||
else if (typeof json.content === 'string' && json.content.length > 0 && json.object !== 'chat.completion.chunk') {
|
||||
for (let i = 0; i < json.content.length; i++) {
|
||||
const str = json.content[i];
|
||||
yield {
|
||||
|
@@ -263,6 +263,8 @@ export const setting_names = [
|
||||
'bypass_status_check',
|
||||
];
|
||||
|
||||
const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
|
||||
|
||||
export function validateTextGenUrl() {
|
||||
const selector = SERVER_INPUTS[settings.type];
|
||||
|
||||
@@ -1045,6 +1047,10 @@ export function isJsonSchemaSupported() {
|
||||
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
|
||||
}
|
||||
|
||||
function isDynamicTemperatureSupported() {
|
||||
return settings.dynatemp && DYNATEMP_BLOCK?.dataset?.tgType?.includes(settings.type);
|
||||
}
|
||||
|
||||
function getLogprobsNumber() {
|
||||
if (settings.type === VLLM || settings.type === INFERMATICAI) {
|
||||
return 5;
|
||||
@@ -1055,6 +1061,7 @@ function getLogprobsNumber() {
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
const canMultiSwipe = !isContinue && !isImpersonate && type !== 'quiet';
|
||||
const dynatemp = isDynamicTemperatureSupported();
|
||||
const { banned_tokens, banned_strings } = getCustomTokenBans();
|
||||
|
||||
let params = {
|
||||
@@ -1063,7 +1070,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'max_new_tokens': maxTokens,
|
||||
'max_tokens': maxTokens,
|
||||
'logprobs': power_user.request_token_probabilities ? getLogprobsNumber() : undefined,
|
||||
'temperature': settings.dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
|
||||
'temperature': dynatemp ? (settings.min_temp + settings.max_temp) / 2 : settings.temp,
|
||||
'top_p': settings.top_p,
|
||||
'typical_p': settings.typical_p,
|
||||
'typical': settings.typical_p,
|
||||
@@ -1081,11 +1088,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'length_penalty': settings.length_penalty,
|
||||
'early_stopping': settings.early_stopping,
|
||||
'add_bos_token': settings.add_bos_token,
|
||||
'dynamic_temperature': settings.dynatemp ? true : undefined,
|
||||
'dynatemp_low': settings.dynatemp ? settings.min_temp : undefined,
|
||||
'dynatemp_high': settings.dynatemp ? settings.max_temp : undefined,
|
||||
'dynatemp_range': settings.dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
|
||||
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined,
|
||||
'dynamic_temperature': dynatemp ? true : undefined,
|
||||
'dynatemp_low': dynatemp ? settings.min_temp : undefined,
|
||||
'dynatemp_high': dynatemp ? settings.max_temp : undefined,
|
||||
'dynatemp_range': dynatemp ? (settings.max_temp - settings.min_temp) / 2 : undefined,
|
||||
'dynatemp_exponent': dynatemp ? settings.dynatemp_exponent : undefined,
|
||||
'smoothing_factor': settings.smoothing_factor,
|
||||
'smoothing_curve': settings.smoothing_curve,
|
||||
'dry_allowed_length': settings.dry_allowed_length,
|
||||
|
@@ -803,7 +803,7 @@ export function getImageSizeFromDataURL(dataUrl) {
|
||||
|
||||
export function getCharaFilename(chid) {
|
||||
const context = getContext();
|
||||
const fileName = context.characters[chid ?? context.characterId].avatar;
|
||||
const fileName = context.characters[chid ?? context.characterId]?.avatar;
|
||||
|
||||
if (fileName) {
|
||||
return fileName.replace(/\.[^/.]+$/, '');
|
||||
|
@@ -1373,7 +1373,11 @@ export function registerVariableCommands() {
|
||||
name: 'times',
|
||||
callback: timesCallback,
|
||||
returns: 'result of the last executed command',
|
||||
namedArgumentList: [],
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'guard', 'disable loop iteration limit', [ARGUMENT_TYPE.STRING], false, false, null, commonEnumProviders.boolean('onOff')(),
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'repeats',
|
||||
|
Reference in New Issue
Block a user