Merge branch 'staging' of https://github.com/city-unit/SillyTavern into feature/exorcism

This commit is contained in:
city-unit 2023-08-30 00:12:00 -04:00
commit 165d4b3b75
42 changed files with 1890 additions and 1369 deletions

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ public/KoboldAI Settings/
public/NovelAI Settings/
public/TextGen Settings/
public/instruct/
public/context/
public/scripts/extensions/third-party/
public/stats.json
/uploads/

View File

@ -166,9 +166,10 @@
"persona_show_notifications": true,
"custom_stopping_strings": "",
"custom_stopping_strings_macro": true,
"fuzzy_search": false,
"fuzzy_search": true,
"encode_tags": false,
"lazy_load": 0
"lazy_load": 100,
"ui_mode": 1
},
"extension_settings": {
"apiUrl": "http://localhost:5100",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "sillytavern",
"version": "1.9.7",
"version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.9.7",
"version": "1.10.0",
"license": "AGPL-3.0",
"dependencies": {
"@agnai/sentencepiece-js": "^1.1.1",

View File

@ -51,7 +51,7 @@
"type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git"
},
"version": "1.9.7",
"version": "1.10.0",
"scripts": {
"start": "node server.js",
"start-multi": "node server.js --disableCsrf",

View File

@ -1,6 +1,6 @@
{
"name": "Minimalist",
"story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
"chat_start": "###",
"example_separator": "###"
"chat_start": "",
"example_separator": ""
}

View File

@ -1,4 +1,3 @@
body.tts .mes[is_user="true"] .mes_narrate,
body.tts .mes[is_system="true"] .mes_narrate {
display: none;
}
@ -365,4 +364,4 @@ body.movingUI #groupMemberListPopout {
body.noShadows * {
text-shadow: none !important;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +0,0 @@
# GPT-2/3 Tokenizer
GPT-2/3 byte pair encoder/decoder/tokenizer based on [@latitudegames/GPT-3-Encoder](https://github.com/latitudegames/GPT-3-Encoder) that works in the browser and Deno.
See also: [JS byte pair encoder for OpenAI's CLIP model](https://github.com/josephrocca/clip-bpe-js).
```js
import {encode, decode} from "https://deno.land/x/gpt_2_3_tokenizer@v0.0.2/mod.js";
let text = "hello world";
console.log(encode(text)); // [258, 18798, 995]
console.log(decode(encode(text))); // "hello world"
```
or:
```js
let mod = await import("https://deno.land/x/gpt_2_3_tokenizer@v0.0.2/mod.js");
mod.encode("hello world"); // [258, 18798, 995]
```
or to include it as a global variable in the browser:
```html
<script type=module>
import tokenizer from "https://deno.land/x/gpt_2_3_tokenizer@v0.0.2/mod.js";
window.tokenizer = tokenizer;
</script>
```
# License
The [original code is MIT Licensed](https://github.com/latitudegames/GPT-3-Encoder/blob/master/LICENSE) and so are any changes made by this repo.

File diff suppressed because one or more lines are too long

View File

@ -1,169 +0,0 @@
import encoder from "./encoder.js";
import bpe_file from "./vocab.bpe.js";
const range = (x, y) => {
const res = Array.from(Array(y).keys()).slice(x)
return res
}
const ord = x => {
return x.charCodeAt(0)
}
const chr = x => {
return String.fromCharCode(x)
}
const textEncoder = new TextEncoder("utf-8")
const encodeStr = str => {
return Array.from(textEncoder.encode(str)).map(x => x.toString())
}
const textDecoder = new TextDecoder("utf-8")
const decodeStr = arr => {
return textDecoder.decode(new Uint8Array(arr));
}
const dictZip = (x, y) => {
const result = {}
x.map((_, i) => { result[x[i]] = y[i] })
return result
}
function bytes_to_unicode() {
const bs = range(ord('!'), ord('~') + 1).concat(range(ord('¡'), ord('¬') + 1), range(ord('®'), ord('ÿ') + 1))
let cs = bs.slice()
let n = 0
for (let b = 0; b < 2 ** 8; b++) {
if (!bs.includes(b)) {
bs.push(b)
cs.push(2 ** 8 + n)
n = n + 1
}
}
cs = cs.map(x => chr(x))
const result = {}
bs.map((_, i) => { result[bs[i]] = cs[i] })
return result
}
function get_pairs(word) {
const pairs = new Set()
let prev_char = word[0]
for (let i = 1; i < word.length; i++) {
const char = word[i]
pairs.add([prev_char, char])
prev_char = char
}
return pairs
}
const pat = /'s|'t|'re|'ve|'m|'l l|'d| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+/gu
const decoder = {}
Object.keys(encoder).map(x => { decoder[encoder[x]] = x })
const lines = bpe_file.split('\n')
// bpe_merges = [tuple(merge_str.split()) for merge_str in bpe_data.split("\n")[1:-1]]
const bpe_merges = lines.slice(1, lines.length - 1).map(x => {
return x.split(/(\s+)/).filter(function(e) { return e.trim().length > 0 })
})
const byte_encoder = bytes_to_unicode()
const byte_decoder = {}
Object.keys(byte_encoder).map(x => { byte_decoder[byte_encoder[x]] = x })
const bpe_ranks = dictZip(bpe_merges, range(0, bpe_merges.length))
const cache = {}
function bpe(token) {
if (Object.hasOwn(cache, token)) {
return cache[token]
}
let word = token.split('')
let pairs = get_pairs(word)
if (!pairs) {
return token
}
while (true) {
const minPairs = {}
Array.from(pairs).map(pair => {
const rank = bpe_ranks[pair]
minPairs[(isNaN(rank) ? 10e10 : rank)] = pair
})
const bigram = minPairs[Math.min(...Object.keys(minPairs).map(x => {
return parseInt(x)
}
))]
if (!(Object.hasOwn(bpe_ranks, bigram))) {
break
}
const first = bigram[0]
const second = bigram[1]
let new_word = []
let i = 0
while (i < word.length) {
const j = word.indexOf(first, i)
if (j === -1) {
new_word = new_word.concat(word.slice(i))
break
}
new_word = new_word.concat(word.slice(i, j))
i = j
if (word[i] === first && i < word.length - 1 && word[i + 1] === second) {
new_word.push(first + second)
i = i + 2
} else {
new_word.push(word[i])
i = i + 1
}
}
word = new_word
if (word.length === 1) {
break
} else {
pairs = get_pairs(word)
}
}
word = word.join(' ')
cache[token] = word
return word
}
export function encode(text) {
let bpe_tokens = []
const matches = Array.from(text.matchAll(pat)).map(x => x[0])
for (let token of matches) {
token = encodeStr(token).map(x => {
return byte_encoder[x]
}).join('')
const new_tokens = bpe(token).split(' ').map(x => encoder[x])
bpe_tokens = bpe_tokens.concat(new_tokens)
}
return bpe_tokens
}
export function decode(tokens) {
let text = tokens.map(x => decoder[x]).join('')
text = decodeStr(text.split('').map(x => byte_decoder[x]))
return text
}

File diff suppressed because one or more lines are too long

2
public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@ -70,6 +70,9 @@ import {
MAX_CONTEXT_DEFAULT,
renderStoryString,
sortEntitiesList,
registerDebugFunction,
ui_mode,
switchSimpleMode,
} from "./scripts/power-user.js";
import {
@ -174,6 +177,7 @@ import {
} from "./scripts/instruct-mode.js";
import { applyLocale } from "./scripts/i18n.js";
import { getTokenCount, getTokenizerModel, saveTokenCache } from "./scripts/tokenizers.js";
import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js";
//exporting functions and vars for mods
export {
@ -288,8 +292,8 @@ export const eventSource = new EventEmitter();
// Check for override warnings every 5 seconds...
setInterval(displayOverrideWarnings, 5000);
// ...or when the chat changes
eventSource.on(event_types.SETTINGS_LOADED, () => { settingsReady = true; });
eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings);
eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona);
eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers);
eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers);
@ -324,6 +328,7 @@ let importFlashTimeout;
export let isChatSaving = false;
let chat_create_date = 0;
let firstRun = false;
let settingsReady = false;
const default_ch_mes = "Hello";
let count_view_mes = 0;
@ -652,7 +657,7 @@ var settings;
export let koboldai_settings;
export let koboldai_setting_names;
var preset_settings = "gui";
var user_avatar = "you.png";
export let user_avatar = "you.png";
export var amount_gen = 80; //default max length of AI generated responses
var max_context = 2048;
@ -1800,11 +1805,7 @@ function getStoppingStrings(isImpersonate, addSpace) {
if (power_user.custom_stopping_strings) {
const customStoppingStrings = getCustomStoppingStrings();
if (power_user.custom_stopping_strings_macro) {
result.push(...customStoppingStrings.map(x => substituteParams(x, name1, name2)));
} else {
result.push(...customStoppingStrings);
}
result.push(...customStoppingStrings);
}
return addSpace ? result.map(x => `${x} `) : result;
@ -1915,7 +1916,7 @@ function cleanGroupMessage(getMessage) {
const regex = new RegExp(`(^|\n)${escapeRegex(name)}:`);
const nameMatch = getMessage.match(regex);
if (nameMatch) {
getMessage = getMessage.substring(nameMatch.index + nameMatch[0].length);
getMessage = getMessage.substring(0, nameMatch.index);
}
}
}
@ -4577,7 +4578,7 @@ function changeMainAPI() {
////////////////////////////////////////////////////
async function getUserAvatars() {
export async function getUserAvatars() {
const response = await fetch("/getuseravatars", {
method: "POST",
headers: getRequestHeaders(),
@ -4601,66 +4602,8 @@ async function getUserAvatars() {
}
}
function setPersonaDescription() {
$("#persona_description").val(power_user.persona_description);
$("#persona_description_position")
.val(power_user.persona_description_position)
.find(`option[value='${power_user.persona_description_position}']`)
.attr("selected", true);
countPersonaDescriptionTokens();
}
function onPersonaDescriptionPositionInput() {
power_user.persona_description_position = Number(
$("#persona_description_position").find(":selected").val()
);
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: power_user.persona_description_position,
};
power_user.persona_descriptions[user_avatar] = object;
}
object.position = power_user.persona_description_position;
}
saveSettingsDebounced();
}
/**
* Counts the number of tokens in a persona description.
*/
const countPersonaDescriptionTokens = debounce(() => {
const description = String($("#persona_description").val());
const count = getTokenCount(description);
$("#persona_description_token_count").text(String(count));
}, durationSaveEdit);
function onPersonaDescriptionInput() {
power_user.persona_description = String($("#persona_description").val());
countPersonaDescriptionTokens();
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: Number($("#persona_description_position").find(":selected").val()),
};
power_user.persona_descriptions[user_avatar] = object;
}
object.description = power_user.persona_description;
}
saveSettingsDebounced();
}
function highlightSelectedAvatar() {
$("#user_avatar_block").find(".avatar").removeClass("selected");
@ -4709,124 +4652,13 @@ export function setUserName(value) {
saveSettings("change_name");
}
export function autoSelectPersona(name) {
for (const [key, value] of Object.entries(power_user.personas)) {
if (value === name) {
console.log(`Auto-selecting persona ${key} for name ${name}`);
$(`.avatar[imgfile="${key}"]`).trigger('click');
return;
}
}
}
async function bindUserNameToPersona() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
const existingPersona = power_user.personas[avatarId];
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>(If empty name is provided, this will unbind the name from this avatar)', 'input', existingPersona || '');
// If the user clicked cancel, don't do anything
if (personaName === false) {
return;
}
if (personaName.length > 0) {
// If the user clicked ok and entered a name, bind the name to the persona
console.log(`Binding persona ${avatarId} to name ${personaName}`);
power_user.personas[avatarId] = personaName;
const descriptor = power_user.persona_descriptions[avatarId];
const isCurrentPersona = avatarId === user_avatar;
// Create a description object if it doesn't exist
if (!descriptor) {
// If the user is currently using this persona, set the description to the current description
power_user.persona_descriptions[avatarId] = {
description: isCurrentPersona ? power_user.persona_description : '',
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.BEFORE_CHAR,
};
}
// If the user is currently using this persona, update the name
if (isCurrentPersona) {
console.log(`Auto-updating user name to ${personaName}`);
setUserName(personaName);
}
} else {
// If the user clicked ok, but didn't enter a name, delete the persona
console.log(`Unbinding persona ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
}
saveSettingsDebounced();
await getUserAvatars();
setPersonaDescription();
}
async function createDummyPersona() {
const fetchResult = await fetch(default_avatar);
const blob = await fetchResult.blob();
const file = new File([blob], "avatar.png", { type: "image/png" });
const formData = new FormData();
formData.append("avatar", file);
jQuery.ajax({
type: "POST",
url: "/uploaduseravatar",
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function (data) {
await getUserAvatars();
},
});
}
function updateUserLockIcon() {
const hasLock = !!chat_metadata['persona'];
$('#lock_user_name').toggleClass('fa-unlock', !hasLock);
$('#lock_user_name').toggleClass('fa-lock', hasLock);
}
function setUserAvatar() {
user_avatar = $(this).attr("imgfile");
reloadUserAvatar();
saveSettingsDebounced();
highlightSelectedAvatar();
const personaName = power_user.personas[user_avatar];
if (personaName && name1 !== personaName) {
const lockedPersona = chat_metadata['persona'];
if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) {
toastr.info(
`To permanently set "${personaName}" as the selected persona, unlock and relock it using the "Lock" button. Otherwise, the selection resets upon reloading the chat.`,
`This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`,
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true },
);
}
setUserName(personaName);
const descriptor = power_user.persona_descriptions[user_avatar];
if (descriptor) {
power_user.persona_description = descriptor.description;
power_user.persona_description_position = descriptor.position;
} else {
power_user.persona_description = '';
power_user.persona_description_position = persona_description_positions.BEFORE_CHAR;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.BEFORE_CHAR };
}
setPersonaDescription();
}
selectCurrentPersona();
}
async function uploadUserAvatar(e) {
@ -4884,188 +4716,14 @@ async function uploadUserAvatar(e) {
$("#form_upload_avatar").trigger("reset");
}
async function setDefaultPersona() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
const currentDefault = power_user.default_persona;
if (power_user.personas[avatarId] === undefined) {
console.warn(`No persona name found for avatar ${avatarId}`);
toastr.warning('You must bind a name to this persona before you can set it as the default.', 'Persona name not set');
return;
}
const personaName = power_user.personas[avatarId];
if (avatarId === currentDefault) {
const confirm = await callPopup('Are you sure you want to remove the default persona?', 'confirm');
if (!confirm) {
console.debug('User cancelled removing default persona');
return;
}
console.log(`Removing default persona ${avatarId}`);
if (power_user.persona_show_notifications) {
toastr.info('This persona will no longer be used by default when you open a new chat.', `Default persona removed`);
}
delete power_user.default_persona;
} else {
const confirm = await callPopup(`<h3>Are you sure you want to set "${personaName}" as the default persona?</h3>
This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`, 'confirm');
if (!confirm) {
console.debug('User cancelled setting default persona');
return;
}
power_user.default_persona = avatarId;
if (power_user.persona_show_notifications) {
toastr.success('This persona will be used by default when you open a new chat.', `Default persona set to ${personaName}`);
}
}
saveSettingsDebounced();
await getUserAvatars();
}
async function deleteUserAvatar() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
if (avatarId == user_avatar) {
console.warn(`User tried to delete their current avatar ${avatarId}`);
toastr.warning('You cannot delete the avatar you are currently using', 'Warning');
return;
}
const confirm = await callPopup('<h3>Are you sure you want to delete this avatar?</h3>All information associated with its linked persona will be lost.', 'confirm');
if (!confirm) {
console.debug('User cancelled deleting avatar');
return;
}
const request = await fetch("/deleteuseravatar", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"avatar": avatarId,
}),
});
if (request.ok) {
console.log(`Deleted avatar ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
if (avatarId === power_user.default_persona) {
toastr.warning('The default persona was deleted. You will need to set a new default persona.', 'Default persona deleted');
power_user.default_persona = null;
}
if (avatarId === chat_metadata['persona']) {
toastr.warning('The locked persona was deleted. You will need to set a new persona for this chat.', 'Persona deleted');
delete chat_metadata['persona'];
await saveMetadata();
}
saveSettingsDebounced();
await getUserAvatars();
updateUserLockIcon();
}
}
async function lockUserNameToChat() {
if (chat_metadata['persona']) {
console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`);
delete chat_metadata['persona'];
await saveMetadata();
if (power_user.persona_show_notifications) {
toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked');
}
updateUserLockIcon();
return;
}
if (!(user_avatar in power_user.personas)) {
console.log(`Creating a new persona ${user_avatar}`);
if (power_user.persona_show_notifications) {
toastr.info(
'Creating a new persona for currently selected user name and avatar...',
'Persona not set for this avatar',
{ timeOut: 10000, extendedTimeOut: 20000, },
);
}
power_user.personas[user_avatar] = name1;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.BEFORE_CHAR };
}
chat_metadata['persona'] = user_avatar;
await saveMetadata();
saveSettingsDebounced();
console.log(`Locking persona for this chat ${user_avatar}`);
if (power_user.persona_show_notifications) {
toastr.success(`User persona is locked to ${name1} in this chat`);
}
updateUserLockIcon();
}
function setChatLockedPersona() {
// Define a persona for this chat
let chatPersona = '';
if (chat_metadata['persona']) {
// If persona is locked in chat metadata, select it
console.log(`Using locked persona ${chat_metadata['persona']}`);
chatPersona = chat_metadata['persona'];
} else if (power_user.default_persona) {
// If default persona is set, select it
console.log(`Using default persona ${power_user.default_persona}`);
chatPersona = power_user.default_persona;
}
// No persona set: user current settings
if (!chatPersona) {
console.debug('No default or locked persona set for this chat');
return;
}
// Find the avatar file
const personaAvatar = $(`.avatar[imgfile="${chatPersona}"]`).trigger('click');
// Avatar missing (persona deleted)
if (chat_metadata['persona'] && personaAvatar.length == 0) {
console.warn('Persona avatar not found, unlocking persona');
delete chat_metadata['persona'];
updateUserLockIcon();
return;
}
// Default persona missing
if (power_user.default_persona && personaAvatar.length == 0) {
console.warn('Default persona avatar not found, clearing default persona');
power_user.default_persona = null;
saveSettingsDebounced();
return;
}
// Persona avatar found, select it
personaAvatar.trigger('click');
updateUserLockIcon();
}
async function doOnboarding(avatarId) {
let simpleUiMode = false;
const template = $('#onboarding_template .onboarding');
template.find('input[name="enable_simple_mode"]').on('input', function () {
simpleUiMode = $(this).is(':checked');
});
const userName = await callPopup(template, 'input', name1);
if (userName) {
@ -5074,9 +4732,15 @@ async function doOnboarding(avatarId) {
power_user.personas[avatarId] = userName;
power_user.persona_descriptions[avatarId] = {
description: '',
position: persona_description_positions.BEFORE_CHAR,
position: persona_description_positions.IN_PROMPT,
};
}
if (simpleUiMode) {
power_user.ui_mode = ui_mode.SIMPLE;
$('#ui_mode_select').val(power_user.ui_mode);
switchSimpleMode();
}
}
//***************SETTINGS****************//
@ -5265,6 +4929,10 @@ function selectKoboldGuiPreset() {
}
async function saveSettings(type) {
if (!settingsReady) {
console.warn('Settings not ready, aborting save');
return;
}
//console.log('Entering settings with name1 = '+name1);
return jQuery.ajax({
@ -6141,7 +5809,7 @@ function hideSwipeButtons() {
$("#chat").children().filter(`[mesid="${count_view_mes - 1}"]`).children('.swipe_left').css('display', 'none');
}
async function saveMetadata() {
export async function saveMetadata() {
if (selected_group) {
await editGroup(selected_group, true, false);
}
@ -6658,6 +6326,7 @@ window["SillyTavern"].getContext = function () {
saveReply,
registerSlashCommand: registerSlashCommand,
registerHelper: registerExtensionHelper,
registedDebugFunction: registerDebugFunction,
};
};
@ -8474,7 +8143,6 @@ $(document).ready(function () {
setUserName($('#your_name').val());
});
$("#create_dummy_persona").on('click', createDummyPersona);
$('#sync_name_button').on('click', async function () {
const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm');
@ -8516,13 +8184,6 @@ $(document).ready(function () {
setTimeout(getStatusNovel, 10);
});
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.delete_avatar', deleteUserAvatar);
$(document).on('click', '.set_default_persona', setDefaultPersona);
$('#lock_user_name').on('click', lockUserNameToChat);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
//**************************CHARACTER IMPORT EXPORT*************************//
$("#character_import_button").click(function () {
$("#character_import_file").click();
@ -9111,4 +8772,5 @@ $(document).ready(function () {
// Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
initAuthorsNote();
initRossMods();
initPersonas();
});

View File

@ -607,10 +607,6 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
eventSource.on(event_types.OAI_PRESET_CHANGED, settings => {
// Save configuration and wrap everything up.
this.saveServiceSettings().then(() => {
this.hidePopup();
this.clearEditForm();
this.renderDebounced();
const mainPrompt = this.getPromptById('main');
this.updateQuickEdit('main', mainPrompt);
@ -619,6 +615,10 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti
const jailbreakPrompt = this.getPromptById('jailbreak');
this.updateQuickEdit('jailbreak', jailbreakPrompt);
this.hidePopup();
this.clearEditForm();
this.renderDebounced();
});
});

View File

@ -1,26 +1,26 @@
#roll_dice {
/* order: 100; */
/* width: 40px;
height: 40px;
margin: 0;
padding: 1px; */
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
/* justify-content: center; */
}
#roll_dice:hover {
opacity: 1;
filter: brightness(1.2);
}
#dice_dropdown {
z-index: 100;
backdrop-filter: blur(--SmartThemeBlurStrength);
}
#roll_dice {
/* order: 100; */
/* width: 40px;
height: 40px;
margin: 0;
padding: 1px; */
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
display: flex;
align-items: center;
/* justify-content: center; */
}
#roll_dice:hover {
opacity: 1;
filter: brightness(1.2);
}
#dice_dropdown {
z-index: 30000;
backdrop-filter: blur(--SmartThemeBlurStrength);
}

View File

@ -394,6 +394,11 @@ function onExpressionsShowDefaultInput() {
}
async function unloadLiveChar() {
if (!modules.includes('talkinghead')) {
console.debug('talkinghead module is disabled');
return;
}
try {
const url = new URL(getApiUrl());
url.pathname = '/api/talkinghead/unload';

View File

@ -6,6 +6,7 @@ import { bufferToBase64, debounce } from "../../utils.js";
import { decodeTextTokens, getTextTokens, tokenizers } from "../../tokenizers.js";
const MODULE_NAME = 'hypebot';
const WAITING_VERBS = ['thinking', 'typing', 'brainstorming', 'cooking', 'conjuring'];
const MAX_PROMPT = 1024;
const MAX_LENGTH = 50;
const MAX_STRING_LENGTH = MAX_PROMPT * 4;
@ -20,8 +21,7 @@ const settings = {
* @returns {string} Random waiting verb
*/
function getWaitingVerb() {
const waitingVerbs = ['thinking', 'typing', 'brainstorming', 'cooking', 'conjuring'];
return waitingVerbs[Math.floor(Math.random() * waitingVerbs.length)];
return WAITING_VERBS[Math.floor(Math.random() * WAITING_VERBS.length)];
}
/**
@ -49,8 +49,7 @@ function getVerb(text) {
* @returns {string} Formatted HTML text
*/
function formatReply(text) {
const verb = getVerb(text);
return DOMPurify.sanitize(`<span class="hypebot_name">${settings.name} ${verb}:</span>&nbsp;<span class="hypebot_text">${text}</span>`);
return `<span class="hypebot_name">${settings.name} ${getVerb(text)}:</span>&nbsp;<span class="hypebot_text">${text}</span>`;
}
let hypeBotBar;
@ -58,13 +57,25 @@ let abortController;
const generateDebounced = debounce(() => generateHypeBot(), 500);
/**
* Sets the HypeBot text. Preserves scroll position of the chat.
* @param {string} text Text to set
*/
function setHypeBotText(text) {
const blockA = $('#chat');
var originalScrollBottom = blockA[0].scrollHeight - (blockA.scrollTop() + blockA.outerHeight());
hypeBotBar.html(DOMPurify.sanitize(text));
var newScrollTop = blockA[0].scrollHeight - (blockA.outerHeight() + originalScrollBottom);
blockA.scrollTop(newScrollTop);
}
/**
* Called when a chat event occurs to generate a HypeBot reply.
* @param {boolean} clear Clear the hypebot bar.
*/
function onChatEvent(clear) {
if (clear) {
hypeBotBar.text('');
setHypeBotText('');
}
abortController?.abort();
@ -80,12 +91,12 @@ async function generateHypeBot() {
}
if (!secret_state[SECRET_KEYS.NOVEL]) {
hypeBotBar.html('<span class="hypebot_nokey">No API key found. Please enter your API key in the NovelAI API Settings</span>');
setHypeBotText('<div class="hypebot_nokey">No API key found. Please enter your API key in the NovelAI API Settings to use the HypeBot.</div>');
return;
}
console.debug('Generating HypeBot reply');
hypeBotBar.html(DOMPurify.sanitize(`<span class="hypebot_name">${settings.name}</span> is ${getWaitingVerb()}...`));
setHypeBotText(`<span class="hypebot_name">${settings.name}</span> is ${getWaitingVerb()}...`);
const context = getContext();
const chat = context.chat.slice();
@ -160,7 +171,9 @@ async function generateHypeBot() {
const ids = Array.from(new Uint16Array(Uint8Array.from(atob(data.output), c => c.charCodeAt(0)).buffer));
const output = decodeTextTokens(tokenizers.GPT2, ids).replace(/<2F>/g, '').trim();
hypeBotBar.html(formatReply(output));
setHypeBotText(formatReply(output));
} else {
setHypeBotText('<div class="hypebot_error">Something went wrong while generating a HypeBot reply. Please try again.</div>');
}
}

View File

@ -12,7 +12,7 @@ let presets = [];
let selected_preset = '';
const defaultSettings = {
quickReplyEnabled: true,
quickReplyEnabled: false,
numberOfSlots: 5,
quickReplySlots: [],
}

View File

@ -72,8 +72,11 @@ async function moduleWorker() {
}
else {
console.debug(DEBUG_PREFIX+"Found trigger word: ", triggerWord, " at index ", triggerPos);
if (triggerPos < messageStart | messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
if (triggerPos < messageStart || messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
messageStart = triggerPos; // + triggerWord.length + 1;
if (!extension_settings.speech_recognition.Streaming.triggerWordsIncluded)
messageStart = triggerPos + triggerWord.length + 1;
}
}
}
@ -93,6 +96,16 @@ async function moduleWorker() {
}
else{
userMessageFormatted = userMessageFormatted.substring(messageStart);
// Trim non alphanumeric character from the start
messageStart = 0;
for(const i of userMessageFormatted) {
if(/^[a-z]$/i.test(i)) {
break;
}
messageStart += 1;
}
userMessageFormatted = userMessageFormatted.substring(messageStart);
userMessageFormatted = userMessageFormatted.charAt(0).toUpperCase() + userMessageFormatted.substring(1);
processTranscript(userMessageFormatted);
}
}

View File

@ -14,6 +14,7 @@ class StreamingSttProvider {
triggerWordsText: "",
triggerWords : [],
triggerWordsEnabled : false,
triggerWordsIncluded: false,
debug : false,
}
@ -26,6 +27,10 @@ class StreamingSttProvider {
<input type="checkbox" id="speech_recognition_streaming_trigger_words_enabled" name="speech_recognition_trigger_words_enabled">\
<small>Enable trigger words</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_trigger_words_included">\
<input type="checkbox" id="speech_recognition_trigger_words_included" name="speech_recognition_trigger_words_included">\
<small>Include trigger words in message</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_streaming_debug">\
<input type="checkbox" id="speech_recognition_streaming_debug" name="speech_recognition_streaming_debug">\
<small>Enable debug pop ups</small>\
@ -42,6 +47,7 @@ class StreamingSttProvider {
array = array.filter((str) => str !== '');
this.settings.triggerWords = array;
this.settings.triggerWordsEnabled = $("#speech_recognition_streaming_trigger_words_enabled").is(':checked');
this.settings.triggerWordsIncluded = $("#speech_recognition_trigger_words_included").is(':checked');
this.settings.debug = $("#speech_recognition_streaming_debug").is(':checked');
console.debug(DEBUG_PREFIX+" Updated settings: ", this.settings);
this.loadSettings(this.settings);
@ -66,6 +72,7 @@ class StreamingSttProvider {
$("#speech_recognition_streaming_trigger_words").val(this.settings.triggerWordsText);
$("#speech_recognition_streaming_trigger_words_enabled").prop('checked',this.settings.triggerWordsEnabled);
$("#speech_recognition_trigger_words_included").prop('checked',this.settings.triggerWordsIncluded);
$("#speech_recognition_streaming_debug").prop('checked',this.settings.debug);
console.debug(DEBUG_PREFIX+"streaming STT settings loaded")

View File

@ -539,8 +539,21 @@ function processReply(str) {
function getRawLastMessage() {
const getLastUsableMessage = () => {
for (const message of context.chat.slice().reverse()) {
if (message.is_system) {
continue;
}
return message.mes;
}
toastr.warning('No usable messages found.', 'Stable Diffusion');
throw new Error('No usable messages found.');
}
const context = getContext();
const lastMessage = context.chat.slice(-1)[0].mes,
const lastMessage = getLastUsableMessage(),
characterDescription = context.characters[context.characterId].description,
situation = context.characters[context.characterId].scenario;
return `((${processReply(lastMessage)})), (${processReply(situation)}:0.7), (${processReply(characterDescription)}:0.5)`

View File

@ -5,13 +5,16 @@ TODO:
*/
import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js"
import { callPopup } from "../../../script.js"
import { initVoiceMap } from "./index.js"
export { CoquiTtsProvider }
const DEBUG_PREFIX = "<Coqui TTS module> ";
const UPDATE_INTERVAL = 1000;
let charactersList = []; // Updated with module worker
let inApiCall = false;
let voiceIdList = []; // Updated with module worker
let coquiApiModels = {}; // Initialized only once
let coquiApiModelsFull = {}; // Initialized only once
let coquiLocalModels = []; // Initialized only once
@ -39,16 +42,11 @@ const languageLabels = {
"ja": "Japanese"
}
const defaultSettings = {
voiceMap: "",
voiceMapDict: {}
}
function throwIfModuleMissing() {
if (!modules.includes('coqui-tts')) {
toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(DEBUG_PREFIX, `Coqui TTS module not loaded.`);
const message = `Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.`
// toastr.error(message, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(DEBUG_PREFIX, message);
}
}
@ -57,46 +55,18 @@ function resetModelSettings() {
$("#coqui_api_model_settings_speaker").val("none");
}
function updateCharactersList() {
let currentcharacters = new Set();
const context = getContext();
for (const i of context.characters) {
currentcharacters.add(i.name);
}
currentcharacters = Array.from(currentcharacters);
currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters
$('#coqui_character_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Character</option>')
.val('none')
for (const charName of charactersList) {
$("#coqui_character_select").append(new Option(charName, charName));
}
console.debug(DEBUG_PREFIX, "Updated character list to:", charactersList);
}
}
class CoquiTtsProvider {
//#############################//
// Extension UI and Settings //
//#############################//
static instance;
settings = {};
settings
// Singleton to allow acces to instance in event functions
constructor() {
if (CoquiTtsProvider.instance === undefined)
CoquiTtsProvider.instance = this;
defaultSettings = {
voiceMap: {},
customVoices: {},
voiceIds: [],
voiceMapDict: {}
}
get settingsHtml() {
@ -104,13 +74,15 @@ class CoquiTtsProvider {
<div class="flex wide100p flexGap10 alignitemscenter">
<div>
<div style="flex: 50%;">
<label for="coqui_character_select">Character:</label>
<select id="coqui_character_select">
<small>To use CoquiTTS, select the origin, language, and model, then click Add Voice. The voice will then be available to add to a character. Voices are saved globally. </small><br>
<label for="coqui_voicename_select">Select Saved Voice:</label>
<select id="coqui_voicename_select">
<!-- Populated by JS -->
</select>
<input id="coqui_remove_char_mapping" class="menu_button" type="button" value="Remove from Voice Map" />
<div class="tts_block">
<input id="coqui_remove_voiceId_mapping" class="menu_button" type="button" value="Remove Voice" />
<input id="coqui_add_voiceId_mapping" class="menu_button" type="button" value="Add Voice" />
</div>
<label for="coqui_model_origin">Models:</label>
<select id="coqui_model_origin">gpu_mode
<option value="none">Select Origin</option>
@ -139,7 +111,7 @@ class CoquiTtsProvider {
<span id="coqui_api_model_install_status">Model installed on extras server</span>
<input id="coqui_api_model_install_button" class="menu_button" type="button" value="Install" />
</div>
<div id="coqui_local_model_div">
<select id="coqui_local_model_name">
<!-- Populated by JS and request -->
@ -153,13 +125,9 @@ class CoquiTtsProvider {
return html
}
loadSettings(settings) {
if (Object.keys(this.settings).length === 0) {
Object.assign(this.settings, defaultSettings)
}
async loadSettings(settings) {
// Only accept keys defined in defaultSettings
this.settings = defaultSettings;
this.settings = this.defaultSettings
for (const key in settings) {
if (key in this.settings) {
@ -169,7 +137,8 @@ class CoquiTtsProvider {
}
}
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
await initLocalModels();
this.updateCustomVoices(); // Overide any manual modification
$("#coqui_api_model_div").hide();
$("#coqui_local_model_div").hide();
@ -180,70 +149,123 @@ class CoquiTtsProvider {
$("#coqui_api_model_install_status").hide();
$("#coqui_api_model_install_button").hide();
$("#coqui_model_origin").on("change", CoquiTtsProvider.onModelOriginChange);
$("#coqui_api_language").on("change", CoquiTtsProvider.onModelLanguageChange);
$("#coqui_api_model_name").on("change", CoquiTtsProvider.onModelNameChange);
$("#coqui_remove_char_mapping").on("click", CoquiTtsProvider.onRemoveClick);
let that = this
$("#coqui_model_origin").on("change", function () { that.onModelOriginChange() });
$("#coqui_api_language").on("change", function () { that.onModelLanguageChange() });
$("#coqui_api_model_name").on("change", function () { that.onModelNameChange() });
updateCharactersList();
$("#coqui_remove_voiceId_mapping").on("click", function () { that.onRemoveClick() });
$("#coqui_add_voiceId_mapping").on("click", function () { that.onAddClick() });
// Load coqui-api settings from json file
fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
await fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
.then(response => response.json())
.then(json => {
coquiApiModels = json;
console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels);
/*
$('#coqui_api_language')
.find('option')
.remove()
.end()
.append('<option value="none">Select model language</option>')
.val('none');
for(let language in coquiApiModels) {
$("#coqui_api_language").append(new Option(languageLabels[language],language));
console.log(DEBUG_PREFIX,"added language",language);
}*/
});
// Load coqui-api FULL settings from json file
fetch("/scripts/extensions/tts/coqui_api_models_settings_full.json")
await fetch("/scripts/extensions/tts/coqui_api_models_settings_full.json")
.then(response => response.json())
.then(json => {
coquiApiModelsFull = json;
console.debug(DEBUG_PREFIX,"initialized coqui-api full model list to", coquiApiModelsFull);
/*
$('#coqui_api_full_language')
.find('option')
.remove()
.end()
.append('<option value="none">Select model language</option>')
.val('none');
for(let language in coquiApiModelsFull) {
$("#coqui_api_full_language").append(new Option(languageLabels[language],language));
console.log(DEBUG_PREFIX,"added language",language);
}*/
});
}
static updateVoiceMap() {
CoquiTtsProvider.instance.settings.voiceMap = "";
for (let i in CoquiTtsProvider.instance.settings.voiceMapDict) {
const voice_settings = CoquiTtsProvider.instance.settings.voiceMapDict[i];
CoquiTtsProvider.instance.settings.voiceMap += i + ":" + voice_settings["model_id"];
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
throwIfModuleMissing()
await this.fetchTtsVoiceObjects()
}
if (voice_settings["model_language"] != null)
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
updateCustomVoices() {
// Takes voiceMapDict and converts it to a string to save to voiceMap
this.settings.customVoices = {};
for (let voiceName in this.settings.voiceMapDict) {
const voiceId = this.settings.voiceMapDict[voiceName];
this.settings.customVoices[voiceName] = voiceId["model_id"];
if (voice_settings["model_speaker"] != null)
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
if (voiceId["model_language"] != null)
this.settings.customVoices[voiceName] += "[" + voiceId["model_language"] + "]";
CoquiTtsProvider.instance.settings.voiceMap += ",";
if (voiceId["model_speaker"] != null)
this.settings.customVoices[voiceName] += "[" + voiceId["model_speaker"] + "]";
}
$("#tts_voice_map").val(CoquiTtsProvider.instance.settings.voiceMap);
//extension_settings.tts.Coqui = extension_settings.tts.Coqui;
// Update UI select list with voices
$("#coqui_voicename_select").empty()
$('#coqui_voicename_select')
.find('option')
.remove()
.end()
.append('<option value="none">Select Voice</option>')
.val('none')
for (const voiceName in this.settings.voiceMapDict) {
$("#coqui_voicename_select").append(new Option(voiceName, voiceName));
}
this.onSettingsChange()
}
onSettingsChange() {
//console.debug(DEBUG_PREFIX, "Settings changes", CoquiTtsProvider.instance.settings);
CoquiTtsProvider.updateVoiceMap();
console.debug(DEBUG_PREFIX, "Settings changes", this.settings);
extension_settings.tts.Coqui = this.settings;
}
async onApplyClick() {
const character = $("#coqui_character_select").val();
async onRefreshClick() {
this.checkReady()
}
async onAddClick() {
if (inApiCall) {
return; //TODO: block dropdown
}
// Ask user for voiceId name to save voice
const voiceName = await callPopup('<h3>Name of Coqui voice to add to voice select dropdown:</h3>', 'input')
const model_origin = $("#coqui_model_origin").val();
const model_language = $("#coqui_api_language").val();
const model_name = $("#coqui_api_model_name").val();
let model_setting_language = $("#coqui_api_model_settings_language").val();
let model_setting_speaker = $("#coqui_api_model_settings_speaker").val();
if (character === "none") {
toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX + " voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
if (!voiceName) {
toastr.error(`Voice name empty, please enter one.`, DEBUG_PREFIX + " voice mapping voice name", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
this.updateCustomVoices(); // Overide any manual modification
return;
}
if (model_origin == "none") {
toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
this.updateCustomVoices(); // Overide any manual modification
return;
}
@ -252,25 +274,25 @@ class CoquiTtsProvider {
if (model_name == "none") {
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
this.updateCustomVoices(); // Overide any manual modification
return;
}
CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
this.settings.voiceMapDict[voiceName] = { model_type: "local", model_id: "local/" + model_id };
console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]);
this.updateCustomVoices(); // Overide any manual modification
return;
}
if (model_language == "none") {
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
this.updateCustomVoices(); // Overide any manual modification
return;
}
if (model_name == "none") {
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
this.updateCustomVoices(); // Overide any manual modification
return;
}
@ -299,45 +321,51 @@ class CoquiTtsProvider {
return;
}
console.debug(DEBUG_PREFIX, "Current voice map: ", CoquiTtsProvider.instance.settings.voiceMap);
console.debug(DEBUG_PREFIX, "Current custom voices: ", this.settings.customVoices);
CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
this.settings.voiceMapDict[voiceName] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]);
CoquiTtsProvider.updateVoiceMap();
this.updateCustomVoices();
initVoiceMap() // Update TTS extension voiceMap
let successMsg = character + ":" + model_id;
let successMsg = voiceName + ":" + model_id;
if (model_setting_language != null)
successMsg += "[" + model_setting_language + "]";
if (model_setting_speaker != null)
successMsg += "[" + model_setting_speaker + "]";
toastr.info(successMsg, DEBUG_PREFIX + " voice map updated", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return
}
// DBG: assume voiceName is correct
// TODO: check voice is correct
async getVoice(voiceName) {
console.log(DEBUG_PREFIX, "getVoice", voiceName);
const output = { voice_id: voiceName };
return output;
let match = await this.fetchTtsVoiceObjects()
match = match.filter(
voice => voice.name == voiceName
)[0]
if (!match) {
throw `TTS Voice name ${voiceName} not found in CoquiTTS Provider voice list`
}
return match;
}
static async onRemoveClick() {
const character = $("#coqui_character_select").val();
async onRemoveClick() {
const voiceName = $("#coqui_voicename_select").val();
if (character === "none") {
toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX + " voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
if (voiceName === "none") {
toastr.error(`Voice not selected, please select one.`, DEBUG_PREFIX + " voice mapping voiceId", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
return;
}
// Todo erase from voicemap
delete (CoquiTtsProvider.instance.settings.voiceMapDict[character]);
CoquiTtsProvider.updateVoiceMap(); // TODO
delete (this.settings.voiceMapDict[voiceName]);
this.updateCustomVoices();
initVoiceMap() // Update TTS extension voiceMap
}
static async onModelOriginChange() {
async onModelOriginChange() {
throwIfModuleMissing()
resetModelSettings();
const model_origin = $('#coqui_model_origin').val();
@ -346,13 +374,10 @@ class CoquiTtsProvider {
$("#coqui_local_model_div").hide();
$("#coqui_api_model_div").hide();
}
// show coqui model selected list (SAFE)
if (model_origin == "coqui-api") {
$("#coqui_local_model_div").hide();
$("#coqui_api_model_div").hide();
$("#coqui_api_model_name").hide();
$("#coqui_api_model_settings").hide();
$('#coqui_api_language')
.find('option')
@ -375,9 +400,6 @@ class CoquiTtsProvider {
// show coqui model full list (UNSAFE)
if (model_origin == "coqui-api-full") {
$("#coqui_local_model_div").hide();
$("#coqui_api_model_div").hide();
$("#coqui_api_model_name").hide();
$("#coqui_api_model_settings").hide();
$('#coqui_api_language')
.find('option')
@ -405,7 +427,7 @@ class CoquiTtsProvider {
}
}
static async onModelLanguageChange() {
async onModelLanguageChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@ -438,7 +460,7 @@ class CoquiTtsProvider {
}
}
static async onModelNameChange() {
async onModelNameChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@ -529,6 +551,8 @@ class CoquiTtsProvider {
$("#coqui_api_model_install_status").text("Model not found on extras server");
}
const onModelNameChange_pointer = this.onModelNameChange;
$("#coqui_api_model_install_button").off("click").on("click", async function () {
try {
$("#coqui_api_model_install_status").text("Downloading model...");
@ -542,7 +566,7 @@ class CoquiTtsProvider {
if (apiResult["status"] == "done") {
$("#coqui_api_model_install_status").text("Model installed and ready to use!");
$("#coqui_api_model_install_button").hide();
CoquiTtsProvider.onModelNameChange();
onModelNameChange_pointer();
}
if (apiResult["status"] == "downloading") {
@ -553,7 +577,7 @@ class CoquiTtsProvider {
} catch (error) {
console.error(error)
toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
CoquiTtsProvider.onModelNameChange();
onModelNameChange_pointer();
}
// will refresh model status
});
@ -656,6 +680,8 @@ class CoquiTtsProvider {
// ts_models/ja/kokoro/tacotron2-DDC
async generateTts(text, voiceId) {
throwIfModuleMissing()
voiceId = this.settings.customVoices[voiceId]
const url = new URL(getApiUrl());
url.pathname = '/api/text-to-speech/coqui/generate-tts';
@ -703,8 +729,11 @@ class CoquiTtsProvider {
}
// Dirty hack to say not implemented
async fetchTtsVoiceIds() {
return [{ name: "Voice samples not implemented for coqui TTS yet, search for the model samples online", voice_id: "", lang: "", }]
async fetchTtsVoiceObjects() {
const voiceIds = Object
.keys(this.settings.voiceMapDict)
.map(voice => ({ name: voice, voice_id: voice, preview_url: false }));
return voiceIds
}
// Do nothing
@ -717,13 +746,7 @@ class CoquiTtsProvider {
}
}
//#############################//
// Module Worker //
//#############################//
async function moduleWorker() {
updateCharactersList();
async function initLocalModels() {
if (!modules.includes('coqui-tts'))
return
@ -748,9 +771,3 @@ async function moduleWorker() {
coquiLocalModelsReceived = true;
}
}
$(document).ready(function () {
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
})

View File

@ -2,6 +2,7 @@ import { getRequestHeaders } from "../../../script.js"
import { getApiUrl } from "../../extensions.js"
import { doExtrasFetch, modules } from "../../extensions.js"
import { getPreviewString } from "./index.js"
import { saveTtsProviderSettings } from "./index.js"
export { EdgeTtsProvider }
@ -30,9 +31,10 @@ class EdgeTtsProvider {
onSettingsChange() {
this.settings.rate = Number($('#edge_tts_rate').val());
$('#edge_tts_rate_output').text(this.settings.rate);
saveTtsProviderSettings()
}
loadSettings(settings) {
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
@ -51,12 +53,20 @@ class EdgeTtsProvider {
$('#edge_tts_rate').val(this.settings.rate || 0);
$('#edge_tts_rate_output').text(this.settings.rate || 0);
$('#edge_tts_rate').on("input", () => {this.onSettingsChange()})
await this.checkReady()
console.info("Settings loaded")
}
async onApplyClick() {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
throwIfModuleMissing()
await this.fetchTtsVoiceObjects()
}
async onRefreshClick() {
return
}
@ -66,7 +76,7 @@ class EdgeTtsProvider {
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceIds()
this.voices = await this.fetchTtsVoiceObjects()
}
const match = this.voices.filter(
voice => voice.name == voiceName
@ -85,7 +95,7 @@ class EdgeTtsProvider {
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
async fetchTtsVoiceObjects() {
throwIfModuleMissing()
const url = new URL(getApiUrl());
@ -144,8 +154,9 @@ class EdgeTtsProvider {
}
function throwIfModuleMissing() {
if (!modules.includes('edge-tts')) {
toastr.error(`Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`)
throw new Error(`Edge TTS module not loaded.`)
const message = `Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`
// toastr.error(message)
throw new Error(message)
}
}

View File

@ -1,5 +1,4 @@
import { deepClone } from "../../utils.js";
import { saveTtsProviderSettings } from "./index.js"
export { ElevenLabsTtsProvider }
class ElevenLabsTtsProvider {
@ -25,16 +24,19 @@ class ElevenLabsTtsProvider {
get settingsHtml() {
let html = `
<label for="elevenlabs_tts_api_key">API Key</label>
<input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
<label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" />
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
<label class="checkbox_label" for="elevenlabs_tts_multilingual">
<input id="elevenlabs_tts_multilingual" type="checkbox" value="${this.defaultSettings.multilingual}" />
Enable Multilingual
</label>
<div class="elevenlabs_tts_settings">
<label for="elevenlabs_tts_api_key">API Key</label>
<input id="elevenlabs_tts_api_key" type="text" class="text_pole" placeholder="<API Key>"/>
<input id="eleven_labs_connect" class="menu_button" type="button" value="Connect" />
<label for="elevenlabs_tts_stability">Stability: <span id="elevenlabs_tts_stability_output"></span></label>
<input id="elevenlabs_tts_stability" type="range" value="${this.defaultSettings.stability}" min="0" max="1" step="0.05" />
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
<label class="checkbox_label" for="elevenlabs_tts_multilingual">
<input id="elevenlabs_tts_multilingual" type="checkbox" value="${this.defaultSettings.multilingual}" />
Enable Multilingual
</label>
</div>
`
return html
}
@ -44,39 +46,49 @@ class ElevenLabsTtsProvider {
this.settings.stability = $('#elevenlabs_tts_stability').val()
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
this.settings.multilingual = $('#elevenlabs_tts_multilingual').prop('checked')
saveTtsProviderSettings()
}
loadSettings(settings) {
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (!settings || Object.keys(settings).length == 0) {
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
// Only accept keys defined in defaultSettings
this.settings = deepClone(this.defaultSettings);
this.settings = this.defaultSettings
if (settings) {
for (const key in settings) {
if (key in this.settings) {
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to TTS Provider: ${key}`
}
}
$('#elevenlabs_tts_stability').val(this.settings.stability)
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost)
$('#elevenlabs_tts_api_key').val(this.settings.apiKey)
$('#tts_auto_generation').prop('checked', this.settings.multilingual)
$('#eleven_labs_connect').on('click', () => {this.onConnectClick()})
$('#elevenlabs_tts_settings').on('input',this.onSettingsChange)
await this.checkReady()
console.info("Settings loaded")
}
async onApplyClick() {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
await this.fetchTtsVoiceObjects()
}
async onRefreshClick() {
}
async onConnectClick() {
// Update on Apply click
return await this.updateApiKey().catch( (error) => {
throw error
toastr.error(`ElevenLabs: ${error}`)
})
}
@ -85,11 +97,12 @@ class ElevenLabsTtsProvider {
// Using this call to validate API key
this.settings.apiKey = $('#elevenlabs_tts_api_key').val()
await this.fetchTtsVoiceIds().catch(error => {
await this.fetchTtsVoiceObjects().catch(error => {
throw `TTS API key validation failed`
})
this.settings.apiKey = this.settings.apiKey
console.debug(`Saved new API_KEY: ${this.settings.apiKey}`)
this.onSettingsChange()
}
//#################//
@ -98,7 +111,7 @@ class ElevenLabsTtsProvider {
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceIds()
this.voices = await this.fetchTtsVoiceObjects()
}
const match = this.voices.filter(
elevenVoice => elevenVoice.name == voiceName
@ -145,7 +158,7 @@ class ElevenLabsTtsProvider {
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
async fetchTtsVoiceObjects() {
const headers = {
'xi-api-key': this.settings.apiKey
}

View File

@ -13,6 +13,7 @@ export { talkingAnimation };
const UPDATE_INTERVAL = 1000
let voiceMapEntries = []
let voiceMap = {} // {charName:voiceid, charName2:voiceid2}
let audioControl
let storedvalue = false;
@ -155,6 +156,11 @@ async function moduleWorker() {
return;
}
// Don't generate if message is a user message and user message narration is disabled
if (message.is_user && !extension_settings.tts.narrate_user) {
return;
}
// New messages, add new chat to history
lastMessageHash = hashNew
currentMessageNumber = lastMessageNumber
@ -210,7 +216,7 @@ function isTtsProcessing() {
let processing = false
// Check job queues
if (ttsJobQueue.length > 0 || audioJobQueue > 0) {
if (ttsJobQueue.length > 0 || audioJobQueue.length > 0) {
processing = true
}
// Check current jobs
@ -224,8 +230,8 @@ function debugTtsPlayback() {
console.log(JSON.stringify(
{
"ttsProviderName": ttsProviderName,
"voiceMap": voiceMap,
"currentMessageNumber": currentMessageNumber,
"isWorkerBusy": isWorkerBusy,
"audioPaused": audioPaused,
"audioJobQueue": audioJobQueue,
"currentAudioJob": currentAudioJob,
@ -285,7 +291,7 @@ async function onTtsVoicesClick() {
let popupText = ''
try {
const voiceIds = await ttsProvider.fetchTtsVoiceIds()
const voiceIds = await ttsProvider.fetchTtsVoiceObjects()
for (const voice of voiceIds) {
popupText += `
@ -486,6 +492,12 @@ function loadSettings() {
if (Object.keys(extension_settings.tts).length === 0) {
Object.assign(extension_settings.tts, defaultSettings)
}
for (const key in defaultSettings) {
if (!(key in extension_settings.tts)) {
extension_settings.tts[key] = defaultSettings[key]
}
}
$('#tts_provider').val(extension_settings.tts.currentProvider)
$('#tts_enabled').prop(
'checked',
extension_settings.tts.enabled
@ -494,6 +506,7 @@ function loadSettings() {
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only)
$('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation)
$('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
$('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
$('body').toggleClass('tts', extension_settings.tts.enabled);
}
@ -501,7 +514,8 @@ const defaultSettings = {
voiceMap: '',
ttsEnabled: false,
currentProvider: "ElevenLabs",
auto_generation: true
auto_generation: true,
narrate_user: false,
}
function setTtsStatus(status, success) {
@ -513,59 +527,17 @@ function setTtsStatus(status, success) {
}
}
function parseVoiceMap(voiceMapString) {
let parsedVoiceMap = {}
for (const [charName, voiceId] of voiceMapString
.split(',')
.map(s => s.split(':'))) {
if (charName && voiceId) {
parsedVoiceMap[charName.trim()] = voiceId.trim()
}
}
return parsedVoiceMap
}
async function voicemapIsValid(parsedVoiceMap) {
let valid = true
for (const characterName in parsedVoiceMap) {
const parsedVoiceName = parsedVoiceMap[characterName]
try {
await ttsProvider.getVoice(parsedVoiceName)
} catch (error) {
console.error(error)
valid = false
}
}
return valid
}
async function updateVoiceMap() {
let isValidResult = false
const value = $('#tts_voice_map').val()
const parsedVoiceMap = parseVoiceMap(value)
isValidResult = await voicemapIsValid(parsedVoiceMap)
if (isValidResult) {
ttsProvider.settings.voiceMap = String(value)
// console.debug(`ttsProvider.voiceMap: ${ttsProvider.settings.voiceMap}`)
voiceMap = parsedVoiceMap
console.debug(`Saved new voiceMap: ${value}`)
saveSettingsDebounced()
} else {
throw 'Voice map is invalid, check console for errors'
}
}
function onApplyClick() {
function onRefreshClick() {
Promise.all([
ttsProvider.onApplyClick(),
updateVoiceMap()
ttsProvider.onRefreshClick(),
// updateVoiceMap()
]).then(() => {
extension_settings.tts[ttsProviderName] = ttsProvider.settings
saveSettingsDebounced()
setTtsStatus('Successfully applied settings', true)
console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`)
initVoiceMap()
updateVoiceMap()
}).catch(error => {
console.error(error)
setTtsStatus(error, false)
@ -582,25 +554,29 @@ function onEnableClick() {
function onAutoGenerationClick() {
extension_settings.tts.auto_generation = $('#tts_auto_generation').prop('checked');
extension_settings.tts.auto_generation = !!$('#tts_auto_generation').prop('checked');
saveSettingsDebounced()
}
function onNarrateDialoguesClick() {
extension_settings.tts.narrate_dialogues_only = $('#tts_narrate_dialogues').prop('checked');
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
saveSettingsDebounced()
}
function onNarrateUserClick() {
extension_settings.tts.narrate_user = !!$('#tts_narrate_user').prop('checked');
saveSettingsDebounced();
}
function onNarrateQuotedClick() {
extension_settings.tts.narrate_quoted_only = $('#tts_narrate_quoted').prop('checked');
extension_settings.tts.narrate_quoted_only = !!$('#tts_narrate_quoted').prop('checked');
saveSettingsDebounced()
}
function onNarrateTranslatedOnlyClick() {
extension_settings.tts.narrate_translated_only = $('#tts_narrate_translated_only').prop('checked');
extension_settings.tts.narrate_translated_only = !!$('#tts_narrate_translated_only').prop('checked');
saveSettingsDebounced();
}
@ -608,13 +584,14 @@ function onNarrateTranslatedOnlyClick() {
// TTS Provider //
//##############//
function loadTtsProvider(provider) {
async function loadTtsProvider(provider) {
//Clear the current config and add new config
$("#tts_provider_settings").html("")
if (!provider) {
provider
return
}
// Init provider references
extension_settings.tts.currentProvider = provider
ttsProviderName = provider
@ -626,38 +603,210 @@ function loadTtsProvider(provider) {
console.warn(`Provider ${ttsProviderName} not in Extension Settings, initiatilizing provider in settings`)
extension_settings.tts[ttsProviderName] = {}
}
// Load voicemap settings
let voiceMapFromSettings
if ("voiceMap" in extension_settings.tts[ttsProviderName]) {
voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap
voiceMap = parseVoiceMap(voiceMapFromSettings)
} else {
voiceMapFromSettings = ""
voiceMap = {}
}
$('#tts_voice_map').val(voiceMapFromSettings)
$('#tts_provider').val(ttsProviderName)
ttsProvider.loadSettings(extension_settings.tts[ttsProviderName])
await ttsProvider.loadSettings(extension_settings.tts[ttsProviderName])
await initVoiceMap()
}
function onTtsProviderChange() {
const ttsProviderSelection = $('#tts_provider').val()
extension_settings.tts.currentProvider = ttsProviderSelection
loadTtsProvider(ttsProviderSelection)
}
function onTtsProviderSettingsInput() {
ttsProvider.onSettingsChange()
// Persist changes to SillyTavern tts extension settings
// Ensure that TTS provider settings are saved to extension settings.
export function saveTtsProviderSettings() {
updateVoiceMap()
extension_settings.tts[ttsProviderName] = ttsProvider.settings
saveSettingsDebounced()
console.info(`Saved settings ${ttsProviderName} ${JSON.stringify(ttsProvider.settings)}`)
}
//###################//
// voiceMap Handling //
//###################//
async function onChatChanged() {
await resetTtsPlayback()
await initVoiceMap()
}
function getCharacters(){
const context = getContext()
let characters = []
if (context.groupId === null){
// Single char chat
characters.push(context.name1)
characters.push(context.name2)
} else {
// Group chat
characters.push(context.name1)
const group = context.groups.find(group => context.groupId == group.id)
for (let member of group.members) {
// Remove suffix
if (member.endsWith('.png')){
member = member.slice(0, -4)
}
characters.push(member)
}
}
return characters
}
function sanitizeId(input) {
// Remove any non-alphanumeric characters except underscore (_) and hyphen (-)
let sanitized = input.replace(/[^a-zA-Z0-9-_]/g, '');
// Ensure first character is always a letter
if (!/^[a-zA-Z]/.test(sanitized)) {
sanitized = 'element_' + sanitized;
}
return sanitized;
}
function parseVoiceMap(voiceMapString) {
let parsedVoiceMap = {}
for (const [charName, voiceId] of voiceMapString
.split(',')
.map(s => s.split(':'))) {
if (charName && voiceId) {
parsedVoiceMap[charName.trim()] = voiceId.trim()
}
}
return parsedVoiceMap
}
/**
* Apply voiceMap based on current voiceMapEntries
*/
function updateVoiceMap() {
const tempVoiceMap = {}
for (const voice of voiceMapEntries){
if (voice.voiceId === null){
continue
}
tempVoiceMap[voice.name] = voice.voiceId
}
if (Object.keys(tempVoiceMap).length !== 0){
voiceMap = tempVoiceMap
console.log(`Voicemap updated to ${JSON.stringify(voiceMap)}`)
}
Object.assign(extension_settings.tts[ttsProviderName].voiceMap, voiceMap)
saveSettingsDebounced()
}
class VoiceMapEntry {
name
voiceId
selectElement
constructor (name, voiceId='disabled') {
this.name = name
this.voiceId = voiceId
this.selectElement = null
}
addUI(voiceIds){
let sanitizedName = sanitizeId(this.name)
let template = `
<div class='tts_voicemap_block_char flex-container flexGap5'>
<span id='tts_voicemap_char_${sanitizedName}'>${this.name}</span>
<select id='tts_voicemap_char_${sanitizedName}_voice'>
<option>disabled</option>
</select>
</div>
`
$('#tts_voicemap_block').append(template)
// Populate voice ID select list
for (const voiceId of voiceIds){
const option = document.createElement('option');
option.innerText = voiceId.name;
option.value = voiceId.name;
$(`#tts_voicemap_char_${sanitizedName}_voice`).append(option)
}
this.selectElement = $(`#tts_voicemap_char_${sanitizedName}_voice`)
this.selectElement.on('change', args => this.onSelectChange(args))
this.selectElement.val(this.voiceId)
}
onSelectChange(args) {
this.voiceId = this.selectElement.find(':selected').val()
updateVoiceMap()
}
}
/**
* Init voiceMapEntries for character select list.
*
*/
export async function initVoiceMap(){
// Clear existing voiceMap state
$('#tts_voicemap_block').empty()
voiceMapEntries = []
// Gate initialization if not enabled or TTS Provider not ready. Prevents error popups.
const enabled = $('#tts_enabled').is(':checked')
if (!enabled){
return
}
// Keep errors inside extension UI rather than toastr. Toastr errors for TTS are annoying.
try {
await ttsProvider.checkReady()
} catch (error) {
const message = `TTS Provider not ready. ${error}`
setTtsStatus(message, false)
return
}
setTtsStatus("TTS Provider Loaded", true)
// Get characters in current chat
const characters = getCharacters()
// Get saved voicemap from provider settings, handling new and old representations
let voiceMapFromSettings = {}
if ("voiceMap" in extension_settings.tts[ttsProviderName]) {
// Handle previous representation
if (typeof extension_settings.tts[ttsProviderName].voiceMap === "string"){
voiceMapFromSettings = parseVoiceMap(extension_settings.tts[ttsProviderName].voiceMap)
// Handle new representation
} else if (typeof extension_settings.tts[ttsProviderName].voiceMap === "object"){
voiceMapFromSettings = extension_settings.tts[ttsProviderName].voiceMap
}
}
// Get voiceIds from provider
let voiceIdsFromProvider
try {
voiceIdsFromProvider = await ttsProvider.fetchTtsVoiceObjects()
}
catch {
toastr.error("TTS Provider failed to return voice ids.")
}
// Build UI using VoiceMapEntry objects
for (const character of characters){
if (character === "SillyTavern System"){
continue
}
// Check provider settings for voiceIds
let voiceId
if (character in voiceMapFromSettings){
voiceId = voiceMapFromSettings[character]
} else {
voiceId = 'disabled'
}
const voiceMapEntry = new VoiceMapEntry(character, voiceId)
voiceMapEntry.addUI(voiceIdsFromProvider)
voiceMapEntries.push(voiceMapEntry)
}
updateVoiceMap()
}
$(document).ready(function () {
function addExtensionControls() {
@ -669,16 +818,23 @@ $(document).ready(function () {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<span>Select TTS Provider</span> </br>
<select id="tts_provider">
<div id="tts_status">
</div>
<span>Select TTS Provider</span> </br>
<div class="tts_block">
<select id="tts_provider" class="flex1">
</select>
<input id="tts_refresh" class="menu_button" type="submit" value="Reload" />
</div>
<div>
<label class="checkbox_label" for="tts_enabled">
<input type="checkbox" id="tts_enabled" name="tts_enabled">
<small>Enabled</small>
</label>
<label class="checkbox_label" for="tts_narrate_user">
<input type="checkbox" id="tts_narrate_user">
<small>Narrate user messages</small>
</label>
<label class="checkbox_label" for="tts_auto_generation">
<input type="checkbox" id="tts_auto_generation">
<small>Auto Generation</small>
@ -696,16 +852,12 @@ $(document).ready(function () {
<small>Narrate only the translated text</small>
</label>
</div>
<label>Voice Map</label>
<textarea id="tts_voice_map" type="text" class="text_pole textarea_compact" rows="4"
placeholder="Enter comma separated map of charName:ttsName. Example: \nAqua:Bella,\nYou:Josh,"></textarea>
<div id="tts_status">
<div id="tts_voicemap_block">
</div>
<hr>
<form id="tts_provider_settings" class="inline-drawer-content">
</form>
<div class="tts_buttons">
<input id="tts_apply" class="menu_button" type="submit" value="Apply" />
<input id="tts_voices" class="menu_button" type="submit" value="Available voices" />
</div>
</div>
@ -714,14 +866,14 @@ $(document).ready(function () {
</div>
`
$('#extensions_settings').append(settingsHtml)
$('#tts_apply').on('click', onApplyClick)
$('#tts_refresh').on('click', onRefreshClick)
$('#tts_enabled').on('click', onEnableClick)
$('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
$('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
$('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_narrate_user').on('click', onNarrateUserClick);
$('#tts_voices').on('click', onTtsVoicesClick)
$('#tts_provider_settings').on('input', onTtsProviderSettingsInput)
for (const provider in ttsProviders) {
$('#tts_provider').append($("<option />").val(provider).text(provider))
}
@ -735,4 +887,6 @@ $(document).ready(function () {
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL) // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged)
eventSource.on(event_types.GROUP_UPDATED, onChatChanged)
})

View File

@ -1,5 +1,6 @@
import { getRequestHeaders } from "../../../script.js"
import { getPreviewString } from "./index.js"
import { getRequestHeaders, callPopup } from "../../../script.js"
import { getPreviewString, saveTtsProviderSettings } from "./index.js"
import { initVoiceMap } from "./index.js"
export { NovelTtsProvider }
@ -14,24 +15,69 @@ class NovelTtsProvider {
audioElement = document.createElement('audio')
defaultSettings = {
voiceMap: {}
voiceMap: {},
customVoices: []
}
get settingsHtml() {
let html = `Use NovelAI's TTS engine.<br>
The Voice IDs in the preview list are only examples, as it can be any string of text. Feel free to try different options!<br>
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>`;
let html = `
<div class="novel_tts_hints">
<div>Use NovelAI's TTS engine.</div>
<div>
The default Voice IDs are only examples. Add custom voices and Novel will create a new random voice for it.
Feel free to try different options!
</div>
<i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
</div>
<label for="tts-novel-custom-voices-add">Custom Voices</label>
<div class="tts_custom_voices">
<select id="tts-novel-custom-voices-select"><select>
<i id="tts-novel-custom-voices-add" class="tts-button fa-solid fa-plus fa-xl success" title="Add"></i>
<i id="tts-novel-custom-voices-delete" class="tts-button fa-solid fa-xmark fa-xl failure" title="Delete"></i>
</div>
`;
return html;
}
onSettingsChange() {
// Add a new Novel custom voice to provider
async addCustomVoice(){
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input')
this.settings.customVoices.push(voiceName)
this.populateCustomVoices()
initVoiceMap() // Update TTS extension voiceMap
saveTtsProviderSettings()
}
loadSettings(settings) {
// Delete selected custom voice from provider
deleteCustomVoice() {
const selected = $("#tts-novel-custom-voices-select").find(':selected').val();
const voiceIndex = this.settings.customVoices.indexOf(selected);
if (voiceIndex !== -1) {
this.settings.customVoices.splice(voiceIndex, 1);
}
this.populateCustomVoices()
initVoiceMap() // Update TTS extension voiceMap
saveTtsProviderSettings()
}
// Create the UI dropdown list of voices in provider
populateCustomVoices(){
let voiceSelect = $("#tts-novel-custom-voices-select")
voiceSelect.empty()
this.settings.customVoices.forEach(voice => {
voiceSelect.append(`<option>${voice}</option>`)
})
}
async loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
}
$("#tts-novel-custom-voices-add").on('click', () => (this.addCustomVoice()))
$("#tts-novel-custom-voices-delete").on('click',() => (this.deleteCustomVoice()))
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
@ -44,11 +90,18 @@ class NovelTtsProvider {
}
}
this.populateCustomVoices()
await this.checkReady()
console.info("Settings loaded")
}
// Perform a simple readiness check by trying to fetch voiceIds
// Doesnt really do much for Novel, not seeing a good way to test this at the moment.
async checkReady(){
await this.fetchTtsVoiceObjects()
}
async onApplyClick() {
async onRefreshClick() {
return
}
@ -72,8 +125,8 @@ class NovelTtsProvider {
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
const voices = [
async fetchTtsVoiceObjects() {
let voices = [
{ name: 'Ligeia', voice_id: 'Ligeia', lang: 'en-US', preview_url: false },
{ name: 'Aini', voice_id: 'Aini', lang: 'en-US', preview_url: false },
{ name: 'Orea', voice_id: 'Orea', lang: 'en-US', preview_url: false },
@ -89,6 +142,12 @@ class NovelTtsProvider {
{ name: 'Lam', voice_id: 'Lam', lang: 'en-US', preview_url: false },
];
// Add in custom voices to the map
let addVoices = this.settings.customVoices.map(voice =>
({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false })
)
voices = voices.concat(addVoices)
return voices;
}

View File

@ -0,0 +1,71 @@
# Provider Requirements.
Because I don't know how, or if you can, and/or maybe I am just too lazy to implement interfaces in JS, here's the requirements of a provider that the extension needs to operate.
### class YourTtsProvider
#### Required
Exported for use in extension index.js, and added to providers list in index.js
1. generateTts(text, voiceId)
2. fetchTtsVoiceObjects()
3. onRefreshClick()
4. checkReady()
5. loadSettings(settingsObject)
6. settings field
7. settingsHtml field
#### Optional
1. previewTtsVoice()
2. separator field
# Requirement Descriptions
### generateTts(text, voiceId)
Must return `audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']`
Must take text to be rendered and the voiceId to identify the voice to be used
### fetchTtsVoiceObjects()
Required.
Used by the TTS extension to get a list of voice objects from the provider.
Must return an list of voice objects representing the available voices.
1. name: a friendly user facing name to assign to characters. Shows in dropdown list next to user.
2. voice_id: the provider specific id of the voice used in fetchTtsGeneration() call
3. preview_url: a URL to a local audio file that will be used to sample voices
4. lang: OPTIONAL language string
### getVoice(voiceName)
Required.
Must return a single voice object matching the provided voiceName. The voice object must have the following at least:
1. name: a friendly user facing name to assign to characters. Shows in dropdown list next to user.
2. voice_id: the provider specific id of the voice used in fetchTtsGeneration() call
3. preview_url: a URL to a local audio file that will be used to sample voices
4. lang: OPTIONAL language indicator
### onRefreshClick()
Required.
Users click this button to reconnect/reinit the selected provider.
Responds to the user clicking the refresh button, which is intended to re-initialize the Provider into a working state, like retrying connections or checking if everything is loaded.
### checkReady()
Required.
Return without error to let TTS extension know that the provider is ready.
Return an error to block the main TTS extension for initializing the provider and UI. The error will be put in the TTS extension UI directly.
### loadSettings(settingsObject)
Required.
Handle the input settings from the TTS extension on provider load.
Put code in here to load your provider settings.
### settings field
Required, used for storing any provider state that needs to be saved.
Anything stored in this field is automatically persisted under extension_settings[providerName] by the main extension in `saveTtsProviderSettings()`, as well as loaded when the provider is selected in `loadTtsProvider(provider)`.
TTS extension doesn't expect any specific contents.
### settingsHtml field
Required, injected into the TTS extension UI. Besides adding it, not relied on by TTS extension directly.
### previewTtsVoice()
Optional.
Function to handle playing previews of voice samples if no direct preview_url is available in fetchTtsVoiceObjects() response
### separator field
Optional.
Used when narrate quoted text is enabled.
Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...`

View File

@ -1,4 +1,5 @@
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
import { saveTtsProviderSettings } from "./index.js"
export { SileroTtsProvider }
@ -8,6 +9,7 @@ class SileroTtsProvider {
//########//
settings
ready = false
voices = []
separator = ' .. '
@ -29,9 +31,10 @@ class SileroTtsProvider {
onSettingsChange() {
// Used when provider settings are updated from UI
this.settings.provider_endpoint = $('#silero_tts_endpoint').val()
saveTtsProviderSettings()
}
loadSettings(settings) {
async loadSettings(settings) {
// Pupulate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings")
@ -60,11 +63,19 @@ class SileroTtsProvider {
}, 2000);
$('#silero_tts_endpoint').val(this.settings.provider_endpoint)
$('#silero_tts_endpoint').on("input", () => {this.onSettingsChange()})
await this.checkReady()
console.info("Settings loaded")
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
await this.fetchTtsVoiceObjects()
}
async onApplyClick() {
async onRefreshClick() {
return
}
@ -74,7 +85,7 @@ class SileroTtsProvider {
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceIds()
this.voices = await this.fetchTtsVoiceObjects()
}
const match = this.voices.filter(
sileroVoice => sileroVoice.name == voiceName
@ -93,7 +104,7 @@ class SileroTtsProvider {
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceIds() {
async fetchTtsVoiceObjects() {
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.json()}`)

View File

@ -50,4 +50,41 @@
.voice_preview .fa-play {
cursor: pointer;
}
}
.tts-button {
margin: 0;
outline: none;
border: none;
cursor: pointer;
transition: 0.3s;
opacity: 0.7;
align-items: center;
justify-content: center;
}
.tts-button:hover {
opacity: 1;
}
.tts_block {
display: flex;
align-items: baseline;
column-gap: 5px;
flex-wrap: wrap;
}
.tts_custom_voices {
display: flex;
align-items: baseline;
gap: 5px;
}
.novel_tts_hints {
font-size: calc(0.9 * var(--mainFontSize));
display: flex;
flex-direction: column;
gap: 5px;
margin-bottom: 5px;
}

View File

@ -1,7 +1,7 @@
import { isMobile } from "../../RossAscends-mods.js";
import { getPreviewString } from "./index.js";
import { talkingAnimation } from './index.js';
import { saveTtsProviderSettings } from "./index.js"
export { SystemTtsProvider }
/**
@ -80,6 +80,7 @@ class SystemTtsProvider {
//########//
settings
ready = false
voices = []
separator = ' ... '
@ -106,10 +107,10 @@ class SystemTtsProvider {
this.settings.pitch = Number($('#system_tts_pitch').val());
$('#system_tts_pitch_output').text(this.settings.pitch);
$('#system_tts_rate_output').text(this.settings.rate);
console.log('Save changes');
saveTtsProviderSettings()
}
loadSettings(settings) {
async loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider settings");
@ -143,19 +144,29 @@ class SystemTtsProvider {
$('#system_tts_rate').val(this.settings.rate || this.defaultSettings.rate);
$('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch);
// Trigger updates
$('#system_tts_rate').on("input", () =>{this.onSettingsChange()})
$('#system_tts_rate').on("input", () => {this.onSettingsChange()})
$('#system_tts_pitch_output').text(this.settings.pitch);
$('#system_tts_rate_output').text(this.settings.rate);
console.info("Settings loaded");
}
async onApplyClick() {
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady(){
await this.fetchTtsVoiceObjects()
}
async onRefreshClick() {
return
}
//#################//
// TTS Interfaces //
//#################//
fetchTtsVoiceIds() {
fetchTtsVoiceObjects() {
if (!('speechSynthesis' in window)) {
return [];
}

View File

@ -1,3 +1,4 @@
import { registerDebugFunction } from "./power-user.js";
import { waitUntilCondition } from "./utils.js";
const storageKey = "language";
@ -48,9 +49,9 @@ function getMissingTranslations() {
console.table(uniqueMissingData);
console.log(missingDataMap);
}
window["getMissingTranslations"] = getMissingTranslations;
toastr.success(`Found ${uniqueMissingData.length} missing translations. See browser console for details.`);
}
export function applyLocale(root = document) {
const overrideLanguage = localStorage.getItem("language");
@ -106,7 +107,6 @@ function addLanguagesToDropdown() {
jQuery(async () => {
waitUntilCondition(() => !!localeData);
window["applyLocale"] = applyLocale;
applyLocale();
addLanguagesToDropdown();
@ -121,4 +121,7 @@ jQuery(async () => {
location.reload();
});
registerDebugFunction('getMissingTranslations', 'Get missing translations', 'Detects missing localization data and dumps the data into the browser console.', getMissingTranslations);
registerDebugFunction('applyLocale', 'Apply locale', 'Reapplies the currently selected locale to the page.', applyLocale);
});

View File

@ -646,6 +646,24 @@ export function adjustNovelInstructionPrompt(prompt) {
return stripedPrompt;
}
function tryParseStreamingError(decoded) {
try {
const data = JSON.parse(decoded);
if (!data) {
return;
}
if (data.message && data.statusCode >= 400) {
toastr.error(data.message, 'Error');
throw new Error(data);
}
}
catch {
// No JSON. Do nothing.
}
}
export async function generateNovelWithStreaming(generate_data, signal) {
generate_data.streaming = nai_settings.streaming_novel;
@ -663,12 +681,14 @@ export async function generateNovelWithStreaming(generate_data, signal) {
let messageBuffer = "";
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
let decoded = decoder.decode(value);
let eventList = [];
tryParseStreamingError(decoded);
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
messageBuffer += response;
messageBuffer += decoded;
eventList = messageBuffer.split("\n\n");
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();

View File

@ -50,6 +50,7 @@ import {
download,
getFileText, getSortableDelay,
parseJsonFile,
resetScrollHeight,
stringFormat,
} from "./utils.js";
import { countTokensOpenAI } from "./tokenizers.js";
@ -3141,6 +3142,10 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this));
});
$("#api_button_openai").on("click", onConnectButtonClick);
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
$("#model_openai_select").on("change", onModelChange);

455
public/scripts/personas.js Normal file
View File

@ -0,0 +1,455 @@
/**
* This is a placeholder file for all the Persona Management code. Will be refactored into a separate file soon.
*/
import { callPopup, characters, chat_metadata, default_avatar, eventSource, event_types, getRequestHeaders, getThumbnailUrl, getUserAvatars, name1, saveMetadata, saveSettingsDebounced, setUserName, this_chid, user_avatar } from "../script.js";
import { persona_description_positions, power_user } from "./power-user.js";
import { getTokenCount } from "./tokenizers.js";
import { debounce, delay } from "./utils.js";
/**
* Uploads an avatar file to the server
* @param {string} url URL for the avatar file
* @param {string} [name] Optional name for the avatar file
* @returns {Promise} Promise object representing the AJAX request
*/
async function uploadUserAvatar(url, name) {
const fetchResult = await fetch(url);
const blob = await fetchResult.blob();
const file = new File([blob], "avatar.png", { type: "image/png" });
const formData = new FormData();
formData.append("avatar", file);
if (name) {
formData.append("overwrite_name", name);
}
return jQuery.ajax({
type: "POST",
url: "/uploaduseravatar",
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function () {
await getUserAvatars();
},
});
}
async function createDummyPersona() {
await uploadUserAvatar(default_avatar);
}
async function convertCharacterToPersona() {
const avatarUrl = characters[this_chid]?.avatar;
if (!avatarUrl) {
console.log("No avatar found for this character");
return;
}
const name = characters[this_chid]?.name;
let description = characters[this_chid]?.description;
const overwriteName = `${name} (Persona).png`;
if (overwriteName in power_user.personas) {
const confirmation = await callPopup("This character exists as a persona already. Are you sure want to overwrite it?", "confirm", "", { okButton: 'Yes' });
if (confirmation === false) {
console.log("User cancelled the overwrite of the persona");
return;
}
}
if (description.includes('{{char}}') || description.includes('{{user}}')) {
await delay(500);
const confirmation = await callPopup("This character has a description that uses {{char}} or {{user}} macros. Do you want to swap them in the persona description?", "confirm", "", { okButton: 'Yes' });
if (confirmation) {
description = description.replace(/{{char}}/gi, '{{personaChar}}').replace(/{{user}}/gi, '{{personaUser}}');
description = description.replace(/{{personaUser}}/gi, '{{char}}').replace(/{{personaChar}}/gi, '{{user}}');
}
}
const thumbnailAvatar = getThumbnailUrl('avatar', avatarUrl);
await uploadUserAvatar(thumbnailAvatar, overwriteName);
power_user.personas[overwriteName] = name;
power_user.persona_descriptions[overwriteName] = {
description: description,
position: persona_description_positions.IN_PROMPT,
};
// If the user is currently using this persona, update the description
if (user_avatar === overwriteName) {
power_user.persona_description = description;
}
saveSettingsDebounced();
console.log("Persona for character created");
toastr.success(`You can now select ${name} as a persona in the Persona Management menu.`, 'Persona Created');
// Refresh the persona selector
await getUserAvatars();
// Reload the persona description
setPersonaDescription();
}
/**
* Counts the number of tokens in a persona description.
*/
const countPersonaDescriptionTokens = debounce(() => {
const description = String($("#persona_description").val());
const count = getTokenCount(description);
$("#persona_description_token_count").text(String(count));
}, 1000);
export function setPersonaDescription() {
if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) {
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
}
$("#persona_description").val(power_user.persona_description);
$("#persona_description_position")
.val(power_user.persona_description_position)
.find(`option[value='${power_user.persona_description_position}']`)
.attr("selected", String(true));
countPersonaDescriptionTokens();
}
export function autoSelectPersona(name) {
for (const [key, value] of Object.entries(power_user.personas)) {
if (value === name) {
console.log(`Auto-selecting persona ${key} for name ${name}`);
$(`.avatar[imgfile="${key}"]`).trigger('click');
return;
}
}
}
async function bindUserNameToPersona() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
const existingPersona = power_user.personas[avatarId];
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>(If empty name is provided, this will unbind the name from this avatar)', 'input', existingPersona || '');
// If the user clicked cancel, don't do anything
if (personaName === false) {
return;
}
if (personaName.length > 0) {
// If the user clicked ok and entered a name, bind the name to the persona
console.log(`Binding persona ${avatarId} to name ${personaName}`);
power_user.personas[avatarId] = personaName;
const descriptor = power_user.persona_descriptions[avatarId];
const isCurrentPersona = avatarId === user_avatar;
// Create a description object if it doesn't exist
if (!descriptor) {
// If the user is currently using this persona, set the description to the current description
power_user.persona_descriptions[avatarId] = {
description: isCurrentPersona ? power_user.persona_description : '',
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
};
}
// If the user is currently using this persona, update the name
if (isCurrentPersona) {
console.log(`Auto-updating user name to ${personaName}`);
setUserName(personaName);
}
} else {
// If the user clicked ok, but didn't enter a name, delete the persona
console.log(`Unbinding persona ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
}
saveSettingsDebounced();
await getUserAvatars();
setPersonaDescription();
}
export function selectCurrentPersona() {
const personaName = power_user.personas[user_avatar];
if (personaName && name1 !== personaName) {
const lockedPersona = chat_metadata['persona'];
if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) {
toastr.info(
`To permanently set "${personaName}" as the selected persona, unlock and relock it using the "Lock" button. Otherwise, the selection resets upon reloading the chat.`,
`This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`,
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }
);
}
setUserName(personaName);
const descriptor = power_user.persona_descriptions[user_avatar];
if (descriptor) {
power_user.persona_description = descriptor.description;
power_user.persona_description_position = descriptor.position;
} else {
power_user.persona_description = '';
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT };
}
setPersonaDescription();
}
}
async function lockUserNameToChat() {
if (chat_metadata['persona']) {
console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`);
delete chat_metadata['persona'];
await saveMetadata();
if (power_user.persona_show_notifications) {
toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked');
}
updateUserLockIcon();
return;
}
if (!(user_avatar in power_user.personas)) {
console.log(`Creating a new persona ${user_avatar}`);
if (power_user.persona_show_notifications) {
toastr.info(
'Creating a new persona for currently selected user name and avatar...',
'Persona not set for this avatar',
{ timeOut: 10000, extendedTimeOut: 20000, },
);
}
power_user.personas[user_avatar] = name1;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT };
}
chat_metadata['persona'] = user_avatar;
await saveMetadata();
saveSettingsDebounced();
console.log(`Locking persona for this chat ${user_avatar}`);
if (power_user.persona_show_notifications) {
toastr.success(`User persona is locked to ${name1} in this chat`);
}
updateUserLockIcon();
}
async function deleteUserAvatar() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
if (avatarId == user_avatar) {
console.warn(`User tried to delete their current avatar ${avatarId}`);
toastr.warning('You cannot delete the avatar you are currently using', 'Warning');
return;
}
const confirm = await callPopup('<h3>Are you sure you want to delete this avatar?</h3>All information associated with its linked persona will be lost.', 'confirm');
if (!confirm) {
console.debug('User cancelled deleting avatar');
return;
}
const request = await fetch("/deleteuseravatar", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"avatar": avatarId,
}),
});
if (request.ok) {
console.log(`Deleted avatar ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
if (avatarId === power_user.default_persona) {
toastr.warning('The default persona was deleted. You will need to set a new default persona.', 'Default persona deleted');
power_user.default_persona = null;
}
if (avatarId === chat_metadata['persona']) {
toastr.warning('The locked persona was deleted. You will need to set a new persona for this chat.', 'Persona deleted');
delete chat_metadata['persona'];
await saveMetadata();
}
saveSettingsDebounced();
await getUserAvatars();
updateUserLockIcon();
}
}
function onPersonaDescriptionInput() {
power_user.persona_description = String($("#persona_description").val());
countPersonaDescriptionTokens();
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: Number($("#persona_description_position").find(":selected").val()),
};
power_user.persona_descriptions[user_avatar] = object;
}
object.description = power_user.persona_description;
}
saveSettingsDebounced();
}
function onPersonaDescriptionPositionInput() {
power_user.persona_description_position = Number(
$("#persona_description_position").find(":selected").val()
);
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: power_user.persona_description_position,
};
power_user.persona_descriptions[user_avatar] = object;
}
object.position = power_user.persona_description_position;
}
saveSettingsDebounced();
}
async function setDefaultPersona() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
const currentDefault = power_user.default_persona;
if (power_user.personas[avatarId] === undefined) {
console.warn(`No persona name found for avatar ${avatarId}`);
toastr.warning('You must bind a name to this persona before you can set it as the default.', 'Persona name not set');
return;
}
const personaName = power_user.personas[avatarId];
if (avatarId === currentDefault) {
const confirm = await callPopup('Are you sure you want to remove the default persona?', 'confirm');
if (!confirm) {
console.debug('User cancelled removing default persona');
return;
}
console.log(`Removing default persona ${avatarId}`);
if (power_user.persona_show_notifications) {
toastr.info('This persona will no longer be used by default when you open a new chat.', `Default persona removed`);
}
delete power_user.default_persona;
} else {
const confirm = await callPopup(`<h3>Are you sure you want to set "${personaName}" as the default persona?</h3>
This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`, 'confirm');
if (!confirm) {
console.debug('User cancelled setting default persona');
return;
}
power_user.default_persona = avatarId;
if (power_user.persona_show_notifications) {
toastr.success('This persona will be used by default when you open a new chat.', `Default persona set to ${personaName}`);
}
}
saveSettingsDebounced();
await getUserAvatars();
}
function updateUserLockIcon() {
const hasLock = !!chat_metadata['persona'];
$('#lock_user_name').toggleClass('fa-unlock', !hasLock);
$('#lock_user_name').toggleClass('fa-lock', hasLock);
}
function setChatLockedPersona() {
// Define a persona for this chat
let chatPersona = '';
if (chat_metadata['persona']) {
// If persona is locked in chat metadata, select it
console.log(`Using locked persona ${chat_metadata['persona']}`);
chatPersona = chat_metadata['persona'];
} else if (power_user.default_persona) {
// If default persona is set, select it
console.log(`Using default persona ${power_user.default_persona}`);
chatPersona = power_user.default_persona;
}
// No persona set: user current settings
if (!chatPersona) {
console.debug('No default or locked persona set for this chat');
return;
}
// Find the avatar file
const personaAvatar = $(`.avatar[imgfile="${chatPersona}"]`).trigger('click');
// Avatar missing (persona deleted)
if (chat_metadata['persona'] && personaAvatar.length == 0) {
console.warn('Persona avatar not found, unlocking persona');
delete chat_metadata['persona'];
updateUserLockIcon();
return;
}
// Default persona missing
if (power_user.default_persona && personaAvatar.length == 0) {
console.warn('Default persona avatar not found, clearing default persona');
power_user.default_persona = null;
saveSettingsDebounced();
return;
}
// Persona avatar found, select it
personaAvatar.trigger('click');
updateUserLockIcon();
}
export function initPersonas() {
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.set_default_persona', setDefaultPersona);
$(document).on('click', '.delete_avatar', deleteUserAvatar);
$('#lock_user_name').on('click', lockUserNameToChat);
$("#create_dummy_persona").on('click', createDummyPersona);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
eventSource.on("charManagementDropdown", (target) => {
if (target === 'convert_to_persona') {
convertCharacterToPersona();
}
});
eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona);
}

View File

@ -13,7 +13,8 @@ import {
getCurrentChatId,
printCharacters,
setCharacterId,
setEditedMessageId
setEditedMessageId,
renderTemplate,
} from "../script.js";
import { isMobile, initMovingUI } from "./RossAscends-mods.js";
import {
@ -50,6 +51,11 @@ const defaultStoryString = "{{#if system}}{{system}}\n{{/if}}{{#if description}}
const defaultExampleSeparator = '***';
const defaultChatStart = '***';
export const ui_mode = {
SIMPLE: 0,
POWER: 1,
}
const avatar_styles = {
ROUND: 0,
RECTANGULAR: 1,
@ -74,7 +80,10 @@ const send_on_enter_options = {
}
export const persona_description_positions = {
BEFORE_CHAR: 0,
IN_PROMPT: 0,
/**
* @deprecated Use persona_description_positions.IN_PROMPT instead.
*/
AFTER_CHAR: 1,
TOP_AN: 2,
BOTTOM_AN: 3,
@ -97,6 +106,7 @@ let power_user = {
multigen_next_chunks: 30,
markdown_escape_strings: '',
ui_mode: ui_mode.POWER,
fast_ui_mode: true,
avatar_style: avatar_styles.ROUND,
chat_display: chat_styles.DEFAULT,
@ -189,7 +199,7 @@ let power_user = {
persona_descriptions: {},
persona_description: '',
persona_description_position: persona_description_positions.BEFORE_CHAR,
persona_description_position: persona_description_positions.IN_PROMPT,
persona_show_notifications: true,
custom_stopping_strings: '',
@ -232,6 +242,13 @@ const storage_keys = {
};
let browser_has_focus = true;
const debug_functions = [];
export function switchSimpleMode() {
$('[data-newbie-hidden]').each(function () {
$(this).toggleClass('displayNone', power_user.ui_mode === ui_mode.SIMPLE);
});
}
function playMessageSound() {
if (!power_user.play_message_sound) {
@ -653,6 +670,22 @@ async function applyMovingUIPreset(name) {
loadMovingUIState()
}
/**
* Register a function to be executed when the debug menu is opened.
* @param {string} functionId Unique ID for the function.
* @param {string} name Name of the function.
* @param {string} description Description of the function.
* @param {function} func Function to be executed.
*/
export function registerDebugFunction(functionId, name, description, func) {
debug_functions.push({ functionId, name, description, func });
}
function showDebugMenu() {
const template = renderTemplate('debug', { functions: debug_functions });
callPopup(template, 'text', '', { wide: true, large: true });
}
switchUiMode();
applyFontScale('forced');
applyThemeColor();
@ -720,6 +753,10 @@ function loadPowerUserSettings(settings, data) {
power_user.chat_width = 50;
}
if (power_user.tokenizer === tokenizers.LEGACY) {
power_user.tokenizer = tokenizers.GPT2;
}
$('#relaxed_api_urls').prop("checked", power_user.relaxed_api_urls);
$('#trim_spaces').prop("checked", power_user.trim_spaces);
$('#continue_on_send').prop("checked", power_user.continue_on_send);
@ -799,6 +836,7 @@ function loadPowerUserSettings(settings, data) {
$("#user-mes-blur-tint-color-picker").attr('color', power_user.user_mes_blur_tint_color);
$("#bot-mes-blur-tint-color-picker").attr('color', power_user.bot_mes_blur_tint_color);
$("#shadow-color-picker").attr('color', power_user.shadow_color);
$("#ui_mode_select").val(power_user.ui_mode).find(`option[value="${power_user.ui_mode}"]`).attr('selected', true);
for (const theme of themes) {
const option = document.createElement('option');
@ -826,6 +864,7 @@ function loadPowerUserSettings(settings, data) {
switchSpoilerMode();
loadMovingUIState();
loadCharListState();
switchSimpleMode();
}
async function loadCharListState() {
@ -1591,7 +1630,7 @@ function setAvgBG() {
/**
* Gets the custom stopping strings from the power user settings.
* @param {number | undefined} limit Number of strings to return. If undefined, returns all strings.
* @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
* @returns {string[]} An array of custom stopping strings
*/
export function getCustomStoppingStrings(limit = undefined) {
@ -1602,15 +1641,27 @@ export function getCustomStoppingStrings(limit = undefined) {
}
// Parse the JSON string
const strings = JSON.parse(power_user.custom_stopping_strings);
let strings = JSON.parse(power_user.custom_stopping_strings);
// Make sure it's an array
if (!Array.isArray(strings)) {
return [];
}
// Make sure all the elements are strings. Apply the limit.
return strings.filter((s) => typeof s === 'string').slice(0, limit);
// Make sure all the elements are strings.
strings = strings.filter((s) => typeof s === 'string');
// Substitute params if necessary
if (power_user.custom_stopping_strings_macro) {
strings = strings.map(x => substituteParams(x));
}
// Apply the limit. If limit is 0, return all strings.
if (limit > 0) {
strings = strings.slice(0, limit);
}
return strings;
} catch (error) {
// If there's an error, return an empty array
console.warn('Error parsing custom stopping strings:', error);
@ -2109,6 +2160,28 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#debug_menu').on('click', function () {
showDebugMenu();
});
$("#ui_mode_select").on('change', function () {
const value = $(this).find(':selected').val();
power_user.ui_mode = Number(value);
saveSettingsDebounced();
switchSimpleMode();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);
if (functionRecord) {
functionRecord.func();
} else {
console.warn(`Debug function ${functionId} not found`);
}
});
$(window).on('focus', function () {
browser_has_focus = true;
});

View File

@ -1,6 +1,5 @@
import {
addOneMessage,
autoSelectPersona,
characters,
chat,
chat_metadata,
@ -26,6 +25,7 @@ import { getMessageTimeStamp } from "./RossAscends-mods.js";
import { resetSelectedGroup } from "./group-chats.js";
import { getRegexedString, regex_placement } from "./extensions/regex/engine.js";
import { chat_styles, power_user } from "./power-user.js";
import { autoSelectPersona } from "./personas.js";
export {
executeSlashCommands,
registerSlashCommand,

View File

@ -455,7 +455,7 @@ function onViewTagsListClick() {
$(list).append('<h3>Tags</h3><i>Click on the tag name to edit it.</i><br>');
$(list).append('<i>Click on color box to assign new color.</i><br><br>');
for (const tag of tags.slice().sort((a, b) => a?.name?.localeCompare(b?.name))) {
for (const tag of tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()))) {
const count = everything.filter(x => x == tag.id).length;
const template = $('#tag_view_template .tag_view_item').clone();
template.attr('id', tag.id);

View File

@ -0,0 +1,25 @@
<h3 data-i18n="Debug Menu">Debug Menu</h3>
<div data-i18n="Debug Warning">
Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.
</div>
<table id="debug_table" class="responsiveTable">
{{#each functions}}
{{#with this}}
<tr>
<td>
<div class="justifyLeft">
<b>{{this.name}}</b>
</div>
<div class="justifyLeft">
{{this.description}}
</div>
<div class="flex-container justifyCenter">
<div class="menu_button menu_button_icon" data-debug-function="{{this.functionId}}" data-i18n="Execute">
Execute
</div>
</div>
</td>
</tr>
{{/with}}
{{/each}}
</table>

View File

@ -1,6 +1,5 @@
import { characters, main_api, nai_settings, online_status, this_chid } from "../script.js";
import { power_user } from "./power-user.js";
import { encode } from "../lib/gpt-2-3-tokenizer/mod.js";
import { power_user, registerDebugFunction } from "./power-user.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
import { groups, selected_group } from "./group-chats.js";
import { getStringHash } from "./utils.js";
@ -12,7 +11,10 @@ const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown';
export const tokenizers = {
NONE: 0,
GPT2: 1,
CLASSIC: 2,
/**
* @deprecated Use GPT2 instead.
*/
LEGACY: 2,
LLAMA: 3,
NERD: 4,
NERD2: 5,
@ -57,17 +59,16 @@ async function resetTokenCache() {
console.debug('Chat Completions: resetting token cache');
Object.keys(tokenCache).forEach(key => delete tokenCache[key]);
await objectStore.removeItem('tokenCache');
toastr.success('Token cache cleared. Please reload the chat to re-tokenize it.');
} catch (e) {
console.log('Chat Completions: unable to reset token cache', e);
}
}
window['resetTokenCache'] = resetTokenCache;
function getTokenizerBestMatch() {
if (main_api === 'novel') {
if (nai_settings.model_novel.includes('krake') || nai_settings.model_novel.includes('euterpe')) {
return tokenizers.CLASSIC;
return tokenizers.GPT2;
}
if (nai_settings.model_novel.includes('clio')) {
return tokenizers.NERD;
@ -104,8 +105,6 @@ function callTokenizer(type, str, padding) {
return guesstimate(str) + padding;
case tokenizers.GPT2:
return countTokensRemote('/tokenize_gpt2', str, padding);
case tokenizers.CLASSIC:
return encode(str).length + padding;
case tokenizers.LLAMA:
return countTokensRemote('/tokenize_llama', str, padding);
case tokenizers.NERD:
@ -438,4 +437,5 @@ export function decodeTextTokens(tokenizerType, ids) {
jQuery(async () => {
await loadTokenCache();
registerDebugFunction('resetTokenCache', 'Reset token cache', 'Purges the calculated token counts. Use this if you want to force a full re-tokenization of all chats or suspect the token counts are wrong.', resetTokenCache);
});

View File

@ -517,7 +517,7 @@ hr {
}
#send_form.no-connection {
background-color: rgba(100, 0, 0, 0.5) !important;
background-color: var(--crimson70a) !important;
}
#send_but_sheld {
@ -882,6 +882,13 @@ textarea {
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 5px 10px;
scrollbar-width: thin;
max-height: 90vh;
max-height: 90svh;
}
textarea.autoSetHeight {
max-height: 50vh;
max-height: 50svh;
}
input,
@ -1768,10 +1775,10 @@ grammarly-extension {
top: 50%;
transform: translateY(-50%);
text-align: center;
box-shadow: 0px 0px 14px var(--black50a);
box-shadow: 0px 0px 14px var(--black70a);
border: 1px solid var(--white30a);
padding: 4px;
background-color: var(--black30a);
background-color: var(--black50a);
border-radius: 10px;
max-height: 90vh;
max-height: 90svh;
@ -1980,14 +1987,19 @@ grammarly-extension {
/* ------ online status indicators and texts. 2 = kobold AI, 3 = Novel AI ----------*/
#online_status2,
#online_status3,
#online_status_horde,
.online_status4 {
opacity: 0.5;
opacity: 0.8;
margin-top: 2px;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 5px;
}
#online_status_indicator2,
#online_status_indicator3,
#online_status_indicator_horde,
.online_status_indicator4 {
border-radius: 7px;
@ -1998,32 +2010,13 @@ grammarly-extension {
}
#online_status_text2,
#online_status_text3,
#online_status_text_horde,
.online_status_text4 {
margin-left: 4px;
display: inline-block;
}
#online_status3 {
opacity: 0.5;
margin-top: 2px;
margin-bottom: 30px;
}
#online_status_indicator3 {
border-radius: 7px;
width: 14px;
height: 14px;
background-color: red;
display: inline-block;
}
#online_status_text3 {
margin-left: 4px;
display: inline-block;
}
#horde_model {
height: 150px;
}
@ -3547,6 +3540,17 @@ a {
font-weight: bold;
}
.onboarding {
display: flex;
flex-direction: column;
gap: 10px;
text-align: left;
}
.onboarding > h3 {
align-self: center;
}
#select_chat_search {
background-color: transparent;
border: none;

View File

@ -10,7 +10,7 @@
"shadow_color": "rgba(0, 0, 0, 1)",
"shadow_width": 2,
"font_scale": 1,
"fast_ui_mode": true,
"fast_ui_mode": false,
"waifuMode": false,
"avatar_style": 0,
"chat_display": 0,
@ -18,4 +18,4 @@
"sheld_width": 0,
"timer_enabled": false,
"hotswap_enabled": true
}
}

View File

@ -24,7 +24,6 @@ function createDefaultFiles() {
}
}
const process = require('process')
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
const net = require("net");
@ -149,11 +148,8 @@ let api_openai = "https://api.openai.com/v1";
let api_claude = "https://api.anthropic.com/v1";
let main_api = "kobold";
let response_generate_novel;
let characters = {};
let response_dw_bg;
let first_run = true;
let color = {
byNum: (mess, fgNum) => {
@ -4149,7 +4145,7 @@ function backupSettings() {
const backupFile = path.join(directories.backups, `settings_${generateTimestamp()}.json`);
fs.copyFileSync(SETTINGS_FILE, backupFile);
let files = fs.readdirSync(directories.backups);
let files = fs.readdirSync(directories.backups).filter(f => f.startsWith('settings_'));
if (files.length > MAX_BACKUPS) {
files = files.map(f => path.join(directories.backups, f));
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
@ -4909,7 +4905,7 @@ async function readAllChunks(readableStream) {
});
readableStream.on('end', () => {
console.log('Finished reading the stream.');
//console.log('Finished reading the stream.');
resolve(chunks);
});