This commit is contained in:
RossAscends
2023-05-13 02:42:01 +09:00
10 changed files with 151 additions and 29 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

@@ -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,
};

View File

@@ -1008,8 +1008,11 @@
<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 reload the page 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 hidden after you reload the page.</div>
<h4 class="horde_model_title">
Model
<div id="horde_refresh" title="Refresh models" class="right_menu_button">
@@ -1039,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 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
@@ -1093,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 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>
@@ -1130,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 hidden after you reload the page.</div>
</div>
<input id="poe_connect" class="menu_button" type="button" value="Connect" />
@@ -1153,9 +1165,12 @@
</div>
</div>
</div>
<label for="auto-connect-checkbox" class="checkbox_label"><input id="auto-connect-checkbox" type="checkbox" />
Auto-connect to Last Server
</label>
<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>

View File

@@ -1339,11 +1339,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") {
@@ -1359,7 +1361,9 @@ function getExtensionPrompt(position = 0, depth = undefined, separator = "\n") {
if (extension_prompt.length && !extension_prompt.endsWith(separator)) {
extension_prompt = extension_prompt + separator;
}
extension_prompt = substituteParams(extension_prompt);
if (extension_prompt.length) {
extension_prompt = substituteParams(extension_prompt);
}
return extension_prompt;
}

View File

@@ -44,11 +44,11 @@ const triggerWords = {
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: name, 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: 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:']",
[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.", */

View File

@@ -75,6 +75,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 +165,7 @@ function onAudioControlClicked() {
function addAudioControl() {
$('#send_but_sheld').prepend('<div id="tts_media_control"/>')
$('#send_but_sheld').on('click', onAudioControlClicked)
$('#tts_media_control').on('click', onAudioControlClicked)
audioControl = document.getElementById('tts_media_control')
updateUiAudioPlayState()
}
@@ -181,7 +182,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)
@@ -241,11 +242,16 @@ 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
? currentTtsJob.mes.replace(/\*[^\*]*?(\*|$)/g, '').trim() // remove asterisks content
: currentTtsJob.mes.replaceAll('*', '').trim() // remove just the asterisks
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`
}

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,19 +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);
// Horde doesn't have a connect button
if (secret_key !== SECRET_KEYS.HORDE) {
$(input_selector).val('');
}
}
}
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) {
@@ -64,4 +95,9 @@ export async function readSecretState() {
} catch {
console.error('Could not read secrets file');
}
}
}
jQuery(() => {
$('#viewSecrets').on('click', viewSecrets);
$(document).on('click', '.clear-api-key', clearSecret);
});

View File

@@ -109,6 +109,24 @@ 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;

View File

@@ -125,6 +125,17 @@ Get in touch with the developers directly:
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.

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');
@@ -2894,6 +2895,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({});