diff --git a/.gitignore b/.gitignore index 531d61538..9e40fb900 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ public/characters/ public/User Avatars/ public/backgrounds/ public/groups/ +public/group chats/ public/worlds/ public/css/bg_load.css public/themes/ diff --git a/colab/GPU.ipynb b/colab/GPU.ipynb index 194ba8dea..ba46cf0d3 100644 --- a/colab/GPU.ipynb +++ b/colab/GPU.ipynb @@ -62,6 +62,8 @@ "#@markdown * prompthero/openjourney - midjourney style model\n", "#@markdown * ckpt/sd15 - base SD 1.5\n", "#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n", + "extras_enable_chromadb = True #@param {type:\"boolean\"}\n", + "#@markdown Enables ChromaDB for Infinity Context plugin\n", "\n", "import subprocess\n", "\n", @@ -84,6 +86,8 @@ " ExtrasModules.append('sd')\n", "if (extras_enable_tts):\n", " ExtrasModules.append('tts')\n", + "if (extras_enable_chromadb):\n", + " ExtrasModules.append('chromadb')\n", "\n", "params.append(f'--classification-model={Emotions_Model}')\n", "params.append(f'--summarization-model={Memory_Model}')\n", @@ -99,6 +103,8 @@ "!npm install -g localtunnel\n", "!pip install -r requirements-complete.txt\n", "!pip install tensorflow==2.12\n", + "!wget https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 -O /tmp/cloudflared-linux-amd64\n", + "!chmod +x /tmp/cloudflared-linux-amd64\n", "\n", "\n", "cmd = f\"python server.py {' '.join(params)}\"\n", diff --git a/package-lock.json b/package-lock.json index 101fa744d..08ffed738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "device-detector-js": "^3.0.3", "exifreader": "^4.12.0", "express": "^4.18.2", + "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", "ipaddr.js": "^2.0.1", @@ -40,7 +41,8 @@ "uniqolor": "^1.1.0", "webp-converter": "2.3.2", "ws": "^8.13.0", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "yauzl": "^2.10.0" }, "bin": { "sillytavern": "server.js" @@ -815,6 +817,14 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-equal": { "version": "0.0.1", "license": "MIT", @@ -1296,6 +1306,14 @@ "reusify": "^1.0.4" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-type": { "version": "16.5.4", "license": "MIT", @@ -1525,6 +1543,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-translate-api-browser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/google-translate-api-browser/-/google-translate-api-browser-3.0.1.tgz", + "integrity": "sha512-KTLodkyGBWMK9IW6QIeJ2zCuju4Z0CLpbkADKo+yLhbSTD4l+CXXpQ/xaynGVAzeBezzJG6qn8MLeqOq3SmW0A==" + }, "node_modules/gpt3-tokenizer": { "version": "1.1.5", "license": "MIT", @@ -2221,6 +2244,11 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, "node_modules/phin": { "version": "2.9.3", "license": "MIT" @@ -3316,6 +3344,15 @@ "engines": { "node": ">=12" } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } } } } diff --git a/package.json b/package.json index 98dd786ed..1c6a170c2 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "device-detector-js": "^3.0.3", "exifreader": "^4.12.0", "express": "^4.18.2", + "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", "ipaddr.js": "^2.0.1", @@ -31,7 +32,8 @@ "uniqolor": "^1.1.0", "webp-converter": "2.3.2", "ws": "^8.13.0", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "yauzl": "^2.10.0" }, "overrides": { "parse-bmfont-xml": { diff --git a/public/NovelAI Settings/Classic-Euterpe.settings b/public/NovelAI Settings/Classic-Euterpe.settings index dcfc70285..e8fe154b4 100644 --- a/public/NovelAI Settings/Classic-Euterpe.settings +++ b/public/NovelAI Settings/Classic-Euterpe.settings @@ -1,8 +1,5 @@ { - "order": [ - 3, - 0 - ], + "order": [3, 0], "temperature": 1.11, "max_length": 90, "min_length": 1, @@ -10,5 +7,7 @@ "repetition_penalty": 1.11, "repetition_penalty_range": 320, "repetition_penalty_frequency": 0, - "repetition_penalty_presence": 0 + "repetition_penalty_presence": 0, + "repetition_penalty_slope": 0, + "max_context":2048 } \ No newline at end of file diff --git a/public/NovelAI Settings/Classic-Krake.settings b/public/NovelAI Settings/Classic-Krake.settings index 8ab90cdfe..1233f92b4 100644 --- a/public/NovelAI Settings/Classic-Krake.settings +++ b/public/NovelAI Settings/Classic-Krake.settings @@ -1,8 +1,5 @@ { - "order": [ - 3, - 0 - ], + "order": [3, 0], "temperature": 1.7, "max_length": 90, "min_length": 1, @@ -10,5 +7,7 @@ "repetition_penalty": 1.06, "repetition_penalty_range": 340, "repetition_penalty_frequency": 0, - "repetition_penalty_presence": 0 + "repetition_penalty_presence": 0, + "repetition_penalty_slope": 0, + "max_context": 2048 } \ No newline at end of file diff --git a/public/NovelAI Settings/Fresh-Coffee-Clio.settings b/public/NovelAI Settings/Fresh-Coffee-Clio.settings new file mode 100644 index 000000000..7d12fd1d1 --- /dev/null +++ b/public/NovelAI Settings/Fresh-Coffee-Clio.settings @@ -0,0 +1,18 @@ +{ + "order": [0, 1, 2, 3], + "temperature": 1, + "max_length": 40, + "min_length": 1, + "top_k": 25, + "top_p": 1, + "tail_free_sampling": 0.925, + "repetition_penalty": 1.9, + "repetition_penalty_range": 768, + "repetition_penalty_slope": 3.33, + "repetition_penalty_frequency": 0.0025, + "repetition_penalty_presence": 0.001, + "use_cache": false, + "return_full_text": false, + "prefix": "vanilla", + "max_context": 8192 +} \ No newline at end of file diff --git a/public/NovelAI Settings/Keelback-Clio.settings b/public/NovelAI Settings/Keelback-Clio.settings new file mode 100644 index 000000000..721cc25ed --- /dev/null +++ b/public/NovelAI Settings/Keelback-Clio.settings @@ -0,0 +1,18 @@ +{ + "order": [4, 5, 0, 3], + "temperature": 1.18, + "max_length": 40, + "min_length": 1, + "top_a": 0.022, + "typical_p": 0.9, + "tail_free_sampling": 0.956, + "repetition_penalty": 1.25, + "repetition_penalty_range": 4096, + "repetition_penalty_slope": 0.9, + "repetition_penalty_frequency": 0, + "repetition_penalty_presence": 0, + "use_cache": false, + "return_full_text": false, + "prefix": "vanilla", + "max_context": 8192 +} \ No newline at end of file diff --git a/public/NovelAI Settings/Long-Press-Clio.settings b/public/NovelAI Settings/Long-Press-Clio.settings new file mode 100644 index 000000000..fdeecdcf0 --- /dev/null +++ b/public/NovelAI Settings/Long-Press-Clio.settings @@ -0,0 +1,19 @@ +{ + "order": [0, 4, 1, 5, 3], + "temperature": 1.155, + "max_length": 40, + "min_length": 1, + "top_k": 25, + "top_a": 0.3, + "typical_p": 0.96, + "tail_free_sampling": 0.895, + "repetition_penalty": 1.0125, + "repetition_penalty_range": 2048, + "repetition_penalty_slope": 3.33, + "repetition_penalty_frequency": 0.011, + "repetition_penalty_presence": 0.005, + "use_cache": false, + "return_full_text": false, + "prefix": "vanilla", + "max_context": 8192 +} \ No newline at end of file diff --git a/public/NovelAI Settings/Talker-Chat-Clio.settings b/public/NovelAI Settings/Talker-Chat-Clio.settings new file mode 100644 index 000000000..6faad1414 --- /dev/null +++ b/public/NovelAI Settings/Talker-Chat-Clio.settings @@ -0,0 +1,19 @@ +{ + "order": [1, 3, 4, 0, 2], + "temperature": 1.05, + "max_length": 40, + "min_length": 1, + "top_k": 79, + "top_p": 0.95, + "top_a": 0.075, + "tail_free_sampling": 0.989, + "repetition_penalty": 1.5, + "repetition_penalty_range": 8192, + "repetition_penalty_slope": 3.33, + "repetition_penalty_frequency": 0.03, + "repetition_penalty_presence": 0.005, + "use_cache": false, + "return_full_text": false, + "prefix": "vanilla", + "max_context": 8192 +} \ No newline at end of file diff --git a/public/NovelAI Settings/Vingt-Un-Clio.settings b/public/NovelAI Settings/Vingt-Un-Clio.settings new file mode 100644 index 000000000..2631abf85 --- /dev/null +++ b/public/NovelAI Settings/Vingt-Un-Clio.settings @@ -0,0 +1,19 @@ +{ + "order": [0, 5, 3, 2, 1], + "temperature": 1.21, + "max_length": 40, + "min_length": 1, + "top_k": 0, + "top_p": 0.912, + "typical_p": 0.912, + "tail_free_sampling": 0.921, + "repetition_penalty": 1.21, + "repetition_penalty_range": 321, + "repetition_penalty_slope": 3.33, + "repetition_penalty_frequency": 0.00621, + "repetition_penalty_presence": 0, + "use_cache": false, + "return_full_text": false, + "prefix": "vanilla", + "max_context": 8192 +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 9ffd95873..4eb5320cc 100644 --- a/public/index.html +++ b/public/index.html @@ -64,6 +64,7 @@ + SillyTavern @@ -275,6 +276,81 @@ +
+
+ Rep. Pen. Range. +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Rep. Pen. Slope +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Rep. Pen. Freq. +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Rep. Pen. Presence +
+
+
+ +
+
+
+ select +
+
+
+
+
+
+ Tail Free Sampling +
+
+
+ +
+
+
+ select +
+
+
+
@@ -381,6 +457,15 @@ Enable this if the streaming doesn't work with your proxy.
+
+ +
+ Unrestricted maximum value for the context size slider. Enable only if you know what you're doing. +
+
Context Size (tokens) @@ -840,12 +925,26 @@
- Prompt that is used when the NSFW toggle is on + Prompt that is used when the NSFW toggle is ON
+
+
+ NSFW avoidance prompt +
+
+
+
+
+ Prompt that is used when the NSFW toggle is OFF +
+
+ +
+
Jailbreak prompt @@ -860,20 +959,44 @@
-
-
- Impersonation prompt -
-
+ +
+
+ Advanced prompt bits +
+
+
+
+
+ Impersonation prompt +
+
+
+
+
+ Prompt that is used for Impersonation function +
+
+ +
+
+
+
+ World Info format template +
+
+
+
+
+ Wraps activated World Info entries before inserting into the prompt. Use {0} to mark a place where the content is inserted. +
+
+ +
-
- Prompt that is used for Impersonation function -
-
- -
+
Logit Bias @@ -989,54 +1112,54 @@
-
- +
+ - + - + - + -

API key

-
Get it here: Register
- Enter 0000000000 to use anonymous mode. -
- -
- - -
-
For privacy reasons, your API key will be hidden after you reload the page.
-

- Model -
-
+

API key

+
Get it here: Register
+ Enter 0000000000 to use anonymous mode. +
+ -

- You can select multiple models.
Avoid sending sensitive information to the Horde. Learn more
- -
+
+ + +
+
For privacy reasons, your API key will be hidden after you reload the page.
+

+ Model +
+
+
+

+ You can select multiple models.
Avoid sending sensitive information to the Horde. Learn more
+ +
Not connected
@@ -1124,7 +1247,14 @@
@@ -1817,6 +1947,8 @@ +
@@ -1937,6 +2069,7 @@ + @@ -2141,35 +2274,36 @@
-
+
-
- -
- -
-

- Advanced Definitions -
- +

- Advanced Definitions
+
+
+ Creator's Comment +
This is not sent to the AI Prompt. + +
+
-
-

Personality summary

-
A brief description of the personality ?
- +

+ Personality summary + ? +

+
-

Scenario

-
Circumstances and context of the dialogue +

+ Scenario ? -

- + +
@@ -2183,13 +2317,13 @@ Chatty
- -
+
+
-

Examples of dialogue

-
Forms a personality more clearly ?
+

Example Dialogue

+
Important to set the character's writing style. ?
- +
@@ -2227,7 +2361,7 @@

Context Template Editor

-

+

@@ -2313,6 +2447,7 @@
+
@@ -2478,6 +2613,7 @@ ${characterName}
+
diff --git a/public/notes/content.md b/public/notes/content.md index c8ac792c3..58c44930f 100644 --- a/public/notes/content.md +++ b/public/notes/content.md @@ -396,6 +396,21 @@ If your subscription tier is Paper, Tablet or Scroll use only Euterpe model othe _Lost API keys can't be restored! Make sure to keep it safe!_ +### Window.ai + +You can use Window.ai browser extension to access AI models with SillyTavern. + +1. Install a browser extension from: [windowai.io](https://windowai.io/) +2. Select OpenAI in SillyTavern's Connection panel and check the "Use Window.ai" option. +3. Use the extension to pick which API to connect to. + +Don't have OpenAI / Claude API access? Use OpenRouter. + +1. Create an OpenRouter account: [openrouter.ai](https://openrouter.ai/) +2. Select OpenRouter as a provider in Window.ai extension. + +OpenRouter works by letting you use keys that they own. It has a free trial, and paid access afterwards. + ## Poe ### API key diff --git a/public/script.js b/public/script.js index 87263296f..0ea1e79d3 100644 --- a/public/script.js +++ b/public/script.js @@ -336,20 +336,20 @@ const system_messages = {

Want to Update to the latest version?

Read the instructions here. Also located in your installation's base folder -
+

In order to begin chatting:

  1. Connect to one of the supported generation APIs (the plug icon)
  2. Create or pick a character from the list (the top-right namecard icon)
-
+

Where to download more characters?

(Not endorsed, your discretion is advised)
  1. Pygmalion AI Discord
  2. CharacterHub (NSFW)
-
+

Where can I get help?

Before going any further, check out the following resources:
    @@ -360,7 +360,7 @@ const system_messages = {
  1. Pygmalion AI Docs
Type /? in any chat to get help on message formatting commands. -
+

Still have questions or suggestions left?

SillyTavern Community Discord
@@ -414,6 +414,11 @@ const system_messages = { export const event_types = { EXTRAS_CONNECTED: 'extras_connected', + MESSAGE_SWIPED: 'message_swiped', + MESSAGE_SENT: 'message_sent', + MESSAGE_RECEIVED: 'message_received', + MESSAGE_EDITED: 'message_edited', + IMPERSONATE_READY: 'impersonate_ready', } export const eventSource = new EventEmitter(); @@ -532,6 +537,7 @@ var selected_button = ""; //which button pressed var create_save_name = ""; var create_fav_chara = ""; var create_save_description = ""; +var create_save_creatorcomment = ""; var create_save_personality = ""; var create_save_first_message = ""; var create_save_avatar = ""; @@ -1131,6 +1137,10 @@ function addCopyToCodeBlocks(messageElement) { function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true } = {}) { var messageText = mes["mes"]; + if (mes?.extra?.display_text) { + messageText = mes.extra.display_text; + } + if (mes.name === name1) { var characterName = name1; //set to user's name by default } else { var characterName = mes.name } @@ -1659,6 +1669,10 @@ class StreamingProcessor { } } playMessageSound(); + + const eventType = this.type !== 'impersonate' ? event_types.MESSAGE_RECEIVED : event_types.IMPERSONATE_READY; + const eventData = this.type !== 'impersonate' ? this.messageId : text; + eventSource.emit(eventType, eventData); } onErrorStreaming() { @@ -1823,7 +1837,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); } else { - sendMessageAsUser(textareaText, messageBias); + await sendMessageAsUser(textareaText, messageBias); } } //////////////////////////////////// @@ -1858,8 +1872,10 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, coreChat.pop(); } - await runGenerationInterceptors(coreChat); - console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); + if (extension_settings.chromadb.n_results !== 0) { + await runGenerationInterceptors(coreChat); + console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); + } if (main_api === 'openai') { message_already_generated = ''; // OpenAI doesn't have multigen @@ -2187,7 +2203,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } let generate_data; - if (main_api == 'kobold') { + if (main_api == 'koboldhorde' || main_api == 'kobold') { generate_data = { prompt: finalPromt, gui_settings: true, @@ -2197,13 +2213,10 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, singleline: kai_settings.single_line, }; - } - else if (main_api == 'koboldhorde') { if (preset_settings != 'gui') { - const maxContext = horde_settings.auto_adjust_context_length ? adjustedParams.maxContextLength : max_context; + const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context; generate_data = getKoboldGenerationData(finalPromt, this_settings, this_amount_gen, maxContext, isImpersonate); } - } else if (main_api == 'textgenerationwebui') { generate_data = getTextGenGenerationData(finalPromt, this_amount_gen, isImpersonate); @@ -2323,7 +2336,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } } - function onSuccess(data) { + async function onSuccess(data) { hideStopButton(); is_send_press = false; if (!data.error) { @@ -2377,6 +2390,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, if (isImpersonate) { $('#send_textarea').val(getMessage).trigger('input'); generatedPromtCache = ""; + eventSource.emit(event_types.IMPERSONATE_READY, getMessage); } else if (type == 'quiet') { resolve(getMessage); @@ -2388,6 +2402,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, else { ({ type, getMessage } = saveReply('appendFinal', getMessage, this_mes_is_name, title)); } + await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); } activateSendButtons(); @@ -2395,7 +2410,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, playMessageSound(); } - generate_loop_counter = 0; } else { ++generate_loop_counter; @@ -2514,7 +2528,7 @@ export function replaceBiasMarkup(str) { return (str ?? '').replace(/{{(\*?.*\*?)}}/g, ''); } -function sendMessageAsUser(textareaText, messageBias) { +async function sendMessageAsUser(textareaText, messageBias) { chat[chat.length] = {}; chat[chat.length - 1]['name'] = name1; chat[chat.length - 1]['is_user'] = true; @@ -2529,6 +2543,9 @@ function sendMessageAsUser(textareaText, messageBias) { } addOneMessage(chat[chat.length - 1]); + // Wait for all handlers to finish before continuing with the prompt + await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); + console.log('message sent as user'); } function getMaxContextSize() { @@ -2544,6 +2561,11 @@ function getMaxContextSize() { if (nai_settings.model_novel == 'krake-v2') { this_max_context -= 160; } + if (nai_settings.model_novel == 'clio-v1') { + // Clio has a max context of 8192 + // TODO: Evaluate the relevance of nerdstash-v1 tokenizer, changes quite a bit. + this_max_context = 8192 - 60 - 160; + } } } if (main_api == 'openai') { @@ -2757,7 +2779,7 @@ function promptItemize(itemizedPrompts, requestedMesId) { Grey color items may not have been included in the context due to certain prompt format settings. -
+
@@ -2845,7 +2867,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
-
+
Total Tokens in Prompt:
${finalPromptTokens}
@@ -2855,7 +2877,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
-
+
`, 'text' ); @@ -2871,7 +2893,7 @@ function promptItemize(itemizedPrompts, requestedMesId) { Grey color items may not have been included in the context due to certain prompt format settings. -
+
@@ -2932,7 +2954,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
-
+
Total Tokens in Prompt:
${totalTokensInPrompt}
@@ -2949,7 +2971,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
-
+
`, 'text' ); } @@ -3001,14 +3023,16 @@ function getNovelGenerationData(finalPromt, this_settings, this_amount_gen) { "temperature": parseFloat(nai_settings.temp_novel), "max_length": this_amount_gen, // this_settings.max_length, // <= why? "min_length": this_settings.min_length, - "tail_free_sampling": this_settings.tail_free_sampling, + "tail_free_sampling": parseFloat(nai_settings.tail_free_sampling_novel), "repetition_penalty": parseFloat(nai_settings.rep_pen_novel), "repetition_penalty_range": parseInt(nai_settings.rep_pen_size_novel), - "repetition_penalty_frequency": this_settings.repetition_penalty_frequency, - "repetition_penalty_presence": this_settings.repetition_penalty_presence, + "repetition_penalty_slope": parseFloat(nai_settings.rep_pen_slope_novel), + "repetition_penalty_frequency": parseFloat(nai_settings.rep_pen_freq_novel), + "repetition_penalty_presence": parseFloat(nai_settings.rep_pen_presence_novel), "top_a": this_settings.top_a, "top_p": this_settings.top_p, "top_k": this_settings.top_k, + "typical_p": this_settings.typical_p, //"stop_sequences": {{187}}, //bad_words_ids = {{50256}, {0}, {1}}; //generate_until_sentence = true; @@ -3559,29 +3583,21 @@ async function getChat() { } else { chat_create_date = humanizedDateTime(); } - getChatResult(); - saveChat(); + await getChatResult(); + await saveChat(); setTimeout(function () { $('#send_textarea').click(); $('#send_textarea').focus(); }, 200); } catch (error) { - getChatResult(); + await getChatResult(); console.log(error); } } -function getChatResult() { +async function getChatResult() { name2 = characters[this_chid].name; - if (chat.length > 1) { - for (let i = 0; i < chat.length; i++) { - const item = chat[i]; - if (item["is_user"]) { - //item['mes'] = item['mes'].replace(default_user_name + ':', name1 + ':'); - //item['name'] = name1; - } - } - } else { + if (chat.length === 0) { const firstMes = characters[this_chid].first_mes || default_ch_mes; chat[0] = { name: name2, @@ -3593,6 +3609,10 @@ function getChatResult() { } printMessages(); select_selected_character(this_chid); + + if (chat.length === 1) { + await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); + } } async function openCharacterChat(file_name) { @@ -3724,6 +3744,10 @@ function changeMainAPI() { main_api = selectedVal; online_status = "no_connection"; + if (main_api == 'openai' && oai_settings.use_window_ai) { + $('#api_button_openai').trigger('click'); + } + if (main_api == "koboldhorde") { is_get_status = true; getStatus(); @@ -3786,182 +3810,166 @@ function reloadUserAvatar() { //***************SETTINGS****************// /////////////////////////////////////////// async function getSettings(type) { - //timer - - //console.log('getSettings() pinging server for settings request'); - jQuery.ajax({ - type: "POST", - url: "/getsettings", - data: JSON.stringify({}), - beforeSend: function () { }, - cache: false, - dataType: "json", - contentType: "application/json", - //processData: false, - success: function (data) { - if (data.result != "file not find" && data.settings) { - settings = JSON.parse(data.settings); - if (settings.username !== undefined) { - if (settings.username !== "") { - name1 = settings.username; - $("#your_name").val(name1); - } - } - - //Load which API we are using - if (settings.main_api != undefined) { - main_api = settings.main_api; - $("#main_api option[value=" + main_api + "]").attr( - "selected", - "true" - ); - changeMainAPI(); - } - - //Load KoboldAI settings - koboldai_setting_names = data.koboldai_setting_names; - koboldai_settings = data.koboldai_settings; - koboldai_settings.forEach(function (item, i, arr) { - koboldai_settings[i] = JSON.parse(item); - }); - - let arr_holder = {}; - - $("#settings_perset").empty(); //RossAscends: uncommented this to prevent settings selector from doubling preset list on refresh - $("#settings_perset").append( - '' - ); //adding in the GUI settings, since it is not loaded dynamically - - koboldai_setting_names.forEach(function (item, i, arr) { - arr_holder[item] = i; - $("#settings_perset").append(``); - //console.log('loading preset #'+i+' -- '+item); - }); - koboldai_setting_names = {}; - koboldai_setting_names = arr_holder; - preset_settings = settings.preset_settings; - - if (preset_settings == "gui") { - selectKoboldGuiPreset(); - } else { - if (typeof koboldai_setting_names[preset_settings] !== "undefined") { - $(`#settings_perset option[value=${koboldai_setting_names[preset_settings]}]`) - .attr("selected", "true"); - } else { - preset_settings = "gui"; - selectKoboldGuiPreset(); - } - } - - novelai_setting_names = data.novelai_setting_names; - novelai_settings = data.novelai_settings; - novelai_settings.forEach(function (item, i, arr) { - novelai_settings[i] = JSON.parse(item); - }); - arr_holder = {}; - - $("#settings_perset_novel").empty(); - - novelai_setting_names.forEach(function (item, i, arr) { - arr_holder[item] = i; - $("#settings_perset_novel").append(``); - }); - novelai_setting_names = {}; - novelai_setting_names = arr_holder; - - nai_settings.preset_settings_novel = settings.preset_settings_novel; - $( - `#settings_perset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]` - ).attr("selected", "true"); - - //Load AI model config settings - - amount_gen = settings.amount_gen; - if (settings.max_context !== undefined) - max_context = parseInt(settings.max_context); - - swipes = settings.swipes !== undefined ? !!settings.swipes : true; // enable swipes by default - $('#swipes-checkbox').prop('checked', swipes); /// swipecode - hideSwipeButtons(); - showSwipeButtons(); - - // Kobold - loadKoboldSettings(settings); - - // Novel - loadNovelSettings(settings); - - // TextGen - loadTextGenSettings(data, settings); - - // OpenAI - loadOpenAISettings(data, settings); - - // Horde - loadHordeSettings(settings); - - // Poe - loadPoeSettings(settings); - - // Load power user settings - loadPowerUserSettings(settings, data); - - // Load- character tags - loadTagsSettings(settings); - - // Load context templates - loadContextTemplatesFromSettings(data, settings); - - // Set context size after loading power user (may override the max value) - $("#max_context").val(max_context); - $("#max_context_counter").text(`${max_context}`); - - $("#amount_gen").val(amount_gen); - $("#amount_gen_counter").text(`${amount_gen}`); - - //Enable GUI deference settings if GUI is selected for Kobold - if (main_api === "kobold") { - } - - //Load User's Name and Avatar - - user_avatar = settings.user_avatar; - reloadUserAvatar(); - highlightSelectedAvatar(); - - //Load the API server URL from settings - api_server = settings.api_server; - $("#api_url_text").val(api_server); - - setWorldInfoSettings(settings, data); - - if (data.enable_extensions) { - const src = "scripts/extensions.js"; - if ($(`script[src="${src}"]`).length === 0) { - const script = document.createElement("script"); - script.type = "module"; - script.src = src; - $("body").append(script); - } - loadExtensionSettings(settings); - } - - api_server_textgenerationwebui = - settings.api_server_textgenerationwebui; - $("#textgenerationwebui_api_url_text").val( - api_server_textgenerationwebui - ); - - selected_button = settings.selected_button; - } - - if (!is_checked_colab) isColab(); - }, - error: function (jqXHR, exception) { - console.log(exception); - console.log(jqXHR); - }, + const response = await fetch("/getsettings", { + method: "POST", + headers: getRequestHeaders(), + body: JSON.stringify({}), + cache: "no-cache", }); + + if (!response.ok) { + toastr.error('Settings could not be loaded. Try reloading the page.'); + throw new Error('Error getting settings'); + } + + const data = await response.json(); + + if (data.result != "file not find" && data.settings) { + settings = JSON.parse(data.settings); + if (settings.username !== undefined) { + if (settings.username !== "") { + name1 = settings.username; + $("#your_name").val(name1); + } + } + + //Load KoboldAI settings + koboldai_setting_names = data.koboldai_setting_names; + koboldai_settings = data.koboldai_settings; + koboldai_settings.forEach(function (item, i, arr) { + koboldai_settings[i] = JSON.parse(item); + }); + + let arr_holder = {}; + + $("#settings_perset").empty(); //RossAscends: uncommented this to prevent settings selector from doubling preset list on refresh + $("#settings_perset").append( + '' + ); //adding in the GUI settings, since it is not loaded dynamically + + koboldai_setting_names.forEach(function (item, i, arr) { + arr_holder[item] = i; + $("#settings_perset").append(``); + //console.log('loading preset #'+i+' -- '+item); + }); + koboldai_setting_names = {}; + koboldai_setting_names = arr_holder; + preset_settings = settings.preset_settings; + + if (preset_settings == "gui") { + selectKoboldGuiPreset(); + } else { + if (typeof koboldai_setting_names[preset_settings] !== "undefined") { + $(`#settings_perset option[value=${koboldai_setting_names[preset_settings]}]`) + .attr("selected", "true"); + } else { + preset_settings = "gui"; + selectKoboldGuiPreset(); + } + } + + novelai_setting_names = data.novelai_setting_names; + novelai_settings = data.novelai_settings; + novelai_settings.forEach(function (item, i, arr) { + novelai_settings[i] = JSON.parse(item); + }); + arr_holder = {}; + + $("#settings_perset_novel").empty(); + + novelai_setting_names.forEach(function (item, i, arr) { + arr_holder[item] = i; + $("#settings_perset_novel").append(``); + }); + novelai_setting_names = {}; + novelai_setting_names = arr_holder; + + nai_settings.preset_settings_novel = settings.preset_settings_novel; + $( + `#settings_perset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]` + ).attr("selected", "true"); + + //Load AI model config settings + + amount_gen = settings.amount_gen; + if (settings.max_context !== undefined) + max_context = parseInt(settings.max_context); + + swipes = settings.swipes !== undefined ? !!settings.swipes : true; // enable swipes by default + $('#swipes-checkbox').prop('checked', swipes); /// swipecode + hideSwipeButtons(); + showSwipeButtons(); + + // Kobold + loadKoboldSettings(settings); + + // Novel + loadNovelSettings(settings); + + // TextGen + loadTextGenSettings(data, settings); + + // OpenAI + loadOpenAISettings(data, settings); + + // Horde + loadHordeSettings(settings); + + // Poe + loadPoeSettings(settings); + + // Load power user settings + loadPowerUserSettings(settings, data); + + // Load character tags + loadTagsSettings(settings); + + // Load context templates + loadContextTemplatesFromSettings(data, settings); + + // Set context size after loading power user (may override the max value) + $("#max_context").val(max_context); + $("#max_context_counter").text(`${max_context}`); + + $("#amount_gen").val(amount_gen); + $("#amount_gen_counter").text(`${amount_gen}`); + + //Load which API we are using + if (settings.main_api != undefined) { + main_api = settings.main_api; + $("#main_api option[value=" + main_api + "]").attr( + "selected", + "true" + ); + changeMainAPI(); + } + + //Load User's Name and Avatar + + user_avatar = settings.user_avatar; + reloadUserAvatar(); + highlightSelectedAvatar(); + + //Load the API server URL from settings + api_server = settings.api_server; + $("#api_url_text").val(api_server); + + setWorldInfoSettings(settings, data); + + api_server_textgenerationwebui = + settings.api_server_textgenerationwebui; + $("#textgenerationwebui_api_url_text").val( + api_server_textgenerationwebui + ); + + selected_button = settings.selected_button; + + if (data.enable_extensions) { + await loadExtensionSettings(settings); + } + } + + if (!is_checked_colab) isColab(); } function selectKoboldGuiPreset() { @@ -4042,39 +4050,8 @@ function setCharacterBlockHeight() { //should be set to an onload for rm_print_characters or windows? } -function messageEditAuto(div) { - let mesBlock = div.closest(".mes_block"); - var text = mesBlock.find(".edit_textarea").val().trim(); - const bias = extractMessageBias(text); - const mes = chat[this_edit_mes_id]; - mes["mes"] = text; - if (mes["swipe_id"] !== undefined) { - mes["swipes"][mes["swipe_id"]] = text; - } - - // editing old messages - if (!mes["extra"]) { - mes["extra"] = {}; - } - - if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { - mes.extra.bias = bias ?? null; - } - else { - mes.extra.bias = null; - } - - mesBlock.find(".mes_text").val(''); - mesBlock.find(".mes_text").val(messageFormatting( - text, - this_edit_mes_chname, - mes.is_system, - mes.is_user, - )); - saveChatDebounced(); -} - -function messageEditDone(div) { +// Common code for message editor done and auto-save +function updateMessage(div) { let mesBlock = div.closest(".mes_block"); var text = mesBlock.find(".edit_textarea").val().trim(); const bias = extractMessageBias(text); @@ -4091,11 +4068,29 @@ function messageEditDone(div) { if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { mes.extra.bias = bias ?? null; - } - else { + } else { mes.extra.bias = null; } + return { mesBlock, text, mes, bias }; +} + +function messageEditAuto(div) { + const { mesBlock, text, mes } = updateMessage(div); + + mesBlock.find(".mes_text").val(''); + mesBlock.find(".mes_text").val(messageFormatting( + text, + this_edit_mes_chname, + mes.is_system, + mes.is_user, + )); + saveChatDebounced(); +} + +async function messageEditDone(div) { + const { mesBlock, text, mes, bias } = updateMessage(div); + mesBlock.find(".mes_text").empty(); mesBlock.find(".mes_edit_buttons").css("display", "none"); mesBlock.find(".mes_buttons").css("display", ""); @@ -4111,6 +4106,8 @@ function messageEditDone(div) { mesBlock.find(".mes_bias").append(messageFormatting(bias)); appendImageToMessage(mes, div.closest(".mes")); addCopyToCodeBlocks(div.closest(".mes")); + await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id); + this_edit_mes_id = undefined; saveChatConditional(); } @@ -4258,30 +4255,38 @@ function select_rm_info(type, charId, previousCharId = null) { toastr.error(`Invalid process (no 'type')`); return; } + if (type !== 'group_create') { + var displayName = String(charId).replace('.png', ''); + } + if (type === 'char_delete') { - toastr.warning(`Character Deleted: ${charId}`); + toastr.warning(`Character Deleted: ${displayName}`); } if (type === 'char_create') { - toastr.success(`Character Created: ${charId}`); + toastr.success(`Character Created: ${displayName}`); } if (type === 'group_create') { toastr.success(`Group Created`); } - if (type === 'char_import') { - toastr.success(`Character Imported: ${charId}`); + if (type === 'group_delete') { + toastr.warning(`Group Deleted`); } + if (type === 'char_import') { + toastr.success(`Character Imported: ${displayName}`); + } + + getCharacters(); selectRightMenuWithAnimation('rm_characters_block'); if (type === 'char_import' || type === 'char_create') { - //$(`#rm_characters_block [title="${charId + '.png'}"]`).scrollIntoView({ behavior: "smooth", block: "end" }); - const element = $(`#rm_characters_block [title="${charId + '.png'}"]`).get(0); + const element = $(`#rm_characters_block [title="${charId}"]`).get(0); element.scrollIntoView({ behavior: 'smooth', block: 'end' }); - $(`#rm_characters_block [title="${charId + '.png'}"]`).parent().addClass('flash animated'); + $(`#rm_characters_block [title="${charId}"]`).parent().addClass('flash animated'); setTimeout(function () { - $(`#rm_characters_block [title="${charId + '.png'}"]`).parent().removeClass('flash animated'); + $(`#rm_characters_block [title="${charId}"]`).parent().removeClass('flash animated'); }, 5000); } @@ -4319,6 +4324,7 @@ export function select_selected_character(chid) { $("#rm_button_back").css("display", "none"); //$("#character_import_button").css("display", "none"); $("#create_button").attr("value", "Save"); // what is the use case for this? + $("#dupe_button").show(); $("#create_button_label").css("display", "none"); // Don't update the navbar name if we're peeking the group member defs @@ -4331,6 +4337,7 @@ export function select_selected_character(chid) { $("#character_popup_text_h3").text(characters[chid].name); $("#character_name_pole").val(characters[chid].name); $("#description_textarea").val(characters[chid].description); + $("#creatorcomment_textarea").val(characters[chid].creatorcomment); $("#personality_textarea").val(characters[chid].personality); $("#firstmessage_textarea").val(characters[chid].first_mes); $("#scenario_pole").val(characters[chid].scenario); @@ -4376,8 +4383,7 @@ function select_rm_create() { $("#export_button").css("display", "none"); $("#create_button_label").css("display", ""); $("#create_button").attr("value", "Create"); - //RossAscends: commented this out as part of the auto-loading token counter - //$('#result_info').html(' '); + $("#dupe_button").hide(); //create text poles $("#rm_button_back").css("display", ""); @@ -4385,6 +4391,7 @@ function select_rm_create() { $("#character_popup_text_h3").text("Create character"); $("#character_name_pole").val(create_save_name); $("#description_textarea").val(create_save_description); + $("#creatorcomment_textarea").val(create_save_creatorcomment); $("#personality_textarea").val(create_save_personality); $("#firstmessage_textarea").val(create_save_first_message); $("#talkativeness_slider").val(create_save_talkativeness); @@ -4476,6 +4483,7 @@ function callPopup(text, type, inputValue = '') { $('#avatarToCrop').cropper({ aspectRatio: 2 / 3, autoCropArea: 1, + viewMode: 2, rotatable: false, crop: function (event) { crop_data = event.detail; @@ -4511,7 +4519,7 @@ function read_bg_load(input) { url: "/downloadbackground", data: formData, beforeSend: function () { - //$('#create_button').attr('value','Creating...'); + }, cache: false, contentType: false, @@ -4790,6 +4798,16 @@ function swipe_left() { // when we swipe left..but no generation. this_mes_div.css('height', this_mes_div_height); const this_mes_block_height = this_mes_block[0].scrollHeight; chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; + if (chat[chat.length - 1].extra) { + // if message has memory attached - remove it to allow regen + if (chat[chat.length - 1].extra.memory) { + delete chat[chat.length - 1].extra.memory; + } + // ditto for display text + if (chat[chat.length - 1].extra.display_text) { + delete chat[chat.length - 1].extra.display_text; + } + } $(this).parent().children('.mes_block').transition({ x: swipe_range, duration: swipe_duration, @@ -4828,6 +4846,7 @@ function swipe_left() { // when we swipe left..but no generation. queue: false, complete: function () { saveChatConditional(); + eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1)); } }); } @@ -4887,9 +4906,15 @@ const swipe_right = () => { chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat } chat[chat.length - 1]['swipe_id']++; //make new slot in array - // if message has memory attached - remove it to allow regen - if (chat[chat.length - 1].extra && chat[chat.length - 1].extra.memory) { - delete chat[chat.length - 1].extra.memory; + if (chat[chat.length - 1].extra) { + // if message has memory attached - remove it to allow regen + if (chat[chat.length - 1].extra.memory) { + delete chat[chat.length - 1].extra.memory; + } + // ditto for display text + if (chat[chat.length - 1].extra.display_text) { + delete chat[chat.length - 1].extra.display_text; + } } //console.log(chat[chat.length-1]['swipes']); if (parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { //if swipe id of last message is the same as the length of the 'swipes' array @@ -4989,6 +5014,7 @@ const swipe_right = () => { saveChatConditional(); } } + eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1)); } }); } @@ -5057,9 +5083,58 @@ function updateVisibleDivs() { //console.log(`${visibleStart},${visibleEnd}`); } + +function importCharacter(file) { + const ext = file.name.match(/\.(\w+)$/); + if ( + !ext || + (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "png" && ext[1] != "webp") + ) { + return; + } + + const format = ext[1].toLowerCase(); + $("#character_import_file_type").val(format); + const formData = new FormData(); + formData.append('avatar', file); + formData.append('file_type', format); + + jQuery.ajax({ + type: "POST", + url: "/importcharacter", + data: formData, + async: false, + beforeSend: function () { + }, + cache: false, + contentType: false, + processData: false, + success: async function (data) { + if (data.file_name !== undefined) { + $("#rm_info_block").transition({ opacity: 0, duration: 0 }); + var $prev_img = $("#avatar_div_div").clone(); + $prev_img + .children("img") + .attr("src", "characters/" + data.file_name + ".png"); + $("#rm_info_avatar").append($prev_img); + + let oldSelectedChar = null; + if (this_chid != undefined && this_chid != "invalid-safety-id") { + oldSelectedChar = characters[this_chid].avatar; + } + + await getCharacters(); + select_rm_info(`char_import`, data.file_name, oldSelectedChar); + $("#rm_info_block").transition({ opacity: 1, duration: 1000 }); + } + }, + error: function (jqXHR, exception) { + $("#create_button").removeAttr("disabled"); + }, + }); +} + $(document).ready(function () { - - //////////INPUT BAR FOCUS-KEEPING LOGIC///////////// $("#rm_print_characters_block").on('scroll', @@ -5334,7 +5409,7 @@ $(document).ready(function () { $("#advanced_div").click(function () { if (!is_advanced_char_open) { is_advanced_char_open = true; - $("#character_popup").css("display", "grid"); + $("#character_popup").css("display", "flex"); $("#character_popup").css("opacity", 0.0); $("#character_popup").transition({ opacity: 1.0, @@ -5412,7 +5487,6 @@ $(document).ready(function () { url: "/deletecharacter", beforeSend: function () { select_rm_info("char_delete", characters[this_chid].name); - //$('#create_button').attr('value','Deleting...'); }, data: msg, cache: false, @@ -5559,6 +5633,8 @@ $(document).ready(function () { create_save_name = ""; $("#description_textarea").val(""); create_save_description = ""; + $("#creatorcomment_textarea").val(""); + create_save_creatorcomment = ""; $("#personality_textarea").val(""); create_save_personality = ""; $("#firstmessage_textarea").val(""); @@ -5593,7 +5669,7 @@ $(document).ready(function () { $("#rm_info_block").transition({ opacity: 0, duration: 0 }); var $prev_img = $("#avatar_div_div").clone(); $("#rm_info_avatar").append($prev_img); - select_rm_info(`char_create`, save_name, oldSelectedChar); + select_rm_info(`char_create`, html, oldSelectedChar); $("#rm_info_block").transition({ opacity: 1.0, duration: 2000 }); crop_data = undefined; @@ -5617,7 +5693,7 @@ $(document).ready(function () { url: url, data: formData, beforeSend: function () { - //$("#create_button").attr("disabled", true); + $("#create_button").attr("disabled", true); $("#create_button").attr("value", "Save"); }, cache: false, @@ -5649,6 +5725,7 @@ $(document).ready(function () { add_mes_without_animation = true; //console.log('form create submission calling addOneMessage'); addOneMessage(chat[0]); + await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); } } $("#create_button").removeAttr("disabled"); @@ -5693,10 +5770,11 @@ $(document).ready(function () { } }); - $("#description_textarea, #personality_textarea, #scenario_pole, #mes_example_textarea, #firstmessage_textarea") + $("#description_textarea, #creatorcomment_textarea, #personality_textarea, #scenario_pole, #mes_example_textarea, #firstmessage_textarea") .on("input", function () { if (menu_type == "create") { create_save_description = $("#description_textarea").val(); + create_save_creatorcomment = $("#creatorcomment_textarea").val(); create_save_personality = $("#personality_textarea").val(); create_save_scenario = $("#scenario_pole").val(); create_save_mes_example = $("#mes_example_textarea").val(); @@ -5778,6 +5856,43 @@ $(document).ready(function () { } }); + $(document).on("click", ".exportChatButton", async function () { + const filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); + const filename = filenamefull.replace('.jsonl', ''); + const body = { + is_group: !!selected_group, + avatar_url: characters[this_chid]?.avatar, + file: `${filename}.jsonl`, + exportfilename: `${filename}.txt`, + } + console.log(body); + try { + const response = await fetch('/exportchat', { + method: 'POST', + body: JSON.stringify(body), + headers: getRequestHeaders(), + }); + const data = await response.json(); + if (!response.ok) { + // display error message + console.log(data.message); + await delay(250); + toastr.error(`Error: ${data.message}`); + return; + } else { + // success, handle response data + console.log(data); + await delay(250); + toastr.success(data.message); + } + } catch (error) { + // display error message + console.log(`An error has occurred: ${error.message}`); + await delay(250); + toastr.error(`Error: ${error.message}`); + } + }); + $("#talkativeness_slider").on("input", function () { if (menu_type == "create") { create_save_talkativeness = $("#talkativeness_slider").val(); @@ -6050,6 +6165,8 @@ $(document).ready(function () { const preset = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; loadNovelPreset(preset); + amount_gen = parseInt($("#amount_gen").val()); + max_context = parseInt($("#max_context").val()); saveSettingsDebounced(); }); @@ -6382,9 +6499,27 @@ $(document).ready(function () { name1 = $("#your_name").val(); if (name1 === undefined || name1 == "") name1 = default_user_name; console.log(name1); + toastr.success(`Your messages will now be sent as ${name1}`, 'User Name updated'); saveSettings("change_name"); } }); + + $('#sync_name_button').on('click', async function () { + const confirmation = await callPopup(`

Are you sure?

All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); + + if (!confirmation) { + return; + } + + for (const mes of chat) { + if (mes.is_user) { + mes.name = name1; + } + } + + await saveChatConditional(); + await reloadCurrentChat(); + }); //Select chat $("#api_button_novel").on('click', async function (e) { @@ -6404,6 +6539,8 @@ $(document).ready(function () { $("#api_button_novel").css("display", "none"); is_get_status_novel = true; is_api_button_press_novel = true; + // Check near immediately rather than waiting for up to 90s + setTimeout(getStatusNovel, 10); }); //**************************CHARACTER IMPORT EXPORT*************************// @@ -6417,53 +6554,7 @@ $(document).ready(function () { } for (const file of e.target.files) { - var ext = file.name.match(/\.(\w+)$/); - if ( - !ext || - (ext[1].toLowerCase() != "json" && ext[1].toLowerCase() != "png" && ext[1] != "webp") - ) { - continue; - } - - var format = ext[1].toLowerCase(); - $("#character_import_file_type").val(format); - var formData = new FormData(); - formData.append('avatar', file); - formData.append('file_type', format); - - jQuery.ajax({ - type: "POST", - url: "/importcharacter", - data: formData, - async: false, - beforeSend: function () { - }, - cache: false, - contentType: false, - processData: false, - success: async function (data) { - if (data.file_name !== undefined) { - $("#rm_info_block").transition({ opacity: 0, duration: 0 }); - var $prev_img = $("#avatar_div_div").clone(); - $prev_img - .children("img") - .attr("src", "characters/" + data.file_name + ".png"); - $("#rm_info_avatar").append($prev_img); - - let oldSelectedChar = null; - if (this_chid != undefined && this_chid != "invalid-safety-id") { - oldSelectedChar = characters[this_chid].avatar; - } - - await getCharacters(); - select_rm_info(`char_import`, data.file_name, oldSelectedChar); - $("#rm_info_block").transition({ opacity: 1, duration: 1000 }); - } - }, - error: function (jqXHR, exception) { - $("#create_button").removeAttr("disabled"); - }, - }); + importCharacter(file); } }); $("#export_button").click(function (e) { @@ -6528,6 +6619,7 @@ $(document).ready(function () { $("#chat_import_file_type").val(format); var formData = new FormData($("#form_import_chat").get(0)); + formData.append('user_name', name1); $("#select_chat_div").html(""); $("#load_select_chat_div").css("display", "block"); @@ -6548,6 +6640,20 @@ $(document).ready(function () { select_rm_characters(); }); + $("#dupe_button").click(async function () { + + const body = { avatar_url: characters[this_chid].avatar }; + const response = await fetch('/dupecharacter', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(body), + }); + if (response.ok) { + toastr.success("Character Duplicated"); + getCharacters(); + } + }); + $(document).on("click", ".select_chat_block, .bookmark_link, .mes_bookmark", async function () { let file_name = $(this).hasClass('mes_bookmark') ? $(this).closest('.mes').attr('bookmark_link') @@ -6780,4 +6886,43 @@ $(document).ready(function () { $(masterElement).val(myValue).trigger('input'); restoreCaretPosition($(this).get(0), caretPosition); }); + + const $dropzone = $(document.body); + + $dropzone.on('dragover', (event) => { + event.preventDefault(); + event.stopPropagation(); + $dropzone.addClass('dragover'); + }); + + $dropzone.on('dragleave', (event) => { + event.preventDefault(); + event.stopPropagation(); + $dropzone.removeClass('dragover'); + }); + + $dropzone.on('drop', (event) => { + event.preventDefault(); + event.stopPropagation(); + $dropzone.removeClass('dragover'); + + const files = event.originalEvent.dataTransfer.files; + processDroppedFiles(files); + }); + + function processDroppedFiles(files) { + const allowedMimeTypes = [ + 'application/json', + 'image/png', + 'image/webp', + ]; + + for (const file of files) { + if (allowedMimeTypes.includes(file.type)) { + importCharacter(file); + } else { + toastr.warning('Unsupported file type: ' + file.name); + } + } + } }) diff --git a/public/scripts/eventemitter.js b/public/scripts/eventemitter.js index eacf7a23b..c7f22bad6 100644 --- a/public/scripts/eventemitter.js +++ b/public/scripts/eventemitter.js @@ -48,7 +48,7 @@ EventEmitter.prototype.removeListener = function (event, listener) { } }; -EventEmitter.prototype.emit = function (event) { +EventEmitter.prototype.emit = async function (event) { var i, listeners, length, args = [].slice.call(arguments, 1); if (typeof this.events[event] === 'object') { @@ -56,7 +56,13 @@ EventEmitter.prototype.emit = function (event) { length = listeners.length; for (i = 0; i < length; i++) { - listeners[i].apply(this, args); + try { + await listeners[i].apply(this, args); + } + catch (err) { + console.error(err); + console.trace('Error in event listener'); + } } } }; @@ -68,4 +74,4 @@ EventEmitter.prototype.once = function (event, listener) { }); }; -export { EventEmitter } \ No newline at end of file +export { EventEmitter } diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 2ce4d4b4c..a762ff366 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -28,6 +28,7 @@ const extension_settings = { tts: {}, sd: {}, chromadb: {}, + translate: {}, }; let modules = []; @@ -342,4 +343,4 @@ $(document).ready(async function () { $("#extensions_details").on('click', showExtensionsDetails); $(document).on('click', '.disable_extension', onDisableExtensionClick); $(document).on('click', '.enable_extension', onEnableExtensionClick); -}); \ No newline at end of file +}); diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 89b789139..14141dce1 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,385 +1,514 @@ -import { saveSettingsDebounced } from "../../../script.js"; -import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; -export { MODULE_NAME }; - -const MODULE_NAME = 'expressions'; -const UPDATE_INTERVAL = 2000; -const DEFAULT_EXPRESSIONS = [ - "admiration", - "amusement", - "anger", - "annoyance", - "approval", - "caring", - "confusion", - "curiosity", - "desire", - "disappointment", - "disapproval", - "disgust", - "embarrassment", - "excitement", - "fear", - "gratitude", - "grief", - "joy", - "love", - "nervousness", - "optimism", - "pride", - "realization", - "relief", - "remorse", - "sadness", - "surprise", - "neutral" -]; - -let expressionsList = null; -let lastCharacter = undefined; -let lastMessage = null; -let spriteCache = {}; -let inApiCall = false; - -function onExpressionsShowDefaultInput() { - const value = $(this).prop('checked'); - extension_settings.expressions.showDefault = value; - saveSettingsDebounced(); - - const existingImageSrc = $('img.expression').prop('src'); - if (existingImageSrc !== undefined) { //if we have an image in src - if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) - $('img.expression').prop('src', ''); //remove it - lastMessage = null; - } - if (value) { - lastMessage = null; - } - } -} - -let isWorkerBusy = false; - -async function moduleWorkerWrapper() { - // Don't touch me I'm busy... - if (isWorkerBusy) { - return; - } - - // I'm free. Let's update! - try { - isWorkerBusy = true; - await moduleWorker(); - } - finally { - isWorkerBusy = false; - } -} - -async function moduleWorker() { - const context = getContext(); - - // non-characters not supported - if (!context.groupId && context.characterId === undefined) { - removeExpression(); - return; - } - - // character changed - if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { - removeExpression(); - spriteCache = {}; - } - - const currentLastMessage = getLastCharacterMessage(); - - // character has no expressions or it is not loaded - if (Object.keys(spriteCache).length === 0) { - await validateImages(currentLastMessage.name); - lastCharacter = context.groupId || context.characterId; - } - - const offlineMode = $('.expression_settings .offline_mode'); - if (!modules.includes('classify')) { - $('.expression_settings').show(); - offlineMode.css('display', 'block'); - lastCharacter = context.groupId || context.characterId; - - if (context.groupId) { - await validateImages(currentLastMessage.name, true); - } - - return; - } - else { - // force reload expressions list on connect to API - if (offlineMode.is(':visible')) { - expressionsList = null; - spriteCache = {}; - expressionsList = await getExpressionsList(); - await validateImages(currentLastMessage.name, true); - } - - offlineMode.css('display', 'none'); - } - - - // check if last message changed - if ((lastCharacter === context.characterId || lastCharacter === context.groupId) - && lastMessage === currentLastMessage.mes) { - return; - } - - // API is busy - if (inApiCall) { - return; - } - - try { - inApiCall = true; - const url = new URL(getApiUrl()); - url.pathname = '/api/classify'; - - const apiResult = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Bypass-Tunnel-Reminder': 'bypass', - }, - body: JSON.stringify({ text: currentLastMessage.mes }) - }); - - if (apiResult.ok) { - const name = context.groupId ? currentLastMessage.name : context.name2; - const force = !!context.groupId; - const data = await apiResult.json(); - let expression = data.classification[0].label; - - // Character won't be angry on you for swiping - if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { - expression = 'joy'; - } - - setExpression(name, expression, force); - } - - } - catch (error) { - console.log(error); - } - finally { - inApiCall = false; - lastCharacter = context.groupId || context.characterId; - lastMessage = currentLastMessage.mes; - } -} - -function getLastCharacterMessage() { - const context = getContext(); - const reversedChat = context.chat.slice().reverse(); - - for (let mes of reversedChat) { - if (mes.is_user || mes.is_system) { - continue; - } - - return { mes: mes.mes, name: mes.name }; - } - - return { mes: '', name: null }; -} - -function removeExpression() { - lastMessage = null; - $('img.expression').off('error'); - $('img.expression').prop('src', ''); - $('img.expression').removeClass('default'); - $('.expression_settings').hide(); -} - -async function validateImages(character, forceRedrawCached) { - if (!character) { - return; - } - - const labels = await getExpressionsList(); - - if (spriteCache[character]) { - if (forceRedrawCached && $('#image_list').data('name') !== character) { - console.log('force redrawing character sprites list') - drawSpritesList(character, labels, spriteCache[character]); - } - - return; - } - - const sprites = await getSpritesList(character); - let validExpressions = drawSpritesList(character, labels, sprites); - spriteCache[character] = validExpressions; -} - -function drawSpritesList(character, labels, sprites) { - let validExpressions = []; - $('.expression_settings').show(); - $('#image_list').empty(); - $('#image_list').data('name', character); - labels.sort().forEach((item) => { - const sprite = sprites.find(x => x.label == item); - - if (sprite) { - validExpressions.push(sprite); - $('#image_list').append(getListItem(item, sprite.path, 'success')); - } - else { - $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); - } - }); - return validExpressions; -} - -function getListItem(item, imageSrc, textClass) { - return ` -
- ${item} - -
- `; -} - -async function getSpritesList(name) { - console.log('getting sprites list'); - - try { - const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); - - let sprites = result.ok ? (await result.json()) : []; - return sprites; - } - catch (err) { - console.log(err); - return []; - } -} - -async function getExpressionsList() { - // get something for offline mode (default images) - if (!modules.includes('classify')) { - return DEFAULT_EXPRESSIONS; - } - - if (Array.isArray(expressionsList)) { - return expressionsList; - } - - const url = new URL(getApiUrl()); - url.pathname = '/api/classify/labels'; - - try { - const apiResult = await fetch(url, { - method: 'GET', - headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, - }); - - if (apiResult.ok) { - - const data = await apiResult.json(); - expressionsList = data.labels; - return expressionsList; - } - } - catch (error) { - console.log(error); - return []; - } -} - -async function setExpression(character, expression, force) { - console.log('entered setExpressions'); - await validateImages(character); - const img = $('img.expression'); - - const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); - console.log('checking for expression images to show..'); - if (sprite) { - console.log('setting expression from character images folder'); - img.attr('src', sprite.path); - img.removeClass('default'); - img.off('error'); - img.on('error', function () { - $(this).attr('src', ''); - if (force && extension_settings.expressions.showDefault) { - setDefault(); - } - }); - } else { - if (extension_settings.expressions.showDefault) { - setDefault(); - } - } - - function setDefault() { - console.log('setting default'); - const defImgUrl = `/img/default-expressions/${expression}.png`; - //console.log(defImgUrl); - img.attr('src', defImgUrl); - img.addClass('default'); - } - document.getElementById("expression-holder").style.display = ''; -} - -function onClickExpressionImage() { - // online mode doesn't need force set - if (modules.includes('classify')) { - return; - } - - const expression = $(this).attr('id'); - const name = getLastCharacterMessage().name; - - if ($(this).find('.failure').length === 0) { - setExpression(name, expression, true); - } -} - -(function () { - function addExpressionImage() { - const html = ` -
- -
`; - $('body').append(html); - } - function addSettings() { - - const html = ` -
-
-
- Expression images -
-
-
-

You are in offline mode. Click on the image below to set the expression.

-
-

Hint: Create new folder in the public/characters/ folder and name it as the name of the character. - Put images with expressions there. File names should follow the pattern: [expression_label].[image_format]

- -
-
-
- `; - $('#extensions_settings').append(html); - $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); - $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); - $(document).on('click', '.expression_list_item', onClickExpressionImage); - $('.expression_settings').hide(); - } - - addExpressionImage(); - addSettings(); - setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); - moduleWorkerWrapper(); -})(); \ No newline at end of file +import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; +import { getContext, getApiUrl, modules, extension_settings } from "../../extensions.js"; +export { MODULE_NAME }; + +const MODULE_NAME = 'expressions'; +const UPDATE_INTERVAL = 2000; +const DEFAULT_EXPRESSIONS = [ + "admiration", + "amusement", + "anger", + "annoyance", + "approval", + "caring", + "confusion", + "curiosity", + "desire", + "disappointment", + "disapproval", + "disgust", + "embarrassment", + "excitement", + "fear", + "gratitude", + "grief", + "joy", + "love", + "nervousness", + "optimism", + "pride", + "realization", + "relief", + "remorse", + "sadness", + "surprise", + "neutral" +]; + +let expressionsList = null; +let lastCharacter = undefined; +let lastMessage = null; +let spriteCache = {}; +let inApiCall = false; + +function onExpressionsShowDefaultInput() { + const value = $(this).prop('checked'); + extension_settings.expressions.showDefault = value; + saveSettingsDebounced(); + + const existingImageSrc = $('img.expression').prop('src'); + if (existingImageSrc !== undefined) { //if we have an image in src + if (!value && existingImageSrc.includes('/img/default-expressions/')) { //and that image is from /img/ (default) + $('img.expression').prop('src', ''); //remove it + lastMessage = null; + } + if (value) { + lastMessage = null; + } + } +} + +let isWorkerBusy = false; + +async function moduleWorkerWrapper() { + // Don't touch me I'm busy... + if (isWorkerBusy) { + return; + } + + // I'm free. Let's update! + try { + isWorkerBusy = true; + await moduleWorker(); + } + finally { + isWorkerBusy = false; + } +} + +async function moduleWorker() { + const context = getContext(); + + // non-characters not supported + if (!context.groupId && context.characterId === undefined) { + removeExpression(); + return; + } + + // character changed + if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { + removeExpression(); + spriteCache = {}; + } + + const currentLastMessage = getLastCharacterMessage(); + + // character has no expressions or it is not loaded + if (Object.keys(spriteCache).length === 0) { + await validateImages(currentLastMessage.name); + lastCharacter = context.groupId || context.characterId; + } + + const offlineMode = $('.expression_settings .offline_mode'); + if (!modules.includes('classify')) { + $('.expression_settings').show(); + offlineMode.css('display', 'block'); + lastCharacter = context.groupId || context.characterId; + + if (context.groupId) { + await validateImages(currentLastMessage.name, true); + } + + return; + } + else { + // force reload expressions list on connect to API + if (offlineMode.is(':visible')) { + expressionsList = null; + spriteCache = {}; + expressionsList = await getExpressionsList(); + await validateImages(currentLastMessage.name, true); + } + + offlineMode.css('display', 'none'); + } + + + // check if last message changed + if ((lastCharacter === context.characterId || lastCharacter === context.groupId) + && lastMessage === currentLastMessage.mes) { + return; + } + + // API is busy + if (inApiCall) { + return; + } + + try { + inApiCall = true; + const url = new URL(getApiUrl()); + url.pathname = '/api/classify'; + + const apiResult = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Bypass-Tunnel-Reminder': 'bypass', + }, + body: JSON.stringify({ text: currentLastMessage.mes }) + }); + + if (apiResult.ok) { + const name = context.groupId ? currentLastMessage.name : context.name2; + const force = !!context.groupId; + const data = await apiResult.json(); + let expression = data.classification[0].label; + + // Character won't be angry on you for swiping + if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { + expression = 'joy'; + } + + setExpression(name, expression, force); + } + + } + catch (error) { + console.log(error); + } + finally { + inApiCall = false; + lastCharacter = context.groupId || context.characterId; + lastMessage = currentLastMessage.mes; + } +} + +function getLastCharacterMessage() { + const context = getContext(); + const reversedChat = context.chat.slice().reverse(); + + for (let mes of reversedChat) { + if (mes.is_user || mes.is_system) { + continue; + } + + return { mes: mes.mes, name: mes.name }; + } + + return { mes: '', name: null }; +} + +function removeExpression() { + lastMessage = null; + $('img.expression').off('error'); + $('img.expression').prop('src', ''); + $('img.expression').removeClass('default'); + $('.expression_settings').hide(); +} + +async function validateImages(character, forceRedrawCached) { + if (!character) { + return; + } + + const labels = await getExpressionsList(); + + if (spriteCache[character]) { + if (forceRedrawCached && $('#image_list').data('name') !== character) { + console.log('force redrawing character sprites list') + drawSpritesList(character, labels, spriteCache[character]); + } + + return; + } + + const sprites = await getSpritesList(character); + let validExpressions = drawSpritesList(character, labels, sprites); + spriteCache[character] = validExpressions; +} + +function drawSpritesList(character, labels, sprites) { + let validExpressions = []; + $('.expression_settings').show(); + $('#image_list').empty(); + $('#image_list').data('name', character); + labels.sort().forEach((item) => { + const sprite = sprites.find(x => x.label == item); + + if (sprite) { + validExpressions.push(sprite); + $('#image_list').append(getListItem(item, sprite.path, 'success')); + } + else { + $('#image_list').append(getListItem(item, '/img/No-Image-Placeholder.svg', 'failure')); + } + }); + return validExpressions; +} + +function getListItem(item, imageSrc, textClass) { + return ` +
+
+ + +
+ ${item} + +
+ `; +} + +async function getSpritesList(name) { + console.log('getting sprites list'); + + try { + const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`); + + let sprites = result.ok ? (await result.json()) : []; + return sprites; + } + catch (err) { + console.log(err); + return []; + } +} + +async function getExpressionsList() { + // get something for offline mode (default images) + if (!modules.includes('classify')) { + return DEFAULT_EXPRESSIONS; + } + + if (Array.isArray(expressionsList)) { + return expressionsList; + } + + const url = new URL(getApiUrl()); + url.pathname = '/api/classify/labels'; + + try { + const apiResult = await fetch(url, { + method: 'GET', + headers: { 'Bypass-Tunnel-Reminder': 'bypass' }, + }); + + if (apiResult.ok) { + + const data = await apiResult.json(); + expressionsList = data.labels; + return expressionsList; + } + } + catch (error) { + console.log(error); + return []; + } +} + +async function setExpression(character, expression, force) { + console.log('entered setExpressions'); + await validateImages(character); + const img = $('img.expression'); + + const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression)); + console.log('checking for expression images to show..'); + if (sprite) { + console.log('setting expression from character images folder'); + img.attr('src', sprite.path); + img.removeClass('default'); + img.off('error'); + img.on('error', function () { + $(this).attr('src', ''); + if (force && extension_settings.expressions.showDefault) { + setDefault(); + } + }); + } else { + if (extension_settings.expressions.showDefault) { + setDefault(); + } + } + + function setDefault() { + console.log('setting default'); + const defImgUrl = `/img/default-expressions/${expression}.png`; + //console.log(defImgUrl); + img.attr('src', defImgUrl); + img.addClass('default'); + } + document.getElementById("expression-holder").style.display = ''; +} + +function onClickExpressionImage() { + // online mode doesn't need force set + if (modules.includes('classify')) { + return; + } + + const expression = $(this).attr('id'); + const name = getLastCharacterMessage().name; + + if ($(this).find('.failure').length === 0) { + setExpression(name, expression, true); + } +} +async function handleFileUpload(url, formData) { + try { + const data = await jQuery.ajax({ + type: "POST", + url: url, + data: formData, + beforeSend: function () { }, + cache: false, + contentType: false, + processData: false, + }); + + // Refresh sprites list + const name = formData.get('name'); + delete spriteCache[name]; + await validateImages(name); + + return data; + } catch (error) { + toastr.error('Failed to upload image'); + } +} + +async function onClickExpressionUpload(event) { + // Prevents the expression from being set + event.stopPropagation(); + + const id = $(this).closest('.expression_list_item').attr('id'); + const name = $('#image_list').data('name'); + + const handleExpressionUploadChange = async (e) => { + const file = e.target.files[0]; + + if (!file) { + return; + } + + const formData = new FormData(); + formData.append('name', name); + formData.append('label', id); + formData.append('avatar', file); + + await handleFileUpload('/upload_sprite', formData); + + // Reset the input + e.target.form.reset(); + }; + + $('#expression_upload') + .off('change') + .on('change', handleExpressionUploadChange) + .trigger('click'); +} + +async function onClickExpressionUploadPackButton() { + const name = $('#image_list').data('name'); + + const handleFileUploadChange = async (e) => { + const file = e.target.files[0]; + + if (!file) { + return; + } + + const formData = new FormData(); + formData.append('name', name); + formData.append('avatar', file); + + const { count } = await handleFileUpload('/upload_sprite_pack', formData); + toastr.success(`Uploaded ${count} image(s) for ${name}`); + + // Reset the input + e.target.form.reset(); + }; + + $('#expression_upload_pack') + .off('change') + .on('change', handleFileUploadChange) + .trigger('click'); +} + +async function onClickExpressionDelete(event) { + // Prevents the expression from being set + event.stopPropagation(); + + const confirmation = await callPopup("

Are you sure?

Once deleted, it's gone forever!", 'confirm'); + + if (!confirmation) { + return; + } + + const id = $(this).closest('.expression_list_item').attr('id'); + const name = $('#image_list').data('name'); + + try { + await fetch('/delete_sprite', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ name, label: id }), + }); + } catch (error) { + toastr.error('Failed to delete image. Try again later.'); + } + + // Refresh sprites list + delete spriteCache[name]; + await validateImages(name); +} + +(function () { + function addExpressionImage() { + const html = ` +
+ +
`; + $('body').append(html); + } + function addSettings() { + + const html = ` +
+
+
+ Expression images +
+
+
+

You are in offline mode. Click on the image below to set the expression.

+
+
+ +
+

Hint: Create new folder in the public/characters/ folder and name it as the name of the character. + Put images with expressions there. File names should follow the pattern: [expression_label].[image_format]

+ +
+
+
+ + +
+
+ `; + $('#extensions_settings').append(html); + $('#expressions_show_default').on('input', onExpressionsShowDefaultInput); + $('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton); + $('#expressions_show_default').prop('checked', extension_settings.expressions.showDefault).trigger('input'); + $(document).on('click', '.expression_list_item', onClickExpressionImage); + $(document).on('click', '.expression_list_upload', onClickExpressionUpload); + $(document).on('click', '.expression_list_delete', onClickExpressionDelete); + $('.expression_settings').hide(); + } + + addExpressionImage(); + addSettings(); + setInterval(moduleWorkerWrapper, UPDATE_INTERVAL); + moduleWorkerWrapper(); +})(); diff --git a/public/scripts/extensions/expressions/style.css b/public/scripts/extensions/expressions/style.css index 53e5745a3..819ad9cd4 100644 --- a/public/scripts/extensions/expressions/style.css +++ b/public/scripts/extensions/expressions/style.css @@ -1,124 +1,146 @@ -.expression-helper { - display: inline-block; - height: 100%; - vertical-align: middle; -} - -#expression-wrapper { - display: flex; - height: calc(100vh - 40px); - width: 100vw; -} - -.expression-holder { - min-width: 100px; - min-height: 100px; - max-height: 90vh; - max-width: 90vh; - width: calc((100vw - var(--sheldWidth)) /2); - position: absolute; - bottom: 1px; - padding: 0; - filter: drop-shadow(2px 2px 2px #51515199); - z-index: 2; - overflow: hidden; - -} - -img.expression { - width: 100%; - height: 100%; - vertical-align: bottom; - object-fit: contain; -} - -img.expression[src=""] { - visibility: hidden; -} - -img.expression.default { - vertical-align: middle; - max-height: 120px; - object-fit: contain !important; - margin-top: 50px; -} - -.debug-image { - display: none; - visibility: collapse; - opacity: 0; - width: 0px; - height: 0px; -} - -.expression_list_item { - position: relative; - max-width: 20%; - max-height: 200px; - background-color: #515151b0; - border-radius: 10px; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.expression_list_title { - position: absolute; - bottom: 0; - left: 0; - text-align: center; - font-weight: 600; - background-color: #000000a8; - width: 100%; - height: 20%; - display: flex; - justify-content: center; - align-items: center; -} - -.expression_list_image { - max-width: 100%; - height: 100%; -} - -#image_list { - display: flex; - flex-direction: row; - column-gap: 1rem; - margin: 1rem; - flex-wrap: wrap; - justify-content: space-evenly; - row-gap: 1rem; -} - -#image_list .success { - color: green; -} - -#image_list .failure { - color: red; -} - -.expression_settings p { - margin-top: 0.5rem; - margin-bottom: 0.5rem; -} - -.expression_settings label { - display: flex; - align-items: center; - flex-direction: row; - margin-left: 0px; -} - -.expression_settings label input { - margin-left: 0px !important; -} - -@media screen and (max-width:1200px) { - div.expression { - display: none; - } -} \ No newline at end of file +.expression-helper { + display: inline-block; + height: 100%; + vertical-align: middle; +} + +#expression-wrapper { + display: flex; + height: calc(100vh - 40px); + width: 100vw; +} + +.expression-holder { + min-width: 100px; + min-height: 100px; + max-height: 90vh; + max-width: 90vh; + width: calc((100vw - var(--sheldWidth)) /2); + position: absolute; + bottom: 1px; + padding: 0; + filter: drop-shadow(2px 2px 2px #51515199); + z-index: 2; + overflow: hidden; + +} + +img.expression { + width: 100%; + height: 100%; + vertical-align: bottom; + object-fit: contain; +} + +img.expression[src=""] { + visibility: hidden; +} + +img.expression.default { + vertical-align: middle; + max-height: 120px; + object-fit: contain !important; + margin-top: 50px; +} + +.debug-image { + display: none; + visibility: collapse; + opacity: 0; + width: 0px; + height: 0px; +} + +.expression_list_item { + position: relative; + max-width: 20%; + max-height: 200px; + background-color: #515151b0; + border-radius: 10px; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.expression_list_title { + position: absolute; + bottom: 0; + left: 0; + text-align: center; + font-weight: 600; + background-color: #000000a8; + width: 100%; + height: 20%; + display: flex; + justify-content: center; + align-items: center; +} + +.expression_list_buttons { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + height: 20%; + padding: 0.25rem; +} + +.expression_list_image { + max-width: 100%; + height: 100%; + object-fit: cover; +} + +#image_list { + display: flex; + flex-direction: row; + column-gap: 1rem; + margin: 1rem; + flex-wrap: wrap; + justify-content: space-evenly; + row-gap: 1rem; +} + +#image_list .success { + color: green; +} + +#image_list .failure { + color: red; +} + +.expression_settings p { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.expression_settings label { + display: flex; + align-items: center; + flex-direction: row; + margin-left: 0px; +} + +.expression_settings label input { + margin-left: 0px !important; +} + +.expression_buttons .menu_button { + width: fit-content; + display: flex; + gap: 10px; + align-items: baseline; + flex-direction: row; +} + +@media screen and (max-width:1200px) { + div.expression { + display: none; + } +} diff --git a/public/scripts/extensions/infinity-context/index.js b/public/scripts/extensions/infinity-context/index.js index 9cc7ab744..9ca71c430 100644 --- a/public/scripts/extensions/infinity-context/index.js +++ b/public/scripts/extensions/infinity-context/index.js @@ -15,7 +15,7 @@ const defaultSettings = { keep_context_step: 1, n_results: 20, - n_results_min: 1, + n_results_min: 0, n_results_max: 100, n_results_step: 1, diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 1cadb1292..c93c34bd2 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -57,27 +57,27 @@ const quietPrompts = { [generationMode.USER]: "[Pause your roleplay and provide a detailed description of {{user}}'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase 'full body portrait,'. Ignore the rest of the story when crafting this description. Do not roleplay as {{char}} when writing this description, and do not attempt to continue the story.]", [generationMode.SCENARIO]: "[Pause your roleplay and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}'s appearance, and {{char}}'s surroundings. Do not roleplay while writing this description.]", - [generationMode.NOW]: `[Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message. - - Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman'). - - Ignore non-visible things such as feelings, personality traits, thoughts, and spoken dialog. - + [generationMode.NOW]: `[Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message. + + Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman'). + + Ignore non-visible things such as feelings, personality traits, thoughts, and spoken dialog. + Add keywords in this precise order: a keyword to describe the location of the scene, - a keyword to mention how many characters of each gender or type are present in the scene (minimum of two characters: + a keyword to mention how many characters of each gender or type are present in the scene (minimum of two characters: {{user}} and {{char}}, example: '2 men ' or '1 man 1 woman ', '1 man 3 robots'), keywords to describe the relative physical positioning of the characters to each other (if a commonly known term for the positioning is known use it instead of describing the positioning in detail) + 'POV', a single keyword or phrase to describe the primary act taking place in the last chat message, - + keywords to describe {{char}}'s physical appearance and facial expression, keywords to describe {{char}}'s actions, keywords to describe {{user}}'s physical appearance and actions. - + If character actions involve direct physical interaction with another character, mention specifically which body parts interacting and how. - + A correctly formatted example response would be: '(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)']`, @@ -386,7 +386,7 @@ function processReply(str) { str = str.replaceAll('“', '') str = str.replaceAll('.', ',') str = str.replaceAll('\n', ', ') - str = str.replace(/[^a-zA-Z0-9,:]+/g, ' ') // Replace everything except alphanumeric characters and commas with spaces + str = str.replace(/[^a-zA-Z0-9,:()]+/g, ' ') // Replace everything except alphanumeric characters and commas with spaces str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one str = str.trim(); diff --git a/public/scripts/extensions/token-counter/index.js b/public/scripts/extensions/token-counter/index.js new file mode 100644 index 000000000..8d31c83ea --- /dev/null +++ b/public/scripts/extensions/token-counter/index.js @@ -0,0 +1,40 @@ +import { callPopup, main_api } from "../../../script.js"; +import { getContext } from "../../extensions.js"; +import { oai_settings } from "../../openai.js"; + +async function doTokenCounter() { + const selectedTokenizer = main_api == 'openai' + ? `tiktoken (${oai_settings.openai_model})` + : $("#tokenizer").find(':selected').text(); + const html = ` +
+

Token Counter

+
+

Type / paste in the box below to see the number of tokens in the text.

+

Selected tokenizer: ${selectedTokenizer}

+ +
Tokens: 0
+
+
`; + + const dialog = $(html); + dialog.find('#token_counter_textarea').on('input', () => { + const text = $('#token_counter_textarea').val(); + const context = getContext(); + const count = context.getTokenCount(text); + $('#token_counter_result').text(count); + }); + + $('#dialogue_popup').addClass('wide_dialogue_popup'); + callPopup(dialog, 'text'); +} + +jQuery(() => { + const buttonHtml = ` +
+
+ Token Counter +
`; + $('#extensionsMenu').prepend(buttonHtml); + $('#token_counter').on('click', doTokenCounter); +}); diff --git a/public/scripts/extensions/token-counter/manifest.json b/public/scripts/extensions/token-counter/manifest.json new file mode 100644 index 000000000..65da1d897 --- /dev/null +++ b/public/scripts/extensions/token-counter/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Token Counter", + "loading_order": 15, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Cohee#1207", + "version": "1.0.0", + "homePage": "https://github.com/Cohee1207/SillyTavern" +} diff --git a/public/scripts/extensions/token-counter/style.css b/public/scripts/extensions/token-counter/style.css new file mode 100644 index 000000000..e69de29bb diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js new file mode 100644 index 000000000..f4708ec0d --- /dev/null +++ b/public/scripts/extensions/translate/index.js @@ -0,0 +1,371 @@ +import { + callPopup, + eventSource, + event_types, + getRequestHeaders, + messageFormatting, + reloadCurrentChat, + saveSettingsDebounced, + substituteParams, +} from "../../../script.js"; +import { extension_settings, getContext } from "../../extensions.js"; + +const autoModeOptions = { + NONE: 'none', + RESPONSES: 'responses', + INPUT: 'inputs', + BOTH: 'both', +}; + +const incomingTypes = [autoModeOptions.RESPONSES, autoModeOptions.BOTH]; +const outgoingTypes = [autoModeOptions.INPUT, autoModeOptions.BOTH]; + +const defaultSettings = { + target_language: 'en', + internal_language: 'en', + provider: 'google', + auto_mode: autoModeOptions.NONE, +}; + +const languageCodes = { + 'Afrikaans': 'af', + 'Albanian': 'sq', + 'Amharic': 'am', + 'Arabic': 'ar', + 'Armenian': 'hy', + 'Azerbaijani': 'az', + 'Basque': 'eu', + 'Belarusian': 'be', + 'Bengali': 'bn', + 'Bosnian': 'bs', + 'Bulgarian': 'bg', + 'Catalan': 'ca', + 'Cebuano': 'ceb', + 'Chinese (Simplified)': 'zh-CN', + 'Chinese (Traditional)': 'zh-TW', + 'Corsican': 'co', + 'Croatian': 'hr', + 'Czech': 'cs', + 'Danish': 'da', + 'Dutch': 'nl', + 'English': 'en', + 'Esperanto': 'eo', + 'Estonian': 'et', + 'Finnish': 'fi', + 'French': 'fr', + 'Frisian': 'fy', + 'Galician': 'gl', + 'Georgian': 'ka', + 'German': 'de', + 'Greek': 'el', + 'Gujarati': 'gu', + 'Haitian Creole': 'ht', + 'Hausa': 'ha', + 'Hawaiian': 'haw', + 'Hebrew': 'iw', + 'Hindi': 'hi', + 'Hmong': 'hmn', + 'Hungarian': 'hu', + 'Icelandic': 'is', + 'Igbo': 'ig', + 'Indonesian': 'id', + 'Irish': 'ga', + 'Italian': 'it', + 'Japanese': 'ja', + 'Javanese': 'jw', + 'Kannada': 'kn', + 'Kazakh': 'kk', + 'Khmer': 'km', + 'Korean': 'ko', + 'Kurdish': 'ku', + 'Kyrgyz': 'ky', + 'Lao': 'lo', + 'Latin': 'la', + 'Latvian': 'lv', + 'Lithuanian': 'lt', + 'Luxembourgish': 'lb', + 'Macedonian': 'mk', + 'Malagasy': 'mg', + 'Malay': 'ms', + 'Malayalam': 'ml', + 'Maltese': 'mt', + 'Maori': 'mi', + 'Marathi': 'mr', + 'Mongolian': 'mn', + 'Myanmar (Burmese)': 'my', + 'Nepali': 'ne', + 'Norwegian': 'no', + 'Nyanja (Chichewa)': 'ny', + 'Pashto': 'ps', + 'Persian': 'fa', + 'Polish': 'pl', + 'Portuguese (Portugal, Brazil)': 'pt', + 'Punjabi': 'pa', + 'Romanian': 'ro', + 'Russian': 'ru', + 'Samoan': 'sm', + 'Scots Gaelic': 'gd', + 'Serbian': 'sr', + 'Sesotho': 'st', + 'Shona': 'sn', + 'Sindhi': 'sd', + 'Sinhala (Sinhalese)': 'si', + 'Slovak': 'sk', + 'Slovenian': 'sl', + 'Somali': 'so', + 'Spanish': 'es', + 'Sundanese': 'su', + 'Swahili': 'sw', + 'Swedish': 'sv', + 'Tagalog (Filipino)': 'tl', + 'Tajik': 'tg', + 'Tamil': 'ta', + 'Telugu': 'te', + 'Thai': 'th', + 'Turkish': 'tr', + 'Ukrainian': 'uk', + 'Urdu': 'ur', + 'Uzbek': 'uz', + 'Vietnamese': 'vi', + 'Welsh': 'cy', + 'Xhosa': 'xh', + 'Yiddish': 'yi', + 'Yoruba': 'yo', + 'Zulu': 'zu', +}; + +function loadSettings() { + for (const key in defaultSettings) { + if (!extension_settings.translate.hasOwnProperty(key)) { + extension_settings.translate[key] = defaultSettings[key]; + } + } + + $(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', true); + $(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', true); + $(`#translation_auto_mode option[value="${extension_settings.translate.auto_mode}"]`).attr('selected', true); +} + +async function translateImpersonate(text) { + const translatedText = await translate(text, extension_settings.translate.target_language); + $("#send_textarea").val(translatedText); +} + +async function translateIncomingMessage(messageId) { + const context = getContext(); + const message = context.chat[messageId]; + + if (typeof message.extra !== 'object') { + message.extra = {}; + } + + // New swipe is being generated. Don't translate that + if ($(`#chat .mes[mesid="${messageId}"] .mes_text`).text() == '...') { + return; + } + + const textToTranslate = substituteParams(message.mes, context.name1, message.name); + const translation = await translate(textToTranslate, extension_settings.translate.target_language); + message.extra.display_text = translation; + + $(`#chat .mes[mesid="${messageId}"] .mes_text`).html(messageFormatting(translation, message.name, message.is_system, message.is_user)); +} + +async function translateProviderGoogle(text, lang) { + const response = await fetch('/google_translate', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ text: text, lang: lang }), + }); + + if (response.ok) { + const result = await response.text(); + return result; + } + + throw new Error(response.statusText); +} + +async function translate(text, lang) { + try { + switch (extension_settings.translate.provider) { + case 'google': + return await translateProviderGoogle(text, lang); + default: + console.error('Unknown translation provider', extension_settings.translate.provider); + return text; + } + } catch (error) { + console.log(error); + toastr.error('Failed to translate message'); + } +} + +async function translateOutgoingMessage(messageId) { + const context = getContext(); + const message = context.chat[messageId]; + + if (typeof message.extra !== 'object') { + message.extra = {}; + } + + const originalText = message.mes; + message.extra.display_text = originalText; + $(`#chat .mes[mesid="${messageId}"] .mes_text`).html(messageFormatting(originalText, message.name, message.is_system, message.is_user)); + message.mes = await translate(originalText, extension_settings.translate.internal_language); + + console.log('translateOutgoingMessage', messageId); +} + +function shouldTranslate(types) { + return types.includes(extension_settings.translate.auto_mode); +} + +function createEventHandler(translateFunction, shouldTranslateFunction) { + return async (data) => { + if (shouldTranslateFunction()) { + await translateFunction(data); + } + }; +} + +// Prevents the chat from being translated in parallel +let translateChatExecuting = false; + +async function onTranslateChatClick() { + if (translateChatExecuting) { + return; + } + + try { + translateChatExecuting = true; + const context = getContext(); + const chat = context.chat; + + toastr.info(`${chat.length} message(s) queued for translation.`, 'Please wait...'); + + for (let i = 0; i < chat.length; i++) { + await translateIncomingMessage(i); + } + + await context.saveChat(); + } catch (error) { + console.log(error); + toastr.error('Failed to translate chat'); + } finally { + translateChatExecuting = false; + } +} + +async function onTranslationsClearClick() { + const confirm = await callPopup('

Are you sure?

This will remove translated text from all messages in the current chat. This action cannot be undone.', 'confirm'); + + if (!confirm) { + return; + } + + const context = getContext(); + const chat = context.chat; + + for (const mes of chat) { + if (mes.extra) { + delete mes.extra.display_text; + } + } + + await context.saveChat(); + await reloadCurrentChat(); +} + +async function translateMessageEdit(messageId) { + const context = getContext(); + const chat = context.chat; + const message = chat[messageId]; + + if (message.is_system || extension_settings.translate.auto_mode == autoModeOptions.NONE) { + return; + } + + if ((message.is_user && shouldTranslate(outgoingTypes)) || (!message.is_user && shouldTranslate(incomingTypes))) { + await translateIncomingMessage(messageId); + } +} + +const handleIncomingMessage = createEventHandler(translateIncomingMessage, () => shouldTranslate(incomingTypes)); +const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () => shouldTranslate(outgoingTypes)); +const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes)); +const handleMessageEdit = createEventHandler(translateMessageEdit, () => true); + +jQuery(() => { + const html = ` +
+
+
+ Chat Translation +
+
+
+ + + + + + + +
+
+
`; + + const buttonHtml = ` +
+
+ Translate Chat +
`; + $('#extensionsMenu').append(buttonHtml); + $('#extensions_settings').append(html); + $('#translate_chat').on('click', onTranslateChatClick); + $('#translation_clear').on('click', onTranslationsClearClick); + + for (const [key, value] of Object.entries(languageCodes)) { + $('#translation_target_language').append(``); + } + + $('#translation_auto_mode').on('change', (event) => { + extension_settings.translate.auto_mode = event.target.value; + saveSettingsDebounced(); + }); + $('#translation_provider').on('change', (event) => { + extension_settings.translate.provider = event.target.value; + saveSettingsDebounced(); + }); + $('#translation_target_language').on('change', (event) => { + extension_settings.translate.target_language = event.target.value; + saveSettingsDebounced(); + }); + $(document).on('click', '.mes_translate', function () { + const context = getContext(); + const messageId = $(this).closest('.mes').attr('mesid'); + translateIncomingMessage(messageId); + context.saveChat(); + }); + + loadSettings(); + + eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage); + eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage); + eventSource.on(event_types.MESSAGE_SENT, handleOutgoingMessage); + eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady); + eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit); + + document.body.classList.add('translate'); +}); diff --git a/public/scripts/extensions/translate/manifest.json b/public/scripts/extensions/translate/manifest.json new file mode 100644 index 000000000..91b41b281 --- /dev/null +++ b/public/scripts/extensions/translate/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Chat Translation", + "loading_order": 1, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "Cohee#1207", + "version": "1.0.0", + "homePage": "https://github.com/Cohee1207/SillyTavern" +} diff --git a/public/scripts/extensions/translate/style.css b/public/scripts/extensions/translate/style.css new file mode 100644 index 000000000..6947934c5 --- /dev/null +++ b/public/scripts/extensions/translate/style.css @@ -0,0 +1,7 @@ +.translation_settings .menu_button { + width: fit-content; + display: flex; + gap: 10px; + align-items: baseline; + flex-direction: row; +} diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 52722712a..fd595b454 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -1,4 +1,4 @@ -import { callPopup, cancelTtsPlay, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js' +import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js' import { extension_settings, getContext } from '../../extensions.js' import { getStringHash } from '../../utils.js' import { ElevenLabsTtsProvider } from './elevenlabs.js' @@ -117,6 +117,11 @@ async function moduleWorker() { return } + // Don't generate if message doesn't have a display text + if (extension_settings.tts.narrate_translated_only && !(message?.extra?.display_text)) { + return; + } + // New messages, add new chat to history lastMessageHash = hashNew currentMessageNumber = lastMessageNumber @@ -138,7 +143,7 @@ function resetTtsPlayback() { // Reset audio element audioElement.currentTime = 0; - audioElement.src = null; + audioElement.src = ''; // Clear any queue items ttsJobQueue.splice(0, ttsJobQueue.length); @@ -151,7 +156,7 @@ function resetTtsPlayback() { function isTtsProcessing() { let processing = false - // Check job queues + // Check job queues if (ttsJobQueue.length > 0 || audioJobQueue > 0) { processing = true } @@ -167,7 +172,7 @@ function debugTtsPlayback() { { "ttsProviderName": ttsProviderName, "currentMessageNumber": currentMessageNumber, - "isWorkerBusy":isWorkerBusy, + "isWorkerBusy": isWorkerBusy, "audioPaused": audioPaused, "audioJobQueue": audioJobQueue, "currentAudioJob": currentAudioJob, @@ -232,7 +237,7 @@ async function onTtsVoicesClick() { popupText += `
${voice.lang || ''} - ${voice.name} + ${voice.name}
` popupText += `` @@ -275,7 +280,7 @@ function onAudioControlClicked() { function addAudioControl() { $('#extensionsMenu').prepend(` -
+
TTS Playback
`) @@ -356,9 +361,10 @@ async function processTtsQueue() { console.debug('New message found, running TTS') currentTtsJob = ttsJobQueue.shift() - let text = extension_settings.tts.narrate_dialogues_only - ? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content - : currentTtsJob.mes.replaceAll('*', '').trim() // remove just the asterisks + let text = extension_settings.tts.narrate_translated_only ? currentTtsJob?.extra?.display_text : currentTtsJob.mes + text = extension_settings.tts.narrate_dialogues_only + ? text.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content + : text.replaceAll('*', '').trim() // remove just the asterisks if (extension_settings.tts.narrate_quoted_only) { const special_quotes = /[“”]/g; // Extend this regex to include other special quotes @@ -415,6 +421,7 @@ function loadSettings() { $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only) $('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only) $('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation) + $('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only); $('body').toggleClass('tts', extension_settings.tts.enabled); } @@ -483,15 +490,15 @@ function onApplyClick() { Promise.all([ ttsProvider.onApplyClick(), updateVoiceMap() - ]).catch(error => { + ]).then(() => { + extension_settings.tts[ttsProviderName] = ttsProvider.settings + saveSettingsDebounced() + setTtsStatus('Successfully applied settings', true) + console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) + }).catch(error => { console.error(error) setTtsStatus(error, false) }) - - extension_settings.tts[ttsProviderName] = ttsProvider.settings - saveSettingsDebounced() - setTtsStatus('Successfully applied settings', true) - console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`) } function onEnableClick() { @@ -520,6 +527,11 @@ function onNarrateQuotedClick() { } +function onNarrateTranslatedOnlyClick() { + extension_settings.tts.narrate_translated_only = $('#tts_narrate_translated_only').prop('checked'); + saveSettingsDebounced(); +} + //##############// // TTS Provider // //##############// @@ -607,6 +619,10 @@ $(document).ready(function () { Narrate quoted only +