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 @@
15 "**/node_modules/*",15 "**/node_modules/*",
16 "public/lib",16 "public/lib",
17 "backups/*",17 "backups/*",
18 "data/*"18 "data/*",
19 "**/dist/*",
20 "dist/*",
21 "cache/*",
22 "src/tokenizers/*",
23 "docker/*",
19 ]24 ]
20}25}
public/script.js+2 -1
@@ -159,7 +159,7 @@ import {
159import { debounce_timeout } from './scripts/constants.js';159import { debounce_timeout } from './scripts/constants.js';
160160
161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';161import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';162import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
163import {163import {
164 tag_map,164 tag_map,
165 tags,165 tags,
@@ -911,6 +911,7 @@ async function firstLoadInit() {
911 initKeyboard();911 initKeyboard();
912 initDynamicStyles();912 initDynamicStyles();
913 initTags();913 initTags();
914 initDefaultSlashCommands();
914 await getUserAvatars(true, user_avatar);915 await getUserAvatars(true, user_avatar);
915 await getCharacters();916 await getCharacters();
916 await getBackgrounds();917 await getBackgrounds();
public/scripts/extensions/caption/index.js+1 -0
@@ -356,6 +356,7 @@ jQuery(async function () {
356 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||356 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
357 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||357 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
358 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'koboldcpp' && textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP]) ||358 (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]) ||
359 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||360 (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
360 extension_settings.caption.source === 'local' ||361 extension_settings.caption.source === 'local' ||
361 extension_settings.caption.source === 'horde';362 extension_settings.caption.source === 'horde';
public/scripts/extensions/caption/settings.html+2 -0
@@ -26,6 +26,7 @@
26 <option value="openai">OpenAI</option>26 <option value="openai">OpenAI</option>
27 <option value="openrouter">OpenRouter</option>27 <option value="openrouter">OpenRouter</option>
28 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>28 <option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
29 <option value="vllm">vLLM</option>
29 </select>30 </select>
30 </div>31 </div>
31 <div class="flex1 flex-container flexFlowColumn flexNoGap">32 <div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -66,6 +67,7 @@
66 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>67 <option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
67 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>68 <option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
68 <option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>69 <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>
69 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>71 <option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
70 </select>72 </select>
71 </div>73 </div>
public/scripts/extensions/shared.js+17 -0
@@ -34,6 +34,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
34 const isCustom = extension_settings.caption.multimodal_api === 'custom';34 const isCustom = extension_settings.caption.multimodal_api === 'custom';
35 const isOoba = extension_settings.caption.multimodal_api === 'ooba';35 const isOoba = extension_settings.caption.multimodal_api === 'ooba';
36 const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp';36 const isKoboldCpp = extension_settings.caption.multimodal_api === 'koboldcpp';
37 const isVllm = extension_settings.caption.multimodal_api === 'vllm';
37 const base64Bytes = base64Img.length * 0.75;38 const base64Bytes = base64Img.length * 0.75;
38 const compressionLimit = 2 * 1024 * 1024;39 const compressionLimit = 2 * 1024 * 1024;
39 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {40 if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
@@ -65,6 +66,14 @@ export async function getMultimodalCaption(base64Img, prompt) {
65 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];66 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
66 }67 }
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
68 if (isLlamaCpp) {77 if (isLlamaCpp) {
69 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];78 requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
70 }79 }
@@ -151,6 +160,14 @@ function throwIfInvalidModel(useReverseProxy) {
151 throw new Error('KoboldCpp server URL is not set.');160 throw new Error('KoboldCpp server URL is not set.');
152 }161 }
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
154 if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {171 if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
155 throw new Error('Custom API URL is not set.');172 throw new Error('Custom API URL is not set.');
156 }173 }
public/scripts/extensions/tts/index.js+2 -0
@@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js';
10import { power_user } from '../../power-user.js';10import { power_user } from '../../power-user.js';
11import { OpenAITtsProvider } from './openai.js';11import { OpenAITtsProvider } from './openai.js';
12import { XTTSTtsProvider } from './xtts.js';12import { XTTSTtsProvider } from './xtts.js';
13import { VITSTtsProvider } from './vits.js';
13import { GSVITtsProvider } from './gsvi.js';14import { GSVITtsProvider } from './gsvi.js';
14import { SBVits2TtsProvider } from './sbvits2.js';15import { SBVits2TtsProvider } from './sbvits2.js';
15import { AllTalkTtsProvider } from './alltalk.js';16import { AllTalkTtsProvider } from './alltalk.js';
@@ -83,6 +84,7 @@ const ttsProviders = {
83 ElevenLabs: ElevenLabsTtsProvider,84 ElevenLabs: ElevenLabsTtsProvider,
84 Silero: SileroTtsProvider,85 Silero: SileroTtsProvider,
85 XTTSv2: XTTSTtsProvider,86 XTTSv2: XTTSTtsProvider,
87 VITS: VITSTtsProvider,
86 GSVI: GSVITtsProvider,88 GSVI: GSVITtsProvider,
87 SBVits2: SBVits2TtsProvider,89 SBVits2: SBVits2TtsProvider,
88 System: SystemTtsProvider,90 System: SystemTtsProvider,
public/scripts/extensions/tts/vits.js+404 -0
@@ -0,0 +1,404 @@
1import { getPreviewString, saveTtsProviderSettings } from './index.js';
2
3export { VITSTtsProvider };
4
5class 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 {
38 system_message_types,38 system_message_types,
39 this_chid,39 this_chid,
40} from '../script.js';40} from '../script.js';
41import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';41import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
42import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';42import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
43import { getMessageTimeStamp } from './RossAscends-mods.js';43import { getMessageTimeStamp } from './RossAscends-mods.js';
44import { hideChatMessageRange } from './chats.js';44import { hideChatMessageRange } from './chats.js';
45import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';45import { getContext, saveMetadataDebounced } from './extensions.js';
46import { getRegexedString, regex_placement } from './extensions/regex/engine.js';46import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
47import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';47import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
48import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';48import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
49import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';49import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
50import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';50import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
@@ -53,7 +53,6 @@ import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCoun
53import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';53import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
54import { registerVariableCommands, resolveVariable } from './variables.js';54import { registerVariableCommands, resolveVariable } from './variables.js';
55import { background_settings } from './backgrounds.js';55import { background_settings } from './backgrounds.js';
56import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
57import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';56import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
58import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';57import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
59import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';58import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
@@ -75,6 +74,7 @@ export const parser = new SlashCommandParser();
75const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);74const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
76const getSlashCommandsHelp = parser.getHelpString.bind(parser);75const getSlashCommandsHelp = parser.getHelpString.bind(parser);
7776
77export function initDefaultSlashCommands() {
78 SlashCommandParser.addCommandObject(SlashCommand.fromProps({78 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
79 name: '?',79 name: '?',
80 callback: helpCommandCallback,80 callback: helpCommandCallback,
@@ -177,7 +177,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
177 ),177 ),
178 SlashCommandNamedArgument.fromProps({178 SlashCommandNamedArgument.fromProps({
179 name: 'at',179 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.',
181 typeList: [ARGUMENT_TYPE.NUMBER],181 typeList: [ARGUMENT_TYPE.NUMBER],
182 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),182 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
183 }),183 }),
@@ -220,7 +220,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
220 ),220 ),
221 SlashCommandNamedArgument.fromProps({221 SlashCommandNamedArgument.fromProps({
222 name: 'at',222 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.',
224 typeList: [ARGUMENT_TYPE.NUMBER],224 typeList: [ARGUMENT_TYPE.NUMBER],
225 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),225 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
226 }),226 }),
@@ -274,7 +274,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
274 ),274 ),
275 SlashCommandNamedArgument.fromProps({275 SlashCommandNamedArgument.fromProps({
276 name: 'at',276 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.',
278 typeList: [ARGUMENT_TYPE.NUMBER],278 typeList: [ARGUMENT_TYPE.NUMBER],
279 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),279 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
280 }),280 }),
@@ -459,7 +459,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
459 ),459 ),
460 SlashCommandNamedArgument.fromProps({460 SlashCommandNamedArgument.fromProps({
461 name: 'at',461 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.',
463 typeList: [ARGUMENT_TYPE.NUMBER],463 typeList: [ARGUMENT_TYPE.NUMBER],
464 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),464 enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
465 }),465 }),
@@ -745,6 +745,32 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
745 new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),745 new SlashCommandEnumValue('success', 'success', enumTypes.enum, '✅'),
746 ],746 ],
747 }),747 }),
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 }),
748 ],774 ],
749 unnamedArgumentList: [775 unnamedArgumentList: [
750 new SlashCommandArgument(776 new SlashCommandArgument(
@@ -1387,6 +1413,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1387 }));1413 }));
13881414
1389 registerVariableCommands();1415 registerVariableCommands();
1416}
13901417
1391const NARRATOR_NAME_KEY = 'narrator_name';1418const NARRATOR_NAME_KEY = 'narrator_name';
1392const NARRATOR_NAME_DEFAULT = 'System';1419const NARRATOR_NAME_DEFAULT = 'System';
@@ -1938,31 +1965,66 @@ async function generateCallback(args, value) {
1938 }1965 }
1939}1966}
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 */
1941async function echoCallback(args, value) {1974async function echoCallback(args, value) {
1942 // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options1975 // Note: We don't need to sanitize input, as toastr is set up by default to escape HTML via toastr options
1943 if (value === '') {1976 if (value === '') {
1944 console.warn('WARN: No argument provided for /echo command');1977 console.warn('WARN: No argument provided for /echo command');
1945 return;1978 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;
1946 }1984 }
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
1949 switch (severity) {2003 switch (severity) {
1950 case 'error':2004 case 'error':
1951 toastr.error(value, title);2005 toastr.error(value, title, options);
1952 break;2006 break;
1953 case 'warning':2007 case 'warning':
1954 toastr.warning(value, title);2008 toastr.warning(value, title, options);
1955 break;2009 break;
1956 case 'success':2010 case 'success':
1957 toastr.success(value, title);2011 toastr.success(value, title, options);
1958 break;2012 break;
1959 case 'info':2013 case 'info':
1960 default:2014 default:
1961 toastr.info(value, title);2015 toastr.info(value, title, options);
1962 break;2016 break;
1963 }2017 }
2018
2019 if (awaitDismissal) {
2020 return new Promise((resolve) => {
2021 resolveToastDismissal = resolve;
2022 });
2023 } else {
1964 return value;2024 return value;
1965 }2025 }
2026}
2027
19662028
1967async function addSwipeCallback(_, arg) {2029async function addSwipeCallback(_, arg) {
1968 const lastMessage = chat[chat.length - 1];2030 const lastMessage = chat[chat.length - 1];
@@ -2428,7 +2490,14 @@ async function sendUserMessageCallback(args, text) {
2428 text = text.trim();2490 text = text.trim();
2429 const compact = isTrueBoolean(args?.compact);2491 const compact = isTrueBoolean(args?.compact);
2430 const bias = extractMessageBias(text);2492 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
2433 if ('name' in args) {2502 if ('name' in args) {
2434 const name = args.name || '';2503 const name = args.name || '';
@@ -2737,7 +2806,13 @@ export async function sendMessageAs(args, text) {
2737 },2806 },
2738 }];2807 }];
27392808
2740 const insertAt = Number(args.at);2809 let 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
2742 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {2817 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
2743 chat.splice(insertAt, 0, message);2818 chat.splice(insertAt, 0, message);
@@ -2784,7 +2859,13 @@ export async function sendNarratorMessage(args, text) {
2784 },2859 },
2785 };2860 };
27862861
2787 const insertAt = Number(args.at);2862 let 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
2789 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {2870 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
2790 chat.splice(insertAt, 0, message);2871 chat.splice(insertAt, 0, message);
@@ -2866,7 +2947,13 @@ async function sendCommentMessage(args, text) {
2866 },2947 },
2867 };2948 };
28682949
2869 const insertAt = Number(args.at);2950 let 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
2871 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {2958 if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
2872 chat.splice(insertAt, 0, message);2959 chat.splice(insertAt, 0, message);
public/scripts/slash-commands/SlashCommandArgument.js+5 -5
@@ -59,7 +59,7 @@ export class SlashCommandArgument {
59 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums59 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
60 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options60 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
61 */61 */
62 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = true) {62 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) {
63 this.description = description;63 this.description = description;
64 this.typeList = types ? Array.isArray(types) ? types : [types] : [];64 this.typeList = types ? Array.isArray(types) ? types : [types] : [];
65 this.isRequired = isRequired ?? false;65 this.isRequired = isRequired ?? false;
@@ -90,7 +90,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
90 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided90 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
91 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values91 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
92 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options92 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
93 * @param {boolean} [props.forceEnum=true] default: true - whether the input must match one of the enum values93 * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
94 */94 */
95 static fromProps(props) {95 static fromProps(props) {
96 return new SlashCommandNamedArgument(96 return new SlashCommandNamedArgument(
@@ -103,7 +103,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
103 props.enumList ?? [],103 props.enumList ?? [],
104 props.aliasList ?? [],104 props.aliasList ?? [],
105 props.enumProvider ?? null,105 props.enumProvider ?? null,
106 props.forceEnum ?? true,106 props.forceEnum ?? false,
107 );107 );
108 }108 }
109109
@@ -120,9 +120,9 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
120 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]120 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
121 * @param {string[]} [aliases=[]]121 * @param {string[]} [aliases=[]]
122 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options122 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
123 * @param {boolean} [forceEnum=true]123 * @param {boolean} [forceEnum=false]
124 */124 */
125 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = true) {125 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) {
126 super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);126 super(description, types, isRequired, acceptsMultiple, defaultValue, enums, enumProvider, forceEnum);
127 this.name = name;127 this.name = name;
128 this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];128 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) => {
43 key = readSecret(request.user.directories, SECRET_KEYS.KOBOLDCPP);43 key = readSecret(request.user.directories, SECRET_KEYS.KOBOLDCPP);
44 }44 }
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) {
47 console.log('No key found for API', request.body.api);51 console.log('No key found for API', request.body.api);
48 return response.sendStatus(400);52 return response.sendStatus(400);
49 }53 }
@@ -110,7 +114,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
110 });114 });
111 }115 }
112116
113 if (request.body.api === 'koboldcpp') {117 if (request.body.api === 'koboldcpp' || request.body.api === 'vllm') {
114 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;118 apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`;
115 }119 }
116120