Merge branch 'staging' of https://github.com/city-unit/SillyTavern into feature/exorcism
This commit is contained in:
commit
165d4b3b75
|
@ -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/
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": ""
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
396
public/script.js
396
public/script.js
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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> <span class="hypebot_text">${text}</span>`);
|
||||
return `<span class="hypebot_name">${settings.name} ${getVerb(text)}:</span> <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>');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ let presets = [];
|
|||
let selected_preset = '';
|
||||
|
||||
const defaultSettings = {
|
||||
quickReplyEnabled: true,
|
||||
quickReplyEnabled: false,
|
||||
numberOfSlots: 5,
|
||||
quickReplySlots: [],
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)`
|
||||
|
|
|
@ -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();
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 `...`
|
|
@ -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()}`)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue