Merge branch 'staging' into generate-array

This commit is contained in:
Cohee
2023-08-26 01:07:19 +03:00
48 changed files with 2127 additions and 415 deletions

View File

@@ -1,5 +1,5 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams } from "../script.js";
import { isSubsetOf, debounce, waitUntilCondition } from "./utils.js";
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate } from "../script.js";
import { isSubsetOf } from "./utils.js";
export {
getContext,
getApiUrl,
@@ -48,6 +48,18 @@ export function saveMetadataDebounced() {
export const extensionsHandlebars = Handlebars.create();
/**
* Provides an ability for extensions to render HTML templates.
* Templates sanitation and localization is forced.
* @param {string} extensionName Extension name
* @param {string} templateId Template ID
* @param {object} templateData Additional data to pass to the template
* @returns {string} Rendered HTML
*/
export function renderExtensionTemplate(extensionName, templateId, templateData = {}, sanitize = true, localize = true) {
return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
}
/**
* Registers a Handlebars helper for use in extensions.
* @param {string} name Handlebars helper name

View File

@@ -0,0 +1,246 @@
/*
TODO:
- Check failed install file (0kb size ?)
*/
//const DEBUG_TONY_SAMA_FORK_MODE = false
import { getRequestHeaders, callPopup } from "../../../script.js";
export { MODULE_NAME };
const MODULE_NAME = 'Assets';
const DEBUG_PREFIX = "<Assets module> ";
let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json"
const extensionName = "assets";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
// DBG
//if (DEBUG_TONY_SAMA_FORK_MODE)
// ASSETS_JSON_URL = "https://raw.githubusercontent.com/Tony-sama/SillyTavern-Content/main/index.json"
let availableAssets = {};
let currentAssets = {};
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
}
function downloadAssetsList(url) {
updateCurrentAssets().then(function () {
fetch(url)
.then(response => response.json())
.then(json => {
availableAssets = {};
$("#assets_menu").empty();
console.debug(DEBUG_PREFIX, "Received assets dictionary", json);
for (const i of json) {
//console.log(DEBUG_PREFIX,i)
if (availableAssets[i["type"]] === undefined)
availableAssets[i["type"]] = [];
availableAssets[i["type"]].push(i);
}
console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets);
for (const assetType in availableAssets) {
let assetTypeMenu = $('<div />', { id: "assets_audio_ambient_div", class: "assets-list-div" });
assetTypeMenu.append(`<h3>${assetType}</h3>`)
for (const i in availableAssets[assetType]) {
const asset = availableAssets[assetType][i];
const elemId = `assets_install_${assetType}_${i}`;
let element = $('<button />', { id: elemId, type: "button", class: "asset-download-button menu_button" })
const label = $("<i class=\"fa-solid fa-download fa-xl\"></i>");
element.append(label);
//if (DEBUG_TONY_SAMA_FORK_MODE)
// assetUrl = assetUrl.replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]);
const assetInstall = async function () {
element.off("click");
label.removeClass("fa-download");
this.classList.add('asset-download-button-loading');
await installAsset(asset["url"], assetType, asset["id"]);
label.addClass("fa-check");
this.classList.remove('asset-download-button-loading');
element.on("click", assetDelete);
element.on("mouseenter", function(){
label.removeClass("fa-check");
label.addClass("fa-trash");
label.addClass("redOverlayGlow");
}).on("mouseleave", function(){
label.addClass("fa-check");
label.removeClass("fa-trash");
label.removeClass("redOverlayGlow");
});
};
const assetDelete = async function() {
element.off("click");
await deleteAsset(assetType, asset["id"]);
label.removeClass("fa-check");
label.removeClass("redOverlayGlow");
label.removeClass("fa-trash");
label.addClass("fa-download");
element.off("mouseenter").off("mouseleave");
element.on("click", assetInstall);
}
if (isAssetInstalled(assetType, asset["id"])) {
console.debug(DEBUG_PREFIX, "installed, checked");
label.toggleClass("fa-download");
label.toggleClass("fa-check");
element.on("click", assetDelete);
element.on("mouseenter", function(){
label.removeClass("fa-check");
label.addClass("fa-trash");
label.addClass("redOverlayGlow");
}).on("mouseleave", function(){
label.addClass("fa-check");
label.removeClass("fa-trash");
label.removeClass("redOverlayGlow");
});
}
else {
console.debug(DEBUG_PREFIX, "not installed, unchecked")
element.prop("checked", false);
element.on("click", assetInstall);
}
console.debug(DEBUG_PREFIX, "Created element for BGM", asset["id"])
$(`<i></i>`)
.append(element)
.append(`<span>${asset["id"]}</span>`)
.appendTo(assetTypeMenu);
}
assetTypeMenu.appendTo("#assets_menu");
}
$("#assets_menu").show();
})
.catch((error) => {
console.error(error);
toastr.error("Problem with assets URL", DEBUG_PREFIX + "Cannot get assets list");
$('#assets-connect-button').addClass("fa-plug-circle-exclamation");
$('#assets-connect-button').addClass("redOverlayGlow");
});
});
}
function isAssetInstalled(assetType, filename) {
for (const i of currentAssets[assetType]) {
//console.debug(DEBUG_PREFIX,i,filename)
if (i.includes(filename))
return true;
}
return false;
}
async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, "Downloading ", url);
const category = assetType;
try {
const body = { url, category, filename };
const result = await fetch('/asset_download', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
cache: 'no-cache',
});
if (result.ok) {
console.debug(DEBUG_PREFIX, "Download success.")
}
}
catch (err) {
console.log(err);
return [];
}
}
async function deleteAsset(assetType, filename) {
console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename);
const category = assetType;
try {
const body = { category, filename };
const result = await fetch('/asset_delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
cache: 'no-cache',
});
if (result.ok) {
console.debug(DEBUG_PREFIX, "Deletion success.")
}
}
catch (err) {
console.log(err);
return [];
}
}
//#############################//
// API Calls //
//#############################//
async function updateCurrentAssets() {
console.debug(DEBUG_PREFIX, "Checking installed assets...")
try {
const result = await fetch(`/get_assets`, {
method: 'POST',
headers: getRequestHeaders(),
});
currentAssets = result.ok ? (await result.json()) : {};
}
catch (err) {
console.log(err);
}
console.debug(DEBUG_PREFIX, "Current assets found:", currentAssets)
}
//#############################//
// Extension load //
//#############################//
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
const assetsJsonUrl = windowHtml.find('#assets-json-url-field');
assetsJsonUrl.val(ASSETS_JSON_URL);
const connectButton = windowHtml.find('#assets-connect-button');
connectButton.on("click", async function () {
const confirmation = await callPopup(`Are you sure you want to connect to '${assetsJsonUrl.val()}'?`, 'confirm')
if (confirmation) {
try {
console.debug(DEBUG_PREFIX, "Confimation, loading assets...");
downloadAssetsList(assetsJsonUrl.val());
connectButton.removeClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow");
connectButton.addClass("fa-plug-circle-check");
} catch (error) {
console.error('Error:', error);
toastr.error(`Cannot get assets list from ${assetsJsonUrl.val()}`);
connectButton.removeClass("fa-plug-circle-check");
connectButton.addClass("fa-plug-circle-exclamation");
connectButton.removeClass("redOverlayGlow");
}
}
else {
console.debug(DEBUG_PREFIX, "Connection refused by user");
}
});
$('#extensions_settings').append(windowHtml);
});

View File

@@ -0,0 +1,11 @@
{
"display_name": "Assets",
"loading_order": 15,
"requires": [],
"optional": [],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -0,0 +1,79 @@
#assets-json-url-field {
width: 85%;
}
#assets-connect-button {
width: 15%;
margin-left: 5px;
}
.assets-connect-div {
display: flex;
flex-direction: row;
padding: 5px;
}
.assets-list-div i {
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
padding: 5px;
}
.assets-list-div i span{
margin-left: 10px;
}
.asset-download-button {
position: relative;
width: 50px;
padding: 8px 16px;
border: none;
outline: none;
border-radius: 2px;
cursor: pointer;
}
.asset-download-button:active {
background: #007a63;
}
.asset-download-button-text {
font: bold 20px "Quicksand", san-serif;
color: #ffffff;
transition: all 0.2s;
}
.asset-download-button-loading .asset-download-button-text {
visibility: hidden;
opacity: 0;
}
.asset-download-button-loading::after {
content: "";
position: absolute;
width: 16px;
height: 16px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
border: 4px solid transparent;
border-top-color: #ffffff;
border-radius: 50%;
animation: asset-download-button-loading-spinner 1s ease infinite;
}
@keyframes asset-download-button-loading-spinner {
from {
transform: rotate(0turn);
}
to {
transform: rotate(1turn);
}
}

View File

@@ -0,0 +1,17 @@
<div id="assets_ui">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Assets</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="assets-json-url-field">Assets URL</label>
<div class="assets-connect-div">
<input id="assets-json-url-field" class="text_pole widthUnset flex1">
<i id="assets-connect-button" class="menu_button fa-solid fa-plug-circle-exclamation fa-xl redOverlayGlow"></i>
</div>
<div class="inline-drawer-content" id="assets_menu">
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,593 @@
/*
Ideas:
- cross fading between bgm / start a different time
- Background based ambient sounds
- import option on background UI ?
- Allow background music edition using background menu
- https://fontawesome.com/icons/music?f=classic&s=solid
- https://codepen.io/noirsociety/pen/rNQxQwm
- https://codepen.io/xrocker/pen/abdKVGy
*/
import { saveSettingsDebounced, getRequestHeaders } from "../../../script.js";
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
import { isDataURL } from "../../utils.js";
export { MODULE_NAME };
const extensionName = "audio";
const extensionFolderPath = `scripts/extensions/${extensionName}`;
const MODULE_NAME = 'Audio';
const DEBUG_PREFIX = "<Audio module> ";
const UPDATE_INTERVAL = 1000;
const ASSETS_BGM_FOLDER = "bgm";
const ASSETS_AMBIENT_FOLDER = "ambient";
const CHARACTER_BGM_FOLDER = "bgm"
const FALLBACK_EXPRESSION = "neutral";
const DEFAULT_EXPRESSIONS = [
//"talkinghead",
"admiration",
"amusement",
"anger",
"annoyance",
"approval",
"caring",
"confusion",
"curiosity",
"desire",
"disappointment",
"disapproval",
"disgust",
"embarrassment",
"excitement",
"fear",
"gratitude",
"grief",
"joy",
"love",
"nervousness",
"optimism",
"pride",
"realization",
"relief",
"remorse",
"sadness",
"surprise",
"neutral"
];
const SPRITE_DOM_ID = "#expression-image";
let fallback_BGMS = null; // Initialized only once with module workers
let ambients = null; // Initialized only once with module workers
let characterMusics = {}; // Updated with module workers
let currentCharacterBGM = null;
let currentExpressionBGM = null;
let currentBackground = null;
let cooldownBGM = 0;
//#############################//
// Extension UI and Settings //
//#############################//
const defaultSettings = {
enabled: false,
bgm_muted: true,
ambient_muted: true,
bgm_volume: 50,
ambient_volume: 50,
bgm_cooldown: 30
}
function loadSettings() {
if (extension_settings.audio === undefined)
extension_settings.audio = {};
if (Object.keys(extension_settings.audio).length === 0) {
Object.assign(extension_settings.audio, defaultSettings)
}
$("#audio_enabled").prop('checked', extension_settings.audio.enabled);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
$("#audio_bgm_volume_slider").val(extension_settings.audio.bgm_volume);
$("#audio_ambient_volume_slider").val(extension_settings.audio.ambient_volume);
if (extension_settings.audio.bgm_muted) {
$("#audio_bgm_mute_icon").removeClass("fa-volume-high");
$("#audio_bgm_mute_icon").addClass("fa-volume-mute");
$("#audio_bgm_mute").addClass("redOverlayGlow");
$("#audio_bgm").prop("muted", true);
}
else{
$("#audio_bgm_mute_icon").addClass("fa-volume-high");
$("#audio_bgm_mute_icon").removeClass("fa-volume-mute");
$("#audio_bgm_mute").removeClass("redOverlayGlow");
$("#audio_bgm").prop("muted", false);
}
if (extension_settings.audio.ambient_muted) {
$("#audio_ambient_mute_icon").removeClass("fa-volume-high");
$("#audio_ambient_mute_icon").addClass("fa-volume-mute");
$("#audio_ambient_mute").addClass("redOverlayGlow");
$("#audio_ambient").prop("muted", true);
}
else{
$("#audio_ambient_mute_icon").addClass("fa-volume-high");
$("#audio_ambient_mute_icon").removeClass("fa-volume-mute");
$("#audio_ambient_mute").removeClass("redOverlayGlow");
$("#audio_ambient").prop("muted", false);
}
$("#audio_bgm_cooldown").val(extension_settings.audio.bgm_cooldown);
$("#audio_debug_div").hide(); // DBG
}
async function onEnabledClick() {
extension_settings.audio.enabled = $('#audio_enabled').is(':checked');
if (extension_settings.audio.enabled) {
if ($("#audio_bgm").attr("src") != "")
$("#audio_bgm")[0].play();
if ($("#audio_ambient").attr("src") != "")
$("#audio_ambient")[0].play();
} else {
$("#audio_bgm")[0].pause();
$("#audio_ambient")[0].pause();
}
saveSettingsDebounced();
}
async function onBGMMuteClick() {
extension_settings.audio.bgm_muted = !extension_settings.audio.bgm_muted;
$("#audio_bgm_mute_icon").toggleClass("fa-volume-high");
$("#audio_bgm_mute_icon").toggleClass("fa-volume-mute");
$("#audio_bgm").prop("muted", !$("#audio_bgm").prop("muted"));
$("#audio_bgm_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onAmbientMuteClick() {
extension_settings.audio.ambient_muted = !extension_settings.audio.ambient_muted;
$("#audio_ambient_mute_icon").toggleClass("fa-volume-high");
$("#audio_ambient_mute_icon").toggleClass("fa-volume-mute");
$("#audio_ambient").prop("muted", !$("#audio_ambient").prop("muted"));
$("#audio_ambient_mute").toggleClass("redOverlayGlow");
saveSettingsDebounced();
}
async function onBGMVolumeChange() {
extension_settings.audio.bgm_volume = ~~($("#audio_bgm_volume_slider").val());
$("#audio_bgm").prop("volume", extension_settings.audio.bgm_volume * 0.01);
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
}
async function onAmbientVolumeChange() {
extension_settings.audio.ambient_volume = ~~($("#audio_ambient_volume_slider").val());
$("#audio_ambient").prop("volume", extension_settings.audio.ambient_volume * 0.01);
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
saveSettingsDebounced();
//console.debug(DEBUG_PREFIX,"UPDATED Ambient MAX TO",extension_settings.audio.ambient_volume);
}
async function onBGMCooldownInput() {
extension_settings.audio.bgm_cooldown = ~~($("#audio_bgm_cooldown").val());
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
saveSettingsDebounced();
console.debug(DEBUG_PREFIX, "UPDATED BGM cooldown to", extension_settings.audio.bgm_cooldown);
}
//#############################//
// API Calls //
//#############################//
async function getAssetsList(type) {
console.debug(DEBUG_PREFIX, "getting assets of type", type);
try {
const result = await fetch(`/get_assets`, {
method: 'POST',
headers: getRequestHeaders(),
});
const assets = result.ok ? (await result.json()) : { type: [] };
console.debug(DEBUG_PREFIX, "Found assets:", assets);
return assets[type];
}
catch (err) {
console.log(err);
return [];
}
}
async function getCharacterBgmList(name) {
console.debug(DEBUG_PREFIX, "getting bgm list for", name);
try {
const result = await fetch(`/get_character_assets_list?name=${encodeURIComponent(name)}&category=${CHARACTER_BGM_FOLDER}`, {
method: 'POST',
headers: getRequestHeaders(),
});
let musics = result.ok ? (await result.json()) : [];
return musics;
}
catch (err) {
console.log(err);
return [];
}
}
//#############################//
// Module Worker //
//#############################//
/*
- Update ambient sound
- Update character BGM
- Solo dynamique expression
- Group only neutral bgm
*/
async function moduleWorker() {
const moduleEnabled = extension_settings.audio.enabled;
if (moduleEnabled) {
if (cooldownBGM > 0)
cooldownBGM -= UPDATE_INTERVAL;
if (fallback_BGMS == null) {
console.debug(DEBUG_PREFIX, "Updating audio bgm assets...");
fallback_BGMS = await getAssetsList(ASSETS_BGM_FOLDER);
fallback_BGMS = fallback_BGMS.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", fallback_BGMS);
}
if (ambients == null) {
console.debug(DEBUG_PREFIX, "Updating audio ambient assets...");
ambients = await getAssetsList(ASSETS_AMBIENT_FOLDER);
ambients = ambients.filter((filename) => filename != ".placeholder")
console.debug(DEBUG_PREFIX, "Detected assets:", ambients);
}
// 1) Update ambient audio
// ---------------------------
let newBackground = $("#bg1").css("background-image");
const custom_background = getContext()["chatMetadata"]["custom_background"];
if (custom_background !== undefined)
newBackground = custom_background
if (!isDataURL(newBackground)) {
newBackground = newBackground.substring(newBackground.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "").replaceAll("%20", "-").replaceAll(" ", "-"); // remove path and spaces
//console.debug(DEBUG_PREFIX,"Current backgroung:",newBackground);
if (currentBackground !== newBackground) {
currentBackground = newBackground;
console.debug(DEBUG_PREFIX, "Changing ambient audio for", currentBackground);
updateAmbient();
}
}
const context = getContext();
//console.debug(DEBUG_PREFIX,context);
if (context.chat.length == 0)
return;
let chatIsGroup = context.chat[0].is_group;
let newCharacter = null;
// 1) Update BGM (single chat)
// -----------------------------
if (!chatIsGroup) {
newCharacter = context.name2;
//console.log(DEBUG_PREFIX,"SOLO CHAT MODE"); // DBG
// 1.1) First time loading chat
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
currentExpressionBGM = FALLBACK_EXPRESSION;
//currentCharacterBGM = newCharacter;
//updateBGM();
//cooldownBGM = BGM_UPDATE_COOLDOWN;
return;
}
// 1.2) Switched chat
if (currentCharacterBGM !== newCharacter) {
currentCharacterBGM = newCharacter;
try {
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM character, will try again");
currentCharacterBGM = null
}
return;
}
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
//console.debug(DEBUG_PREFIX,"(SOLO) BGM switch on cooldown:",cooldownBGM);
return;
}
try {
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentExpressionBGM = newExpression;
console.debug(DEBUG_PREFIX, "(SOLO) Updated current character expression to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM expression, will try again");
currentCharacterBGM = null
}
return;
}
return;
}
// 2) Update BGM (group chat)
// -----------------------------
newCharacter = context.chat[context.chat.length - 1].name;
const userName = context.name1;
if (newCharacter !== undefined && newCharacter != userName) {
//console.log(DEBUG_PREFIX,"GROUP CHAT MODE"); // DBG
// 2.1) First time character appear
if (characterMusics[newCharacter] === undefined) {
await loadCharacterBGM(newCharacter);
return;
}
// 2.2) Switched chat
if (currentCharacterBGM !== newCharacter) {
// Check cooldown
if (cooldownBGM > 0) {
//console.debug(DEBUG_PREFIX,"(GROUP) BGM switch on cooldown:",cooldownBGM);
return;
}
try {
currentCharacterBGM = newCharacter;
await updateBGM();
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
currentCharacterBGM = newCharacter;
currentExpressionBGM = FALLBACK_EXPRESSION;
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
}
catch (error) {
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
currentCharacterBGM = null
}
return;
}
/*
const newExpression = getNewExpression();
// 1.3) Same character but different expression
if (currentExpressionBGM !== newExpression) {
// Check cooldown
if (cooldownBGM > 0) {
console.debug(DEBUG_PREFIX,"BGM switch on cooldown:",cooldownBGM);
return;
}
cooldownBGM = BGM_UPDATE_COOLDOWN;
currentExpressionBGM = newExpression;
console.debug(DEBUG_PREFIX,"Updated current character expression to",currentExpressionBGM);
updateBGM();
return;
}
return;*/
}
// Case 3: Same character/expression or BGM switch on cooldown keep playing same BGM
//console.debug(DEBUG_PREFIX,"Nothing to do for",currentCharacterBGM, newCharacter, currentExpressionBGM, cooldownBGM);
}
}
async function loadCharacterBGM(newCharacter) {
console.debug(DEBUG_PREFIX, "New character detected, loading BGM folder of", newCharacter);
// 1.1) First time character appear, load its music folder
const audio_file_paths = await getCharacterBgmList(newCharacter);
//console.debug(DEBUG_PREFIX, "Recieved", audio_file_paths);
// Initialise expression/files mapping
characterMusics[newCharacter] = {};
for (const e of DEFAULT_EXPRESSIONS)
characterMusics[newCharacter][e] = [];
for (const i of audio_file_paths) {
//console.debug(DEBUG_PREFIX,"File found:",i);
for (const e of DEFAULT_EXPRESSIONS)
if (i.includes(e))
characterMusics[newCharacter][e].push(i);
}
console.debug(DEBUG_PREFIX, "Updated BGM map of", newCharacter, "to", characterMusics[newCharacter]);
}
function getNewExpression() {
let newExpression;
// HACK: use sprite file name as expression detection
if (!$(SPRITE_DOM_ID).length) {
console.error(DEBUG_PREFIX, "ERROR: expression sprite does not exist, cannot extract expression from ", SPRITE_DOM_ID)
return FALLBACK_EXPRESSION;
}
const spriteFile = $("#expression-image").attr("src");
newExpression = spriteFile.substring(spriteFile.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "");
//
// No sprite to detect expression
if (newExpression == "") {
//console.info(DEBUG_PREFIX,"Warning: no expression extracted from sprite, switch to",FALLBACK_EXPRESSION);
newExpression = FALLBACK_EXPRESSION;
}
if (!DEFAULT_EXPRESSIONS.includes(newExpression)) {
console.info(DEBUG_PREFIX, "Warning:", newExpression, " is not a handled expression, expected one of", FALLBACK_EXPRESSION);
return FALLBACK_EXPRESSION;
}
return newExpression;
}
async function updateBGM() {
let audio_files = characterMusics[currentCharacterBGM][currentExpressionBGM];// Try char expression BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No BGM for", currentCharacterBGM, currentExpressionBGM);
audio_files = characterMusics[currentCharacterBGM][FALLBACK_EXPRESSION]; // Try char FALLBACK BGM
if (audio_files === undefined || audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM for", currentCharacterBGM, FALLBACK_EXPRESSION, "switch to ST BGM");
audio_files = fallback_BGMS; // ST FALLBACK BGM
if (audio_files.length == 0) {
console.debug(DEBUG_PREFIX, "No default BGM file found, bgm folder may be empty.");
return;
}
}
}
const audio_file_path = audio_files[Math.floor(Math.random() * audio_files.length)];
console.log(DEBUG_PREFIX, "Updating BGM");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
try {
const response = await fetch(audio_file_path);
if (!response.ok) {
console.log(DEBUG_PREFIX, "File not found!")
}
else {
console.log(DEBUG_PREFIX, "Switching BGM to", currentExpressionBGM)
const audio = $("#audio_bgm");
if (audio.attr("src") == audio_file_path) {
console.log(DEBUG_PREFIX, "Already playing, ignored");
return;
}
audio.animate({ volume: 0.0 }, 2000, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.bgm_volume * 0.01;
audio.animate({ volume: extension_settings.audio.bgm_volume * 0.01 }, 2000);
})
}
} catch (error) {
console.log(DEBUG_PREFIX, "Error while trying to fetch", audio_file_path, ":", error);
}
}
async function updateAmbient() {
let audio_file_path = null;
for (const i of ambients) {
console.debug(i)
if (i.includes(currentBackground)) {
audio_file_path = i;
break;
}
}
if (audio_file_path === null) {
console.debug(DEBUG_PREFIX, "No ambient file found for background", currentBackground);
const audio = $("#audio_ambient");
audio.attr("src", "");
audio[0].pause();
return;
}
//const audio_file_path = AMBIENT_FOLDER+currentBackground+".mp3";
console.log(DEBUG_PREFIX, "Updating ambient");
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
const audio = $("#audio_ambient");
audio.animate({ volume: 0.0 }, 2000, function () {
audio.attr("src", audio_file_path);
audio[0].play();
audio.volume = extension_settings.audio.ambient_volume * 0.01;
audio.animate({ volume: extension_settings.audio.ambient_volume * 0.01 }, 2000);
});
}
//#############################//
// Extension load //
//#############################//
// This function is called when the extension is loaded
jQuery(async () => {
// This is an example of loading HTML from a file
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
$('#extensions_settings').append(windowHtml);
loadSettings();
$("#audio_bgm").attr("loop", true);
$("#audio_ambient").attr("loop", true);
$("#audio_bgm").hide();
$("#audio_ambient").hide();
$("#audio_bgm_mute").on("click", onBGMMuteClick);
$("#audio_ambient_mute").on("click", onAmbientMuteClick);
$("#audio_enabled").on("click", onEnabledClick);
$("#audio_bgm_volume_slider").on("input", onBGMVolumeChange);
$("#audio_ambient_volume_slider").on("input", onAmbientVolumeChange);
$("#audio_bgm_cooldown").on("input", onBGMCooldownInput);
// Reset assets container, will be redected like if ST restarted
$("#audio_refresh_assets").on("click", function(){
console.debug(DEBUG_PREFIX,"Refreshing audio assets");
fallback_BGMS = null;
ambients = null;
characterMusics = {};
currentCharacterBGM = null;
currentExpressionBGM = null;
currentBackground = null;
})
// DBG
$("#audio_debug").on("click", function () {
if ($("#audio_debug").is(':checked')) {
$("#audio_bgm").show();
$("#audio_ambient").show();
}
else {
$("#audio_bgm").hide();
$("#audio_ambient").hide();
}
});
//
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
moduleWorker();
});

View File

@@ -0,0 +1,11 @@
{
"display_name": "Dynamic Audio",
"loading_order": 14,
"requires": [],
"optional": ["classify"],
"js": "index.js",
"css": "style.css",
"author": "Keij#6799 and Deffcolony",
"version": "0.1.0",
"homePage": "https://github.com/SillyTavern/SillyTavern"
}

View File

@@ -0,0 +1,22 @@
.mixer-div {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 5px;
}
.audio-mute-button {
padding: 5px;
width: 50px;
height: 30px;
}
.audio-mute-button-muted {
color: red;
}
#audio_refresh_assets {
width: 50px;
height: 30px;
}

View File

@@ -0,0 +1,66 @@
<div id="audio_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Dynamic Audio</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div>
<label class="checkbox_label" for="audio_enabled">
<input type="checkbox" id="audio_enabled" name="audio_enabled">
<small>Enabled</small>
</label>
<div id="audio_debug_div">
<label class="checkbox_label" for="audio_debug">
<input type="checkbox" id="audio_debug" name="audio_debug">
<small>Debug</small>
</label>
</div>
<div>
<label for="audio_refresh_assets">Refresh assets</label>
<div id="audio_refresh_assets" class="menu_button">
<i class="fa-solid fa-refresh fa-lg"></i>
</div>
</div>
</div>
<div>
<div>
<label for="audio_bgm_volume_slider">Music <span id="audio_bgm_volume"></span></label>
<div class="mixer-div">
<div id="audio_bgm_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg" id="audio_bgm_mute_icon"></i>
</div>
<input type="range" class ="slider" id ="audio_bgm_volume_slider" value = "0" maxlength ="100">
</div>
<audio id="audio_bgm" controls src="">
</div>
<div>
<label for="audio_ambient_volume_slider">Ambient <span id="audio_ambient_volume"></span></label>
<div class="mixer-div">
<div id="audio_ambient_mute" class="menu_button audio-mute-button">
<i class="fa-solid fa-volume-high fa-lg" id="audio_ambient_mute_icon"></i>
</div>
<input type="range" class ="slider" id ="audio_ambient_volume_slider" value = "0" maxlength ="100">
</div>
<audio id="audio_ambient" controls src="">
</div>
<div>
<label for="audio_bgm_cooldown">Music update cooldown (in seconds)</label>
<input id="audio_bgm_cooldown" class="text_pole wide30p">
</div>
</div>
<div>
<b>Hint:</b>
<i>
Create new folder in the
<b>public/characters/</b>
folder and name it as the name of the character.
Create a folder name <b>bgm</b> inside of it.
Put bgm music with expressions there. File names should follow the pattern:
<it>[expression_label]_[number].mp3</it>
By default one of the <it>neutral_[number].mp3</it> will play if classify module is not active.
</i>
</div>
</div>
</div>
</div>

View File

@@ -57,7 +57,6 @@ export function getCfgPrompt(guidanceScale, isNegative) {
substituteParams(
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
)
?.trim()
);
}
@@ -67,7 +66,6 @@ export function getCfgPrompt(guidanceScale, isNegative) {
substituteParams(
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
)
?.trim()
);
}
@@ -76,7 +74,6 @@ export function getCfgPrompt(guidanceScale, isNegative) {
substituteParams(
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
)
?.trim()
);
}

View File

@@ -1,6 +1,6 @@
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { dragElement, isMobile } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js";
import { loadMovingUIState, power_user } from "../../power-user.js";
import { onlyUnique, debounce, getCharaFilename } from "../../utils.js";
export { MODULE_NAME };
@@ -334,7 +334,7 @@ async function setImage(img, path) {
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
//position absolute prevent the original from jumping around during transition
img.css('position', 'absolute');
img.css('position', 'absolute').width(imgWidth).height(imgHeight);
expressionClone.addClass('expression-animating');
//fade the clone in
expressionClone.css({
@@ -800,20 +800,7 @@ function drawSpritesList(character, labels, sprites) {
}
function getListItem(item, imageSrc, textClass) {
return `
<div id="${item}" class="expression_list_item">
<div class="expression_list_buttons">
<div class="menu_button expression_list_upload" title="Upload image">
<i class="fa-solid fa-upload"></i>
</div>
<div class="menu_button expression_list_delete" title="Delete image">
<i class="fa-solid fa-trash"></i>
</div>
</div>
<span class="expression_list_title ${textClass}">${item}</span>
<img class="expression_list_image" src="${imageSrc}" />
</div>
`;
return renderExtensionTemplate(MODULE_NAME, 'list-item', { item, imageSrc, textClass });
}
async function getSpritesList(name) {
@@ -919,7 +906,7 @@ async function setExpression(character, expression, force) {
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
//position absolute prevent the original from jumping around during transition
img.css('position', 'absolute');
img.css('position', 'absolute').width(imgWidth).height(imgHeight);
expressionClone.addClass('expression-animating');
//fade the clone in
expressionClone.css({
@@ -992,7 +979,6 @@ async function setExpression(character, expression, force) {
});
}
}
@@ -1241,54 +1227,7 @@ function setExpressionOverrideHtml(forceClear = false) {
$('body').append(element);
}
function addSettings() {
const html = `
<div class="expression_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Character Expressions</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<!-- Toggle button for aituber/static images -->
<div class="toggle_button">
<label class="switch">
<input id="image_type_toggle" type="checkbox">
<span class="slider round"></span>
<label for="image_type_toggle">Image Type - talkinghead (extras)</label>
</label>
</div>
<div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small>
</div>
<div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
</div>
<div id="image_list"></div>
<div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button">
<i class="fa-solid fa-file-zipper"></i>
<span>Upload sprite pack (ZIP)</span>
</div>
<div id="expression_override_cleanup_button" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span>Remove all image overrides</span>
</div>
</div>
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
</div>
</div>
<form>
<input type="file" id="expression_upload_pack" name="expression_upload_pack" accept="application/zip" hidden>
<input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden>
</form>
</div>
`;
$('#extensions_settings').append(html);
$('#extensions_settings').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
$('#expression_upload_pack_button').on('click', onClickExpressionUploadPackButton);

View File

@@ -0,0 +1,12 @@
<div id="{{item}}" class="expression_list_item">
<div class="expression_list_buttons">
<div class="menu_button expression_list_upload" title="Upload image">
<i class="fa-solid fa-upload"></i>
</div>
<div class="menu_button expression_list_delete" title="Delete image">
<i class="fa-solid fa-trash"></i>
</div>
</div>
<span class="expression_list_title {{textClass}}">{{item}}</span>
<img class="expression_list_image" src="{{imageSrc}}" />
</div>

View File

@@ -0,0 +1,44 @@
<div class="expression_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Character Expressions</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<!-- Toggle button for aituber/static images -->
<div class="toggle_button">
<label class="switch">
<input id="image_type_toggle" type="checkbox">
<span class="slider round"></span>
<label for="image_type_toggle">Image Type - talkinghead (extras)</label>
</label>
</div>
<div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small>
</div>
<div class="flex-container flexnowrap">
<input id="expression_override" type="text" class="text_pole" placeholder="Override folder name" />
<input id="expression_override_button" class="menu_button" type="submit" value="Submit" />
</div>
<div id="image_list"></div>
<div class="expression_buttons flex-container spaceEvenly">
<div id="expression_upload_pack_button" class="menu_button">
<i class="fa-solid fa-file-zipper"></i>
<span>Upload sprite pack (ZIP)</span>
</div>
<div id="expression_override_cleanup_button" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
<span>Remove all image overrides</span>
</div>
</div>
<p class="hint"><b>Hint:</b> <i>Create new folder in the <b>public/characters/</b> folder and name it as the name of the character.
Put images with expressions there. File names should follow the pattern: <tt>[expression_label].[image_format]</tt></i></p>
<label for="expressions_show_default"><input id="expressions_show_default" type="checkbox">Show default images (emojis) if missing</label>
</div>
</div>
<form>
<input type="file" id="expression_upload_pack" name="expression_upload_pack" accept="application/zip" hidden>
<input type="file" id="expression_upload" name="expression_upload" accept="image/*" hidden>
</form>
</div>

View File

@@ -6,13 +6,13 @@
#expression-wrapper {
display: flex;
height: calc(100vh - 40px);
height: calc(100vh - var(--topBarBlockSize));
width: 100vw;
}
#visual-novel-wrapper {
display: flex;
height: calc(100vh - 40px);
height: calc(100vh - var(--topBarBlockSize));
width: 100vw;
position: relative;
overflow: hidden;
@@ -180,4 +180,4 @@ img.expression.default {
div.expression {
display: none;
}
}
}

View File

@@ -567,7 +567,7 @@ jQuery(function () {
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="0" />
After scenario
After Main Prompt / Story String
</label>
<label>
<input type="radio" name="memory_position" value="1" />

View File

@@ -55,6 +55,9 @@ const defaultSettings = {
}
function loadSettings() {
if (extension_settings.rvc === undefined)
extension_settings.rvc = {};
if (Object.keys(extension_settings.rvc).length === 0) {
Object.assign(extension_settings.rvc, defaultSettings)
}
@@ -174,9 +177,9 @@ async function onDeleteClick() {
saveSettingsDebounced();
}
async function onClickUpload() {
async function onChangeUploadFiles() {
const url = new URL(getApiUrl());
const inputFiles = $("#rvc_model_upload_file").get(0).files;
const inputFiles = $("#rvc_model_upload_files").get(0).files;
let formData = new FormData();
for (const file of inputFiles)
@@ -195,7 +198,7 @@ async function onClickUpload() {
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
alert('The file has been uploaded successfully.');
alert('The files have been uploaded successfully.');
}
$(document).ready(function () {
@@ -208,6 +211,7 @@ $(document).ready(function () {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<h4 class="center">Characters Voice Mapping</h4>
<div>
<label class="checkbox_label" for="rvc_enabled">
<input type="checkbox" id="rvc_enabled" name="rvc_enabled">
@@ -218,54 +222,103 @@ $(document).ready(function () {
placeholder="Voice map will appear here for debug purpose"></textarea>
</div>
<div>
<label for="rvc_character_select">Character:</label>
<select id="rvc_character_select">
<!-- Populated by JS -->
</select>
<label for="rvc_model_select">Voice:</label>
<select id="rvc_model_select">
<!-- Populated by JS -->
</select>
<div>
<label for="rvc_model_upload_file">Select models to upload (zip files)</label>
<input
type="file"
id="rvc_model_upload_file"
accept=".zip,.rar,.7zip,.7z" multiple />
<button id="rvc_model_upload_button"> Upload </button>
<button id="rvc_model_refresh_button"> Refresh Voices </button>
<div class="background_controls">
<label for="rvc_character_select">Character:</label>
<select id="rvc_character_select">
<!-- Populated by JS -->
</select>
<div id="rvc_delete" class="menu_button">
<i class="fa-solid fa-times"></i>
Remove
</div>
</div>
<div class="background_controls">
<label for="rvc_model_select">Voice:</label>
<select id="rvc_model_select">
<!-- Populated by JS -->
</select>
<div id="rvc_model_refresh_button" class="menu_button">
<i class="fa-solid fa-refresh"></i>
<!-- Refresh -->
</div>
<div id="rvc_model_upload_select_button" class="menu_button">
<i class="fa-solid fa-upload"></i>
Upload
</div>
<input
type="file"
id="rvc_model_upload_files"
accept=".zip,.rar,.7zip,.7z" multiple />
</div>
</div>
<div>
<small>
Upload one archive per model. With .pth and .index (optional) inside.<br/>
Supported format: .zip .rar .7zip .7z
</small>
</div>
<div>
<h4>Model Settings</h4>
</div>
<div>
<label for="rvc_pitch_extraction">
Pitch Extraction
</label>
<select id="rvc_pitch_extraction">
<option value="dio">dio</option>
<option value="pm">pm</option>
<option value="harvest">harvest</option>
<option value="torchcrepe">torchcrepe</option>
<option value="rmvpe">rmvpe</option>
<option value="">None</option>
</select>
<small>
Tips: dio and pm faster, harvest slower but good.<br/>
Torchcrepe and rmvpe are good but uses GPU.
</small>
</div>
<div>
<label for="rvc_index_rate">
Search feature ratio (<span id="rvc_index_rate_value"></span>)
</label>
<input id="rvc_index_rate" type="range" min="0" max="1" step="0.01" value="0.5" />
<small>
Controls accent strength, too high may produce artifact.
</small>
</div>
<div>
<label for="rvc_filter_radius">Filter radius (<span id="rvc_filter_radius_value"></span>)</label>
<input id="rvc_filter_radius" type="range" min="0" max="7" step="1" value="3" />
<small>
Higher can reduce breathiness but may increase run time.
</small>
</div>
<div>
<label for="rvc_pitch_offset">Pitch offset (<span id="rvc_pitch_offset_value"></span>)</label>
<input id="rvc_pitch_offset" type="range" min="-20" max="20" step="1" value="0" />
<small>
Recommended +12 key for male to female conversion and -12 key for female to male conversion.
</small>
</div>
<div>
<label for="rvc_rms_mix_rate">Mix rate (<span id="rvc_rms_mix_rate_value"></span>)</label>
<input id="rvc_rms_mix_rate" type="range" min="0" max="1" step="0.01" value="1" />
<small>
Closer to 0 is closer to TTS and 1 is closer to trained voice.
Can help mask noise and sound more natural when set relatively low.
</small>
</div>
<div>
<label for="rvc_protect">Protect amount (<span id="rvc_protect_value"></span>)</label>
<input id="rvc_protect" type="range" min="0" max="1" step="0.01" value="0.33" />
<small>
Avoid non voice sounds. Lower is more being ignored.
</small>
</div>
<span>Select Pitch Extraction</span> </br>
<select id="rvc_pitch_extraction">
<option value="dio">dio</option>
<option value="pm">pm</option>
<option value="harvest">harvest</option>
<option value="torchcrepe">torchcrepe</option>
<option value="rmvpe">rmvpe</option>
<option value="">None</option>
</select>
<label for="rvc_index_rate">
Index rate for feature retrieval (<span id="rvc_index_rate_value"></span>)
</label>
<input id="rvc_index_rate" type="range" min="0" max="1" step="0.01" value="0.5" />
<label for="rvc_filter_radius">Filter radius (<span id="rvc_filter_radius_value"></span>)</label>
<input id="rvc_filter_radius" type="range" min="0" max="7" step="1" value="3" />
<label for="rvc_pitch_offset">Pitch offset (<span id="rvc_pitch_offset_value"></span>)</label>
<input id="rvc_pitch_offset" type="range" min="-100" max="100" step="1" value="0" />
<label for="rvc_rms_mix_rate">Mix rate (<span id="rvc_rms_mix_rate_value"></span>)</label>
<input id="rvc_rms_mix_rate" type="range" min="0" max="1" step="0.01" value="1" />
<label for="rvc_protect">Protect amount (<span id="rvc_protect_value"></span>)</label>
<input id="rvc_protect" type="range" min="0" max="1" step="0.01" value="0.33" />
<div id="rvc_status">
</div>
<div class="rvc_buttons">
<input id="rvc_apply" class="menu_button" type="submit" value="Apply" />
<input id="rvc_delete" class="menu_button" type="submit" value="Delete" />
</div>
</div>
</div>
@@ -284,8 +337,11 @@ $(document).ready(function () {
$("#rvc_apply").on("click", onApplyClick);
$("#rvc_delete").on("click", onDeleteClick);
$("#rvc_model_upload_file").show();
$("#rvc_model_upload_button").on("click", onClickUpload);
$("#rvc_model_upload_files").hide();
$("#rvc_model_upload_select_button").on("click", function() {$("#rvc_model_upload_files").click()});
$("#rvc_model_upload_files").on("change", onChangeUploadFiles);
//$("#rvc_model_upload_button").on("click", onClickUpload);
$("#rvc_model_refresh_button").on("click", refreshVoiceList);
}
@@ -323,7 +379,7 @@ async function get_models_list(model_id) {
/*
Send an audio file to RVC to convert voice
*/
async function rvcVoiceConversion(response, character) {
async function rvcVoiceConversion(response, character, text) {
let apiResult
// Check voice map
@@ -341,8 +397,6 @@ async function rvcVoiceConversion(response, character) {
const voice_settings = extension_settings.rvc.voiceMap[character];
console.log("Sending tts audio data to RVC on extras server")
var requestData = new FormData();
requestData.append('AudioFile', audioData, 'record');
requestData.append("json", JSON.stringify({
@@ -352,9 +406,12 @@ async function rvcVoiceConversion(response, character) {
"indexRate": voice_settings["indexRate"],
"filterRadius": voice_settings["filterRadius"],
"rmsMixRate": voice_settings["rmsMixRate"],
"protect": voice_settings["protect"]
"protect": voice_settings["protect"],
"text": text
}));
console.log("Sending tts audio data to RVC on extras server",requestData)
const url = new URL(getApiUrl());
url.pathname = '/api/voice-conversion/rvc/process-audio';
@@ -405,11 +462,13 @@ async function moduleWorker() {
function updateCharactersList() {
let currentcharacters = new Set();
for (const i of getContext().characters) {
const context = getContext();
for (const i of context.characters) {
currentcharacters.add(i.name);
}
currentcharacters = Array.from(currentcharacters)
currentcharacters = Array.from(currentcharacters);
currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters

View File

@@ -11,7 +11,6 @@ export { CoquiTtsProvider }
const DEBUG_PREFIX = "<Coqui TTS module> ";
const UPDATE_INTERVAL = 1000;
let inApiCall = false;
let charactersList = []; // Updated with module worker
let coquiApiModels = {}; // Initialized only once
let coquiApiModelsFull = {}; // Initialized only once
@@ -40,6 +39,12 @@ 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 });
@@ -54,11 +59,13 @@ function resetModelSettings() {
function updateCharactersList() {
let currentcharacters = new Set();
for (const i of getContext().characters) {
const context = getContext();
for (const i of context.characters) {
currentcharacters.add(i.name);
}
currentcharacters = Array.from(currentcharacters)
currentcharacters = Array.from(currentcharacters);
currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters
@@ -83,11 +90,13 @@ class CoquiTtsProvider {
// Extension UI and Settings //
//#############################//
settings
static instance;
settings = {};
defaultSettings = {
voiceMap: "",
voiceMapDict: {}
// Singleton to allow acces to instance in event functions
constructor() {
if (CoquiTtsProvider.instance === undefined)
CoquiTtsProvider.instance = this;
}
get settingsHtml() {
@@ -145,8 +154,12 @@ class CoquiTtsProvider {
}
loadSettings(settings) {
if (Object.keys(this.settings).length === 0) {
Object.assign(this.settings, defaultSettings)
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
this.settings = defaultSettings;
for (const key in settings) {
if (key in this.settings) {
@@ -156,7 +169,7 @@ class CoquiTtsProvider {
}
}
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
$("#coqui_api_model_div").hide();
$("#coqui_local_model_div").hide();
@@ -167,24 +180,12 @@ class CoquiTtsProvider {
$("#coqui_api_model_install_status").hide();
$("#coqui_api_model_install_button").hide();
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() });
$("#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);
$("#coqui_remove_char_mapping").on("click", function () { that.onRemoveClick() });
// Load characters list
$('#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));
}
updateCharactersList();
// Load coqui-api settings from json file
fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
@@ -192,18 +193,6 @@ class CoquiTtsProvider {
.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
@@ -212,49 +201,33 @@ class CoquiTtsProvider {
.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);
}*/
});
}
updateVoiceMap() {
this.settings.voiceMap = "";
for (let i in this.settings.voiceMapDict) {
const voice_settings = this.settings.voiceMapDict[i];
this.settings.voiceMap += i + ":" + voice_settings["model_id"];
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"];
if (voice_settings["model_language"] != null)
this.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
if (voice_settings["model_speaker"] != null)
this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
this.settings.voiceMap += ",";
CoquiTtsProvider.instance.settings.voiceMap += ",";
}
$("#tts_voice_map").val(this.settings.voiceMap);
extension_settings.tts.Coqui = this.settings;
$("#tts_voice_map").val(CoquiTtsProvider.instance.settings.voiceMap);
//extension_settings.tts.Coqui = extension_settings.tts.Coqui;
}
onSettingsChange() {
console.debug(DEBUG_PREFIX, "Settings changes", this.settings);
extension_settings.tts.Coqui = this.settings;
//console.debug(DEBUG_PREFIX, "Settings changes", CoquiTtsProvider.instance.settings);
CoquiTtsProvider.updateVoiceMap();
}
async onApplyClick() {
if (inApiCall) {
return; // TOdo block dropdown
}
const character = $("#coqui_character_select").val();
const model_origin = $("#coqui_model_origin").val();
const model_language = $("#coqui_api_language").val();
@@ -262,16 +235,15 @@ class CoquiTtsProvider {
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 });
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // 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 });
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
@@ -280,25 +252,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 });
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
this.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]);
this.updateVoiceMap(); // Overide any manual modification
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
return;
}
if (model_language == "none") {
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // 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 });
this.updateVoiceMap(); // Overide any manual modification
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
@@ -327,13 +299,13 @@ class CoquiTtsProvider {
return;
}
console.debug(DEBUG_PREFIX, "Current voice map: ", this.settings.voiceMap);
console.debug(DEBUG_PREFIX, "Current voice map: ", CoquiTtsProvider.instance.settings.voiceMap);
this.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
CoquiTtsProvider.instance.settings.voiceMapDict[character] = { 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, ":", this.settings.voiceMapDict[character]);
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
this.updateVoiceMap();
CoquiTtsProvider.updateVoiceMap();
let successMsg = character + ":" + model_id;
if (model_setting_language != null)
@@ -352,7 +324,7 @@ class CoquiTtsProvider {
return output;
}
async onRemoveClick() {
static async onRemoveClick() {
const character = $("#coqui_character_select").val();
if (character === "none") {
@@ -361,11 +333,11 @@ class CoquiTtsProvider {
}
// Todo erase from voicemap
delete (this.settings.voiceMapDict[character]);
this.updateVoiceMap(); // TODO
delete (CoquiTtsProvider.instance.settings.voiceMapDict[character]);
CoquiTtsProvider.updateVoiceMap(); // TODO
}
async onModelOriginChange() {
static async onModelOriginChange() {
throwIfModuleMissing()
resetModelSettings();
const model_origin = $('#coqui_model_origin').val();
@@ -378,6 +350,9 @@ class CoquiTtsProvider {
// 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')
@@ -400,6 +375,9 @@ 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')
@@ -427,7 +405,7 @@ class CoquiTtsProvider {
}
}
async onModelLanguageChange() {
static async onModelLanguageChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@@ -460,7 +438,7 @@ class CoquiTtsProvider {
}
}
async onModelNameChange() {
static async onModelNameChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@@ -551,8 +529,6 @@ 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...");
@@ -566,7 +542,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();
onModelNameChange_pointer();
CoquiTtsProvider.onModelNameChange();
}
if (apiResult["status"] == "downloading") {
@@ -577,7 +553,7 @@ class CoquiTtsProvider {
} catch (error) {
console.error(error)
toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
onModelNameChange_pointer();
CoquiTtsProvider.onModelNameChange();
}
// will refresh model status
});

View File

@@ -412,7 +412,7 @@ async function tts(text, voiceId, char) {
// RVC injection
if (extension_settings.rvc.enabled)
response = await rvcVoiceConversion(response, char)
response = await rvcVoiceConversion(response, char, text)
addAudioJob(response)
completeTtsJob()

View File

@@ -6,6 +6,7 @@ import {
power_user,
context_presets,
} from "./power-user.js";
import { resetScrollHeight } from "./utils.js";
/**
* @type {any[]} Instruct mode presets.
@@ -16,7 +17,8 @@ const controls = [
{ id: "instruct_enabled", property: "enabled", isCheckbox: true },
{ id: "instruct_wrap", property: "wrap", isCheckbox: true },
{ id: "instruct_system_prompt", property: "system_prompt", isCheckbox: false },
{ id: "instruct_system_sequence", property: "system_sequence", isCheckbox: false },
{ id: "instruct_system_sequence_prefix", property: "system_sequence_prefix", isCheckbox: false },
{ id: "instruct_system_sequence_suffix", property: "system_sequence_suffix", isCheckbox: false },
{ id: "instruct_separator_sequence", property: "separator_sequence", isCheckbox: false },
{ id: "instruct_input_sequence", property: "input_sequence", isCheckbox: false },
{ id: "instruct_output_sequence", property: "output_sequence", isCheckbox: false },
@@ -24,6 +26,7 @@ const controls = [
{ id: "instruct_names", property: "names", isCheckbox: true },
{ id: "instruct_macro", property: "macro", isCheckbox: true },
{ id: "instruct_names_force_groups", property: "names_force_groups", isCheckbox: true },
{ id: "instruct_first_output_sequence", property: "first_output_sequence", isCheckbox: false },
{ id: "instruct_last_output_sequence", property: "last_output_sequence", isCheckbox: false },
{ id: "instruct_activation_regex", property: "activation_regex", isCheckbox: false },
];
@@ -53,6 +56,9 @@ export function loadInstructMode(data) {
$element.on('input', function () {
power_user.instruct[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
saveSettingsDebounced();
if (!control.isCheckbox) {
resetScrollHeight($element);
}
});
});
@@ -200,9 +206,10 @@ export function getInstructStoppingSequences() {
if (power_user.instruct.enabled) {
const input_sequence = power_user.instruct.input_sequence;
const output_sequence = power_user.instruct.output_sequence;
const first_output_sequence = power_user.instruct.first_output_sequence;
const last_output_sequence = power_user.instruct.last_output_sequence;
const combined_sequence = `${input_sequence}\n${output_sequence}\n${last_output_sequence}`;
const combined_sequence = `${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}`;
combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
}
@@ -210,6 +217,11 @@ export function getInstructStoppingSequences() {
return result;
}
export const force_output_sequence = {
FIRST: 1,
LAST: 2,
}
/**
* Formats instruct mode chat message.
* @param {string} name Character name.
@@ -219,10 +231,10 @@ export function getInstructStoppingSequences() {
* @param {string} forceAvatar Force avatar string.
* @param {string} name1 User name.
* @param {string} name2 Character name.
* @param {boolean} forceLastOutputSequence Force to use last outline sequence (if configured).
* @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
* @returns {string} Formatted instruct mode chat message.
*/
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceLastOutputSequence) {
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) {
let includeNames = isNarrator ? false : power_user.instruct.names;
if (!isNarrator && power_user.instruct.names_force_groups && (selected_group || forceAvatar)) {
@@ -231,8 +243,12 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
if (sequence === power_user.instruct.output_sequence && forceLastOutputSequence && power_user.instruct.last_output_sequence) {
sequence = power_user.instruct.last_output_sequence;
if (forceOutputSequence && sequence === power_user.instruct.output_sequence) {
if (forceOutputSequence === force_output_sequence.FIRST && power_user.instruct.first_output_sequence) {
sequence = power_user.instruct.first_output_sequence;
} else if (forceOutputSequence === force_output_sequence.LAST && power_user.instruct.last_output_sequence) {
sequence = power_user.instruct.last_output_sequence;
}
}
if (power_user.instruct.macro) {
@@ -254,14 +270,14 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt){
if (power_user.instruct.system_sequence) {
const separator = power_user.instruct.wrap ? '\n' : '';
const separator = power_user.instruct.wrap ? '\n' : '';
if (power_user.instruct.system_sequence.includes("{{sys}}")) {
return power_user.instruct.system_sequence.replace(/{{sys}}/gi, systemPrompt);
} else {
return power_user.instruct.system_sequence + separator + systemPrompt;
}
if (power_user.instruct.system_sequence_prefix) {
systemPrompt = power_user.instruct.system_sequence_prefix + separator + systemPrompt;
}
if (power_user.instruct.system_sequence_suffix) {
systemPrompt = systemPrompt + separator + power_user.instruct.system_sequence_suffix;
}
return systemPrompt;

View File

@@ -185,7 +185,7 @@ function loadNovelSettingsUi(ui_settings) {
$("#top_a_novel").val(ui_settings.top_a);
$("#top_a_counter_novel").text(Number(ui_settings.top_a).toFixed(2));
$("#typical_p_novel").val(ui_settings.typical_p);
$("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(2));
$("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(3));
$("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").text(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(ui_settings.phrase_rep_pen || "off");
@@ -269,8 +269,8 @@ const sliders = [
{
sliderId: "#typical_p_novel",
counterId: "#typical_p_counter_novel",
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
format: (val) => Number(val).toFixed(3),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(3); },
},
{
sliderId: "#mirostat_tau_novel",

View File

@@ -34,6 +34,7 @@ import {
} from "./PromptManager.js";
import {
getCustomStoppingStrings,
persona_description_positions,
power_user,
} from "./power-user.js";
@@ -120,6 +121,7 @@ const j2_max_topk = 10.0;
const j2_max_freq = 5.0;
const j2_max_pres = 5.0;
const openrouter_website_model = 'OR_Website';
const openai_max_stop_strings = 4;
let biasCache = undefined;
let model_list = [];
@@ -683,9 +685,9 @@ function preparePromptsForChatCompletion({Scenario, charPersonality, name2, worl
// Tavern Extras - Summary
const summary = extensionPrompts['1_memory'];
if (summary && summary.content) systemPrompts.push({
if (summary && summary.value) systemPrompts.push({
role: 'system',
content: summary.content,
content: summary.value,
identifier: 'summary'
});
@@ -1138,6 +1140,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
"max_tokens": oai_settings.openai_max_tokens,
"stream": stream,
"logit_bias": logit_bias,
"stop": getCustomStoppingStrings(openai_max_stop_strings),
};
// Proxy is only supported for Claude and OpenAI
@@ -1151,6 +1154,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
generate_data['use_claude'] = true;
generate_data['top_k'] = Number(oai_settings.top_k_openai);
generate_data['exclude_assistant'] = oai_settings.exclude_assistant;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
// Don't add a prefill on quiet gens (summarization)
if (!isQuiet && !oai_settings.exclude_assistant) {
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
@@ -2687,8 +2691,8 @@ async function onModelChange() {
oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.freq_pen_openai);
$('#freq_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input');
oai_settings.freq_pen_openai = Math.min(2.0, oai_settings.pres_pen_openai);
$('#pres_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.freq_pen_openai).trigger('input');
oai_settings.pres_pen_openai = Math.min(2.0, oai_settings.pres_pen_openai);
$('#pres_pen_openai').attr('min', -2.0).attr('max', 2.0).val(oai_settings.pres_pen_openai).trigger('input');
oai_settings.top_k_openai = Math.min(200, oai_settings.top_k_openai);
$('#top_k_openai').attr('max', 200).val(oai_settings.top_k_openai).trigger('input');

View File

@@ -29,7 +29,7 @@ import {
import { registerSlashCommand } from "./slash-commands.js";
import { tokenizers } from "./tokenizers.js";
import { delay } from "./utils.js";
import { delay, resetScrollHeight } from "./utils.js";
export {
loadPowerUserSettings,
@@ -46,7 +46,7 @@ export {
export const MAX_CONTEXT_DEFAULT = 4096;
const MAX_CONTEXT_UNLOCKED = 65536;
const defaultStoryString = "{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}";
const defaultStoryString = "{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}";
const defaultExampleSeparator = '***';
const defaultChatStart = '***';
@@ -159,19 +159,21 @@ let power_user = {
default_instruct: '',
instruct: {
enabled: false,
preset: "Alpaca",
system_prompt: "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}.\n",
input_sequence: "### Instruction:",
output_sequence: "### Response:",
first_output_sequence: "",
last_output_sequence: "",
system_sequence_prefix: "",
system_sequence_suffix: "",
stop_sequence: "",
separator_sequence: "",
wrap: true,
macro: true,
names: false,
system_prompt: "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n\nWrite {{char}}'s next reply in a fictional roleplay chat between {{user}} and {{char}}. Write 1 reply only.",
system_sequence: '',
stop_sequence: '',
input_sequence: '### Instruction:',
output_sequence: '### Response:',
last_output_sequence: '',
preset: 'Alpaca',
separator_sequence: '',
macro: false,
names_force_groups: true,
activation_regex: '',
activation_regex: "",
},
context: {
@@ -482,9 +484,19 @@ async function applyShadowWidth() {
}
async function applyFontScale() {
async function applyFontScale(type) {
power_user.font_scale = Number(localStorage.getItem(storage_keys.font_scale) ?? 1);
document.documentElement.style.setProperty('--fontScale', power_user.font_scale);
//this is to allow forced setting on page load, theme swap, etc
if (type === 'forced') {
document.documentElement.style.setProperty('--fontScale', power_user.font_scale);
} else {
//this is to prevent the slider from updating page in real time
$("#font_scale").off('mouseup touchend').on('mouseup touchend', () => {
document.documentElement.style.setProperty('--fontScale', power_user.font_scale);
})
}
$("#font_scale_counter").text(power_user.font_scale);
$("#font_scale").val(power_user.font_scale);
}
@@ -522,7 +534,7 @@ async function applyTheme(name) {
key: 'font_scale',
action: async () => {
localStorage.setItem(storage_keys.font_scale, power_user.font_scale);
await applyFontScale();
await applyFontScale('forced');
}
},
{
@@ -641,7 +653,7 @@ async function applyMovingUIPreset(name) {
}
switchUiMode();
applyFontScale();
applyFontScale('forced');
applyThemeColor();
applyChatWidth();
applyAvatarStyle();
@@ -894,6 +906,9 @@ function loadContextSettings() {
$element.on('input', function () {
power_user.context[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
saveSettingsDebounced();
if (!control.isCheckbox) {
resetScrollHeight($element);
}
});
});
@@ -1527,8 +1542,19 @@ function setAvgBG() {
}
export function getCustomStoppingStrings() {
/**
* Gets the custom stopping strings from the power user settings.
* @param {number | undefined} limit Number of strings to return. If undefined, returns all strings.
* @returns {string[]} An array of custom stopping strings
*/
export function getCustomStoppingStrings(limit = undefined) {
try {
// If there's no custom stopping strings, return an empty array
if (!power_user.custom_stopping_strings) {
return [];
}
// Parse the JSON string
const strings = JSON.parse(power_user.custom_stopping_strings);
@@ -1537,8 +1563,8 @@ export function getCustomStoppingStrings() {
return [];
}
// Make sure all the elements are strings
return strings.filter((s) => typeof s === 'string');
// Make sure all the elements are strings. Apply the limit.
return strings.filter((s) => typeof s === 'string').slice(0, limit);
} catch (error) {
// If there's an error, return an empty array
console.warn('Error parsing custom stopping strings:', error);

View File

@@ -338,6 +338,10 @@ function onTagFilterClick(listElement) {
}
}
runTagFilters(listElement);
}
function runTagFilters(listElement) {
const tagIds = [...($(listElement).find(".tag.selected:not(.actionable)").map((_, el) => $(el).attr("id")))];
const excludedTagIds = [...($(listElement).find(".tag.excluded:not(.actionable)").map((_, el) => $(el).attr("id")))];
const filterHelper = getFilterHelper($(listElement));
@@ -364,6 +368,9 @@ function printTagFilters(type = tag_filter_types.character) {
}
for (const tag of tagsToDisplay) {
appendTagToList(FILTER_SELECTOR, tag, { removable: false, selectable: true, isGeneralList: true });
if (tag.excluded) {
runTagFilters(FILTER_SELECTOR);
}
}
for (const tagId of selectedTagIds) {