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
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]"
labels: ''
assignees: ''

View File

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

View File

@ -16,6 +16,7 @@ Method 1 - GIT
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.
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.
Method 2 - ZIP

View File

@ -10,6 +10,19 @@
"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",
"execution_count": null,
@ -42,9 +55,13 @@
"#@markdown Enables Silero text-to-speech module\n",
"extras_enable_sd = True #@param {type:\"boolean\"}\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 * 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 * stabilityai/stable-diffusion-2-1-base - base SD 2.1\n",
"\n",
"import subprocess\n",
"\n",
@ -78,6 +95,7 @@
"%cd /\n",
"!git clone https://github.com/Cohee1207/SillyTavern-extras\n",
"%cd /SillyTavern-extras\n",
"!git clone https://github.com/Cohee1207/tts_samples\n",
"!npm install -g localtunnel\n",
"!pip install -r requirements-complete.txt\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 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 allowKeysExposure = false; // If true, private API keys could be fetched to the frontend.
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",
"version": "1.5.0",
"version": "1.5.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.5.0",
"version": "1.5.1",
"license": "AGPL-3.0",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",

View File

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

View File

@ -1005,10 +1005,14 @@
Adjust response length to worker capabilities
</label>
<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>
<input id="horde_api_key" name="horde_api_key" class="text_pole" maxlength="500" type="text" placeholder="0000000000">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<div class="flex-container">
<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">
Model
<div id="horde_refresh" title="Refresh models" class="right_menu_button">
@ -1038,8 +1042,11 @@
<li>Enter it in the box below:</li>
</ol>
</span>
<input id="api_key_novel" name="api_key_novel" class="text_pole" maxlength="500" size="35" type="text">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<div class="flex-container">
<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">
<div id="api_loading_novel" class="api-load-icon fa-solid fa-hourglass fa-spin"></div>
<h4>Novel AI Model
@ -1092,8 +1099,11 @@
<li>Enter it in the box below:</li>
</ol>
</span>
<input id="api_key_openai" name="api_key_openai" class="text_pole" maxlength="500" value="" type="text">
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<div class="flex-container">
<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">
<div id="api_loading_openai" class=" api-load-icon fa-solid fa-hourglass fa-spin"></div>
</form>
@ -1129,8 +1139,11 @@
</ol>
</span>
<div class="widthFreeExpand">
<input id="poe_token" class="text_pole" type="text" maxlength="100" />
<div class="neutral_warning">Your API key will removed from here after you click "Connect" for privacy reasons.</div>
<div class="flex-container">
<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>
<input id="poe_connect" class="menu_button" type="button" value="Connect" />
@ -1152,9 +1165,12 @@
</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" />
Auto-connect to Last Server
</label>
<a id="viewSecrets" href="javascript:void(0);">View hidden API keys</a>
</div>
</div>
</div>
@ -1440,7 +1456,10 @@
<div class="drawer-icon fa-solid fa-face-smile closedIcon" title="User Settings"></div>
</div>
<div id="user-settings-block" class="drawer-content closedDrawer">
<div class="flex-container wide100p alignitemscenter spaceBetween">
<h3>User Settings</h3>
<div id="version_display"></div>
</div>
<div class="flex-container spaceEvenly">
<div name="UI Customization" class="flex-container drawer25pWidth">
<div class="ui-settings">
@ -2027,7 +2046,7 @@
<hr>
<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>
<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 id="scenario_div">
@ -2037,7 +2056,7 @@
<span class="note-link-span">?</span>
</a>
</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 id="talkativeness_div">
@ -2057,7 +2076,7 @@
<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>
</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 id="character_popup_ok" class="menu_button">Save</div>
@ -2322,6 +2341,7 @@
<span class="name_text">${characterName}</span>
<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="Edit" class="mes_edit fa-solid fa-pencil "></div>
</div>
@ -2427,7 +2447,7 @@
<div id="loading_mes">
<div alt="" class="fa-solid fa-hourglass-half"></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>
</form>
</div>

View File

@ -1,6 +1,6 @@
{
"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": "",
"stop_sequence": "",
"input_sequence": "### Instruction:",

View File

@ -1,9 +1,9 @@
{
"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:",
"stop_sequence": "",
"input_sequence": "USER: ",
"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",
"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": "",
"stop_sequence": "",
"input_sequence": "### Instruction:",

View File

@ -416,7 +416,53 @@ _When using Pygmalion models these anchors are automatically disabled, since Pyg
## 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

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:
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.
### Method 2 - ZIP

View File

@ -107,7 +107,7 @@ import {
} from "./scripts/poe.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 {
tag_map,
@ -203,6 +203,9 @@ hljs.addPlugin({ "before:highlightElement": ({ el }) => { el.textContent = el.in
let converter;
reloadMarkdownProcessor();
// array for prompt token calculations
let itemizedPrompts = [];
/* let bg_menu_toggle = false; */
export const systemUserName = "SillyTavern System";
let default_user_name = "You";
@ -283,9 +286,8 @@ const system_messages = {
mes: [
'Hi there! The following chat formatting commands are supported:',
'<ol>',
'<li><tt>*text*</tt> format the actions that your character does</li>',
'<li><tt>{{text}}</tt> set the behavioral bias for the AI character</li>',
'<li><tt>{{}}</tt> cancel a previously set bias</li>',
'<li><tt>{{text}}</tt> sets a permanent behavioral bias for the AI</li>',
'<li><tt>{{}}</tt> removes any active character bias</li>',
'</ol>',
].join('')
},
@ -296,22 +298,24 @@ const system_messages = {
is_user: false,
is_name: true,
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>',
"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>',
'<ol>',
'<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>',
'</ol>',
"<h3>Running on Colab and can't get an answer from the AI or getting Out of Memory errors?</h3>",
'Set a lower Context Size in AI generation settings (leftmost icon).<br>Values in range of 1400-1600 Tokens would be the safest choice.',
'<hr class="sysHR">',
'<h3>Where to download more characters?</h3>',
'<i>(Not endorsed, your discretion is advised)</i>',
'<ol>',
'<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>',
'</ol>',
'<hr class="sysHR">',
'<h3>Where can I get help?</h3>',
'Before going any further, check out the following resources:',
'<ol>',
@ -322,6 +326,7 @@ const system_messages = {
'<li><a target="_blank" href="https://docs.alpindale.dev/">Pygmalion AI Docs</a></li>',
'</ol>',
'Type <tt>/?</tt> in any chat to get help on message formatting commands.',
'<hr class="sysHR">',
'<h3>Still have questions or suggestions left?</h3>',
'<a target="_blank" href="https://discord.gg/RZdyAEUPvj">SillyTavern Community Discord</a>',
'<br/>',
@ -386,7 +391,16 @@ $(document).ajaxError(function myErrorHandler(_, xhr) {
async function getClientVersion() {
try {
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) {
console.log("Couldn't get client version", err);
}
@ -551,13 +565,13 @@ $.ajaxPrefilter((options, originalOptions, xhr) => {
///// initialization protocol ////////
$.get("/csrf-token").then(async (data) => {
token = data.token;
sendSystemMessage(system_message_types.WELCOME);
await readSecretState();
await getClientVersion();
await getSettings("def");
await getUserAvatars();
await getCharacters();
await getBackgrounds();
await getUserAvatars();
sendSystemMessage(system_message_types.WELCOME);
});
function checkOnlineStatus() {
@ -751,8 +765,8 @@ function printCharacters() {
printTags();
printGroups();
favsToHotswap();
sortCharactersList();
favsToHotswap();
}
async function getCharacters() {
@ -1115,6 +1129,27 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
if (isSystem) {
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 () {
@ -1327,11 +1362,13 @@ function cleanGroupMessage(getMessage) {
}
function getAllExtensionPrompts() {
return substituteParams(Object
const value = Object
.values(extension_prompts)
.filter(x => x.value)
.map(x => x.value.trim())
.join('\n'));
.join('\n');
return value.length ? substituteParams(value) : '';
}
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)) {
extension_prompt = extension_prompt + separator;
}
if (extension_prompt.length) {
extension_prompt = substituteParams(extension_prompt);
}
return extension_prompt;
}
function baseChatReplace(value, name1, name2) {
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) {
value = collapseNewlines(value);
@ -1370,9 +1409,10 @@ function appendToStoryString(value, prefix) {
}
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 == 'textgenerationwebui' && textgenerationwebui_settings.streaming);
|| (main_api == 'textgenerationwebui' && textgenerationwebui_settings.streaming))
&& !isMultigenEnabled(); // Multigen has a quasi-streaming mode which breaks the real streaming
}
class StreamingProcessor {
@ -1585,7 +1625,15 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
const isImpersonate = type == "impersonate";
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);
@ -1819,6 +1867,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
this_max_context = Number(max_context);
}
// Adjust token limit for Horde
let adjustedParams;
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
const allAnchors = getAllExtensionPrompts();
const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.AFTER_SCENARIO);
let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' ');
let { worldInfoString, worldInfoBefore, worldInfoAfter } = getWorldInfoPrompt(chat2);
// 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;
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) {
mesSendString += formatInstructModePrompt(isImpersonate);
const name = isImpersonate ? (is_pygmalion ? 'You' : name1) : name2;
mesSendString += formatInstructModePrompt(name, isImpersonate);
}
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 = mesSendString; //This edit simply removes the first "<START>" that is prepended to all context prompts
}
let finalPromt = worldInfoBefore +
let finalPromt =
worldInfoBefore +
storyString +
worldInfoAfter +
afterScenarioAnchor +
@ -2093,6 +2153,33 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
generatedPromtCache +
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 (!isMultigenEnabled() || tokens_already_generated == 0) {
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, '');
if (power_user.collapse_newlines) {
@ -2229,11 +2311,14 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
hideSwipeButtons();
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()) {
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;
promptBias = '';
if (!streamingProcessor.isStopped && shouldContinueMultigen(getMessage)) {
if (!streamingProcessor.isStopped && shouldContinueMultigen(getMessage, isImpersonate)) {
streamingProcessor.isFinished = false;
runGenerate(getMessage);
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;
({ this_mes_is_name, getMessage } = extractNameFromMessage(getMessage, force_name2, isImpersonate));
if (!isImpersonate) {
if (tokens_already_generated == 0) {
console.log("New message");
({ 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");
({ 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();
tokens_already_generated += this_amount_gen; // add new gen amt to any prev gen counter..
getMessage = message_already_generated;
@ -2294,6 +2386,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
if (getMessage.length > 0) {
if (isImpersonate) {
$('#send_textarea').val(getMessage).trigger('input');
generatedPromtCache = "";
}
else if (type == 'quiet') {
resolve(getMessage);
@ -2368,6 +2461,147 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
//console.log('generate ending');
} //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) {
$("#chat .mes").removeClass('lastInContext');
@ -2442,12 +2676,24 @@ function getGenerateUrl() {
return generate_url;
}
function shouldContinueMultigen(getMessage) {
const nameString = is_pygmalion ? 'You:' : `${name1}:`;
return message_already_generated.indexOf(nameString) === -1 && //if there is no 'You:' in the response msg
message_already_generated.indexOf('<|endoftext|>') === -1 && //if there is no <endoftext> stamp in the response msg
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...
function shouldContinueMultigen(getMessage, isImpersonate) {
if (power_user.instruct.enabled && power_user.instruct.stop_sequence) {
if (message_already_generated.indexOf(power_user.instruct.stop_sequence) !== -1) {
return false;
}
}
// 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) {
@ -2563,6 +2809,12 @@ function cleanUpMessage(getMessage, isImpersonate) {
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
if (selected_group) {
getMessage = cleanGroupMessage(getMessage);
@ -3715,6 +3967,7 @@ function select_rm_create() {
$("#renameCharButton").css('display', 'none');
$("#name_div").removeClass('displayNone');
$("#name_div").addClass('displayBlock');
updateFavButtonState(false);
$("#form_create").attr("actiontype", "createcharacter");
}
@ -3748,7 +4001,6 @@ function updateFavButtonState(state) {
$("#fav_checkbox").val(fav_ch_checked);
$("#favorite_button").toggleClass('fav_on', fav_ch_checked);
$("#favorite_button").toggleClass('fav_off', !fav_ch_checked);
}
function callPopup(text, type, inputValue = '') {
@ -5311,27 +5563,6 @@ $(document).ready(function () {
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 () {
$("#shadow_select_chat_popup").transition({
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***

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ async function doDiceRoll() {
function addDiceRollButton() {
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 = `
<div id="dice_dropdown">

View File

@ -34,39 +34,40 @@ const generationMode = {
}
const triggerWords = {
[generationMode.CHARACTER]: ['yourself', 'you', 'bot', 'AI', 'character'],
[generationMode.USER]: ['me', 'user', 'myself'],
[generationMode.SCENARIO]: ['scenario', 'world', 'surroundings', 'scenery'],
[generationMode.NOW]: ['now', 'last'],
[generationMode.FACE]: ['selfie', 'face'],
[generationMode.CHARACTER]: ['you'],
[generationMode.USER]: ['me'],
[generationMode.SCENARIO]: ['scene'],
[generationMode.NOW]: ['last'],
[generationMode.FACE]: ['face'],
}
const quietPrompts = {
//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
[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.", */
[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.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 = [
`${m('what')} requests an SD generation. Supported "what" arguments:`,
`${m('(argument)')} requests SD to make an image. Supported arguments:`,
'<ul>',
`<li>${m(j(triggerWords[generationMode.CHARACTER]))} AI character image</li>`,
`<li>${m(j(triggerWords[generationMode.USER]))} user character image</li>`,
`<li>${m(j(triggerWords[generationMode.SCENARIO]))} world scenario image</li>`,
`<li>${m(j(triggerWords[generationMode.FACE]))} character face-up selfie image</li>`,
`<li>${m(j(triggerWords[generationMode.CHARACTER]))} AI character full body selfie</li>`,
`<li>${m(j(triggerWords[generationMode.FACE]))} AI character face-only selfie</li>`,
`<li>${m(j(triggerWords[generationMode.USER]))} user character full body selfie</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>`,
'</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>');
const defaultSettings = {
@ -236,9 +237,17 @@ function getQuietPrompt(mode, trigger) {
function processReply(str) {
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
.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;
}
@ -258,7 +267,7 @@ async function generatePicture(_, trigger) {
const prompt = processReply(await new Promise(
async function promptPromise(resolve, reject) {
try {
await context.generate('quiet', { resolve, reject, quiet_prompt });
await context.generate('quiet', { resolve, reject, quiet_prompt, force_name2: true, });
}
catch {
reject();
@ -268,6 +277,8 @@ async function generatePicture(_, trigger) {
context.deactivateSendButtons();
hideSwipeButtons();
console.log('Processed Stable Diffusion prompt:', prompt);
const url = new URL(getApiUrl());
url.pathname = '/api/image';
const result = await fetch(url, {
@ -294,7 +305,7 @@ async function generatePicture(_, trigger) {
sendMessage(prompt, base64Image);
}
} catch (err) {
console.error(err);
console.trace(err);
throw new Error('SD prompt text generation failed.')
}
finally {
@ -325,7 +336,7 @@ async function sendMessage(prompt, image) {
function addSDGenButtons() {
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 = `
@ -411,8 +422,8 @@ $("#sd_dropdown [id]").on("click", function () {
}
else if (id == "sd_world") {
console.log("doing /sd world");
generatePicture('sd', 'world');
console.log("doing /sd scene");
generatePicture('sd', 'scene');
}
else if (id == "sd_last") {
@ -422,7 +433,7 @@ $("#sd_dropdown [id]").on("click", function () {
});
jQuery(async () => {
getContext().registerSlashCommand('sd', generatePicture, ['picture', 'image'], helpString, true, true);
getContext().registerSlashCommand('sd', generatePicture, [], helpString, true, true);
const settingsHtml = `
<div class="sd_settings">

View File

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

View File

@ -48,10 +48,8 @@ async function moduleWorker() {
return;
}
// Chat/character/group changed
// Chat changed
if (
(context.groupId && lastGroupId !== context.groupId) ||
context.characterId !== lastCharacterId ||
context.chatId !== lastChatId
) {
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
if (
message.mes === '...' ||
message.mes === '' ||
(context.streamingProcessor && !context.streamingProcessor.isFinished)
) {
return
@ -164,7 +163,7 @@ function onAudioControlClicked() {
function addAudioControl() {
$('#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')
updateUiAudioPlayState()
}
@ -181,7 +180,7 @@ function completeCurrentAudioJob() {
*/
async function addAudioJob(response) {
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}`
}
audioJobQueue.push(audioData)
@ -240,12 +239,26 @@ async function processTtsQueue() {
console.debug('New message found, running TTS')
currentTtsJob = ttsJobQueue.shift()
const text = extension_settings.tts.narrate_dialogues_only
? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '') // remove asterisks content
: currentTtsJob.mes.replaceAll('*', '') // remove just the asterisks
let text = extension_settings.tts.narrate_dialogues_only
? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content
: 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
try {
if (!text) {
console.warn('Got empty text in TTS queue job.');
return;
}
if (!voiceMap[char]) {
throw `${char} not in voicemap. Configure character in extension settings voice map`
}
@ -282,6 +295,7 @@ function loadSettings() {
extension_settings.tts.enabled
)
$('#tts_narrate_dialogues').prop('checked', extension_settings.tts.narrate_dialogues_only)
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only)
}
const defaultSettings = {
@ -374,6 +388,13 @@ function onNarrateDialoguesClick() {
saveSettingsDebounced()
}
function onNarrateQuotedClick() {
extension_settings.tts.narrate_quoted_only = $('#tts_narrate_quoted').prop('checked');
saveSettingsDebounced()
}
//##############//
// TTS Provider //
//##############//
@ -453,6 +474,10 @@ $(document).ready(function () {
<input type="checkbox" id="tts_narrate_dialogues">
Narrate dialogues only
</label>
<label class="checkbox_label" for="tts_narrate_quoted">
<input type="checkbox" id="tts_narrate_quoted">
Narrate quoted only
</label>
</div>
<label>Voice Map</label>
<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_enabled').on('click', onEnableClick)
$('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
$('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
$('#tts_voices').on('click', onTtsVoicesClick)
$('#tts_provider_settings').on('input', onTtsProviderSettingsInput)
for (const provider in ttsProviders) {

View File

@ -9,6 +9,7 @@ class SileroTtsProvider {
settings
voices = []
separator = ' .. '
defaultSettings = {
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'
settings
voices = []
separator = ' ... '
defaultSettings = {
voiceMap: {},

View File

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

View File

@ -9,6 +9,7 @@ import {
getRequestHeaders,
substituteParams,
} from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import {
groups,
selected_group,
@ -632,10 +633,10 @@ function loadInstructMode() {
}
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 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);
return text;
}
@ -649,10 +650,11 @@ export function formatInstructStoryString(story) {
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 separator = power_user.instruct.wrap ? '\n' : '';
const text = separator + sequence;
const text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence);
return text;
}
@ -995,6 +997,7 @@ $(document).ready(() => {
power_user.sort_order = $(this).find(":selected").data('order');
power_user.sort_rule = $(this).find(":selected").data('rule');
sortCharactersList();
favsToHotswap();
saveSettingsDebounced();
});

View File

@ -1,4 +1,4 @@
import { getRequestHeaders } from "../script.js";
import { callPopup, getRequestHeaders } from "../script.js";
export const SECRET_KEYS = {
HORDE: 'api_key_horde',
@ -14,14 +14,50 @@ const INPUT_MAP = {
[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() {
for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
const validSecret = !!secret_state[secret_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 async function writeSecret(key, value) {
@ -60,3 +96,8 @@ export async function readSecretState() {
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 getSlashCommandsHelp = parser.getHelpString.bind(parser);
parser.addCommand('help', helpCommandCallback, ['?'], ' displays a help information', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">name</span> sets a background by file name', false, true);
parser.addCommand('help', helpCommandCallback, ['?'], ' displays this help message', true, 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() {
sendSystemMessage(system_message_types.HELP);

View File

@ -183,3 +183,9 @@ export async function initScrollHeight(element) {
$(element).css("height", `${newHeight}px`);
//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;
}
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-regular::before {
vertical-align: middle;
@ -191,6 +226,11 @@ code {
transition: background-image 0.5s ease-in-out;
}
#version_display {
padding: 5px;
opacity: 0.8;
}
#bg1 {
background-image: url('backgrounds/tavern day.jpg');
z-index: -2;
@ -2093,6 +2133,7 @@ input[type="range"]::-webkit-slider-thumb {
width: 20px;
opacity: 0.5;
}
.mes_buttons {
float: right;
height: 20px;
@ -2101,6 +2142,7 @@ input[type="range"]::-webkit-slider-thumb {
right: 0px;
}
.mes_prompt,
.mes_copy,
.mes_edit {
cursor: pointer;
@ -2117,7 +2159,8 @@ input[type="range"]::-webkit-slider-thumb {
opacity: 1;
}
.last_mes .mes_copy {
.last_mes .mes_copy,
.last_mes .mes_prompt {
grid-row-start: 1;
position: relative;
right: -30px;
@ -3516,6 +3559,10 @@ toolcool-color-picker {
justify-content: space-evenly;
}
.spaceBetween {
justify-content: space-between;
}
.widthNatural {
width: unset !important;
min-width: unset !important;
@ -3961,6 +4008,7 @@ body.waifuMode #avatar_zoom_popup {
}
#sheld,
#character_popup,
#world_popup {
@ -3973,6 +4021,11 @@ body.waifuMode #avatar_zoom_popup {
top: 42px;
}
#character_popup,
#world_popup {
overflow-y: auto;
}
#character_popup,
#world_popup,
#send_form {

View File

@ -1,18 +1,23 @@
# SillyTavern
## Based on a fork of TavernAI 1.2.8
### 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.
### 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.
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?
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?
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
@ -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:**
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.**
## Questions or suggestions?
### We now have a community Discord server!
### We now have a community Discord server
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:
* Discord: Cohee#1207 or RossAscends#1779
* Reddit: /u/RossAscends or /u/sillylossy
* [Post a GitHub issue](https://github.com/Cohee1207/SillyTavern/issues)
## This version includes
* A heavily modified TavernAI 1.2.8 (more than 50% of code rewritten or optimized)
* Swipes
* 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.
## UI Extensions 🚀
| 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" /> |
@ -115,6 +123,27 @@ Get in touch with the developers directly:
> DO NOT RUN START.BAT WITH ADMIN PERMISSIONS
### 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)
2. download the zip from this GitHub repo
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.
### Linux
1. Run the `start.sh` script.
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
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;
```
to
```
const whitelistMode = false;
```
Save the file.
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***
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'
### 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.
- 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?
@ -187,9 +233,10 @@ We're moving to 100% original content only policy, so old background images have
You can find them archived here:
https://files.catbox.moe/1xevnc.zip
<https://files.catbox.moe/1xevnc.zip>
## 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/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
* RossAscends' additions: AGPL v3
* 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 oobabooga for compiling presets for TextGen
* poe-api client adapted from https://github.com/ading2210/poe-api (GPL v3)
* GraphQL files for poe: https://github.com/muharamdani/poe (ISC License)
* KoboldAI Presets from KAI Lite: https://lite.koboldai.net/
* poe-api client adapted from <https://github.com/ading2210/poe-api> (GPL v3)
* GraphQL files for poe: <https://github.com/muharamdani/poe> (ISC License)
* KoboldAI Presets from KAI Lite: <https://lite.koboldai.net/>
* 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
* Thanks paniphons for providing a FAQ document

View File

@ -77,6 +77,7 @@ const whitelistMode = config.whitelistMode;
const autorun = config.autorun && !cliArguments.ssl;
const enableExtensions = config.enableExtensions;
const listen = config.listen;
const allowKeysExposure = config.allowKeysExposure;
const axios = require('axios');
const tiktoken = require('@dqbd/tiktoken');
@ -308,7 +309,7 @@ app.get('/deviceinfo', function (request, response) {
return response.send(deviceInfo);
});
app.get('/version', function (_, response) {
let pkgVersion, gitRevision;
let pkgVersion, gitRevision, gitBranch;
try {
const pkgJson = require('./package.json');
pkgVersion = pkgJson.version;
@ -316,13 +317,18 @@ app.get('/version', function (_, response) {
gitRevision = require('child_process')
.execSync('git rev-parse --short HEAD', { cwd: __dirname })
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD', { cwd: __dirname })
.toString().trim();
}
}
catch {
// suppress exception
}
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', '');
let chat_data = request.body.chat;
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) {
response.send(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
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 (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) {
console.error(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) {
if (!request.body) return response_getstatus.sendStatus(400);
api_server = request.body.api_server;
@ -2808,7 +2812,7 @@ function migrateSecrets() {
if (typeof hordeKey === 'string') {
console.log('Migrating Horde key...');
writeSecret(SECRET_KEYS.HORDE, hordeKey);
delete settings.hordeKey;
delete settings.horde_settings.api_key;
modified = true;
}
@ -2880,6 +2884,7 @@ app.post('/generate_horde', jsonParser, async (request, response) => {
}
};
console.log(args.data);
try {
const data = await postAsync(url, args);
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) {
if (!fs.existsSync(SECRETS_FILE)) {
const emptyFile = JSON.stringify({});