Merge branch 'staging' into translation-improvements

fcc00e0b263bcdea477e45dfecce36d2df28d0fa

Cohee <18619528+Cohee1207@users.noreply.github.com>

10 files changed, +551 -28Showing whitespace changes
jsconfig.json+6 -1
@@ -15,6 +15,11 @@
1515 "**/node_modules/*",
1616 "public/lib",
1717 "backups/*",
1818 "data/*",
19+ "**/dist/*",
20+ "dist/*",
21+ "cache/*",
22+ "src/tokenizers/*",
23+ "docker/*",
1924 ]
2025}
public/script.js+2 -1
@@ -159,7 +159,7 @@ import {
159159import { debounce_timeout } from './scripts/constants.js';
160160
161161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
162162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
163163import {
164164 tag_map,
165165 tags,
@@ -911,6 +911,7 @@ async function firstLoadInit() {
911911 initKeyboard();
912912 initDynamicStyles();
913913 initTags();
914+ initDefaultSlashCommands();
914915 await getUserAvatars(true, user_avatar);
915916 await getCharacters();
916917 await getBackgrounds();
public/scripts/extensions/caption/index.js+1 -0
@@ -356,6 +356,7 @@ jQuery(async function () {
356356 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
357357 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
358358 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'koboldcpp' && textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP]) ||
359+ (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'vllm' && textgenerationwebui_settings.server_urls[textgen_types.VLLM]) ||
359360 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
360361 extension_settings.caption.source === 'local' ||
361362 extension_settings.caption.source === 'horde';
public/scripts/extensions/caption/settings.html+2 -0
@@ -26,6 +26,7 @@
2626 <option value="openai">OpenAI</option>
2727 <option value="openrouter">OpenRouter</option>
2828 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
29+ <option value="vllm">vLLM</option>
2930 </select>
3031 </div>
3132 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -66,6 +67,7 @@
6667 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
6768 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
6869 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
70+ <option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
6971 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
7072 </select>
7173 </div>
public/scripts/extensions/shared.js+17 -0
@@ -34,6 +34,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
3434 const isCustom = extension_settings.caption.multimodal_api === 'custom';
3535 const isOoba = extension_settings.caption.multimodal_api === 'ooba';
3636 const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp';
37+ const isVllm = extension_settings.caption.multimodal_api === 'vllm';
3738 const base64Bytes = base64Img.length * 0.75;
3839 const compressionLimit = 2 * 1024 * 1024;
3940 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
@@ -65,6 +66,14 @@ export async function getMultimodalCaption(base64Img, prompt) {
6566 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
6667 }
6768
69+ if (isVllm) {
70+ if (extension_settings.caption.multimodal_model === 'vllm_current') {
71+ requestBody.model = textgenerationwebui_settings.vllm_model;
72+ }
73+
74+ requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
75+ }
76+
6877 if (isLlamaCpp) {
6978 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
7079 }
@@ -151,6 +160,14 @@ function throwIfInvalidModel(useReverseProxy) {
151160 throw new Error('KoboldCpp server URL is not set.');
152161 }
153162
163+ if (extension_settings.caption.multimodal_api === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM]) {
164+ throw new Error('vLLM server URL is not set.');
165+ }
166+
167+ if (extension_settings.caption.multimodal_api === 'vllm' && extension_settings.caption.multimodal_model === 'vllm_current' && !textgenerationwebui_settings.vllm_model) {
168+ throw new Error('vLLM model is not set.');
169+ }
170+
154171 if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
155172 throw new Error('Custom API URL is not set.');
156173 }
public/scripts/extensions/tts/index.js+2 -0
@@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js';
1010import { power_user } from '../../power-user.js';
1111import { OpenAITtsProvider } from './openai.js';
1212import { XTTSTtsProvider } from './xtts.js';
13+import { VITSTtsProvider } from './vits.js';
1314import { GSVITtsProvider } from './gsvi.js';
1415import { SBVits2TtsProvider } from './sbvits2.js';
1516import { AllTalkTtsProvider } from './alltalk.js';
@@ -83,6 +84,7 @@ const ttsProviders = {
8384 ElevenLabs: ElevenLabsTtsProvider,
8485 Silero: SileroTtsProvider,
8586 XTTSv2: XTTSTtsProvider,
87+ VITS: VITSTtsProvider,
8688 GSVI: GSVITtsProvider,
8789 SBVits2: SBVits2TtsProvider,
8890 System: SystemTtsProvider,
public/scripts/extensions/tts/vits.js+404 -0
@@ -0,0 +1,404 @@
1+import { getPreviewString, saveTtsProviderSettings } from './index.js';
2+
3+export { VITSTtsProvider };
4+
5+class VITSTtsProvider {
6+ //########//
7+ // Config //
8+ //########//
9+
10+ settings;
11+ ready = false;
12+ voices = [];
13+ separator = '. ';
14+ audioElement = document.createElement('audio');
15+
16+ /**
17+ * Perform any text processing before passing to TTS engine.
18+ * @param {string} text Input text
19+ * @returns {string} Processed text
20+ */
21+ processText(text) {
22+ return text;
23+ }
24+
25+ audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
26+
27+ languageLabels = {
28+ 'Auto': 'auto',
29+ 'Chinese': 'zh',
30+ 'English': 'en',
31+ 'Japanese': 'ja',
32+ 'Korean': 'ko',
33+ };
34+
35+ langKey2LangCode = {
36+ 'zh': 'zh-CN',
37+ 'en': 'en-US',
38+ 'ja': 'ja-JP',
39+ 'ko': 'ko-KR',
40+ };
41+
42+ modelTypes = {
43+ VITS: 'VITS',
44+ W2V2_VITS: 'W2V2-VITS',
45+ BERT_VITS2: 'BERT-VITS2',
46+ };
47+
48+ defaultSettings = {
49+ provider_endpoint: 'http://localhost:23456',
50+ format: 'wav',
51+ lang: 'auto',
52+ length: 1.0,
53+ noise: 0.33,
54+ noisew: 0.4,
55+ segment_size: 50,
56+ streaming: false,
57+ dim_emotion: 0,
58+ sdp_ratio: 0.2,
59+ emotion: 0,
60+ text_prompt: '',
61+ style_text: '',
62+ style_weight: 1,
63+ };
64+
65+ get settingsHtml() {
66+ let html = `
67+ <label for="vits_lang">Text Language</label>
68+ <select id="vits_lang">`;
69+
70+ for (let language in this.languageLabels) {
71+ if (this.languageLabels[language] == this.settings?.lang) {
72+ html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
73+ continue;
74+ }
75+ html += `<option value="${this.languageLabels[language]}">${language}</option>`;
76+ }
77+
78+ html += `
79+ </select>
80+ <label>VITS / W2V2-VITS / Bert-VITS2 Settings:</label><br/>
81+ <label for="vits_endpoint">Provider Endpoint:</label>
82+ <input id="vits_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
83+ <span>Use <a target="_blank" href="https://github.com/Artrajz/vits-simple-api">vits-simple-api</a>.</span><br/>
84+
85+ <label for="vits_format">Audio format:</label>
86+ <select id="vits_format">`;
87+
88+ for (let format of this.audioFormats) {
89+ if (format == this.settings?.format) {
90+ html += `<option value="${format}" selected="selected">${format}</option>`;
91+ continue;
92+ }
93+ html += `<option value="${format}">${format}</option>`;
94+ }
95+
96+ html += `
97+ </select>
98+ <label for="vits_length">Audio length: <span id="vits_length_output">${this.defaultSettings.length}</span></label>
99+ <input id="vits_length" type="range" value="${this.defaultSettings.length}" min="0.0" max="5" step="0.01" />
100+
101+ <label for="vits_noise">Noise: <span id="vits_noise_output">${this.defaultSettings.noise}</span></label>
102+ <input id="vits_noise" type="range" value="${this.defaultSettings.noise}" min="0.1" max="2" step="0.01" />
103+
104+ <label for="vits_noisew">SDP noise: <span id="vits_noisew_output">${this.defaultSettings.noisew}</span></label>
105+ <input id="vits_noisew" type="range" value="${this.defaultSettings.noisew}" min="0.1" max="2" step="0.01" />
106+
107+ <label for="vits_segment_size">Segment Size: <span id="vits_segment_size_output">${this.defaultSettings.segment_size}</span></label>
108+ <input id="vits_segment_size" type="range" value="${this.defaultSettings.segment_size}" min="0" max="1000" step="1" />
109+
110+ <label for="vits_streaming" class="checkbox_label">
111+ <input id="vits_streaming" type="checkbox" />
112+ <span>Streaming</span>
113+ </label>
114+
115+ <label>W2V2-VITS Settings:</label><br/>
116+ <label for="vits_dim_emotion">Dimensional emotion:</label>
117+ <input id="vits_dim_emotion" type="number" class="text_pole" min="0" max="5457" step="1" value="${this.defaultSettings.dim_emotion}"/>
118+
119+ <label>BERT-VITS2 Settings:</label><br/>
120+ <label for="vits_sdp_ratio">sdp_ratio: <span id="vits_sdp_ratio_output">${this.defaultSettings.sdp_ratio}</span></label>
121+ <input id="vits_sdp_ratio" type="range" value="${this.defaultSettings.sdp_ratio}" min="0.0" max="1" step="0.01" />
122+
123+ <label for="vits_emotion">emotion: <span id="vits_emotion_output">${this.defaultSettings.emotion}</span></label>
124+ <input id="vits_emotion" type="range" value="${this.defaultSettings.emotion}" min="0" max="9" step="1" />
125+
126+ <label for="vits_text_prompt">Text Prompt:</label>
127+ <input id="vits_text_prompt" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.text_prompt}"/>
128+
129+ <label for="vits_style_text">Style text:</label>
130+ <input id="vits_style_text" type="text" class="text_pole" maxlength="512" value="${this.defaultSettings.style_text}"/>
131+
132+ <label for="vits_style_weight">Style weight <span id="vits_style_weight_output">${this.defaultSettings.style_weight}</span></label>
133+ <input id="vits_style_weight" type="range" value="${this.defaultSettings.style_weight}" min="0" max="1" step="0.01" />
134+ `;
135+
136+ return html;
137+ }
138+
139+ onSettingsChange() {
140+ // Used when provider settings are updated from UI
141+ this.settings.provider_endpoint = $('#vits_endpoint').val();
142+ this.settings.lang = $('#vits_lang').val();
143+ this.settings.format = $('#vits_format').val();
144+ this.settings.dim_emotion = $('#vits_dim_emotion').val();
145+ this.settings.text_prompt = $('#vits_text_prompt').val();
146+ this.settings.style_text = $('#vits_style_text').val();
147+
148+ // Update the default TTS settings based on input fields
149+ this.settings.length = $('#vits_length').val();
150+ this.settings.noise = $('#vits_noise').val();
151+ this.settings.noisew = $('#vits_noisew').val();
152+ this.settings.segment_size = $('#vits_segment_size').val();
153+ this.settings.streaming = $('#vits_streaming').is(':checked');
154+ this.settings.sdp_ratio = $('#vits_sdp_ratio').val();
155+ this.settings.emotion = $('#vits_emotion').val();
156+ this.settings.style_weight = $('#vits_style_weight').val();
157+
158+ // Update the UI to reflect changes
159+ $('#vits_length_output').text(this.settings.length);
160+ $('#vits_noise_output').text(this.settings.noise);
161+ $('#vits_noisew_output').text(this.settings.noisew);
162+ $('#vits_segment_size_output').text(this.settings.segment_size);
163+ $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
164+ $('#vits_emotion_output').text(this.settings.emotion);
165+ $('#vits_style_weight_output').text(this.settings.style_weight);
166+
167+ saveTtsProviderSettings();
168+ this.changeTTSSettings();
169+ }
170+
171+ async loadSettings(settings) {
172+ // Pupulate Provider UI given input settings
173+ if (Object.keys(settings).length == 0) {
174+ console.info('Using default TTS Provider settings');
175+ }
176+
177+ // Only accept keys defined in defaultSettings
178+ this.settings = this.defaultSettings;
179+
180+ for (const key in settings) {
181+ if (key in this.settings) {
182+ this.settings[key] = settings[key];
183+ } else {
184+ console.debug(`Ignoring non-user-configurable setting: ${key}`);
185+ }
186+ }
187+
188+ // Set initial values from the settings
189+ $('#vits_endpoint').val(this.settings.provider_endpoint);
190+ $('#vits_lang').val(this.settings.lang);
191+ $('#vits_format').val(this.settings.format);
192+ $('#vits_length').val(this.settings.length);
193+ $('#vits_noise').val(this.settings.noise);
194+ $('#vits_noisew').val(this.settings.noisew);
195+ $('#vits_segment_size').val(this.settings.segment_size);
196+ $('#vits_streaming').prop('checked', this.settings.streaming);
197+ $('#vits_dim_emotion').val(this.settings.dim_emotion);
198+ $('#vits_sdp_ratio').val(this.settings.sdp_ratio);
199+ $('#vits_emotion').val(this.settings.emotion);
200+ $('#vits_text_prompt').val(this.settings.text_prompt);
201+ $('#vits_style_text').val(this.settings.style_text);
202+ $('#vits_style_weight').val(this.settings.style_weight);
203+
204+ // Update the UI to reflect changes
205+ $('#vits_length_output').text(this.settings.length);
206+ $('#vits_noise_output').text(this.settings.noise);
207+ $('#vits_noisew_output').text(this.settings.noisew);
208+ $('#vits_segment_size_output').text(this.settings.segment_size);
209+ $('#vits_sdp_ratio_output').text(this.settings.sdp_ratio);
210+ $('#vits_emotion_output').text(this.settings.emotion);
211+ $('#vits_style_weight_output').text(this.settings.style_weight);
212+
213+ // Register input/change event listeners to update settings on user interaction
214+ $('#vits_endpoint').on('input', () => { this.onSettingsChange(); });
215+ $('#vits_lang').on('change', () => { this.onSettingsChange(); });
216+ $('#vits_format').on('change', () => { this.onSettingsChange(); });
217+ $('#vits_length').on('change', () => { this.onSettingsChange(); });
218+ $('#vits_noise').on('change', () => { this.onSettingsChange(); });
219+ $('#vits_noisew').on('change', () => { this.onSettingsChange(); });
220+ $('#vits_segment_size').on('change', () => { this.onSettingsChange(); });
221+ $('#vits_streaming').on('change', () => { this.onSettingsChange(); });
222+ $('#vits_dim_emotion').on('change', () => { this.onSettingsChange(); });
223+ $('#vits_sdp_ratio').on('change', () => { this.onSettingsChange(); });
224+ $('#vits_emotion').on('change', () => { this.onSettingsChange(); });
225+ $('#vits_text_prompt').on('change', () => { this.onSettingsChange(); });
226+ $('#vits_style_text').on('change', () => { this.onSettingsChange(); });
227+ $('#vits_style_weight').on('change', () => { this.onSettingsChange(); });
228+
229+ await this.checkReady();
230+
231+ console.info('VITS: Settings loaded');
232+ }
233+
234+ // Perform a simple readiness check by trying to fetch voiceIds
235+ async checkReady() {
236+ await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
237+ }
238+
239+ async onRefreshClick() {
240+ return;
241+ }
242+
243+ //#################//
244+ // TTS Interfaces //
245+ //#################//
246+
247+ async getVoice(voiceName) {
248+ if (this.voices.length == 0) {
249+ this.voices = await this.fetchTtsVoiceObjects();
250+ }
251+ const match = this.voices.filter(
252+ v => v.name == voiceName,
253+ )[0];
254+ if (!match) {
255+ throw `TTS Voice name ${voiceName} not found`;
256+ }
257+ return match;
258+ }
259+
260+ async getVoiceById(voiceId) {
261+ if (this.voices.length == 0) {
262+ this.voices = await this.fetchTtsVoiceObjects();
263+ }
264+ const match = this.voices.filter(
265+ v => v.voice_id == voiceId,
266+ )[0];
267+ if (!match) {
268+ throw `TTS Voice id ${voiceId} not found`;
269+ }
270+ return match;
271+ }
272+
273+ async generateTts(text, voiceId) {
274+ const response = await this.fetchTtsGeneration(text, voiceId);
275+ return response;
276+ }
277+
278+ //###########//
279+ // API CALLS //
280+ //###########//
281+ async fetchTtsVoiceObjects() {
282+ const response = await fetch(`${this.settings.provider_endpoint}/voice/speakers`);
283+ if (!response.ok) {
284+ throw new Error(`HTTP ${response.status}: ${await response.json()}`);
285+ }
286+ const jsonData = await response.json();
287+ const voices = [];
288+
289+ const addVoices = (modelType) => {
290+ jsonData[modelType].forEach(voice => {
291+ voices.push({
292+ name: `[${modelType}] ${voice.name} (${voice.lang})`,
293+ voice_id: `${modelType}&${voice.id}`,
294+ preview_url: false,
295+ lang: voice.lang,
296+ });
297+ });
298+ };
299+ for (const key in this.modelTypes) {
300+ addVoices(this.modelTypes[key]);
301+ }
302+
303+ this.voices = voices; // Assign to the class property
304+ return voices; // Also return this list
305+ }
306+
307+ // Each time a parameter is changed, we change the configuration
308+ async changeTTSSettings() {
309+ }
310+
311+ /**
312+ * Fetch TTS generation from the API.
313+ * @param {string} inputText Text to generate TTS for
314+ * @param {string} voiceId Voice ID to use (model_type&speaker_id))
315+ * @returns {Promise<Response|string>} Fetch response
316+ */
317+ async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
318+ console.info(`Generating new TTS for voice_id ${voiceId}`);
319+
320+ const streaming = !forceNoStreaming && this.settings.streaming;
321+ const [model_type, speaker_id] = voiceId.split('&');
322+ const params = new URLSearchParams();
323+ params.append('text', inputText);
324+ params.append('id', speaker_id);
325+ if (streaming) {
326+ params.append('streaming', streaming);
327+ // Streaming response only supports MP3
328+ }
329+ else {
330+ params.append('format', this.settings.format);
331+ }
332+ params.append('lang', lang ?? this.settings.lang);
333+ params.append('length', this.settings.length);
334+ params.append('noise', this.settings.noise);
335+ params.append('noisew', this.settings.noisew);
336+ params.append('segment_size', this.settings.segment_size);
337+
338+ if (model_type == this.modelTypes.W2V2_VITS) {
339+ params.append('emotion', this.settings.dim_emotion);
340+ }
341+ else if (model_type == this.modelTypes.BERT_VITS2) {
342+ params.append('sdp_ratio', this.settings.sdp_ratio);
343+ params.append('emotion', this.settings.emotion);
344+ if (this.settings.text_prompt) {
345+ params.append('text_prompt', this.settings.text_prompt);
346+ }
347+ if (this.settings.style_text) {
348+ params.append('style_text', this.settings.style_text);
349+ params.append('style_weight', this.settings.style_weight);
350+ }
351+ }
352+
353+ const url = `${this.settings.provider_endpoint}/voice/${model_type.toLowerCase()}`;
354+
355+ if (streaming) {
356+ return url + `?${params.toString()}`;
357+ }
358+
359+ const response = await fetch(
360+ url,
361+ {
362+ method: 'POST',
363+ headers: {
364+ 'Content-Type': 'application/x-www-form-urlencoded',
365+ },
366+ body: params,
367+ },
368+ );
369+ if (!response.ok) {
370+ toastr.error(response.statusText, 'TTS Generation Failed');
371+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
372+ }
373+ return response;
374+ }
375+
376+ /**
377+ * Preview TTS for a given voice ID.
378+ * @param {string} id Voice ID
379+ */
380+ async previewTtsVoice(id) {
381+ this.audioElement.pause();
382+ this.audioElement.currentTime = 0;
383+ const voice = await this.getVoiceById(id);
384+ const lang = voice.lang.includes(this.settings.lang) ? this.settings.lang : voice.lang[0];
385+
386+ let lang_code = this.langKey2LangCode[lang];
387+ const text = getPreviewString(lang_code);
388+ const response = await this.fetchTtsGeneration(text, id, lang, true);
389+ if (typeof response != 'string') {
390+ if (!response.ok) {
391+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
392+ }
393+ const audio = await response.blob();
394+ const url = URL.createObjectURL(audio);
395+ this.audioElement.src = url;
396+ this.audioElement.play();
397+ }
398+ }
399+
400+ // Interface not used
401+ async fetchTtsFromHistory(history_item_id) {
402+ return Promise.resolve(history_item_id);
403+ }
404+}
public/scripts/slash-commands.js+106 -19
@@ -38,13 +38,13 @@ import {
3838 system_message_types,
3939 this_chid,
4040} from '../script.js';
4141import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
4242import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
4343import { getMessageTimeStamp } from './RossAscends-mods.js';
4444import { hideChatMessageRange } from './chats.js';
4545import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
4646import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
4747import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
4848import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
4949import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
5050import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
@@ -53,7 +53,6 @@ import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCoun
5353import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
5454import { registerVariableCommands, resolveVariable } from './variables.js';
5555import { background_settings } from './backgrounds.js';
56-import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
5756import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
5857import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
5958import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
@@ -75,6 +74,7 @@ export const parser = new SlashCommandParser();
7574const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
7675const getSlashCommandsHelp = parser.getHelpString.bind(parser);
7776
77+export function initDefaultSlashCommands() {
7878 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
7979 name: '?',
8080 callback: helpCommandCallback,
@@ -177,7 +177,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
177177 ),
178178 SlashCommandNamedArgument.fromProps({
179179 name: 'at',
180- description: 'position to insert the message',
180+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
181181 typeList: [ARGUMENT_TYPE.NUMBER],
182182 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
183183 }),
@@ -220,7 +220,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
220220 ),
221221 SlashCommandNamedArgument.fromProps({
222222 name: 'at',
223- description: 'position to insert the message',
223+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
224224 typeList: [ARGUMENT_TYPE.NUMBER],
225225 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
226226 }),
@@ -274,7 +274,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
274274 ),
275275 SlashCommandNamedArgument.fromProps({
276276 name: 'at',
277- description: 'position to insert the message',
277+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
278278 typeList: [ARGUMENT_TYPE.NUMBER],
279279 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
280280 }),
@@ -459,7 +459,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
459459 ),
460460 SlashCommandNamedArgument.fromProps({
461461 name: 'at',
462- description: 'position to insert the message',
462+ description: 'position to insert the message (index-based, corresponding to message id). If not set, the message will be inserted at the end of the chat.\nNegative values are accepted and will work similarly to how \'depth\' usually works. For example, -1 will insert the message right before the last message in chat.',
463463 typeList: [ARGUMENT_TYPE.NUMBER],
464464 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
465465 }),
@@ -745,6 +745,32 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
745745 new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),
746746 ],
747747 }),
748+ SlashCommandNamedArgument.fromProps({
749+ name: 'timeout',
750+ description: 'time in milliseconds to display the toast message. Set this and \'extendedTimeout\' to 0 to show indefinitely until dismissed.',
751+ typeList: [ARGUMENT_TYPE.NUMBER],
752+ defaultValue: `${toastr.options.timeOut}`,
753+ }),
754+ SlashCommandNamedArgument.fromProps({
755+ name: 'extendedTimeout',
756+ description: 'time in milliseconds to display the toast message. Set this and \'timeout\' to 0 to show indefinitely until dismissed.',
757+ typeList: [ARGUMENT_TYPE.NUMBER],
758+ defaultValue: `${toastr.options.extendedTimeOut}`,
759+ }),
760+ SlashCommandNamedArgument.fromProps({
761+ name: 'preventDuplicates',
762+ description: 'prevent duplicate toasts with the same message from being displayed.',
763+ typeList: [ARGUMENT_TYPE.BOOLEAN],
764+ defaultValue: 'false',
765+ enumList: commonEnumProviders.boolean('trueFalse')(),
766+ }),
767+ SlashCommandNamedArgument.fromProps({
768+ name: 'awaitDismissal',
769+ description: 'wait for the toast to be dismissed before continuing.',
770+ typeList: [ARGUMENT_TYPE.BOOLEAN],
771+ defaultValue: 'false',
772+ enumList: commonEnumProviders.boolean('trueFalse')(),
773+ }),
748774 ],
749775 unnamedArgumentList: [
750776 new SlashCommandArgument(
@@ -1387,6 +1413,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
13871413 }));
13881414
13891415 registerVariableCommands();
1416+}
13901417
13911418const NARRATOR_NAME_KEY = 'narrator_name';
13921419const NARRATOR_NAME_DEFAULT = 'System';
@@ -1938,31 +1965,66 @@ async function generateCallback(args, value) {
19381965 }
19391966}
19401967
1968+/**
1969+ *
1970+ * @param {{title?: string, severity?: string, timeout?: string, extendedTimeout?: string, preventDuplicates?: string, awaitDismissal?: string}} args - named arguments from the slash command
1971+ * @param {string} value - The string to echo (unnamed argument from the slash command)
1972+ * @returns {Promise<string>} The text that was echoed
1973+ */
19411974async function echoCallback(args, value) {
19421975 // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
19431976 if (value === '') {
19441977 console.warn('WARN: No argument provided for /echo command');
19451978 return '';
1979+ }
1980+
1981+ if (args.severity && !['error', 'warning', 'success', 'info'].includes(args.severity)) {
1982+ toastr.warning(`Invalid severity provided for /echo command: ${args.severity}`);
1983+ args.severity = null;
19461984 }
1947- const title = args?.title !== undefined && typeof args?.title === 'string' ? args.title : undefined;
1985+
1948- const severity = args?.severity !== undefined && typeof args?.severity === 'string' ? args.severity : 'info';
1986+ const title = args.title ? args.title : undefined;
1987+ const severity = args.severity ? args.severity : 'info';
1988+
1989+ /** @type {ToastrOptions} */
1990+ const options = {};
1991+ if (args.timeout && !isNaN(parseInt(args.timeout))) options.timeOut = parseInt(args.timeout);
1992+ if (args.extendedTimeout && !isNaN(parseInt(args.extendedTimeout))) options.extendedTimeOut = parseInt(args.extendedTimeout);
1993+ if (isTrueBoolean(args.preventDuplicates)) options.preventDuplicates = true;
1994+
1995+ // Prepare possible await handling
1996+ let awaitDismissal = isTrueBoolean(args.awaitDismissal);
1997+ let resolveToastDismissal;
1998+
1999+ if (awaitDismissal) {
2000+ options.onHidden = () => resolveToastDismissal(value);
2001+ }
2002+
19492003 switch (severity) {
19502004 case 'error':
19512005 toastr.error(value, title, options);
19522006 break;
19532007 case 'warning':
19542008 toastr.warning(value, title, options);
19552009 break;
19562010 case 'success':
19572011 toastr.success(value, title, options);
19582012 break;
19592013 case 'info':
19602014 default:
19612015 toastr.info(value, title, options);
19622016 break;
19632017 }
2018+
2019+ if (awaitDismissal) {
2020+ return new Promise((resolve) => {
2021+ resolveToastDismissal = resolve;
2022+ });
2023+ } else {
19642024 return value;
19652025 }
2026+}
2027+
19662028
19672029async function addSwipeCallback(_, arg) {
19682030 const lastMessage = chat[chat.length - 1];
@@ -2428,7 +2490,14 @@ async function sendUserMessageCallback(args, text) {
24282490 text = text.trim();
24292491 const compact = isTrueBoolean(args?.compact);
24302492 const bias = extractMessageBias(text);
2431- const insertAt = Number(args?.at);
2493+
2494+ let insertAt = Number(args?.at);
2495+
2496+ // Convert possible depth parameter to index
2497+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2498+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2499+ insertAt = chat.length + insertAt;
2500+ }
24322501
24332502 if ('name' in args) {
24342503 const name = args.name || '';
@@ -2737,7 +2806,13 @@ export async function sendMessageAs(args, text) {
27372806 },
27382807 }];
27392808
27402809 constlet insertAt = Number(args.at);
2810+
2811+ // Convert possible depth parameter to index
2812+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2813+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2814+ insertAt = chat.length + insertAt;
2815+ }
27412816
27422817 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
27432818 chat.splice(insertAt, 0, message);
@@ -2784,7 +2859,13 @@ export async function sendNarratorMessage(args, text) {
27842859 },
27852860 };
27862861
27872862 constlet insertAt = Number(args.at);
2863+
2864+ // Convert possible depth parameter to index
2865+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2866+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2867+ insertAt = chat.length + insertAt;
2868+ }
27882869
27892870 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
27902871 chat.splice(insertAt, 0, message);
@@ -2866,7 +2947,13 @@ async function sendCommentMessage(args, text) {
28662947 },
28672948 };
28682949
28692950 constlet insertAt = Number(args.at);
2951+
2952+ // Convert possible depth parameter to index
2953+ if (!isNaN(insertAt) && (insertAt < 0 || insertAt === Number(-0))) {
2954+ // Negative value means going back from current chat length. (E.g.: 8 messages, Depth 1 means insert at index 7)
2955+ insertAt = chat.length + insertAt;
2956+ }
28702957
28712958 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
28722959 chat.splice(insertAt, 0, message);
public/scripts/slash-commands/SlashCommandArgument.js+5 -5
@@ -59,7 +59,7 @@ export class SlashCommandArgument {
5959 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
6060 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
6161 */
6262 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = truefalse) {
6363 this.description = description;
6464 this.typeList = types ? Array.isArray(types) ? types : [types] : [];
6565 this.isRequired = isRequired ?? false;
@@ -90,7 +90,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
9090 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
9191 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
9292 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
9393 * @param {boolean} [props.forceEnum=truefalse] default: truefalse - whether the input must match one of the enum values
9494 */
9595 static fromProps(props) {
9696 return new SlashCommandNamedArgument(
@@ -103,7 +103,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
103103 props.enumList ?? [],
104104 props.aliasList ?? [],
105105 props.enumProvider ?? null,
106106 props.forceEnum ?? truefalse,
107107 );
108108 }
109109
@@ -120,9 +120,9 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
120120 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
121121 * @param {string[]} [aliases=[]]
122122 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
123123 * @param {boolean} [forceEnum=truefalse]
124124 */
125125 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = truefalse) {
126126 super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
127127 this.name = name;
128128 this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
src/endpoints/openai.js+6 -2
@@ -43,7 +43,11 @@ router.post('/caption-image', jsonParser, async (request, response) => {
4343 key = readSecret(request.user.directories, SECRET_KEYS.KOBOLDCPP);
4444 }
4545
46- if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp'].includes(request.body.api) === false) {
46+ if (request.body.api === 'vllm') {
47+ key = readSecret(request.user.directories, SECRET_KEYS.VLLM);
48+ }
49+
50+ if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
4751 console.log('No key found for API', request.body.api);
4852 return response.sendStatus(400);
4953 }
@@ -110,7 +114,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
110114 });
111115 }
112116
113117 if (request.body.api === 'koboldcpp' || request.body.api === 'vllm') {
114118 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
115119 }
116120