Merge branch 'staging' into swipe-nums-for-all-mes

This commit is contained in:
Cohee
2024-10-01 19:27:20 +03:00
16 changed files with 742 additions and 304 deletions

View File

@ -2,17 +2,26 @@
pushd %~dp0
git --version > nul 2>&1
if %errorlevel% neq 0 (
echo Git is not installed on this system. Skipping update.
echo If you installed with a zip file, you will need to download the new zip and install it manually.
echo Git is not installed on this system.
echo Install it from https://git-scm.com/downloads
goto end
) else (
if not exist .git (
echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
echo See: https://docs.sillytavern.app/installation/windows/
goto end
)
call git pull --rebase --autostash
if %errorlevel% neq 0 (
REM incase there is still something wrong
echo There were errors while updating. Please download the latest version manually.
echo There were errors while updating.
echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
goto end
)
)
set NODE_ENV=production
call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
node server.js %*
:end
pause
popd

View File

@ -5,8 +5,14 @@ pushd %~dp0
echo Checking Git installation
git --version > nul 2>&1
if %errorlevel% neq 0 (
echo Git is not installed on this system. Skipping update.
echo If you installed with a zip file, you will need to download the new zip and install it manually.
echo Git is not installed on this system.
echo Install it from https://git-scm.com/downloads
goto end
)
if not exist .git (
echo Not running from a Git repository. Reinstall using an officially supported method to get updates.
echo See: https://docs.sillytavern.app/installation/windows/
goto end
)
@ -89,7 +95,8 @@ git pull --rebase --autostash origin %TARGET_BRANCH%
:install
if %errorlevel% neq 0 (
echo There were errors while updating. Please check manually.
echo There were errors while updating.
echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
goto end
)

View File

@ -118,7 +118,6 @@ extras:
classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
captioningModel: Xenova/vit-gpt2-image-captioning
embeddingModel: Cohee/jina-embeddings-v2-base-en
promptExpansionModel: Cohee/fooocus_expansion-onnx
speechToTextModel: Xenova/whisper-small
textToSpeechModel: Xenova/speecht5_tts
# -- OPENAI CONFIGURATION --

View File

@ -4797,7 +4797,7 @@ export function removeMacros(str) {
* @param {boolean} [compact] Send as a compact display message.
* @param {string} [name] Name of the user sending the message. Defaults to name1.
* @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
* @returns {Promise<void>} A promise that resolves when the message is inserted.
* @returns {Promise<any>} A promise that resolves to the message when it is inserted.
*/
export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@ -4844,6 +4844,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
await saveChatConditional();
}
return message;
}
/**

View File

@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@ -2134,18 +2136,42 @@ function migrateSettings() {
name: 'classify-expressions',
aliases: ['expressions'],
callback: async (args) => {
const list = await getExpressionsList();
switch (String(args.format).toLowerCase()) {
/** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
// @ts-ignore
let returnType = args.return;
// Old legacy return type handling
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
switch (type) {
case 'json':
return JSON.stringify(list);
returnType = 'object';
break;
default:
return list.join(', ');
returnType = 'pipe';
break;
}
}
// Now the actual new return type handling
const list = await getExpressionsList();
return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'pipe',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
typeList: [ARGUMENT_TYPE.STRING],
enumList: [
new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),

View File

@ -20,7 +20,7 @@ import {
} from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce, isFalseBoolean } from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
@ -31,6 +31,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME };
const MODULE_NAME = 'sd';
@ -221,7 +222,6 @@ const defaultSettings = {
// Refine mode
refine_mode: false,
expand: false,
interactive_mode: false,
multimodal_captioning: false,
snap: false,
@ -240,7 +240,7 @@ const defaultSettings = {
drawthings_auth: '',
hr_upscaler: 'Latent',
hr_scale: 2.0,
hr_scale: 1.0,
hr_scale_min: 1.0,
hr_scale_max: 4.0,
hr_scale_step: 0.1,
@ -260,10 +260,6 @@ const defaultSettings = {
clip_skip: 1,
// NovelAI settings
novel_upscale_ratio_min: 1.0,
novel_upscale_ratio_max: 4.0,
novel_upscale_ratio_step: 0.1,
novel_upscale_ratio: 1.0,
novel_anlas_guard: false,
novel_sm: false,
novel_sm_dyn: false,
@ -416,7 +412,6 @@ async function loadSettings() {
$('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');
$('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');
$('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');
$('#sd_novel_upscale_ratio').val(extension_settings.sd.novel_upscale_ratio).trigger('input');
$('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard);
$('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
$('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
@ -430,7 +425,6 @@ async function loadSettings() {
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
$('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
@ -644,37 +638,13 @@ async function onSaveStyleClick() {
saveSettingsDebounced();
}
async function expandPrompt(prompt) {
try {
const response = await fetch('/api/sd/expand', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ prompt: prompt }),
});
if (!response.ok) {
throw new Error('API returned an error.');
}
const data = await response.json();
return data.prompt;
} catch {
return prompt;
}
}
/**
* Modifies prompt based on auto-expansion and user inputs.
* Modifies prompt based on user inputs.
* @param {string} prompt Prompt to refine
* @param {boolean} allowExpand Whether to allow auto-expansion
* @param {boolean} isNegative Whether the prompt is a negative one
* @returns {Promise<string>} Refined prompt
*/
async function refinePrompt(prompt, allowExpand, isNegative = false) {
if (allowExpand && extension_settings.sd.expand) {
prompt = await expandPrompt(prompt);
}
async function refinePrompt(prompt, isNegative) {
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 callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 5, okButton: 'Continue' });
@ -800,11 +770,6 @@ function combinePrefixes(str1, str2, macro = '') {
return process(result);
}
function onExpandInput() {
extension_settings.sd.expand = !!$(this).prop('checked');
saveSettingsDebounced();
}
function onRefineModeInput() {
extension_settings.sd.refine_mode = !!$('#sd_refine_mode').prop('checked');
saveSettingsDebounced();
@ -969,12 +934,6 @@ async function onViewAnlasClick() {
toastr.info(`Free image generation: ${unlimitedGeneration ? 'Yes' : 'No'}`, `Anlas: ${anlas}`);
}
function onNovelUpscaleRatioInput() {
extension_settings.sd.novel_upscale_ratio = Number($('#sd_novel_upscale_ratio').val());
$('#sd_novel_upscale_ratio_value').val(extension_settings.sd.novel_upscale_ratio.toFixed(1));
saveSettingsDebounced();
}
function onNovelAnlasGuardInput() {
extension_settings.sd.novel_anlas_guard = !!$('#sd_novel_anlas_guard').prop('checked');
saveSettingsDebounced();
@ -2272,6 +2231,25 @@ function getRawLastMessage() {
return `((${processReply(lastMessage.mes)})), (${processReply(character.scenario)}:0.7), (${processReply(character.description)}:0.5)`;
}
/**
* Ensure that the selected option exists in the dropdown.
* @param {string} setting Setting key
* @param {string} selector Dropdown selector
* @returns {void}
*/
function ensureSelectionExists(setting, selector) {
/** @type {HTMLSelectElement} */
const selectElement = document.querySelector(selector);
if (!selectElement) {
return;
}
const options = Array.from(selectElement.options);
const value = extension_settings.sd[setting];
if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) {
extension_settings.sd[setting] = selectElement.selectedOptions[0].value;
}
}
/**
* Generates an image based on the given trigger word.
* @param {string} initiator The initiator of the image generation
@ -2292,8 +2270,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
return;
}
extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val();
extension_settings.sd.model = $('#sd_model').find(':selected').val();
ensureSelectionExists('sampler', '#sd_sampler');
ensureSelectionExists('model', '#sd_model');
trigger = trigger.trim();
const generationType = getGenerationType(trigger);
@ -2441,7 +2419,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
}
if (generationType !== generationMode.FREE) {
prompt = await refinePrompt(prompt, true);
prompt = await refinePrompt(prompt, false);
}
return prompt;
@ -2469,7 +2447,7 @@ function generateFreeModePrompt(trigger, combineNegatives) {
return message.original_avatar.replace(/\.[^/.]+$/, '');
}
}
throw new Error('No usable messages found.');
return '';
};
const key = getLastCharacterKey();
@ -3031,7 +3009,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
width: width,
height: height,
negative_prompt: negativePrompt,
upscale_ratio: extension_settings.sd.novel_upscale_ratio,
upscale_ratio: extension_settings.sd.hr_scale,
decrisper: extension_settings.sd.novel_decrisper,
sm: sm,
sm_dyn: sm_dyn,
@ -3613,8 +3591,8 @@ async function sdMessageButton(e) {
try {
setBusyIcon(true);
if (hasSavedImage) {
const prompt = await refinePrompt(message.extra.title, false, false);
const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, false, true) : '';
const prompt = await refinePrompt(message.extra.title, false);
const negative = hasSavedNegative ? await refinePrompt(message.extra.negative, true) : '';
message.extra.title = prompt;
const generationType = message?.extra?.generationType ?? generationMode.FREE;
@ -3756,8 +3734,8 @@ async function onImageSwiped({ message, element, direction }) {
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
const callback = () => { };
const hasNegative = message.extra.negative;
const prompt = await refinePrompt(message.extra.title, false, false);
const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : '';
const prompt = await refinePrompt(message.extra.title, false);
const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, true) : '';
const characterName = context.groupId
? context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString()
: context.characters[context.characterId]?.name;
@ -3788,12 +3766,85 @@ async function onImageSwiped({ message, element, direction }) {
await context.saveChat();
}
/**
* Applies the command arguments to the extension settings.
* @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments
* @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture
* @param {NamedArguments | NamedArgumentsCapture} args - Command arguments
* @returns {Record<string, any>} - Current settings before applying the command arguments
*/
function applyCommandArguments(args) {
const overrideSettings = {};
const currentSettings = {};
const settingMap = {
'edit': 'refine_mode',
'extend': 'free_extend',
'multimodal': 'multimodal_captioning',
'seed': 'seed',
'width': 'width',
'height': 'height',
'steps': 'steps',
'cfg': 'scale',
'skip': 'clip_skip',
'model': 'model',
'sampler': 'sampler',
'scheduler': 'scheduler',
'vae': 'vae',
'upscaler': 'hr_upscaler',
'scale': 'hr_scale',
'hires': 'enable_hr',
'denoise': 'denoising_strength',
'2ndpass': 'hr_second_pass_steps',
'faces': 'restore_faces',
};
for (const [param, setting] of Object.entries(settingMap)) {
if (args[param] === undefined || defaultSettings[setting] === undefined) {
continue;
}
currentSettings[setting] = extension_settings.sd[setting];
const value = String(args[param]);
const type = typeof defaultSettings[setting];
switch (type) {
case 'boolean':
overrideSettings[setting] = isTrueBoolean(value) || !isFalseBoolean(value);
break;
case 'number':
overrideSettings[setting] = Number(value);
break;
default:
overrideSettings[setting] = value;
break;
}
}
Object.assign(extension_settings.sd, overrideSettings);
return currentSettings;
}
jQuery(async () => {
await addSDGenButtons();
const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine',
callback: (args, trigger) => generatePicture(initiators.command, args, String(trigger)),
returns: 'URL of the generated image, or an empty string if the generation failed',
callback: async (args, trigger) => {
const currentSettings = applyCommandArguments(args);
try {
return await generatePicture(initiators.command, args, String(trigger));
} catch (error) {
console.error('Failed to generate image:', error);
return '';
} finally {
if (Object.keys(currentSettings).length) {
Object.assign(extension_settings.sd, currentSettings);
saveSettingsDebounced();
}
}
},
aliases: ['sd', 'img', 'image'],
namedArgumentList: [
new SlashCommandNamedArgument(
@ -3803,6 +3854,164 @@ jQuery(async () => {
name: 'negative',
description: 'negative prompt prefix',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'extend',
description: 'auto-extend free mode prompts with the LLM',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'edit',
description: 'edit the prompt before generation',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'multimodal',
description: 'use multimodal captioning (for portraits only)',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'snap',
description: 'snap auto-adjusted dimensions to the nearest known resolution (portraits and backgrounds only)',
typeList: [ARGUMENT_TYPE.BOOLEAN],
enumProvider: commonEnumProviders.boolean('trueFalse'),
isRequired: false,
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'seed',
description: 'random seed',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'width',
description: 'image width',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'height',
description: 'image height',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'steps',
description: 'number of steps',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'cfg',
description: 'CFG scale',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'skip',
description: 'CLIP skip layers',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'model',
description: 'model override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_model', true),
}),
SlashCommandNamedArgument.fromProps({
name: 'sampler',
description: 'sampler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_sampler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'scheduler',
description: 'scheduler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_scheduler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'vae',
description: 'VAE name override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_vae', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'upscaler',
description: 'upscaler override',
isRequired: false,
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_hr_upscaler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'hires',
description: 'enable high-res fix',
isRequired: false,
typeList: [ARGUMENT_TYPE.BOOLEAN],
acceptsMultiple: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
SlashCommandNamedArgument.fromProps({
name: 'scale',
description: 'upscale amount',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'denoise',
description: 'denoising strength',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: '2ndpass',
description: 'second pass steps',
isRequired: false,
typeList: [ARGUMENT_TYPE.NUMBER],
acceptsMultiple: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'faces',
description: 'restore faces',
isRequired: false,
typeList: [ARGUMENT_TYPE.BOOLEAN],
acceptsMultiple: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
],
unnamedArgumentList: [
@ -3823,6 +4032,66 @@ jQuery(async () => {
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-source',
aliases: ['sd-source', 'img-source'],
returns: 'a name of the current generation source',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'source name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_source', true),
}),
],
helpString: 'If an argument is provided, change the source of the image generation, e.g. <code>/imagine-source comfy</code>. Returns the current source.',
callback: async (_args, name) => {
if (!name) {
return extension_settings.sd.source;
}
const isKnownSource = Object.keys(sources).includes(String(name));
if (!isKnownSource) {
throw new Error('The value provided is not a valid image generation source.');
}
const option = document.querySelector(`#sd_source [value="${name}"]`);
if (!(option instanceof HTMLOptionElement)) {
throw new Error('Could not find the source option in the dropdown.');
}
option.selected = true;
await onSourceChange();
return extension_settings.sd.source;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-style',
aliases: ['sd-style', 'img-style'],
returns: 'a name of the current style',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'style name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
enumProvider: getSelectEnumProvider('sd_style', false),
}),
],
helpString: 'If an argument is provided, change the style of the image generation, e.g. <code>/imagine-style MyStyle</code>. Returns the current style.',
callback: async (_args, name) => {
if (!name) {
return extension_settings.sd.style;
}
const option = document.querySelector(`#sd_style [value="${name}"]`);
if (!(option instanceof HTMLOptionElement)) {
throw new Error('Could not find the style option in the dropdown.');
}
option.selected = true;
onStyleSelect();
return extension_settings.sd.style;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-comfy-workflow',
callback: changeComfyWorkflow,
@ -3832,7 +4101,7 @@ jQuery(async () => {
description: 'workflow name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)),
enumProvider: getSelectEnumProvider('sd_comfy_workflow', false),
}),
],
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
@ -3874,7 +4143,6 @@ jQuery(async () => {
$('#sd_hr_scale').on('input', onHrScaleInput);
$('#sd_denoising_strength').on('input', onDenoisingStrengthInput);
$('#sd_hr_second_pass_steps').on('input', onHrSecondPassStepsInput);
$('#sd_novel_upscale_ratio').on('input', onNovelUpscaleRatioInput);
$('#sd_novel_anlas_guard').on('input', onNovelAnlasGuardInput);
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_novel_sm').on('input', onNovelSmInput);
@ -3887,7 +4155,6 @@ jQuery(async () => {
$('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
$('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
$('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick);
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_delete_style').on('click', onDeleteStyleClick);

View File

@ -27,11 +27,6 @@
<span data-i18n="sd_free_extend_txt">Extend free mode prompts</span>
<small data-i18n="sd_free_extend_small">(interactive/commands)</small>
</label>
<label for="sd_expand" class="checkbox_label" data-i18n="[title]sd_expand" title="Automatically extend prompts using text generation model">
<input id="sd_expand" type="checkbox" />
<span data-i18n="sd_expand_txt">Auto-extend prompts</span>
<span class="right_menu_button fa-solid fa-triangle-exclamation" data-i18n="[title]sd_expand_warning" title="May produce unexpected results. Manual prompt editing is recommended."></span>
</label>
<label for="sd_snap" class="checkbox_label" data-i18n="[title]sd_snap" title="Snap generation requests with a forced aspect ratio (portraits, backgrounds) to the nearest known resolution, while trying to preserve the absolute pixel counts (recommended for SDXL).">
<input id="sd_snap" type="checkbox" />
<span data-i18n="sd_snap_txt">Snap auto-adjusted resolutions</span>
@ -308,7 +303,7 @@
</div>
<div class="flex-container">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings">
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,drawthings,novel">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
@ -332,14 +327,6 @@
<input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="novel">
<small>
<span data-i18n="Upscale by">Upscale by</span>
</small>
<input class="neo-range-slider" type="range" id="sd_novel_upscale_ratio" name="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
<input class="neo-range-input" type="number" id="sd_novel_upscale_ratio_value" data-for="sd_novel_upscale_ratio" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" >
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras">
<small>
<span data-i18n="CLIP Skip">CLIP Skip</span>

View File

@ -384,6 +384,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt) {
if (!systemPrompt) {
return '';
}
const separator = power_user.instruct.wrap ? '\n' : '';
if (power_user.instruct.system_sequence_prefix) {

View File

@ -70,6 +70,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@ -242,6 +243,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sendas',
callback: sendMessageAs,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
@ -269,6 +271,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -301,6 +311,7 @@ export function initDefaultSlashCommands() {
name: 'sys',
callback: sendNarratorMessage,
aliases: ['nar'],
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -316,6 +327,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -355,6 +374,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'comment',
callback: sendCommentMessage,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -370,6 +390,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -509,7 +537,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'ask',
callback: askCharacter,
returns: 'the generated text',
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
@ -518,6 +546,14 @@ export function initDefaultSlashCommands() {
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'pipe',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -556,6 +592,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'send',
callback: sendUserMessageCallback,
returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@ -578,6 +615,14 @@ export function initDefaultSlashCommands() {
defaultValue: '{{user}}',
enumProvider: commonEnumProviders.personas,
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'none',
enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
forceEnum: true,
}),
],
unnamedArgumentList: [
new SlashCommandArgument(
@ -1568,12 +1613,21 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'listinjects',
callback: listInjectsCallback,
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>format</code> argument to change the output format.',
returns: 'JSON object of script injections',
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the <code>return</code> argument to change the return type.',
returns: 'Optionalls the JSON object of script injections',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'popup-html',
enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'output format',
description: '!!! DEPRECATED - use "return" instead !!! output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
@ -1842,37 +1896,43 @@ function injectCallback(args, value) {
}
async function listInjectsCallback(args) {
/** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
let returnType = args.return;
// Old legacy return type handling
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
type !== 'none' && toastr.info('No script injections for the current chat');
return JSON.stringify({});
}
switch (type) {
case 'none':
returnType = 'none';
break;
case 'chat':
returnType = 'chat-html';
break;
case 'popup':
default:
returnType = 'popup-html';
break;
}
}
const injects = Object.entries(chat_metadata.script_injects)
// Now the actual new return type handling
const buildTextValue = (injects) => {
const injectsStr = Object.entries(injects)
.map(([id, inject]) => {
const position = Object.entries(extension_prompt_types);
const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return `* **${id}**: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
})
.join('\n');
return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
};
const converter = new showdown.Converter();
const messageText = `### Script injections:\n${injects}`;
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
switch (type) {
case 'none':
break;
case 'chat':
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
break;
case 'popup':
default:
await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
break;
}
return JSON.stringify(chat_metadata.script_injects);
return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
}
/**
@ -2559,7 +2619,7 @@ async function askCharacter(args, text) {
// Not supported in group chats
// TODO: Maybe support group chats?
if (selected_group) {
toastr.error('Cannot run /ask command in a group chat!');
toastr.warning('Cannot run /ask command in a group chat!');
return '';
}
@ -2633,7 +2693,9 @@ async function askCharacter(args, text) {
}
}
return askResult;
const message = askResult ? chat[chat.length - 1] : null;
return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
}
async function hideMessageCallback(_, arg) {
@ -2908,7 +2970,7 @@ function findPersonaByName(name) {
async function sendUserMessageCallback(args, text) {
if (!text) {
console.warn('WARN: No text provided for /send command');
toastr.warning('You must specify text to send');
return;
}
@ -2924,16 +2986,17 @@ async function sendUserMessageCallback(args, text) {
insertAt = chat.length + insertAt;
}
let message;
if ('name' in args) {
const name = args.name || '';
const avatar = findPersonaByName(name) || user_avatar;
await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
}
else {
await sendMessageAsUser(text, bias, insertAt, compact);
message = await sendMessageAsUser(text, bias, insertAt, compact);
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
async function deleteMessagesByNameCallback(_, name) {
@ -3027,7 +3090,7 @@ async function continueChatCallback(args, prompt) {
resolve();
} catch (error) {
console.error('Error running /continue command:', error);
reject();
reject(error);
}
});
@ -3221,30 +3284,20 @@ export function getNameAndAvatarForMessage(character, name = null) {
export async function sendMessageAs(args, text) {
if (!text) {
toastr.warning('You must specify text to send as');
return '';
}
let name;
let name = args.name?.trim();
let mesText;
if (args.name) {
name = args.name.trim();
if (!name && !text) {
toastr.warning('You must specify a name and text to send as');
return '';
}
} else {
if (!name) {
const namelessWarningKey = 'sendAsNamelessWarningShown';
if (localStorage.getItem(namelessWarningKey) !== 'true') {
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
localStorage.setItem(namelessWarningKey, 'true');
}
name = name2;
if (!text) {
toastr.warning('You must specify text to send as');
return '';
}
}
mesText = text.trim();
@ -3321,11 +3374,12 @@ export async function sendMessageAs(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function sendNarratorMessage(args, text) {
if (!text) {
toastr.warning('You must specify text to send');
return '';
}
@ -3374,7 +3428,7 @@ export async function sendNarratorMessage(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function promptQuietForLoudResponse(who, text) {
@ -3420,6 +3474,7 @@ export async function promptQuietForLoudResponse(who, text) {
async function sendCommentMessage(args, text) {
if (!text) {
toastr.warning('You must specify text to send');
return '';
}
@ -3462,7 +3517,7 @@ async function sendCommentMessage(args, text) {
await saveChatConditional();
}
return '';
return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
/**

View File

@ -508,6 +508,14 @@ export class SlashCommandClosure {
return v;
});
}
value ??= '';
// Make sure that if unnamed args are split, it should always return an array
if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
value = [value];
}
return value;
}

View File

@ -36,6 +36,7 @@ export const enumIcons = {
message: '💬',
voice: '🎤',
server: '🖥️',
popup: '🗔',
true: '✔️',
false: '❌',
@ -152,6 +153,35 @@ export const commonEnumProviders = {
].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
},
/**
* Enum values for numbers and variable names
*
* Includes all variable names and the ability to specify any number
*
* @param {SlashCommandExecutor} executor - The executor of the slash command
* @param {SlashCommandScope} scope - The scope of the slash command
* @returns {SlashCommandEnumValue[]} The enum values
*/
numbersAndVariables: (executor, scope) => [
...commonEnumProviders.variables('all')(executor, scope),
new SlashCommandEnumValue(
'any variable name',
null,
enumTypes.variable,
enumIcons.variable,
(input) => /^\w*$/.test(input),
(input) => input,
),
new SlashCommandEnumValue(
'any number',
null,
enumTypes.number,
enumIcons.number,
(input) => input == '' || !Number.isNaN(Number(input)),
(input) => input,
),
],
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*

View File

@ -0,0 +1,80 @@
import { sendSystemMessage, system_message_types } from '../../script.js';
import { callGenericPopup, POPUP_TYPE } from '../popup.js';
import { escapeHtml } from '../utils.js';
import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
export const slashCommandReturnHelper = {
// Without this, VSCode formatter fucks up JS docs. Don't ask me why.
_: false,
/**
* Gets/creates the enum list of types of return relevant for a slash command
*
* @param {object} [options={}] Options
* @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
* @param {boolean} [options.allowObject=false] Allow option to return the value as an object
* @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
* @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
* @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway
* @returns {SlashCommandEnumValue[]} The enum list
*/
enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, ''),
allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, ''),
new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
new SlashCommandEnumValue('none', 'No return value'),
].filter(x => !!x),
/**
* Handles the return value based on the specified type
*
* @param {SlashCommandReturnType} type The type of return
* @param {object|number|string} value The value to return
* @param {object} [options={}] Options
* @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
* @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested
* @returns {Promise<*>} The processed return value
*/
async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
const shouldHtml = type.endsWith('html');
const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
switch (type) {
case 'popup-html':
case 'popup-text':
case 'chat-text':
case 'chat-html':
case 'toast-text':
case 'toast-html': {
const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
return '';
}
case 'pipe':
return stringValue ?? '';
case 'object':
return JSON.stringify(value);
case 'console':
console.info(value);
return '';
case 'none':
return '';
default:
throw new Error(`Unknown return type: ${type}`);
}
},
};

View File

@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
@ -305,7 +306,28 @@ export function replaceVariableMacros(input) {
}
async function listVariablesCallback(args) {
const type = String(args?.format || '').toLowerCase().trim() || 'popup';
/** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
let returnType = args.return;
// Old legacy return type handling
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
switch (type) {
case 'none':
returnType = 'none';
break;
case 'chat':
returnType = 'chat-html';
break;
case 'popup':
default:
returnType = 'popup-html';
break;
}
}
// Now the actual new return type handling
const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
if (!chat_metadata.variables) {
chat_metadata.variables = {};
@ -317,35 +339,24 @@ async function listVariablesCallback(args) {
const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
const buildTextValue = (_) => {
const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
const chatName = getCurrentChatId();
const message = [
includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
].filter(x => x).join('\n\n');
return message;
};
const jsonVariables = [
...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
];
const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
const chatName = getCurrentChatId();
const converter = new showdown.Converter();
const message = [
includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
].filter(x => x).join('\n\n');
const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
switch (type) {
case 'none':
break;
case 'chat':
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
break;
case 'popup':
default:
await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
break;
}
return JSON.stringify(jsonVariables);
return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
}
/**
@ -669,8 +680,8 @@ function deleteGlobalVariable(name) {
}
/**
* Parses a series of numeric values from a string.
* @param {string} value A space-separated list of numeric values or variable names
* Parses a series of numeric values from a string or a string array.
* @param {string|string[]} value A space-separated list of numeric values or variable names
* @param {SlashCommandScope} scope Scope
* @returns {number[]} An array of numeric values
*/
@ -679,11 +690,17 @@ function parseNumericSeries(value, scope = null) {
return [value];
}
const array = value
.split(' ')
.map(i => i.trim())
/** @type {(string|number)[]} */
let values = Array.isArray(value) ? value : value.split(' ');
// If a JSON array was provided as the only value, convert it to an array
if (values.length === 1 && typeof values[0] === 'string' && values[0].startsWith('[')) {
values = convertValueType(values[0], 'array');
}
const array = values.map(i => typeof i === 'string' ? i.trim() : i)
.filter(i => i !== '')
.map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))
.map(i => isNaN(Number(i)) ? Number(resolveVariable(String(i), scope)) : Number(i))
.filter(i => !isNaN(i));
return array;
@ -703,7 +720,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
const result = singleOperand ? operation(array[0]) : operation(array);
if (isNaN(result) || !isFinite(result)) {
if (isNaN(result)) {
return 0;
}
@ -731,7 +748,7 @@ function maxValuesCallback(args, value) {
}
function subValuesCallback(args, value) {
return performOperation(value, (array) => array[0] - array[1], false, args._scope);
return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
}
function divValuesCallback(args, value) {
@ -910,7 +927,7 @@ export function registerVariableCommands() {
name: 'listvar',
callback: listVariablesCallback,
aliases: ['listchatvar'],
helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>format</code> argument to change the output format.',
helpString: 'List registered chat variables. Displays variables in a popup by default. Use the <code>return</code> argument to change the return type.',
returns: 'JSON list of local variables',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
@ -926,9 +943,18 @@ export function registerVariableCommands() {
new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'popup-html',
enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
description: 'output format',
description: '!!! DEPRECATED - use "return" instead !!! output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
@ -1595,36 +1621,15 @@ export function registerVariableCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'add',
callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
callback: (args, value) => addValuesCallback(args, value),
returns: 'sum of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to sum',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: (executor, scope) => {
const vars = commonEnumProviders.variables('all')(executor, scope);
vars.push(
new SlashCommandEnumValue(
'any variable name',
null,
enumTypes.variable,
enumIcons.variable,
(input) => /^\w*$/.test(input),
(input) => input,
),
new SlashCommandEnumValue(
'any number',
null,
enumTypes.number,
enumIcons.number,
(input) => input == '' || !Number.isNaN(Number(input)),
(input) => input,
),
);
return vars;
},
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -1632,7 +1637,9 @@ export function registerVariableCommands() {
helpString: `
<div>
Performs an addition of the set of values and passes the result down the pipe.
Can use variable names.
</div>
<div>
Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
</div>
<div>
<strong>Example:</strong>
@ -1640,6 +1647,9 @@ export function registerVariableCommands() {
<li>
<pre><code class="language-stscript">/add 10 i 30 j</code></pre>
</li>
<li>
<pre><code class="language-stscript">/add ["count", 15, 2, "i"]</code></pre>
</li>
</ul>
</div>
`,
@ -1651,16 +1661,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to multiply',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.
Performs a multiplication of the set of values and passes the result down the pipe.
</div>
<div>
Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
</div>
<div>
<strong>Examples:</strong>
@ -1668,6 +1682,9 @@ export function registerVariableCommands() {
<li>
<pre><code class="language-stscript">/mul 10 i 30 j</code></pre>
</li>
<li>
<pre><code class="language-stscript">/mul ["count", 15, 2, "i"]</code></pre>
</li>
</ul>
</div>
`,
@ -1679,16 +1696,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to find the max',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.
Returns the maximum value of the set of values and passes the result down the pipe.
</div>
<div>
Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
</div>
<div>
<strong>Examples:</strong>
@ -1696,6 +1717,9 @@ export function registerVariableCommands() {
<li>
<pre><code class="language-stscript">/max 10 i 30 j</code></pre>
</li>
<li>
<pre><code class="language-stscript">/max ["count", 15, 2, "i"]</code></pre>
</li>
</ul>
</div>
`,
@ -1707,17 +1731,20 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to find the min',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Returns the minimum value of the set of values and passes the result down the pipe.
Can use variable names.
</div>
<div>
Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
</div>
<div>
<strong>Example:</strong>
@ -1725,6 +1752,9 @@ export function registerVariableCommands() {
<li>
<pre><code class="language-stscript">/min 10 i 30 j</code></pre>
</li>
<li>
<pre><code class="language-stscript">/min ["count", 15, 2, "i"]</code></pre>
</li>
</ul>
</div>
`,
@ -1735,18 +1765,21 @@ export function registerVariableCommands() {
returns: 'difference of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to find the difference',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
description: 'values to subtract, starting form the first provided value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs a subtraction of the set of values and passes the result down the pipe.
Can use variable names.
</div>
<div>
Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
</div>
<div>
<strong>Example:</strong>
@ -1754,6 +1787,9 @@ export function registerVariableCommands() {
<li>
<pre><code class="language-stscript">/sub i 5</code></pre>
</li>
<li>
<pre><code class="language-stscript">/sub ["count", 4, "i"]</code></pre>
</li>
</ul>
</div>
`,
@ -1767,17 +1803,18 @@ export function registerVariableCommands() {
description: 'dividend',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'divisor',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs a division of two values and passes the result down the pipe.
@ -1802,17 +1839,18 @@ export function registerVariableCommands() {
description: 'dividend',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'divisor',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs a modulo operation of two values and passes the result down the pipe.
@ -1837,17 +1875,18 @@ export function registerVariableCommands() {
description: 'base',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
SlashCommandArgument.fromProps({
description: 'exponent',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs a power operation of two values and passes the result down the pipe.
@ -1872,7 +1911,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -1900,7 +1939,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -1929,7 +1968,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -1957,7 +1996,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -1985,7 +2024,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@ -2013,7 +2052,7 @@ export function registerVariableCommands() {
description: 'value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],

View File

@ -323,7 +323,7 @@ async function sendMakerSuiteRequest(request, response) {
? (stream ? 'streamGenerateContent' : 'generateContent')
: (isText ? 'generateText' : 'generateMessage');
const generateResponse = await fetch(`${apiUrl.origin}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
const generateResponse = await fetch(`${apiUrl}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
body: JSON.stringify(body),
method: 'POST',
headers: {

View File

@ -9,41 +9,6 @@ const { jsonParser } = require('../express-common');
const { readSecret, SECRET_KEYS } = require('./secrets.js');
const FormData = require('form-data');
/**
* Sanitizes a string.
* @param {string} x String to sanitize
* @returns {string} Sanitized string
*/
function safeStr(x) {
x = String(x);
x = x.replace(/ +/g, ' ');
x = x.trim();
x = x.replace(/^[\s,.]+|[\s,.]+$/g, '');
return x;
}
const splitStrings = [
', extremely',
', intricate,',
];
const dangerousPatterns = '[]【】()|:';
/**
* Removes patterns from a string.
* @param {string} x String to sanitize
* @param {string} pattern Pattern to remove
* @returns {string} Sanitized string
*/
function removePattern(x, pattern) {
for (let i = 0; i < pattern.length; i++) {
let p = pattern[i];
let regex = new RegExp('\\' + p, 'g');
x = x.replace(regex, '');
}
return x;
}
/**
* Gets the comfy workflows.
* @param {import('../users.js').UserDirectoryList} directories
@ -391,40 +356,6 @@ router.post('/sd-next/upscalers', jsonParser, async (request, response) => {
}
});
/**
* SD prompt expansion using GPT-2 text generation model.
* Adapted from: https://github.com/lllyasviel/Fooocus/blob/main/modules/expansion.py
*/
router.post('/expand', jsonParser, async (request, response) => {
const originalPrompt = request.body.prompt;
if (!originalPrompt) {
console.warn('No prompt provided for SD expansion.');
return response.send({ prompt: '' });
}
console.log('Refine prompt input:', originalPrompt);
const splitString = splitStrings[Math.floor(Math.random() * splitStrings.length)];
let prompt = safeStr(originalPrompt) + splitString;
try {
const task = 'text-generation';
const module = await import('../transformers.mjs');
const pipe = await module.default.getPipeline(task);
const result = await pipe(prompt, { num_beams: 1, max_new_tokens: 256, do_sample: true });
const newText = result[0].generated_text;
const newPrompt = safeStr(removePattern(newText, dangerousPatterns));
console.log('Refine prompt output:', newPrompt);
return response.send({ prompt: newPrompt });
} catch {
console.warn('Failed to load transformers.js pipeline.');
return response.send({ prompt: originalPrompt });
}
});
const comfy = express.Router();
comfy.post('/ping', jsonParser, async (request, response) => {

View File

@ -31,12 +31,6 @@ const tasks = {
configField: 'extras.embeddingModel',
quantized: true,
},
'text-generation': {
defaultModel: 'Cohee/fooocus_expansion-onnx',
pipeline: null,
configField: 'extras.promptExpansionModel',
quantized: false,
},
'automatic-speech-recognition': {
defaultModel: 'Xenova/whisper-small',
pipeline: null,