Merge branch 'dev' of github.com:Cohee1207/SillyTavern into dev

This commit is contained in:
Aisu Wata
2023-05-13 22:16:32 -03:00
33 changed files with 767 additions and 190 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: "Create a report to help us improve. PAY ATTENTION: Support requests for extenal programs (reverse proxies, 3rd party servers, other peoples' forks) will be refused!"
title: "[BUG]" title: "[BUG]"
labels: '' labels: ''
assignees: '' assignees: ''

View File

@ -1,7 +1,7 @@
--- ---
name: Feature request name: Feature request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '' title: "[Feature Request] "
labels: '' labels: ''
assignees: '' assignees: ''

View File

@ -16,6 +16,7 @@ Method 1 - GIT
We always recommend users install using 'git'. Here's why: We always recommend users install using 'git'. Here's why:
When you have installed via `git clone`, all you have to do to update is type `git pull` in a command line in the ST folder. When you have installed via `git clone`, all you have to do to update is type `git pull` in a command line in the ST folder.
Alternatively, if the command prompt gives you problems (and you have GitHub Desktop installed), you can use the 'Repository' menu and select 'Pull'.
The updates are applied automatically and safely. The updates are applied automatically and safely.
Method 2 - ZIP Method 2 - ZIP

View File

@ -10,6 +10,19 @@
"SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj" "SillyTavern community Discord (support and discussion): https://discord.gg/RZdyAEUPvj"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#@title <-- Tap this if you run on Mobile { display-mode: \"form\" }\n",
"#Taken from KoboldAI colab\n",
"%%html\n",
"<b>Press play on the audio player to keep the tab alive. (Uses only 13MB of data)</b><br/>\n",
"<audio src=\"https://henk.tech/colabkobold/silence.m4a\" controls>"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@ -42,9 +55,13 @@
"#@markdown Enables Silero text-to-speech module\n", "#@markdown Enables Silero text-to-speech module\n",
"extras_enable_sd = True #@param {type:\"boolean\"}\n", "extras_enable_sd = True #@param {type:\"boolean\"}\n",
"#@markdown Enables SD picture generation\n", "#@markdown Enables SD picture generation\n",
"SD_Model = \"ckpt/anything-v4.5-vae-swapped\" #@param [ \"ckpt/anything-v4.5-vae-swapped\", \"ckpt/sd15\" ]\n", "SD_Model = \"ckpt/anything-v4.5-vae-swapped\" #@param [ \"ckpt/anything-v4.5-vae-swapped\", \"hakurei/waifu-diffusion\", \"philz1337/clarity\", \"prompthero/openjourney\", \"ckpt/sd15\", \"stabilityai/stable-diffusion-2-1-base\" ]\n",
"#@markdown * ckpt/anything-v4.5-vae-swapped - anime style model\n", "#@markdown * ckpt/anything-v4.5-vae-swapped - anime style model\n",
"#@markdown * hakurei/waifu-diffusion - anime style model\n",
"#@markdown * philz1337/clarity - realistic style model\n",
"#@markdown * prompthero/openjourney - midjourney style model\n",
"#@markdown * ckpt/sd15 - base SD 1.5\n", "#@markdown * ckpt/sd15 - base SD 1.5\n",
"#@markdown * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n",
"\n", "\n",
"import subprocess\n", "import subprocess\n",
"\n", "\n",
@ -78,6 +95,7 @@
"%cd /\n", "%cd /\n",
"!git clone https://github.com/Cohee1207/SillyTavern-extras\n", "!git clone https://github.com/Cohee1207/SillyTavern-extras\n",
"%cd /SillyTavern-extras\n", "%cd /SillyTavern-extras\n",
"!git clone https://github.com/Cohee1207/tts_samples\n",
"!npm install -g localtunnel\n", "!npm install -g localtunnel\n",
"!pip install -r requirements-complete.txt\n", "!pip install -r requirements-complete.txt\n",
"!pip install tensorflow==2.11\n", "!pip install tensorflow==2.11\n",

View File

@ -8,7 +8,17 @@ const disableThumbnails = false; //Disables the generation of thumbnails, opting
const autorun = true; //Autorun in the browser. true/false const autorun = true; //Autorun in the browser. true/false
const enableExtensions = true; //Enables support for TavernAI-extras project const enableExtensions = true; //Enables support for TavernAI-extras project
const listen = true; // If true, Can be access from other device or PC. otherwise can be access only from hosting machine. const listen = true; // If true, Can be access from other device or PC. otherwise can be access only from hosting machine.
const allowKeysExposure = false; // If true, private API keys could be fetched to the frontend.
module.exports = { module.exports = {
port, whitelist, whitelistMode, basicAuthMode, basicAuthUser, autorun, enableExtensions, listen, disableThumbnails port,
whitelist,
whitelistMode,
basicAuthMode,
basicAuthUser,
autorun,
enableExtensions,
listen,
disableThumbnails,
allowKeysExposure,
}; };

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sillytavern", "name": "sillytavern",
"version": "1.5.0", "version": "1.5.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sillytavern", "name": "sillytavern",
"version": "1.5.0", "version": "1.5.1",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@dqbd/tiktoken": "^1.0.2", "@dqbd/tiktoken": "^1.0.2",

View File

@ -40,7 +40,7 @@
"type": "git", "type": "git",
"url": "https://github.com/Cohee1207/SillyTavern.git" "url": "https://github.com/Cohee1207/SillyTavern.git"
}, },
"version": "1.5.0", "version": "1.5.1",
"scripts": { "scripts": {
"start": "node server.js" "start": "node server.js"
}, },

View File

@ -1005,10 +1005,14 @@
Adjust response length to worker capabilities Adjust response length to worker capabilities
</label> </label>
<h4>API key</h4> <h4>API key</h4>
<h5>Get it here: <a target="_blank" href="https://horde.koboldai.net/register">Register</a> <h5>Get it here: <a target="_blank" href="https://horde.koboldai.net/register">Register</a><br>
Enter <span class="monospace">0000000000</span> to use anonymous mode.
</h5> </h5>
<input id="horde_api_key" name="horde_api_key" class="text_pole" maxlength="500" type="text" placeholder="0000000000"> <div class="flex-container">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div> <input id="horde_api_key" name="horde_api_key" class="text_pole flex1" maxlength="500" type="text" placeholder="0000000000">
<div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_horde"></div>
</div>
<div class="neutral_warning">For privacy reasons, your API key will be hidden after you reload the page.</div>
<h4 class="horde_model_title"> <h4 class="horde_model_title">
Model Model
<div id="horde_refresh" title="Refresh models" class="right_menu_button"> <div id="horde_refresh" title="Refresh models" class="right_menu_button">
@ -1038,8 +1042,11 @@
<li>Enter it in the box below:</li> <li>Enter it in the box below:</li>
</ol> </ol>
</span> </span>
<input id="api_key_novel" name="api_key_novel" class="text_pole" maxlength="500" size="35" type="text"> <div class="flex-container">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div> <input id="api_key_novel" name="api_key_novel" class="text_pole flex1" maxlength="500" size="35" type="text">
<div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_novel"></div>
</div>
<div class="neutral_warning">For privacy reasons, your API key will be hidden after you reload the page.</div>
<input id="api_button_novel" class="menu_button" type="submit" value="Connect"> <input id="api_button_novel" class="menu_button" type="submit" value="Connect">
<div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div> <div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
<h4>Novel AI Model <h4>Novel AI Model
@ -1092,8 +1099,11 @@
<li>Enter it in the box below:</li> <li>Enter it in the box below:</li>
</ol> </ol>
</span> </span>
<input id="api_key_openai" name="api_key_openai" class="text_pole" maxlength="500" value="" type="text"> <div class="flex-container">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div> <input id="api_key_openai" name="api_key_openai" class="text_pole flex1" maxlength="500" value="" type="text">
<div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_openai"></div>
</div>
<div class="neutral_warning">For privacy reasons, your API key will be hidden after you reload the page.</div>
<input id="api_button_openai" class="menu_button" type="submit" value="Connect"> <input id="api_button_openai" class="menu_button" type="submit" value="Connect">
<div id="api_loading_openai" class=" api-load-icon fa-solid fa-hourglass fa-spin"></div> <div id="api_loading_openai" class=" api-load-icon fa-solid fa-hourglass fa-spin"></div>
</form> </form>
@ -1129,8 +1139,11 @@
</ol> </ol>
</span> </span>
<div class="widthFreeExpand"> <div class="widthFreeExpand">
<input id="poe_token" class="text_pole" type="text" maxlength="100" /> <div class="flex-container">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div> <input id="poe_token" class="text_pole flex1" type="text" maxlength="100" />
<div title="Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_poe"></div>
</div>
<div class="neutral_warning">For privacy reasons, your API key will be hidden after you reload the page.</div>
</div> </div>
<input id="poe_connect" class="menu_button" type="button" value="Connect" /> <input id="poe_connect" class="menu_button" type="button" value="Connect" />
@ -1152,9 +1165,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex-container alignitemscenter spaceBetween wide100p">
<label for="auto-connect-checkbox" class="checkbox_label"><input id="auto-connect-checkbox" type="checkbox" /> <label for="auto-connect-checkbox" class="checkbox_label"><input id="auto-connect-checkbox" type="checkbox" />
Auto-connect to Last Server Auto-connect to Last Server
</label> </label>
<a id="viewSecrets" href="javascript:void(0);">View hidden API keys</a>
</div>
</div> </div>
</div> </div>
@ -1440,7 +1456,10 @@
<div class="drawer-icon fa-solid fa-face-smile closedIcon" title="User Settings"></div> <div class="drawer-icon fa-solid fa-face-smile closedIcon" title="User Settings"></div>
</div> </div>
<div id="user-settings-block" class="drawer-content closedDrawer"> <div id="user-settings-block" class="drawer-content closedDrawer">
<div class="flex-container wide100p alignitemscenter spaceBetween">
<h3>User Settings</h3> <h3>User Settings</h3>
<div id="version_display"></div>
</div>
<div class="flex-container spaceEvenly"> <div class="flex-container spaceEvenly">
<div name="UI Customization" class="flex-container drawer25pWidth"> <div name="UI Customization" class="flex-container drawer25pWidth">
<div class="ui-settings"> <div class="ui-settings">
@ -2027,7 +2046,7 @@
<hr> <hr>
<h4>Personality summary</h4> <h4>Personality summary</h4>
<h5>A brief description of the personality <a href="/notes#personalitysummary" class="notes-link" target="_blank"><span class="note-link-span">?</span></a></h5> <h5>A brief description of the personality <a href="/notes#personalitysummary" class="notes-link" target="_blank"><span class="note-link-span">?</span></a></h5>
<textarea id="personality_textarea" name="personality" placeholder="" form="form_create" class="text_pole" autocomplete="off" rows="2"></textarea> <textarea id="personality_textarea" name="personality" placeholder="" form="form_create" class="text_pole" autocomplete="off" rows="2" maxlength="20000"></textarea>
</div> </div>
<div id="scenario_div"> <div id="scenario_div">
@ -2037,7 +2056,7 @@
<span class="note-link-span">?</span> <span class="note-link-span">?</span>
</a> </a>
</h5> </h5>
<textarea id="scenario_pole" name="scenario" class="text_pole" maxlength="9999" value="" autocomplete="off" form="form_create" rows="2"></textarea> <textarea id="scenario_pole" name="scenario" class="text_pole" maxlength="20000" value="" autocomplete="off" form="form_create" rows="2"></textarea>
</div> </div>
<div id="talkativeness_div"> <div id="talkativeness_div">
@ -2057,7 +2076,7 @@
<h4>Examples of dialogue</h4> <h4>Examples of dialogue</h4>
<h5>Forms a personality more clearly <a href="/notes#examplesofdialogue" class="notes-link" target="_blank"><span class="note-link-span">?</span></a></h5> <h5>Forms a personality more clearly <a href="/notes#examplesofdialogue" class="notes-link" target="_blank"><span class="note-link-span">?</span></a></h5>
</div> </div>
<textarea id="mes_example_textarea" name="mes_example" placeholder="" form="form_create"></textarea> <textarea id="mes_example_textarea" name="mes_example" placeholder="" form="form_create" maxlength="20000"></textarea>
</div> </div>
<div id="character_popup_ok" class="menu_button">Save</div> <div id="character_popup_ok" class="menu_button">Save</div>
@ -2322,6 +2341,7 @@
<span class="name_text">${characterName}</span> <span class="name_text">${characterName}</span>
<div class="mes_buttons"> <div class="mes_buttons">
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal "></div>
<div title="Copy" class="mes_copy fa-solid fa-copy "></div> <div title="Copy" class="mes_copy fa-solid fa-copy "></div>
<div title="Edit" class="mes_edit fa-solid fa-pencil "></div> <div title="Edit" class="mes_edit fa-solid fa-pencil "></div>
</div> </div>
@ -2427,7 +2447,7 @@
<div id="loading_mes"> <div id="loading_mes">
<div alt="" class="fa-solid fa-hourglass-half"></div> <div alt="" class="fa-solid fa-hourglass-half"></div>
</div> </div>
<div id="send_but" class="fa-solid fa-feather-pointed"></div> <div id="send_but" class="fa-solid fa-feather-pointed" title="Send a message"></div>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,6 +1,6 @@
{ {
"name": "Alpaca", "name": "Alpaca",
"system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.", "system_prompt": "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"system_sequence": "", "system_sequence": "",
"stop_sequence": "", "stop_sequence": "",
"input_sequence": "### Instruction:", "input_sequence": "### Instruction:",

View File

@ -1,9 +1,9 @@
{ {
"name": "Koala", "name": "Koala",
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.", "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"system_sequence": "BEGINNING OF CONVERSATION:", "system_sequence": "BEGINNING OF CONVERSATION:",
"stop_sequence": "", "stop_sequence": "",
"input_sequence": "USER: ", "input_sequence": "USER: ",
"output_sequence": "GPT: ", "output_sequence": "GPT: ",
"wrap": true "wrap": false
} }

View File

@ -0,0 +1,9 @@
{
"name": "Vicuna 1.0",
"system_prompt": "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"system_sequence": "",
"stop_sequence": "",
"input_sequence": "### Human:",
"output_sequence": "### Assistant:",
"wrap": true
}

View File

@ -0,0 +1,9 @@
{
"name": "Vicuna 1.1",
"system_prompt": "A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"system_sequence": "BEGINNING OF CONVERSATION:",
"stop_sequence": "",
"input_sequence": "USER: ",
"output_sequence": "ASSISTANT: ",
"wrap": true
}

View File

@ -1,6 +1,6 @@
{ {
"name": "WizardLM", "name": "WizardLM",
"system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.", "system_prompt": "Write {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
"system_sequence": "", "system_sequence": "",
"stop_sequence": "", "stop_sequence": "",
"input_sequence": "### Instruction:", "input_sequence": "### Instruction:",

View File

@ -416,7 +416,53 @@ _When using Pygmalion models these anchors are automatically disabled, since Pyg
## Instruct Mode ## Instruct Mode
_This section is under construction. Please check later._ Instruct Mode allows you to adjust the prompting for instruction-following models, such as Alpaca, Metharme, WizardLM, etc.
**This is not supported for OpenAI API.**
### Instruct Mode Settings
#### System Prompt
Added to the beginning of each prompt. Should define the instructions for the model to follow.
For example:
```
Write one reply in internet RP style for {{char}}. Be verbose and creative.
```
#### Presets
Provides ready-made presets with prompts and sequences for some well-known instruct models.
*Changing a preset resets your system prompt to default!*
#### Input Sequence
Text added before the user's input.
#### Output Sequence
Text added before the character's reply.
#### System Sequence
Text added before the system prompt.
#### Stop Sequence
Text that denotes the end of the reply. Will be trimmed from the output text.
#### Include Names
If enabled, prepend character and user names to chat history logs after inserting the sequences.
*Always enabled for group chats!*
#### Wrap Sequences with Newline
Each sequence text will be wrapped with newline characters when inserted to the prompt. Required for Alpaca and its derivatives.
## Chat import ## Chat import

View File

@ -22,6 +22,7 @@ You definitely installed via git, so just 'git pull' inside the SillyTavern dire
We always recommend users install using 'git'. Here's why: We always recommend users install using 'git'. Here's why:
When you have installed via 'git clone', all you have to do to update is type 'git pull' in a command line in the ST folder. When you have installed via 'git clone', all you have to do to update is type 'git pull' in a command line in the ST folder.
Alternatively, if the command prompt gives you problems (and you have GitHub Desktop installed), you can use the 'Repository' menu and select 'Pull'.
The updates are applied automatically and safely. The updates are applied automatically and safely.
### Method 2 - ZIP ### Method 2 - ZIP

View File

@ -107,7 +107,7 @@ import {
} from "./scripts/poe.js"; } from "./scripts/poe.js";
import { debounce, delay, restoreCaretPosition, saveCaretPosition } from "./scripts/utils.js"; import { debounce, delay, restoreCaretPosition, saveCaretPosition } from "./scripts/utils.js";
import { extension_settings, loadExtensionSettings } from "./scripts/extensions.js"; import { extension_settings, getContext, loadExtensionSettings } from "./scripts/extensions.js";
import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js"; import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js";
import { import {
tag_map, tag_map,
@ -203,6 +203,9 @@ hljs.addPlugin({ "before:highlightElement": ({ el }) => { el.textContent = el.in
let converter; let converter;
reloadMarkdownProcessor(); reloadMarkdownProcessor();
// array for prompt token calculations
let itemizedPrompts = [];
/* let bg_menu_toggle = false; */ /* let bg_menu_toggle = false; */
export const systemUserName = "SillyTavern System"; export const systemUserName = "SillyTavern System";
let default_user_name = "You"; let default_user_name = "You";
@ -283,9 +286,8 @@ const system_messages = {
mes: [ mes: [
'Hi there! The following chat formatting commands are supported:', 'Hi there! The following chat formatting commands are supported:',
'<ol>', '<ol>',
'<li><tt>*text*</tt> format the actions that your character does</li>', '<li><tt>{{text}}</tt> sets a permanent behavioral bias for the AI</li>',
'<li><tt>{{text}}</tt> set the behavioral bias for the AI character</li>', '<li><tt>{{}}</tt> removes any active character bias</li>',
'<li><tt>{{}}</tt> cancel a previously set bias</li>',
'</ol>', '</ol>',
].join('') ].join('')
}, },
@ -296,22 +298,24 @@ const system_messages = {
is_user: false, is_user: false,
is_name: true, is_name: true,
mes: [ mes: [
'<h2>Welcome to SillyTavern!</h2>', '<h2>Welcome to <span id="version_display_welcome">SillyTavern</span>!</h2>',
'<div id="version_display_welcome"></div>',
'<h3>Want to Update to the latest version?</h3>', '<h3>Want to Update to the latest version?</h3>',
"Read the <a href='/notes/update.html' target='_blank'>instructions here</a>. Also located in your installation's base folder", "Read the <a href='/notes/update.html' target='_blank'>instructions here</a>. Also located in your installation's base folder",
'<hr class="sysHR">',
'<h3>In order to begin chatting:</h3>', '<h3>In order to begin chatting:</h3>',
'<ol>', '<ol>',
'<li>Connect to one of the supported generation APIs (the plug icon)</li>', '<li>Connect to one of the supported generation APIs (the plug icon)</li>',
'<li>Create or pick a character from the list (the top-right namecard icon)</li>', '<li>Create or pick a character from the list (the top-right namecard icon)</li>',
'</ol>', '</ol>',
"<h3>Running on Colab and can't get an answer from the AI or getting Out of Memory errors?</h3>", '<hr class="sysHR">',
'Set a lower Context Size in AI generation settings (leftmost icon).<br>Values in range of 1400-1600 Tokens would be the safest choice.',
'<h3>Where to download more characters?</h3>', '<h3>Where to download more characters?</h3>',
'<i>(Not endorsed, your discretion is advised)</i>', '<i>(Not endorsed, your discretion is advised)</i>',
'<ol>', '<ol>',
'<li><a target="_blank" href="https://discord.gg/pygmalionai">Pygmalion AI Discord</a></li>', '<li><a target="_blank" href="https://discord.gg/pygmalionai">Pygmalion AI Discord</a></li>',
'<li><a target="_blank" href="https://www.characterhub.org/">CharacterHub (NSFW)</a></li>', '<li><a target="_blank" href="https://www.characterhub.org/">CharacterHub (NSFW)</a></li>',
'</ol>', '</ol>',
'<hr class="sysHR">',
'<h3>Where can I get help?</h3>', '<h3>Where can I get help?</h3>',
'Before going any further, check out the following resources:', 'Before going any further, check out the following resources:',
'<ol>', '<ol>',
@ -322,6 +326,7 @@ const system_messages = {
'<li><a target="_blank" href="https://docs.alpindale.dev/">Pygmalion AI Docs</a></li>', '<li><a target="_blank" href="https://docs.alpindale.dev/">Pygmalion AI Docs</a></li>',
'</ol>', '</ol>',
'Type <tt>/?</tt> in any chat to get help on message formatting commands.', 'Type <tt>/?</tt> in any chat to get help on message formatting commands.',
'<hr class="sysHR">',
'<h3>Still have questions or suggestions left?</h3>', '<h3>Still have questions or suggestions left?</h3>',
'<a target="_blank" href="https://discord.gg/RZdyAEUPvj">SillyTavern Community Discord</a>', '<a target="_blank" href="https://discord.gg/RZdyAEUPvj">SillyTavern Community Discord</a>',
'<br/>', '<br/>',
@ -386,7 +391,16 @@ $(document).ajaxError(function myErrorHandler(_, xhr) {
async function getClientVersion() { async function getClientVersion() {
try { try {
const response = await fetch('/version'); const response = await fetch('/version');
CLIENT_VERSION = await response.text(); const data = await response.json();
CLIENT_VERSION = data.agent;
let displayVersion = `SillyTavern ${data.pkgVersion}`;
if (data.gitRevision && data.gitBranch) {
displayVersion += ` '${data.gitBranch}' (${data.gitRevision})`;
}
$('#version_display').text(displayVersion);
$('#version_display_welcome').text(displayVersion);
} catch (err) { } catch (err) {
console.log("Couldn't get client version", err); console.log("Couldn't get client version", err);
} }
@ -551,13 +565,13 @@ $.ajaxPrefilter((options, originalOptions, xhr) => {
///// initialization protocol //////// ///// initialization protocol ////////
$.get("/csrf-token").then(async (data) => { $.get("/csrf-token").then(async (data) => {
token = data.token; token = data.token;
sendSystemMessage(system_message_types.WELCOME);
await readSecretState(); await readSecretState();
await getClientVersion(); await getClientVersion();
await getSettings("def"); await getSettings("def");
await getUserAvatars();
await getCharacters(); await getCharacters();
await getBackgrounds(); await getBackgrounds();
await getUserAvatars();
sendSystemMessage(system_message_types.WELCOME);
}); });
function checkOnlineStatus() { function checkOnlineStatus() {
@ -751,8 +765,8 @@ function printCharacters() {
printTags(); printTags();
printGroups(); printGroups();
favsToHotswap();
sortCharactersList(); sortCharactersList();
favsToHotswap();
} }
async function getCharacters() { async function getCharacters() {
@ -1115,6 +1129,27 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
if (isSystem) { if (isSystem) {
newMessage.find(".mes_edit").hide(); newMessage.find(".mes_edit").hide();
newMessage.find(".mes_prompt").hide(); //dont'd need prompt display for sys messages
}
// don't need prompt butons for user messages
if (params.isUser === true) {
newMessage.find(".mes_prompt").hide();
}
//shows or hides the Prompt display button
let mesIdToFind = Number(newMessage.attr('mesId'));
if (itemizedPrompts.length !== 0) {
for (var i = 0; i < itemizedPrompts.length; i++) {
if (itemizedPrompts[i].mesId === mesIdToFind) {
newMessage.find(".mes_prompt").show();
} else {
console.log('no cache found for mesID, hiding prompt button and continuing search');
newMessage.find(".mes_prompt").hide();
}
}
} else { //hide all when prompt cache is empty
$(".mes_prompt").hide();
} }
newMessage.find('.avatar img').on('error', function () { newMessage.find('.avatar img').on('error', function () {
@ -1327,11 +1362,13 @@ function cleanGroupMessage(getMessage) {
} }
function getAllExtensionPrompts() { function getAllExtensionPrompts() {
return substituteParams(Object const value = Object
.values(extension_prompts) .values(extension_prompts)
.filter(x => x.value) .filter(x => x.value)
.map(x => x.value.trim()) .map(x => x.value.trim())
.join('\n')); .join('\n');
return value.length ? substituteParams(value) : '';
} }
function getExtensionPrompt(position = 0, depth = undefined, separator = "\n") { function getExtensionPrompt(position = 0, depth = undefined, separator = "\n") {
@ -1347,13 +1384,15 @@ function getExtensionPrompt(position = 0, depth = undefined, separator = "\n") {
if (extension_prompt.length && !extension_prompt.endsWith(separator)) { if (extension_prompt.length && !extension_prompt.endsWith(separator)) {
extension_prompt = extension_prompt + separator; extension_prompt = extension_prompt + separator;
} }
if (extension_prompt.length) {
extension_prompt = substituteParams(extension_prompt); extension_prompt = substituteParams(extension_prompt);
}
return extension_prompt; return extension_prompt;
} }
function baseChatReplace(value, name1, name2) { function baseChatReplace(value, name1, name2) {
if (value !== undefined && value.length > 0) { if (value !== undefined && value.length > 0) {
value = substituteParams(value, is_pygmalion ? "You:" : name1, name2); value = substituteParams(value, is_pygmalion ? "You" : name1, name2);
if (power_user.collapse_newlines) { if (power_user.collapse_newlines) {
value = collapseNewlines(value); value = collapseNewlines(value);
@ -1370,9 +1409,10 @@ function appendToStoryString(value, prefix) {
} }
function isStreamingEnabled() { function isStreamingEnabled() {
return (main_api == 'openai' && oai_settings.stream_openai) return ((main_api == 'openai' && oai_settings.stream_openai)
|| (main_api == 'poe' && poe_settings.streaming) || (main_api == 'poe' && poe_settings.streaming)
|| (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming); || (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming))
&& !isMultigenEnabled(); // Multigen has a quasi-streaming mode which breaks the real streaming
} }
class StreamingProcessor { class StreamingProcessor {
@ -1585,7 +1625,15 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
const isImpersonate = type == "impersonate"; const isImpersonate = type == "impersonate";
const isInstruct = power_user.instruct.enabled; const isInstruct = power_user.instruct.enabled;
message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
// Name for the multigen prefix
const magName = isImpersonate ? (is_pygmalion ? 'You' : name1) : name2;
if (isInstruct) {
message_already_generated = formatInstructModePrompt(magName, isImpersonate);
} else {
message_already_generated = `${magName}: `;
}
const interruptedByCommand = processCommands($("#send_textarea").val(), type); const interruptedByCommand = processCommands($("#send_textarea").val(), type);
@ -1819,6 +1867,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
this_max_context = Number(max_context); this_max_context = Number(max_context);
} }
// Adjust token limit for Horde // Adjust token limit for Horde
let adjustedParams; let adjustedParams;
if (main_api == 'kobold' && horde_settings.use_horde && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) { if (main_api == 'kobold' && horde_settings.use_horde && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) {
@ -1834,11 +1884,12 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
} }
} }
console.log();
// Extension added strings // Extension added strings
const allAnchors = getAllExtensionPrompts(); const allAnchors = getAllExtensionPrompts();
const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.AFTER_SCENARIO); const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.AFTER_SCENARIO);
let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' '); let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' ');
let { worldInfoString, worldInfoBefore, worldInfoAfter } = getWorldInfoPrompt(chat2); let { worldInfoString, worldInfoBefore, worldInfoAfter } = getWorldInfoPrompt(chat2);
// hack for regeneration of the first message // hack for regeneration of the first message
@ -2006,8 +2057,16 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
const isBottom = j === mesSend.length - 1; const isBottom = j === mesSend.length - 1;
mesSendString += mesSend[j]; mesSendString += mesSend[j];
// Add quiet generation prompt at depth 0
if (isBottom && quiet_prompt && quiet_prompt.length) {
const name = is_pygmalion ? 'You' : name1;
const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, true) : `\n${name}: ${quiet_prompt}`;
mesSendString += quietAppend;
}
if (isInstruct && isBottom && tokens_already_generated === 0) { if (isInstruct && isBottom && tokens_already_generated === 0) {
mesSendString += formatInstructModePrompt(isImpersonate); const name = isImpersonate ? (is_pygmalion ? 'You' : name1) : name2;
mesSendString += formatInstructModePrompt(name, isImpersonate);
} }
if (!isInstruct && isImpersonate && isBottom && tokens_already_generated === 0) { if (!isInstruct && isImpersonate && isBottom && tokens_already_generated === 0) {
@ -2084,7 +2143,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
mesSendString = '<START>\n' + mesSendString; mesSendString = '<START>\n' + mesSendString;
//mesSendString = mesSendString; //This edit simply removes the first "<START>" that is prepended to all context prompts //mesSendString = mesSendString; //This edit simply removes the first "<START>" that is prepended to all context prompts
} }
let finalPromt = worldInfoBefore + let finalPromt =
worldInfoBefore +
storyString + storyString +
worldInfoAfter + worldInfoAfter +
afterScenarioAnchor + afterScenarioAnchor +
@ -2093,6 +2153,33 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
generatedPromtCache + generatedPromtCache +
promptBias; promptBias;
//set array object for prompt token itemization of this message
let thisPromptBits = {
mesId: count_view_mes,
worldInfoBefore: worldInfoBefore,
allAnchors: allAnchors,
summarizeString: (extension_prompts['1_memory']?.value || ''),
authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),
worldInfoString: worldInfoString,
storyString: storyString,
worldInfoAfter: worldInfoAfter,
afterScenarioAnchor: afterScenarioAnchor,
examplesString: examplesString,
mesSendString: mesSendString,
generatedPromtCache: generatedPromtCache,
promptBias: promptBias,
finalPromt: finalPromt,
charDescription: charDescription,
charPersonality: charPersonality,
scenarioText: scenarioText,
promptBias: promptBias,
storyString: storyString,
this_max_context: this_max_context,
padding: power_user.token_padding
}
itemizedPrompts.push(thisPromptBits);
if (zeroDepthAnchor && zeroDepthAnchor.length) { if (zeroDepthAnchor && zeroDepthAnchor.length) {
if (!isMultigenEnabled() || tokens_already_generated == 0) { if (!isMultigenEnabled() || tokens_already_generated == 0) {
const trimBothEnds = !force_name2 && !is_pygmalion; const trimBothEnds = !force_name2 && !is_pygmalion;
@ -2110,11 +2197,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
} }
} }
// Add quiet generation prompt at depth 0
if (quiet_prompt && quiet_prompt.length) {
finalPromt += `\n${quiet_prompt}`;
}
finalPromt = finalPromt.replace(/\r/gm, ''); finalPromt = finalPromt.replace(/\r/gm, '');
if (power_user.collapse_newlines) { if (power_user.collapse_newlines) {
@ -2229,11 +2311,14 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
hideSwipeButtons(); hideSwipeButtons();
let getMessage = await streamingProcessor.generate(); let getMessage = await streamingProcessor.generate();
// Cohee: Basically a dead-end code... (disabled by isStreamingEnabled)
// I wasn't able to get multigen working with real streaming
// consistently without screwing the interim prompting
if (isMultigenEnabled()) { if (isMultigenEnabled()) {
tokens_already_generated += this_amount_gen; // add new gen amt to any prev gen counter.. tokens_already_generated += this_amount_gen;
message_already_generated += getMessage; message_already_generated += getMessage;
promptBias = ''; promptBias = '';
if (!streamingProcessor.isStopped && shouldContinueMultigen(getMessage)) { if (!streamingProcessor.isStopped && shouldContinueMultigen(getMessage, isImpersonate)) {
streamingProcessor.isFinished = false; streamingProcessor.isFinished = false;
runGenerate(getMessage); runGenerate(getMessage);
console.log('returning to make generate again'); console.log('returning to make generate again');
@ -2265,6 +2350,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let this_mes_is_name; let this_mes_is_name;
({ this_mes_is_name, getMessage } = extractNameFromMessage(getMessage, force_name2, isImpersonate)); ({ this_mes_is_name, getMessage } = extractNameFromMessage(getMessage, force_name2, isImpersonate));
if (!isImpersonate) {
if (tokens_already_generated == 0) { if (tokens_already_generated == 0) {
console.log("New message"); console.log("New message");
({ type, getMessage } = saveReply(type, getMessage, this_mes_is_name, title)); ({ type, getMessage } = saveReply(type, getMessage, this_mes_is_name, title));
@ -2273,8 +2360,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
console.log("Should append message"); console.log("Should append message");
({ type, getMessage } = saveReply('append', getMessage, this_mes_is_name, title)); ({ type, getMessage } = saveReply('append', getMessage, this_mes_is_name, title));
} }
} else {
let chunk = cleanUpMessage(message_already_generated, true);
let extract = extractNameFromMessage(chunk, force_name2, isImpersonate);
$('#send_textarea').val(extract.getMessage).trigger('input');
}
if (shouldContinueMultigen(getMessage)) { if (shouldContinueMultigen(getMessage, isImpersonate)) {
hideSwipeButtons(); hideSwipeButtons();
tokens_already_generated += this_amount_gen; // add new gen amt to any prev gen counter.. tokens_already_generated += this_amount_gen; // add new gen amt to any prev gen counter..
getMessage = message_already_generated; getMessage = message_already_generated;
@ -2294,6 +2386,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
if (getMessage.length > 0) { if (getMessage.length > 0) {
if (isImpersonate) { if (isImpersonate) {
$('#send_textarea').val(getMessage).trigger('input'); $('#send_textarea').val(getMessage).trigger('input');
generatedPromtCache = "";
} }
else if (type == 'quiet') { else if (type == 'quiet') {
resolve(getMessage); resolve(getMessage);
@ -2368,6 +2461,147 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
//console.log('generate ending'); //console.log('generate ending');
} //generate ends } //generate ends
function promptItemize(itemizedPrompts, requestedMesId) {
let incomingMesId = Number(requestedMesId);
let thisPromptSet = undefined;
for (var i = 0; i < itemizedPrompts.length; i++) {
if (itemizedPrompts[i].mesId === incomingMesId) {
thisPromptSet = i;
}
}
if (thisPromptSet === undefined) {
console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
console.log(itemizedPrompts);
return null;
}
let finalPromptTokens = getTokenCount(itemizedPrompts[thisPromptSet].finalPromt);
let allAnchorsTokens = getTokenCount(itemizedPrompts[thisPromptSet].allAnchors);
let summarizeStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].summarizeString);
let authorsNoteStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].authorsNoteString);
let afterScenarioAnchorTokens = getTokenCount(itemizedPrompts[thisPromptSet].afterScenarioAnchor);
let zeroDepthAnchorTokens = getTokenCount(itemizedPrompts[thisPromptSet].afterScenarioAnchor);
let worldInfoStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].worldInfoString);
let storyStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].storyString);
let examplesStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].examplesString);
let charPersonalityTokens = getTokenCount(itemizedPrompts[thisPromptSet].charPersonality);
let charDescriptionTokens = getTokenCount(itemizedPrompts[thisPromptSet].charDescription);
let scenarioTextTokens = getTokenCount(itemizedPrompts[thisPromptSet].scenarioText);
let promptBiasTokens = getTokenCount(itemizedPrompts[thisPromptSet].promptBias);
let mesSendStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].mesSendString)
let ActualChatHistoryTokens = mesSendStringTokens - allAnchorsTokens + power_user.token_padding;
let thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
let thisPrompt_padding = itemizedPrompts[thisPromptSet].padding;
let totalTokensInPrompt =
storyStringTokens + //chardefs total
worldInfoStringTokens +
ActualChatHistoryTokens + //chat history
allAnchorsTokens + // AN and/or legacy anchors
//afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
//zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
promptBiasTokens + //{{}}
- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPromt'
let storyStringTokensPercentage = ((storyStringTokens / (totalTokensInPrompt + thisPrompt_padding)) * 100).toFixed(2);
let ActualChatHistoryTokensPercentage = ((ActualChatHistoryTokens / (totalTokensInPrompt + thisPrompt_padding)) * 100).toFixed(2);
let promptBiasTokensPercentage = ((promptBiasTokens / (totalTokensInPrompt + thisPrompt_padding)) * 100).toFixed(2);
let worldInfoStringTokensPercentage = ((worldInfoStringTokens / (totalTokensInPrompt + thisPrompt_padding)) * 100).toFixed(2);
let allAnchorsTokensPercentage = ((allAnchorsTokens / (totalTokensInPrompt + thisPrompt_padding)) * 100).toFixed(2);
let selectedTokenizer = $("#tokenizer").find(':selected').text();
callPopup(
`
<h3>Prompt Itemization</h3>
Tokenizer: ${selectedTokenizer}<br>
<span class="tokenItemizingSubclass">
Only the white numbers really matter. All numbers are estimates.
Grey color items may not have been included in the context due to certain prompt format settings.
</span>
<hr class="sysHR">
<div class="justifyLeft">
<div class="flex-container">
<div class="flex-container flex1 flexFlowColumns flexNoGap wide50p tokenGraph">
<div class="wide100p" style="background-color: indianred; height: ${storyStringTokensPercentage}%;"></div>
<div class="wide100p" style="background-color: gold; height: ${worldInfoStringTokensPercentage}%;"></div>
<div class="wide100p" style="background-color: palegreen; height: ${ActualChatHistoryTokensPercentage}%;"></div>
<div class="wide100p" style="background-color: cornflowerblue; height: ${allAnchorsTokensPercentage}%;"></div>
<div class="wide100p" style="background-color: mediumpurple; height: ${promptBiasTokensPercentage}%;"></div>
</div>
<div class="flex-container wide50p">
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="flex-container wide100p">
<div class="flex1" style="color: indianred;"> Character Definitions:</div>
<div class=""> ${storyStringTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Description: </div>
<div class="tokenItemizingSubclass">${charDescriptionTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Personality:</div>
<div class="tokenItemizingSubclass"> ${charPersonalityTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Scenario: </div>
<div class="tokenItemizingSubclass">${scenarioTextTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
<div class="tokenItemizingSubclass"> ${examplesStringTokens}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: gold;">World Info:</div>
<div class="">${worldInfoStringTokens}</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: palegreen;">Chat History:</div>
<div class=""> ${ActualChatHistoryTokens}</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">
<div class="wide100p flex-container">
<div class="flex1" style="color: cornflowerblue;">Extensions:</div>
<div class="">${allAnchorsTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Summarize: </div>
<div class="tokenItemizingSubclass">${summarizeStringTokens}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Author's Note:</div>
<div class="tokenItemizingSubclass"> ${authorsNoteStringTokens}</div>
</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: mediumpurple;">{{}} Bias:</div><div class="">${promptBiasTokens}</div>
</div>
</div>
</div>
<hr class="sysHR">
<div class="wide100p flex-container flexFlowColumns">
<div class="flex-container wide100p">
<div class="flex1">Total Tokens in Prompt:</div><div class=""> ${totalTokensInPrompt}</div>
</div>
<!-- <div class="flex1">finalPromt:</div><div class=""> ${finalPromptTokens}</div> -->
<div class="flex-container wide100p">
<div class="flex1">Max Context:</div><div class="">${thisPrompt_max_context}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">- Padding:</div><div class=""> ${thisPrompt_padding}</div>
</div>
<div class="flex-container wide100p">
<div class="flex1">Actual Max Context Allowed:</div><div class="">${thisPrompt_max_context - thisPrompt_padding}</div>
</div>
</div>
</div>
<hr class="sysHR">
`, 'text'
);
}
function setInContextMessages(lastmsg, type) { function setInContextMessages(lastmsg, type) {
$("#chat .mes").removeClass('lastInContext'); $("#chat .mes").removeClass('lastInContext');
@ -2442,12 +2676,24 @@ function getGenerateUrl() {
return generate_url; return generate_url;
} }
function shouldContinueMultigen(getMessage) { function shouldContinueMultigen(getMessage, isImpersonate) {
const nameString = is_pygmalion ? 'You:' : `${name1}:`; if (power_user.instruct.enabled && power_user.instruct.stop_sequence) {
return message_already_generated.indexOf(nameString) === -1 && //if there is no 'You:' in the response msg if (message_already_generated.indexOf(power_user.instruct.stop_sequence) !== -1) {
message_already_generated.indexOf('<|endoftext|>') === -1 && //if there is no <endoftext> stamp in the response msg return false;
tokens_already_generated < parseInt(amount_gen) && //if the gen'd msg is less than the max response length.. }
getMessage.length > 0; //if we actually have gen'd text at all... }
// stopping name string
const nameString = isImpersonate ? `${name2}:` : (is_pygmalion ? 'You:' : `${name1}:`);
// if there is no 'You:' in the response msg
const doesNotContainName = message_already_generated.indexOf(nameString) === -1;
//if there is no <endoftext> stamp in the response msg
const isNotEndOfText = message_already_generated.indexOf('<|endoftext|>') === -1;
//if the gen'd msg is less than the max response length..
const notReachedMax = tokens_already_generated < parseInt(amount_gen);
//if we actually have gen'd text at all...
const msgHasText = getMessage.length > 0;
return doesNotContainName && isNotEndOfText && notReachedMax && msgHasText;
} }
function extractNameFromMessage(getMessage, force_name2, isImpersonate) { function extractNameFromMessage(getMessage, force_name2, isImpersonate) {
@ -2563,6 +2809,12 @@ function cleanUpMessage(getMessage, isImpersonate) {
getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence)); getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
} }
} }
if (power_user.instruct.enabled && power_user.instruct.input_sequence && isImpersonate) {
getMessage = getMessage.replaceAll(power_user.instruct.input_sequence, '');
}
if (power_user.instruct.enabled && power_user.instruct.output_sequence && !isImpersonate) {
getMessage = getMessage.replaceAll(power_user.instruct.output_sequence, '');
}
// clean-up group message from excessive generations // clean-up group message from excessive generations
if (selected_group) { if (selected_group) {
getMessage = cleanGroupMessage(getMessage); getMessage = cleanGroupMessage(getMessage);
@ -3715,6 +3967,7 @@ function select_rm_create() {
$("#renameCharButton").css('display', 'none'); $("#renameCharButton").css('display', 'none');
$("#name_div").removeClass('displayNone'); $("#name_div").removeClass('displayNone');
$("#name_div").addClass('displayBlock'); $("#name_div").addClass('displayBlock');
updateFavButtonState(false);
$("#form_create").attr("actiontype", "createcharacter"); $("#form_create").attr("actiontype", "createcharacter");
} }
@ -3748,7 +4001,6 @@ function updateFavButtonState(state) {
$("#fav_checkbox").val(fav_ch_checked); $("#fav_checkbox").val(fav_ch_checked);
$("#favorite_button").toggleClass('fav_on', fav_ch_checked); $("#favorite_button").toggleClass('fav_on', fav_ch_checked);
$("#favorite_button").toggleClass('fav_off', !fav_ch_checked); $("#favorite_button").toggleClass('fav_off', !fav_ch_checked);
} }
function callPopup(text, type, inputValue = '') { function callPopup(text, type, inputValue = '') {
@ -5311,27 +5563,6 @@ $(document).ready(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
/* $("#donation").click(function () {
$("#shadow_tips_popup").css("display", "block");
$("#shadow_tips_popup").transition({
opacity: 1.0,
duration: 100,
easing: animation_easing,
complete: function () { },
});
}); */
/* $("#tips_cross").click(function () {
$("#shadow_tips_popup").transition({
opacity: 0.0,
duration: 100,
easing: animation_easing,
complete: function () {
$("#shadow_tips_popup").css("display", "none");
},
});
}); */
$("#select_chat_cross").click(function () { $("#select_chat_cross").click(function () {
$("#shadow_select_chat_popup").transition({ $("#shadow_select_chat_popup").transition({
opacity: 0, opacity: 0,
@ -5375,6 +5606,13 @@ $(document).ready(function () {
}); });
} }
$(document).on("pointerup", ".mes_prompt", function () {
let mesIdForItemization = $(this).closest('.mes').attr('mesId');
if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
promptItemize(itemizedPrompts, mesIdForItemization);
}
})
//******************** //********************
//***Message Editor*** //***Message Editor***

View File

@ -27,6 +27,7 @@ import {
SECRET_KEYS, SECRET_KEYS,
secret_state, secret_state,
} from "./secrets.js"; } from "./secrets.js";
import { sortByCssOrder } from "./utils.js";
var NavToggle = document.getElementById("nav-toggle"); var NavToggle = document.getElementById("nav-toggle");
var RPanelPin = document.getElementById("rm_button_panel_pin"); var RPanelPin = document.getElementById("rm_button_panel_pin");
@ -275,7 +276,7 @@ export async function favsToHotswap() {
const maxCount = 6; const maxCount = 6;
let count = 0; let count = 0;
$(selector).each(function () { $(selector).sort(sortByCssOrder).each(function () {
if ($(this).hasClass('is_fav') && count < maxCount) { if ($(this).hasClass('is_fav') && count < maxCount) {
const isCharacter = $(this).hasClass('character_select'); const isCharacter = $(this).hasClass('character_select');
const isGroup = $(this).hasClass('group_select'); const isGroup = $(this).hasClass('group_select');

View File

@ -95,8 +95,9 @@ async function activateExtensions() {
for (let entry of extensions) { for (let entry of extensions) {
const name = entry[0]; const name = entry[0];
const manifest = entry[1]; const manifest = entry[1];
const elementExists = document.getElementById(name) !== null;
if (activeExtensions.has(name)) { if (elementExists || activeExtensions.has(name)) {
continue; continue;
} }

View File

@ -96,6 +96,7 @@ $(document).ready(function () {
function addSendPictureButton() { function addSendPictureButton() {
const sendButton = document.createElement('div'); const sendButton = document.createElement('div');
sendButton.id = 'send_picture'; sendButton.id = 'send_picture';
sendButton.title = 'Send a picture to chat';
sendButton.classList.add('fa-solid'); sendButton.classList.add('fa-solid');
$(sendButton).hide(); $(sendButton).hide();
$(sendButton).on('click', () => $('#img_file').click()); $(sendButton).on('click', () => $('#img_file').click());

View File

@ -29,7 +29,7 @@ async function doDiceRoll() {
function addDiceRollButton() { function addDiceRollButton() {
const buttonHtml = ` const buttonHtml = `
<div id="roll_dice" class="fa-solid fa-dice" /></div> <div id="roll_dice" class="fa-solid fa-dice" title="Roll the dice" /></div>
`; `;
const dropdownHtml = ` const dropdownHtml = `
<div id="dice_dropdown"> <div id="dice_dropdown">

View File

@ -34,39 +34,40 @@ const generationMode = {
} }
const triggerWords = { const triggerWords = {
[generationMode.CHARACTER]: ['yourself', 'you', 'bot', 'AI', 'character'], [generationMode.CHARACTER]: ['you'],
[generationMode.USER]: ['me', 'user', 'myself'], [generationMode.USER]: ['me'],
[generationMode.SCENARIO]: ['scenario', 'world', 'surroundings', 'scenery'], [generationMode.SCENARIO]: ['scene'],
[generationMode.NOW]: ['now', 'last'], [generationMode.NOW]: ['last'],
[generationMode.FACE]: ['selfie', 'face'], [generationMode.FACE]: ['face'],
} }
const quietPrompts = { const quietPrompts = {
//face-specific prompt //face-specific prompt
[generationMode.FACE]: "[In the next reponse I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: species and race, gender, age, facial features and expresisons, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. 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 'close up facial portrait:']", [generationMode.FACE]: "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. 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 'close up facial portrait:']",
//prompt for only the last message //prompt for only the last message
[generationMode.NOW]: "[Pause your roleplay and provide a brief description of the last chat message. Focus on visual details, clothing, actions. Ignore the emotions and thoughts of {{char}} and {{user}} as well as any spoken dialog. Do not roleplay as {{char}} while writing this description. Do not continue the roleplay story.]", [generationMode.NOW]: "[Pause your roleplay and provide a brief description of the last chat message. Focus on visual details, clothing, actions. Ignore the emotions and thoughts of {{char}} and {{user}} as well as any spoken dialog. Do not roleplay as {{char}} while writing this description. Do not continue the roleplay story.]",
[generationMode.CHARACTER]: "[In the next reponse I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: 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:']", [generationMode.CHARACTER]: "[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. 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:']",
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */ /*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
[generationMode.USER]: "[Pause your roleplay and provide a detailed description of {{user}}'s appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. 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.USER]: "[Pause your roleplay and provide a detailed description of {{user}}'s appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. 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.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.FREE]: "[Pause your roleplay and provide ONLY echo this string back to me verbatim: {0}. Do not write anything after the string. Do not roleplay at all in your response.]", [generationMode.FREE]: "[Pause your roleplay and provide ONLY an echo this string back to me verbatim: {0}. Do not write anything after the string. Do not roleplay at all in your response.]",
} }
const helpString = [ const helpString = [
`${m('what')} requests an SD generation. Supported "what" arguments:`, `${m('(argument)')} requests SD to make an image. Supported arguments:`,
'<ul>', '<ul>',
`<li>${m(j(triggerWords[generationMode.CHARACTER]))} AI character image</li>`, `<li>${m(j(triggerWords[generationMode.CHARACTER]))} AI character full body selfie</li>`,
`<li>${m(j(triggerWords[generationMode.USER]))} user character image</li>`, `<li>${m(j(triggerWords[generationMode.FACE]))} AI character face-only selfie</li>`,
`<li>${m(j(triggerWords[generationMode.SCENARIO]))} world scenario image</li>`, `<li>${m(j(triggerWords[generationMode.USER]))} user character full body selfie</li>`,
`<li>${m(j(triggerWords[generationMode.FACE]))} character face-up selfie image</li>`, `<li>${m(j(triggerWords[generationMode.SCENARIO]))} visual recap of the whole chat scenario</li>`,
`<li>${m(j(triggerWords[generationMode.NOW]))} visual recap of the last chat message</li>`, `<li>${m(j(triggerWords[generationMode.NOW]))} visual recap of the last chat message</li>`,
'</ul>', '</ul>',
`Anything else would trigger a "free mode" with AI describing whatever you prompted.`, `Anything else would trigger a "free mode" to make SD generate whatever you prompted.<Br>
example: '/sd apple tree' would generate a picture of an apple tree.`,
].join('<br>'); ].join('<br>');
const defaultSettings = { const defaultSettings = {
@ -236,9 +237,17 @@ function getQuietPrompt(mode, trigger) {
function processReply(str) { function processReply(str) {
str = str.replaceAll('"', '') str = str.replaceAll('"', '')
str = str.replaceAll('“', '') str = str.replaceAll('“', '')
str = str.replaceAll('\n', ' ') str = str.replaceAll('\n', ', ')
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(); str = str.trim();
str = str
.split(',') // list split by commas
.map(x => x.trim()) // trim each entry
.filter(x => x) // remove empty entries
.join(', '); // join it back with proper spacing
return str; return str;
} }
@ -258,7 +267,7 @@ async function generatePicture(_, trigger) {
const prompt = processReply(await new Promise( const prompt = processReply(await new Promise(
async function promptPromise(resolve, reject) { async function promptPromise(resolve, reject) {
try { try {
await context.generate('quiet', { resolve, reject, quiet_prompt }); await context.generate('quiet', { resolve, reject, quiet_prompt, force_name2: true, });
} }
catch { catch {
reject(); reject();
@ -268,6 +277,8 @@ async function generatePicture(_, trigger) {
context.deactivateSendButtons(); context.deactivateSendButtons();
hideSwipeButtons(); hideSwipeButtons();
console.log('Processed Stable Diffusion prompt:', prompt);
const url = new URL(getApiUrl()); const url = new URL(getApiUrl());
url.pathname = '/api/image'; url.pathname = '/api/image';
const result = await fetch(url, { const result = await fetch(url, {
@ -294,7 +305,7 @@ async function generatePicture(_, trigger) {
sendMessage(prompt, base64Image); sendMessage(prompt, base64Image);
} }
} catch (err) { } catch (err) {
console.error(err); console.trace(err);
throw new Error('SD prompt text generation failed.') throw new Error('SD prompt text generation failed.')
} }
finally { finally {
@ -325,7 +336,7 @@ async function sendMessage(prompt, image) {
function addSDGenButtons() { function addSDGenButtons() {
const buttonHtml = ` const buttonHtml = `
<div id="sd_gen" class="fa-solid fa-paintbrush" /></div> <div id="sd_gen" class="fa-solid fa-paintbrush" title="Trigger Stable Diffusion" /></div>
`; `;
const waitButtonHtml = ` const waitButtonHtml = `
@ -411,8 +422,8 @@ $("#sd_dropdown [id]").on("click", function () {
} }
else if (id == "sd_world") { else if (id == "sd_world") {
console.log("doing /sd world"); console.log("doing /sd scene");
generatePicture('sd', 'world'); generatePicture('sd', 'scene');
} }
else if (id == "sd_last") { else if (id == "sd_last") {
@ -422,7 +433,7 @@ $("#sd_dropdown [id]").on("click", function () {
}); });
jQuery(async () => { jQuery(async () => {
getContext().registerSlashCommand('sd', generatePicture, ['picture', 'image'], helpString, true, true); getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true);
const settingsHtml = ` const settingsHtml = `
<div class="sd_settings"> <div class="sd_settings">

View File

@ -7,6 +7,7 @@ class ElevenLabsTtsProvider {
settings settings
voices = [] voices = []
separator = ' ... ... ... '
get settings() { get settings() {
return this.settings return this.settings

View File

@ -48,10 +48,8 @@ async function moduleWorker() {
return; return;
} }
// Chat/character/group changed // Chat changed
if ( if (
(context.groupId && lastGroupId !== context.groupId) ||
context.characterId !== lastCharacterId ||
context.chatId !== lastChatId context.chatId !== lastChatId
) { ) {
currentMessageNumber = context.chat.length ? context.chat.length : 0 currentMessageNumber = context.chat.length ? context.chat.length : 0
@ -75,6 +73,7 @@ async function moduleWorker() {
// We're currently swiping or streaming. Don't generate voice // We're currently swiping or streaming. Don't generate voice
if ( if (
message.mes === '...' || message.mes === '...' ||
message.mes === '' ||
(context.streamingProcessor && !context.streamingProcessor.isFinished) (context.streamingProcessor && !context.streamingProcessor.isFinished)
) { ) {
return return
@ -164,7 +163,7 @@ function onAudioControlClicked() {
function addAudioControl() { function addAudioControl() {
$('#send_but_sheld').prepend('<div id="tts_media_control"/>') $('#send_but_sheld').prepend('<div id="tts_media_control"/>')
$('#send_but_sheld').on('click', onAudioControlClicked) $('#tts_media_control').attr('title', 'TTS play/pause').on('click', onAudioControlClicked)
audioControl = document.getElementById('tts_media_control') audioControl = document.getElementById('tts_media_control')
updateUiAudioPlayState() updateUiAudioPlayState()
} }
@ -181,7 +180,7 @@ function completeCurrentAudioJob() {
*/ */
async function addAudioJob(response) { async function addAudioJob(response) {
const audioData = await response.blob() const audioData = await response.blob()
if (!audioData.type in ['audio/mpeg', 'audio/wav']) { if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave']) {
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}` throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
} }
audioJobQueue.push(audioData) audioJobQueue.push(audioData)
@ -240,12 +239,26 @@ async function processTtsQueue() {
console.debug('New message found, running TTS') console.debug('New message found, running TTS')
currentTtsJob = ttsJobQueue.shift() currentTtsJob = ttsJobQueue.shift()
const text = extension_settings.tts.narrate_dialogues_only let text = extension_settings.tts.narrate_dialogues_only
? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '') // remove asterisks content ? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content
: currentTtsJob.mes.replaceAll('*', '') // remove just the asterisks : currentTtsJob.mes.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
text = text.replace(special_quotes, '"');
const matches = text.match(/".*?"/g); // Matches text inside double quotes, non-greedily
const partJoiner = (ttsProvider?.separator || ' ... ');
text = matches ? matches.join(partJoiner) : text;
}
console.log(`TTS: ${text}`)
const char = currentTtsJob.name const char = currentTtsJob.name
try { try {
if (!text) {
console.warn('Got empty text in TTS queue job.');
return;
}
if (!voiceMap[char]) { if (!voiceMap[char]) {
throw `${char} not in voicemap. Configure character in extension settings voice map` throw `${char} not in voicemap. Configure character in extension settings voice map`
} }
@ -282,6 +295,7 @@ function loadSettings() {
extension_settings.tts.enabled extension_settings.tts.enabled
) )
$('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only) $('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only)
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only)
} }
const defaultSettings = { const defaultSettings = {
@ -374,6 +388,13 @@ function onNarrateDialoguesClick() {
saveSettingsDebounced() saveSettingsDebounced()
} }
function onNarrateQuotedClick() {
extension_settings.tts.narrate_quoted_only = $('#tts_narrate_quoted').prop('checked');
saveSettingsDebounced()
}
//##############// //##############//
// TTS Provider // // TTS Provider //
//##############// //##############//
@ -453,6 +474,10 @@ $(document).ready(function () {
<input type="checkbox" id="tts_narrate_dialogues"> <input type="checkbox" id="tts_narrate_dialogues">
Narrate dialogues only Narrate dialogues only
</label> </label>
<label class="checkbox_label" for="tts_narrate_quoted">
<input type="checkbox" id="tts_narrate_quoted">
Narrate quoted only
</label>
</div> </div>
<label>Voice Map</label> <label>Voice Map</label>
<textarea id="tts_voice_map" type="text" class="text_pole textarea_compact" rows="4" <textarea id="tts_voice_map" type="text" class="text_pole textarea_compact" rows="4"
@ -475,6 +500,7 @@ $(document).ready(function () {
$('#tts_apply').on('click', onApplyClick) $('#tts_apply').on('click', onApplyClick)
$('#tts_enabled').on('click', onEnableClick) $('#tts_enabled').on('click', onEnableClick)
$('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick); $('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
$('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
$('#tts_voices').on('click', onTtsVoicesClick) $('#tts_voices').on('click', onTtsVoicesClick)
$('#tts_provider_settings').on('input', onTtsProviderSettingsInput) $('#tts_provider_settings').on('input', onTtsProviderSettingsInput)
for (const provider in ttsProviders) { for (const provider in ttsProviders) {

View File

@ -9,6 +9,7 @@ class SileroTtsProvider {
settings settings
voices = [] voices = []
separator = ' .. '
defaultSettings = { defaultSettings = {
provider_endpoint: "http://localhost:8001/tts", provider_endpoint: "http://localhost:8001/tts",

View File

@ -21,6 +21,7 @@ class SystemTtsProvider {
fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet' fallbackPreview = 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet'
settings settings
voices = [] voices = []
separator = ' ... '
defaultSettings = { defaultSettings = {
voiceMap: {}, voiceMap: {},

View File

@ -1,4 +1,5 @@
import { saveSettingsDebounced, changeMainAPI, callPopup, setGenerationProgress, CLIENT_VERSION, getRequestHeaders } from "../script.js"; import { saveSettingsDebounced, changeMainAPI, callPopup, setGenerationProgress, CLIENT_VERSION, getRequestHeaders } from "../script.js";
import { SECRET_KEYS, writeSecret } from "./secrets.js";
import { delay } from "./utils.js"; import { delay } from "./utils.js";
export { export {
@ -217,5 +218,10 @@ jQuery(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$("#horde_api_key").on("input", async function () {
const key = $(this).val().trim();
await writeSecret(SECRET_KEYS.HORDE, key);
});
$("#horde_refresh").on("click", getHordeModels); $("#horde_refresh").on("click", getHordeModels);
}) })

View File

@ -9,6 +9,7 @@ import {
getRequestHeaders, getRequestHeaders,
substituteParams, substituteParams,
} from "../script.js"; } from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import { import {
groups, groups,
selected_group, selected_group,
@ -632,10 +633,10 @@ function loadInstructMode() {
} }
export function formatInstructModeChat(name, mes, isUser) { export function formatInstructModeChat(name, mes, isUser) {
const includeNames = power_user.instruct.names || (selected_group && !isUser); const includeNames = power_user.instruct.names || !!selected_group;
const sequence = isUser ? power_user.instruct.input_sequence : power_user.instruct.output_sequence; const sequence = isUser ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
const separator = power_user.instruct.wrap ? '\n' : ''; const separator = power_user.instruct.wrap ? '\n' : '';
const textArray = includeNames ? [sequence, name, ': ', mes, separator] : [sequence, mes, separator]; const textArray = includeNames ? [sequence, `${name}: ${mes}`, separator] : [sequence, mes, separator];
const text = textArray.filter(x => x).join(separator); const text = textArray.filter(x => x).join(separator);
return text; return text;
} }
@ -649,10 +650,11 @@ export function formatInstructStoryString(story) {
return text; return text;
} }
export function formatInstructModePrompt(isImpersonate) { export function formatInstructModePrompt(name, isImpersonate) {
const includeNames = power_user.instruct.names || !!selected_group;
const sequence = isImpersonate ? power_user.instruct.input_sequence : power_user.instruct.output_sequence; const sequence = isImpersonate ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
const separator = power_user.instruct.wrap ? '\n' : ''; const separator = power_user.instruct.wrap ? '\n' : '';
const text = separator + sequence; const text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence);
return text; return text;
} }
@ -995,6 +997,7 @@ $(document).ready(() => {
power_user.sort_order = $(this).find(":selected").data('order'); power_user.sort_order = $(this).find(":selected").data('order');
power_user.sort_rule = $(this).find(":selected").data('rule'); power_user.sort_rule = $(this).find(":selected").data('rule');
sortCharactersList(); sortCharactersList();
favsToHotswap();
saveSettingsDebounced(); saveSettingsDebounced();
}); });

View File

@ -1,4 +1,4 @@
import { getRequestHeaders } from "../script.js"; import { callPopup, getRequestHeaders } from "../script.js";
export const SECRET_KEYS = { export const SECRET_KEYS = {
HORDE: 'api_key_horde', HORDE: 'api_key_horde',
@ -14,14 +14,50 @@ const INPUT_MAP = {
[SECRET_KEYS.NOVEL]: '#api_key_novel', [SECRET_KEYS.NOVEL]: '#api_key_novel',
} }
async function clearSecret() {
const key = $(this).data('key');
await writeSecret(key, '');
secret_state[key] = false;
updateSecretDisplay();
$(INPUT_MAP[key]).val('');
}
function updateSecretDisplay() { function updateSecretDisplay() {
for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) { for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
const validSecret = !!secret_state[secret_key]; const validSecret = !!secret_state[secret_key];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key'; const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$(input_selector).attr('placeholder', placeholder).val(''); $(input_selector).attr('placeholder', placeholder);
} }
} }
async function viewSecrets() {
const response = await fetch('/viewsecrets', {
method: 'POST',
headers: getRequestHeaders(),
});
if (response.status == 403) {
callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.conf file and restart the SillyTavern server.</p>', 'text');
return;
}
if (!response.ok) {
return;
}
$('#dialogue_popup').addClass('wide_dialogue_popup');
const data = await response.json();
const table = document.createElement('table');
table.classList.add('responsiveTable');
$(table).append('<thead><th>Key</th><th>Value</th></thead>');
for (const [key,value] of Object.entries(data)) {
$(table).append(`<tr><td>${DOMPurify.sanitize(key)}</td><td>${DOMPurify.sanitize(value)}</td></tr>`);
}
callPopup(table.outerHTML, 'text');
}
export let secret_state = {}; export let secret_state = {};
export async function writeSecret(key, value) { export async function writeSecret(key, value) {
@ -60,3 +96,8 @@ export async function readSecretState() {
console.error('Could not read secrets file'); console.error('Could not read secrets file');
} }
} }
jQuery(() => {
$('#viewSecrets').on('click', viewSecrets);
$(document).on('click', '.clear-api-key', clearSecret);
});

View File

@ -73,8 +73,8 @@ const parser = new SlashCommandParser();
const registerSlashCommand = parser.addCommand.bind(parser); const registerSlashCommand = parser.addCommand.bind(parser);
const getSlashCommandsHelp = parser.getHelpString.bind(parser); const getSlashCommandsHelp = parser.getHelpString.bind(parser);
parser.addCommand('help', helpCommandCallback, ['?'], ' displays a help information', true, true); parser.addCommand('help', helpCommandCallback, ['?'], ' displays this help message', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">name</span> sets a background by file name', false, true); parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> sets a background according to filename, partial names allowed, will set the first one alphebetically if multiple files begin with the provided argument string', false, true);
function helpCommandCallback() { function helpCommandCallback() {
sendSystemMessage(system_message_types.HELP); sendSystemMessage(system_message_types.HELP);

View File

@ -183,3 +183,9 @@ export async function initScrollHeight(element) {
$(element).css("height", `${newHeight}px`); $(element).css("height", `${newHeight}px`);
//resetScrollHeight(element); //resetScrollHeight(element);
} }
export function sortByCssOrder(a, b) {
const _a = Number($(a).css('order'));
const _b = Number($(b).css('order'));
return _a - _b;
}

View File

@ -109,6 +109,41 @@ body {
background-clip: content-box; background-clip: content-box;
} }
table.responsiveTable {
width: 100%;
margin: 10px 0;
}
.responsiveTable tr {
display: flex;
}
.responsiveTable,
.responsiveTable th,
.responsiveTable td {
flex: 1;
border: 1px solid;
border-collapse: collapse;
word-break: break-all;
padding: 5px;
}
.sysHR {
border-top: 2px solid grey;
}
.tokenItemizingSubclass {
font-size: calc(var(--mainFontSize) * 0.8);
color: var(--SmartThemeEmColor);
}
.tokenGraph {
border-radius: 10px;
border: 1px solid var(--white30a);
max-height: 100%;
overflow: hidden;
}
.fa-solid::before, .fa-solid::before,
.fa-regular::before { .fa-regular::before {
vertical-align: middle; vertical-align: middle;
@ -191,6 +226,11 @@ code {
transition: background-image 0.5s ease-in-out; transition: background-image 0.5s ease-in-out;
} }
#version_display {
padding: 5px;
opacity: 0.8;
}
#bg1 { #bg1 {
background-image: url('backgrounds/tavern day.jpg'); background-image: url('backgrounds/tavern day.jpg');
z-index: -2; z-index: -2;
@ -2093,6 +2133,7 @@ input[type="range"]::-webkit-slider-thumb {
width: 20px; width: 20px;
opacity: 0.5; opacity: 0.5;
} }
.mes_buttons { .mes_buttons {
float: right; float: right;
height: 20px; height: 20px;
@ -2101,6 +2142,7 @@ input[type="range"]::-webkit-slider-thumb {
right: 0px; right: 0px;
} }
.mes_prompt,
.mes_copy, .mes_copy,
.mes_edit { .mes_edit {
cursor: pointer; cursor: pointer;
@ -2117,7 +2159,8 @@ input[type="range"]::-webkit-slider-thumb {
opacity: 1; opacity: 1;
} }
.last_mes .mes_copy { .last_mes .mes_copy,
.last_mes .mes_prompt {
grid-row-start: 1; grid-row-start: 1;
position: relative; position: relative;
right: -30px; right: -30px;
@ -3516,6 +3559,10 @@ toolcool-color-picker {
justify-content: space-evenly; justify-content: space-evenly;
} }
.spaceBetween {
justify-content: space-between;
}
.widthNatural { .widthNatural {
width: unset !important; width: unset !important;
min-width: unset !important; min-width: unset !important;
@ -3665,7 +3712,7 @@ toolcool-color-picker {
flex-basis: 100%; flex-basis: 100%;
} }
#max_context_unlocked:not(:checked) + div { #max_context_unlocked:not(:checked)+div {
display: none; display: none;
} }
@ -3961,6 +4008,7 @@ body.waifuMode #avatar_zoom_popup {
} }
#sheld, #sheld,
#character_popup, #character_popup,
#world_popup { #world_popup {
@ -3973,6 +4021,11 @@ body.waifuMode #avatar_zoom_popup {
top: 42px; top: 42px;
} }
#character_popup,
#world_popup {
overflow-y: auto;
}
#character_popup, #character_popup,
#world_popup, #world_popup,
#send_form { #send_form {

View File

@ -1,18 +1,23 @@
# SillyTavern # SillyTavern
## Based on a fork of TavernAI 1.2.8 ## Based on a fork of TavernAI 1.2.8
### Brought to you by Cohee, RossAscends and the SillyTavern community ### Brought to you by Cohee, RossAscends and the SillyTavern community
NOTE: We have added [a FAQ](faq.md) to answer most of your questions and help you get started. NOTE: We have added [a FAQ](faq.md) to answer most of your questions and help you get started.
### What is SillyTavern or TavernAI? ### What is SillyTavern or TavernAI?
Tavern is a user interface you can install on your computer (and Android phones) that allows you to interact with text generation AIs and chat/roleplay with characters you or the community create. Tavern is a user interface you can install on your computer (and Android phones) that allows you to interact with text generation AIs and chat/roleplay with characters you or the community create.
SillyTavern is a fork of TavernAI 1.2.8 which is under more active development and has added many major features. At this point, they can be thought of as completely independent programs. SillyTavern is a fork of TavernAI 1.2.8 which is under more active development and has added many major features. At this point, they can be thought of as completely independent programs.
### What do I need other than Tavern? ### What do I need other than Tavern?
On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](faq.md). On its own Tavern is useless, as it's just a user interface. You have to have access to an AI system backend that can act as the roleplay character. There are various supported backends: OpenAPI API (GPT), KoboldAI (either running locally or on Google Colab), and more. You can read more about this in [the FAQ](faq.md).
### Do I need a powerful PC to run Tavern? ### Do I need a powerful PC to run Tavern?
Since Tavern is only a user interface, it has tiny hardware requirements, it will run on anything. It's the AI system backend that needs to be powerful. Since Tavern is only a user interface, it has tiny hardware requirements, it will run on anything. It's the AI system backend that needs to be powerful.
## Mobile support ## Mobile support
@ -21,13 +26,13 @@ Since Tavern is only a user interface, it has tiny hardware requirements, it wil
> **This fork can be run natively on Android phones using Termux. Please refer to this guide by ArroganceComplex#2659:** > **This fork can be run natively on Android phones using Termux. Please refer to this guide by ArroganceComplex#2659:**
https://rentry.org/STAI-Termux <https://rentry.org/STAI-Termux>
**.webp character cards import/export is not supported in Termux. Use either JSON or PNG formats instead.** **.webp character cards import/export is not supported in Termux. Use either JSON or PNG formats instead.**
## Questions or suggestions? ## Questions or suggestions?
### We now have a community Discord server! ### We now have a community Discord server
Get support, share favorite characters and prompts: Get support, share favorite characters and prompts:
@ -36,11 +41,13 @@ Get support, share favorite characters and prompts:
*** ***
Get in touch with the developers directly: Get in touch with the developers directly:
* Discord: Cohee#1207 or RossAscends#1779 * Discord: Cohee#1207 or RossAscends#1779
* Reddit: /u/RossAscends or /u/sillylossy * Reddit: /u/RossAscends or /u/sillylossy
* [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues) * [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues)
## This version includes ## This version includes
* A heavily modified TavernAI 1.2.8 (more than 50% of code rewritten or optimized) * A heavily modified TavernAI 1.2.8 (more than 50% of code rewritten or optimized)
* Swipes * Swipes
* Group chats: multi-bot rooms for characters to talk to you or each other * Group chats: multi-bot rooms for characters to talk to you or each other
@ -60,6 +67,7 @@ Get in touch with the developers directly:
* Sending images to chat, and the AI interpreting the content. * Sending images to chat, and the AI interpreting the content.
## UI Extensions 🚀 ## UI Extensions 🚀
| Name | Description | Required <a href="https://github.com/Cohee1207/TavernAI-extras#modules" target="_blank">Extra Modules</a> | Screenshot | | Name | Description | Required <a href="https://github.com/Cohee1207/TavernAI-extras#modules" target="_blank">Extra Modules</a> | Screenshot |
| ---------------- | ---------------------------------| ---------------------------- | ---------- | | ---------------- | ---------------------------------| ---------------------------- | ---------- |
| Image Captioning | Send a cute picture to your bot!<br><br>Picture select option will appear beside the "Message send" button. | `caption` | <img src="https://user-images.githubusercontent.com/18619528/224161576-ddfc51cd-995e-44ec-bf2d-d2477d603f0c.png" style="max-width:200px" /> | | Image Captioning | Send a cute picture to your bot!<br><br>Picture select option will appear beside the "Message send" button. | `caption` | <img src="https://user-images.githubusercontent.com/18619528/224161576-ddfc51cd-995e-44ec-bf2d-d2477d603f0c.png" style="max-width:200px" /> |
@ -115,6 +123,27 @@ Get in touch with the developers directly:
> DO NOT RUN START.BAT WITH ADMIN PERMISSIONS > DO NOT RUN START.BAT WITH ADMIN PERMISSIONS
### Windows ### Windows
Installing via Git (recommended for easy updating)
Easy to follow guide with pretty pictures:
<https://docs.alpindale.dev/pygmalion-extras/sillytavern/#windows-installation>
1. Install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
2. Install [GitHub Desktop](https://central.github.com/deployments/desktop/desktop/latest/win32)
3. Open Windows Explorer (`Win+E`)
4. Browse to or Create a folder that is not controlled or monitored by Windows. (ex: C:\MySpecialFolder\)
5. Open a Command Prompt inside that folder by clicking in the 'Address Bar' at the top, typing `cmd`, and pressing Enter.
6. Once the black box (Command Prompt) pops up, type ONE of the following into it and press Enter:
* for Main Branch: `git clone <https://github.com/Cohee1207/SillyTavern> -b main`
* for Dev Branch: `git clone <https://github.com/Cohee1207/SillyTavern> -b dev`
7. Once everything is cloned, double click `Start.bat` to make NodeJS install its requirements.
8. The server will then start, and SillyTavern will popup in your browser.
Installing via zip download
1. install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended) 1. install [NodeJS](https://nodejs.org/en) (latest LTS version is recommended)
2. download the zip from this GitHub repo 2. download the zip from this GitHub repo
3. unzip it into a folder of your choice 3. unzip it into a folder of your choice
@ -122,9 +151,21 @@ Get in touch with the developers directly:
5. Once the server has prepared everything for you, it will open a tab in your browser. 5. Once the server has prepared everything for you, it will open a tab in your browser.
### Linux ### Linux
1. Run the `start.sh` script. 1. Run the `start.sh` script.
2. Enjoy. 2. Enjoy.
## API keys management
SillyTavern saves your API keys to a `secrets.json` file in the server directory.
By default they will not be exposed to a frontend after you enter them and reload the page.
In order to enable viewing your keys by clicking a button in the API block:
1. Set the value of `allowKeysExposure` to `true` in `config.conf` file.
2. Restart the SillyTavern server.
## Remote connections ## Remote connections
Most often this is for people who want to use SillyTavern on their mobile phones while at home. Most often this is for people who want to use SillyTavern on their mobile phones while at home.
@ -133,10 +174,13 @@ If you want to enable other devices to connect to your TAI server, open 'config.
``` ```
const whitelistMode = true; const whitelistMode = true;
``` ```
to to
``` ```
const whitelistMode = false; const whitelistMode = false;
``` ```
Save the file. Save the file.
Restart your TAI server. Restart your TAI server.
@ -156,12 +200,14 @@ The `whitelist` array in `config.conf` will be ignored if `whitelist.txt` exists
***Disclaimer: Anyone else who knows your IP address and TAI port number will be able to connect as well*** ***Disclaimer: Anyone else who knows your IP address and TAI port number will be able to connect as well***
To connect over wifi you'll need your PC's local wifi IP address To connect over wifi you'll need your PC's local wifi IP address
- (For Windows: windows button > type 'cmd.exe' in the search bar> type 'ipconfig' in the console, hit Enter > "IPv4" listing)
* (For Windows: windows button > type 'cmd.exe' in the search bar> type 'ipconfig' in the console, hit Enter > "IPv4" listing)
if you want other people on the internet to connect, check [here](https://whatismyipaddress.com/) for 'IPv4' if you want other people on the internet to connect, check [here](https://whatismyipaddress.com/) for 'IPv4'
### Still Unable To Connect? ### Still Unable To Connect?
- Create an inbound/outbound firewall rule for the port found in `config.conf`. Do NOT mistake this for portforwarding on your router, otherwise someone could find your chat logs and that's a big no-no. - Create an inbound/outbound firewall rule for the port found in `config.conf`. Do NOT mistake this for portforwarding on your router, otherwise someone could find your chat logs and that's a big no-no.
- Enable the Private Network profile type in Settings > Network and Internet > Ethernet. This is VERY important for Windows 11, otherwise you would be unable to connect even with the aforementioned firewall rules. * Enable the Private Network profile type in Settings > Network and Internet > Ethernet. This is VERY important for Windows 11, otherwise you would be unable to connect even with the aforementioned firewall rules.
## Performance issues? ## Performance issues?
@ -187,9 +233,10 @@ We're moving to 100% original content only policy, so old background images have
You can find them archived here: You can find them archived here:
https://files.catbox.moe/1xevnc.zip <https://files.catbox.moe/1xevnc.zip>
## Screenshots ## Screenshots
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png"> <img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649245-8061c60f-63dc-488e-9325-f151b7a3ec2d.png">
<img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png"> <img width="400" alt="image" src="https://user-images.githubusercontent.com/18619528/228649856-fbdeef05-d727-4d5a-be80-266cbbc6b811.png">
@ -204,13 +251,13 @@ GNU Affero General Public License for more details.**
* Cohee's modifications and derived code: AGPL v3 * Cohee's modifications and derived code: AGPL v3
* RossAscends' additions: AGPL v3 * RossAscends' additions: AGPL v3
* Portions of CncAnon's TavernAITurbo mod: Unknown license * Portions of CncAnon's TavernAITurbo mod: Unknown license
* Waifu mode inspired by the work of PepperTaco (https://github.com/peppertaco/Tavern/) * Waifu mode inspired by the work of PepperTaco (<https://github.com/peppertaco/Tavern/>)
* Thanks Pygmalion University for being awesome testers and suggesting cool features! * Thanks Pygmalion University for being awesome testers and suggesting cool features!
* Thanks oobabooga for compiling presets for TextGen * Thanks oobabooga for compiling presets for TextGen
* poe-api client adapted from https://github.com/ading2210/poe-api (GPL v3) * poe-api client adapted from <https://github.com/ading2210/poe-api> (GPL v3)
* GraphQL files for poe: https://github.com/muharamdani/poe (ISC License) * GraphQL files for poe: <https://github.com/muharamdani/poe> (ISC License)
* KoboldAI Presets from KAI Lite: https://lite.koboldai.net/ * KoboldAI Presets from KAI Lite: <https://lite.koboldai.net/>
* Noto Sans font by Google (OFL license) * Noto Sans font by Google (OFL license)
* Icon theme by Font Awesome https://fontawesome.com (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) * Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Linux startup script by AlpinDale * Linux startup script by AlpinDale
* Thanks paniphons for providing a FAQ document * Thanks paniphons for providing a FAQ document

View File

@ -77,6 +77,7 @@ const whitelistMode = config.whitelistMode;
const autorun = config.autorun && !cliArguments.ssl; const autorun = config.autorun && !cliArguments.ssl;
const enableExtensions = config.enableExtensions; const enableExtensions = config.enableExtensions;
const listen = config.listen; const listen = config.listen;
const allowKeysExposure = config.allowKeysExposure;
const axios = require('axios'); const axios = require('axios');
const tiktoken = require('@dqbd/tiktoken'); const tiktoken = require('@dqbd/tiktoken');
@ -308,7 +309,7 @@ app.get('/deviceinfo', function (request, response) {
return response.send(deviceInfo); return response.send(deviceInfo);
}); });
app.get('/version', function (_, response) { app.get('/version', function (_, response) {
let pkgVersion, gitRevision; let pkgVersion, gitRevision, gitBranch;
try { try {
const pkgJson = require('./package.json'); const pkgJson = require('./package.json');
pkgVersion = pkgJson.version; pkgVersion = pkgJson.version;
@ -316,13 +317,18 @@ app.get('/version', function (_, response) {
gitRevision = require('child_process') gitRevision = require('child_process')
.execSync('git rev-parse --short HEAD', { cwd: __dirname }) .execSync('git rev-parse --short HEAD', { cwd: __dirname })
.toString().trim(); .toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD', { cwd: __dirname })
.toString().trim();
} }
} }
catch { catch {
// suppress exception // suppress exception
} }
finally { finally {
response.send(`SillyTavern:${gitRevision || pkgVersion}:Cohee#1207`) const agent = `SillyTavern:${gitRevision || pkgVersion}:Cohee#1207`;
response.send({ agent, pkgVersion, gitRevision, gitBranch });
} }
}) })
@ -522,7 +528,7 @@ app.post("/savechat", jsonParser, function (request, response) {
var dir_name = String(request.body.avatar_url).replace('.png', ''); var dir_name = String(request.body.avatar_url).replace('.png', '');
let chat_data = request.body.chat; let chat_data = request.body.chat;
let jsonlData = chat_data.map(JSON.stringify).join('\n'); let jsonlData = chat_data.map(JSON.stringify).join('\n');
fs.writeFile(chatsPath + dir_name + "/" + request.body.file_name + '.jsonl', jsonlData, 'utf8', function (err) { fs.writeFile(`${chatsPath + dir_name}/${sanitize(request.body.file_name)}.jsonl`, jsonlData, 'utf8', function (err) {
if (err) { if (err) {
response.send(err); response.send(err);
return console.log(err); return console.log(err);
@ -546,11 +552,10 @@ app.post("/getchat", jsonParser, function (request, response) {
if (err === null) { //if there is a dir, then read the requested file from the JSON call if (err === null) { //if there is a dir, then read the requested file from the JSON call
fs.stat(chatsPath + dir_name + "/" + request.body.file_name + ".jsonl", function (err, stat) { fs.stat(`${chatsPath + dir_name}/${sanitize(request.body.file_name)}.jsonl`, function (err, stat) {
if (err === null) { //if no error (the file exists), read the file if (err === null) { //if no error (the file exists), read the file
if (stat !== undefined) { if (stat !== undefined) {
fs.readFile(chatsPath + dir_name + "/" + request.body.file_name + ".jsonl", 'utf8', (err, data) => { fs.readFile(`${chatsPath + dir_name}/${sanitize(request.body.file_name)}.jsonl`, 'utf8', (err, data) => {
if (err) { if (err) {
console.error(err); console.error(err);
response.send(err); response.send(err);
@ -579,9 +584,8 @@ app.post("/getchat", jsonParser, function (request, response) {
} }
} }
}); });
}); });
app.post("/getstatus", jsonParser, async function (request, response_getstatus = response) { app.post("/getstatus", jsonParser, async function (request, response_getstatus = response) {
if (!request.body) return response_getstatus.sendStatus(400); if (!request.body) return response_getstatus.sendStatus(400);
api_server = request.body.api_server; api_server = request.body.api_server;
@ -2808,7 +2812,7 @@ function migrateSecrets() {
if (typeof hordeKey === 'string') { if (typeof hordeKey === 'string') {
console.log('Migrating Horde key...'); console.log('Migrating Horde key...');
writeSecret(SECRET_KEYS.HORDE, hordeKey); writeSecret(SECRET_KEYS.HORDE, hordeKey);
delete settings.hordeKey; delete settings.horde_settings.api_key;
modified = true; modified = true;
} }
@ -2841,7 +2845,7 @@ app.post('/writesecret', jsonParser, (request, response) => {
const key = request.body.key; const key = request.body.key;
const value = request.body.value; const value = request.body.value;
writeSecret(key,value); writeSecret(key, value);
return response.send('ok'); return response.send('ok');
}); });
@ -2880,6 +2884,7 @@ app.post('/generate_horde', jsonParser, async (request, response) => {
} }
}; };
console.log(args.data);
try { try {
const data = await postAsync(url, args); const data = await postAsync(url, args);
return response.send(data); return response.send(data);
@ -2888,6 +2893,27 @@ app.post('/generate_horde', jsonParser, async (request, response) => {
} }
}); });
app.post('/viewsecrets', jsonParser, async (_, response) => {
if (!allowKeysExposure) {
console.error('secrets.json could not be viewed unless the value of allowKeysExposure in config.conf is set to true');
return response.sendStatus(403);
}
if (!fs.existsSync(SECRETS_FILE)) {
console.error('secrets.json does not exist');
return response.sendStatus(404);
}
try {
const fileContents = fs.readFileSync(SECRETS_FILE);
const secrets = JSON.parse(fileContents);
return response.send(secrets);
} catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
function writeSecret(key, value) { function writeSecret(key, value) {
if (!fs.existsSync(SECRETS_FILE)) { if (!fs.existsSync(SECRETS_FILE)) {
const emptyFile = JSON.stringify({}); const emptyFile = JSON.stringify({});