mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'ouoertheo/tts-ui-voicemap' of https://github.com/ouoertheo/SillyTavern into ouoertheo/tts-ui-voicemap
This commit is contained in:
246
public/scripts/extensions/assets/index.js
Normal file
246
public/scripts/extensions/assets/index.js
Normal 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);
|
||||
});
|
11
public/scripts/extensions/assets/manifest.json
Normal file
11
public/scripts/extensions/assets/manifest.json
Normal 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"
|
||||
}
|
79
public/scripts/extensions/assets/style.css
Normal file
79
public/scripts/extensions/assets/style.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
17
public/scripts/extensions/assets/window.html
Normal file
17
public/scripts/extensions/assets/window.html
Normal 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>
|
593
public/scripts/extensions/audio/index.js
Normal file
593
public/scripts/extensions/audio/index.js
Normal 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();
|
||||
});
|
11
public/scripts/extensions/audio/manifest.json
Normal file
11
public/scripts/extensions/audio/manifest.json
Normal 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"
|
||||
}
|
22
public/scripts/extensions/audio/style.css
Normal file
22
public/scripts/extensions/audio/style.css
Normal 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;
|
||||
}
|
66
public/scripts/extensions/audio/window.html
Normal file
66
public/scripts/extensions/audio/window.html
Normal 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>
|
@ -1,6 +1,7 @@
|
||||
import { getBase64Async } from "../../utils.js";
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings } from "../../extensions.js";
|
||||
import { callPopup, saveSettingsDebounced } from "../../../script.js";
|
||||
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@ -52,7 +53,7 @@ async function sendCaptionedMessage(caption, image) {
|
||||
name: context.name1,
|
||||
is_user: true,
|
||||
is_name: true,
|
||||
send_date: Date.now(),
|
||||
send_date: getMessageTimeStamp(),
|
||||
mes: messageText,
|
||||
extra: {
|
||||
image: image,
|
||||
|
@ -23,7 +23,8 @@ const defaultSettings = {
|
||||
};
|
||||
const settingType = {
|
||||
guidance_scale: 0,
|
||||
negative_prompt: 1
|
||||
negative_prompt: 1,
|
||||
positive_prompt: 2
|
||||
}
|
||||
|
||||
// Used for character and chat CFG values
|
||||
@ -36,19 +37,19 @@ function setCharCfg(tempValue, setting) {
|
||||
const avatarName = getCharaFilename();
|
||||
|
||||
// Assign temp object
|
||||
let tempCharaCfg;
|
||||
let tempCharaCfg = {
|
||||
name: avatarName
|
||||
};
|
||||
|
||||
switch(setting) {
|
||||
case settingType.guidance_scale:
|
||||
tempCharaCfg = {
|
||||
"name": avatarName,
|
||||
"guidance_scale": Number(tempValue)
|
||||
}
|
||||
tempCharaCfg["guidance_scale"] = Number(tempValue);
|
||||
break;
|
||||
case settingType.negative_prompt:
|
||||
tempCharaCfg = {
|
||||
"name": avatarName,
|
||||
"negative_prompt": tempValue
|
||||
}
|
||||
tempCharaCfg["negative_prompt"] = tempValue;
|
||||
break;
|
||||
case settingType.positive_prompt:
|
||||
tempCharaCfg["positive_prompt"] = tempValue;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
@ -66,7 +67,11 @@ function setCharCfg(tempValue, setting) {
|
||||
const tempAssign = Object.assign(existingCharaCfg, tempCharaCfg);
|
||||
|
||||
// If both values are default, remove the entry
|
||||
if (!existingCharaCfg.useChara && (tempAssign.guidance_scale ?? 1.00) === 1.00 && (tempAssign.negative_prompt?.length ?? 0) === 0) {
|
||||
if (!existingCharaCfg.useChara &&
|
||||
(tempAssign.guidance_scale ?? 1.00) === 1.00 &&
|
||||
(tempAssign.negative_prompt?.length ?? 0) === 0 &&
|
||||
(tempAssign.positive_prompt?.length ?? 0) === 0)
|
||||
{
|
||||
extension_settings.cfg.chara.splice(existingCharaCfgIndex, 1);
|
||||
}
|
||||
} else if (avatarName && tempValue.length > 0) {
|
||||
@ -95,6 +100,9 @@ function setChatCfg(tempValue, setting) {
|
||||
case settingType.negative_prompt:
|
||||
chat_metadata[metadataKeys.negative_prompt] = tempValue;
|
||||
break;
|
||||
case settingType.positive_prompt:
|
||||
chat_metadata[metadataKeys.positive_prompt] = tempValue;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -174,20 +182,40 @@ function loadSettings() {
|
||||
$('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2));
|
||||
$('#chat_cfg_guidance_scale_counter').text(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2));
|
||||
$('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? '');
|
||||
$('#chat_cfg_positive_prompt').val(chat_metadata[metadataKeys.positive_prompt] ?? '');
|
||||
$('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false);
|
||||
if (chat_metadata[metadataKeys.negative_combine]?.length > 0) {
|
||||
chat_metadata[metadataKeys.negative_combine].forEach((element) => {
|
||||
$(`input[name="cfg_negative_combine"][value="${element}"]`)
|
||||
if (chat_metadata[metadataKeys.prompt_combine]?.length > 0) {
|
||||
chat_metadata[metadataKeys.prompt_combine].forEach((element) => {
|
||||
$(`input[name="cfg_prompt_combine"][value="${element}"]`)
|
||||
.prop("checked", true);
|
||||
});
|
||||
}
|
||||
|
||||
// Display the negative separator in quotes if not quoted already
|
||||
let promptSeparatorDisplay = [];
|
||||
const promptSeparator = chat_metadata[metadataKeys.prompt_separator];
|
||||
if (promptSeparator) {
|
||||
promptSeparatorDisplay.push(promptSeparator);
|
||||
if (!promptSeparator.startsWith(`"`)) {
|
||||
promptSeparatorDisplay.unshift(`"`);
|
||||
}
|
||||
|
||||
if (!promptSeparator.endsWith(`"`)) {
|
||||
promptSeparatorDisplay.push(`"`);
|
||||
}
|
||||
}
|
||||
|
||||
$('#cfg_prompt_separator').val(promptSeparatorDisplay.length === 0 ? '' : promptSeparatorDisplay.join(''));
|
||||
|
||||
$('#cfg_prompt_insertion_depth').val(chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1);
|
||||
|
||||
// Set character CFG if it exists
|
||||
if (!selected_group) {
|
||||
const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename());
|
||||
$('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00);
|
||||
$('#chara_cfg_guidance_scale_counter').text(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2));
|
||||
$('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? '');
|
||||
$('#chara_cfg_positive_prompt').val(charaCfg?.positive_prompt ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,26 +232,50 @@ async function initialLoadSettings() {
|
||||
$('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale);
|
||||
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
|
||||
$('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt);
|
||||
$('#global_cfg_positive_prompt').val(extension_settings.cfg.global.positive_prompt);
|
||||
}
|
||||
|
||||
function migrateSettings() {
|
||||
let performSave = false;
|
||||
let performSettingsSave = false;
|
||||
let performMetaSave = false;
|
||||
|
||||
if (power_user.guidance_scale) {
|
||||
extension_settings.cfg.global.guidance_scale = power_user.guidance_scale;
|
||||
delete power_user['guidance_scale'];
|
||||
performSave = true;
|
||||
performSettingsSave = true;
|
||||
}
|
||||
|
||||
if (power_user.negative_prompt) {
|
||||
extension_settings.cfg.global.negative_prompt = power_user.negative_prompt;
|
||||
delete power_user['negative_prompt'];
|
||||
performSave = true;
|
||||
performSettingsSave = true;
|
||||
}
|
||||
|
||||
if (performSave) {
|
||||
if (chat_metadata["cfg_negative_combine"]) {
|
||||
chat_metadata[metadataKeys.prompt_combine] = chat_metadata["cfg_negative_combine"];
|
||||
chat_metadata["cfg_negative_combine"] = undefined;
|
||||
performMetaSave = true;
|
||||
}
|
||||
|
||||
if (chat_metadata["cfg_negative_insertion_depth"]) {
|
||||
chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata["cfg_negative_insertion_depth"];
|
||||
chat_metadata["cfg_negative_insertion_depth"] = undefined;
|
||||
performMetaSave = true;
|
||||
}
|
||||
|
||||
if (chat_metadata["cfg_negative_separator"]) {
|
||||
chat_metadata[metadataKeys.prompt_separator] = chat_metadata["cfg_negative_separator"];
|
||||
chat_metadata["cfg_negative_separator"] = undefined;
|
||||
performMetaSave = true;
|
||||
}
|
||||
|
||||
if (performSettingsSave) {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
if (performMetaSave) {
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called when the extension is loaded
|
||||
@ -255,6 +307,10 @@ jQuery(async () => {
|
||||
setChatCfg($(this).val(), settingType.negative_prompt);
|
||||
});
|
||||
|
||||
windowHtml.find('#chat_cfg_positive_prompt').on('input', function() {
|
||||
setChatCfg($(this).val(), settingType.positive_prompt);
|
||||
});
|
||||
|
||||
windowHtml.find('#chara_cfg_guidance_scale').on('input', function() {
|
||||
const value = $(this).val();
|
||||
const success = setCharCfg(value, settingType.guidance_scale);
|
||||
@ -267,6 +323,10 @@ jQuery(async () => {
|
||||
setCharCfg($(this).val(), settingType.negative_prompt);
|
||||
});
|
||||
|
||||
windowHtml.find('#chara_cfg_positive_prompt').on('input', function() {
|
||||
setCharCfg($(this).val(), settingType.positive_prompt);
|
||||
});
|
||||
|
||||
windowHtml.find('#global_cfg_guidance_scale').on('input', function() {
|
||||
extension_settings.cfg.global.guidance_scale = Number($(this).val());
|
||||
$('#global_cfg_guidance_scale_counter').text(extension_settings.cfg.global.guidance_scale.toFixed(2));
|
||||
@ -278,14 +338,29 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
windowHtml.find(`input[name="cfg_negative_combine"]`).on('input', function() {
|
||||
const values = windowHtml.find(`input[name="cfg_negative_combine"]`)
|
||||
windowHtml.find('#global_cfg_positive_prompt').on('input', function() {
|
||||
extension_settings.cfg.global.positive_prompt = $(this).val();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
windowHtml.find(`input[name="cfg_prompt_combine"]`).on('input', function() {
|
||||
const values = windowHtml.find(`input[name="cfg_prompt_combine"]`)
|
||||
.filter(":checked")
|
||||
.map(function() { return parseInt($(this).val()) })
|
||||
.get()
|
||||
.filter((e) => e !== NaN) || [];
|
||||
.filter((e) => !Number.isNaN(e)) || [];
|
||||
|
||||
chat_metadata[metadataKeys.negative_combine] = values;
|
||||
chat_metadata[metadataKeys.prompt_combine] = values;
|
||||
saveMetadataDebounced();
|
||||
});
|
||||
|
||||
windowHtml.find(`#cfg_prompt_insertion_depth`).on('input', function() {
|
||||
chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val());
|
||||
saveMetadataDebounced();
|
||||
});
|
||||
|
||||
windowHtml.find(`#cfg_prompt_separator`).on('input', function() {
|
||||
chat_metadata[metadataKeys.prompt_separator] = $(this).val();
|
||||
saveMetadataDebounced();
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { chat_metadata, this_chid } from "../../../script.js";
|
||||
import { chat_metadata, substituteParams, this_chid } from "../../../script.js";
|
||||
import { extension_settings, getContext } from "../../extensions.js"
|
||||
import { selected_group } from "../../group-chats.js";
|
||||
import { getCharaFilename } from "../../utils.js";
|
||||
@ -11,46 +11,20 @@ export const cfgType = {
|
||||
export const metadataKeys = {
|
||||
guidance_scale: "cfg_guidance_scale",
|
||||
negative_prompt: "cfg_negative_prompt",
|
||||
negative_combine: "cfg_negative_combine",
|
||||
groupchat_individual_chars: "cfg_groupchat_individual_chars"
|
||||
positive_prompt: "cfg_positive_prompt",
|
||||
prompt_combine: "cfg_prompt_combine",
|
||||
groupchat_individual_chars: "cfg_groupchat_individual_chars",
|
||||
prompt_insertion_depth: "cfg_prompt_insertion_depth",
|
||||
prompt_separator: "cfg_prompt_separator"
|
||||
}
|
||||
|
||||
// Gets the CFG value from hierarchy of chat -> character -> global
|
||||
// Returns undefined values which should be handled in the respective backend APIs
|
||||
export function getCfg() {
|
||||
let splitNegativePrompt = [];
|
||||
// Gets the CFG guidance scale
|
||||
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
|
||||
export function getGuidanceScale() {
|
||||
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
|
||||
const guidanceScale = getGuidanceScale(charaCfg);
|
||||
const chatNegativeCombine = chat_metadata[metadataKeys.negative_combine] ?? [];
|
||||
|
||||
// If there's a guidance scale, continue. Otherwise assume undefined
|
||||
if (guidanceScale?.value && guidanceScale?.value !== 1) {
|
||||
if (guidanceScale.type === cfgType.chat || chatNegativeCombine.includes(cfgType.chat)) {
|
||||
splitNegativePrompt.push(chat_metadata[metadataKeys.negative_prompt]?.trim());
|
||||
}
|
||||
|
||||
if (guidanceScale.type === cfgType.chara || chatNegativeCombine.includes(cfgType.chara)) {
|
||||
splitNegativePrompt.push(charaCfg.negative_prompt?.trim())
|
||||
}
|
||||
|
||||
if (guidanceScale.type === cfgType.global || chatNegativeCombine.includes(cfgType.global)) {
|
||||
splitNegativePrompt.push(extension_settings.cfg.global.negative_prompt?.trim());
|
||||
}
|
||||
|
||||
const combinedNegatives = splitNegativePrompt.filter((e) => e.length > 0).join(", ");
|
||||
console.debug(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedNegatives}`)
|
||||
|
||||
return {
|
||||
guidanceScale: guidanceScale.value,
|
||||
negativePrompt: combinedNegatives
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the guidance scale is 1, ignore the CFG negative prompt since it won't be used anyways
|
||||
function getGuidanceScale(charaCfg) {
|
||||
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
|
||||
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
|
||||
|
||||
if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) {
|
||||
return {
|
||||
type: cfgType.chat,
|
||||
@ -65,8 +39,52 @@ function getGuidanceScale(charaCfg) {
|
||||
};
|
||||
}
|
||||
|
||||
if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) {
|
||||
return {
|
||||
type: cfgType.global,
|
||||
value: extension_settings.cfg.global.guidance_scale
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the CFG prompt
|
||||
export function getCfgPrompt(guidanceScale, isNegative) {
|
||||
let splitCfgPrompt = [];
|
||||
|
||||
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
|
||||
if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) {
|
||||
splitCfgPrompt.unshift(
|
||||
substituteParams(
|
||||
chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
|
||||
if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) {
|
||||
splitCfgPrompt.unshift(
|
||||
substituteParams(
|
||||
isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) {
|
||||
splitCfgPrompt.unshift(
|
||||
substituteParams(
|
||||
isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// This line is a bit hacky with a JSON.stringify and JSON.parse. Fix this if possible.
|
||||
const customSeparator = JSON.parse(chat_metadata[metadataKeys.prompt_separator] || JSON.stringify("\n")) ?? "\n";
|
||||
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
|
||||
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
|
||||
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
|
||||
|
||||
return {
|
||||
type: cfgType.global,
|
||||
value: extension_settings.cfg.global.guidance_scale
|
||||
value: combinedCfgPrompt,
|
||||
depth: insertionDepth
|
||||
};
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
<small>
|
||||
<b>Unique to this chat.</b><br>
|
||||
</small>
|
||||
<label for="chat_cfg_negative_prompt">
|
||||
<label for="chat_cfg_guidance_scale">
|
||||
<span data-i18n="Scale">Scale</span>
|
||||
<small data-i18n="1 = disabled">1 = disabled</small>
|
||||
</label>
|
||||
@ -33,6 +33,11 @@
|
||||
<span data-i18n="Negative Prompt">Negative Prompt</span>
|
||||
</label>
|
||||
<textarea id="chat_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
|
||||
<label for="chat_cfg_positive_prompt">
|
||||
<span data-i18n="Positive Prompt">Positive Prompt</span>
|
||||
</label>
|
||||
<textarea id="chat_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
</div>
|
||||
<div id="groupchat_cfg_use_chara_container">
|
||||
<label class="checkbox_label" for="groupchat_cfg_use_chara">
|
||||
@ -53,7 +58,7 @@
|
||||
<div class="inline-drawer-content">
|
||||
<small><b>Will be automatically added as the CFG for this character.</b></small>
|
||||
<br />
|
||||
<label for="chara_cfg_negative_prompt">
|
||||
<label for="chara_cfg_guidance_scale">
|
||||
<span data-i18n="Scale">Scale</span>
|
||||
<small data-i18n="1 = disabled">1 = disabled</small>
|
||||
</label>
|
||||
@ -72,6 +77,11 @@
|
||||
<span data-i18n="Negative Prompt">Negative Prompt</span>
|
||||
</label>
|
||||
<textarea id="chara_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
|
||||
<label for="chara_cfg_positive_prompt">
|
||||
<span data-i18n="Positive Prompt">Positive Prompt</span>
|
||||
</label>
|
||||
<textarea id="chara_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,7 +96,7 @@
|
||||
<div class="inline-drawer-content">
|
||||
<small><b>Will be used as the default CFG options for every chat unless overridden.</b></small>
|
||||
<br />
|
||||
<label for="global_cfg_negative_prompt">
|
||||
<label for="global_cfg_guidance_scale">
|
||||
<span data-i18n="Scale">Scale</span>
|
||||
<small data-i18n="1 = disabled">1 = disabled</small>
|
||||
</label>
|
||||
@ -105,39 +115,56 @@
|
||||
<span data-i18n="Negative Prompt">Negative Prompt</span>
|
||||
</label>
|
||||
<textarea id="global_cfg_negative_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
|
||||
<label for="global_cfg_positive_prompt">
|
||||
<span data-i18n="Positive Prompt">Positive Prompt</span>
|
||||
</label>
|
||||
<textarea id="global_cfg_positive_prompt" rows="2" class="text_pole textarea_compact" data-i18n="[placeholder]write short replies, write replies using past tense" placeholder="write short replies, write replies using past tense"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="cfg_negative_combine_container">
|
||||
<div id="cfg_prompt_combine_container">
|
||||
<hr class="sysHR">
|
||||
<div class="inline-drawer">
|
||||
<div id="defaultANBlockToggle" class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Negative Cascading</b>
|
||||
<b>CFG Prompt Cascading</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<small>
|
||||
<b>Combine negative prompts from other boxes.</b>
|
||||
<br />
|
||||
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
|
||||
</small>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<small>
|
||||
<b>Combine positive/negative prompts from other boxes.</b>
|
||||
<br />
|
||||
For example, ticking the chat, global, and character boxes combine all negative prompts into a comma-separated string.
|
||||
</small>
|
||||
</div>
|
||||
<br />
|
||||
<label for="cfg_negative_combine">
|
||||
<span data-i18n="Scale">Always Include</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_negative_combine" value="0" />
|
||||
<span data-i18n="Chat Negatives">Chat Negatives</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_negative_combine" value="1" />
|
||||
<span data-i18n="Character Negatives">Character Negatives</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_negative_combine" value="2" />
|
||||
<span data-i18n="Global Negatives">Global Negatives</span>
|
||||
</label>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="cfg_prompt_combine">
|
||||
<span data-i18n="Scale">Always Include</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_prompt_combine" value="0" />
|
||||
<span data-i18n="Chat Negatives">Chat Negatives</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_prompt_combine" value="1" />
|
||||
<span data-i18n="Character Negatives">Character Negatives</span>
|
||||
</label>
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" name="cfg_prompt_combine" value="2" />
|
||||
<span data-i18n="Global Negatives">Global Negatives</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label>
|
||||
Custom Separator: <input id="cfg_prompt_separator" class="text_pole textarea_compact widthUnset" placeholder=""\n"" type="text" />
|
||||
</label>
|
||||
<label>
|
||||
Insertion Depth: <input id="cfg_prompt_insertion_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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);
|
||||
|
12
public/scripts/extensions/expressions/list-item.html
Normal file
12
public/scripts/extensions/expressions/list-item.html
Normal 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>
|
44
public/scripts/extensions/expressions/settings.html
Normal file
44
public/scripts/extensions/expressions/settings.html
Normal 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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, CHARACTERS_PER_TOKEN_RATIO, substituteParams, max_context, } from "../../../script.js";
|
||||
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, substituteParams, } from "../../../script.js";
|
||||
import { humanizedDateTime } from "../../RossAscends-mods.js";
|
||||
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
|
||||
import { CHARACTERS_PER_TOKEN_RATIO } from "../../tokenizers.js";
|
||||
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
|
@ -70,7 +70,7 @@ const defaultSettings = {
|
||||
promptMaxWords: 1000,
|
||||
promptWordsStep: 25,
|
||||
promptInterval: 10,
|
||||
promptMinInterval: 1,
|
||||
promptMinInterval: 0,
|
||||
promptMaxInterval: 100,
|
||||
promptIntervalStep: 1,
|
||||
promptForceWords: 0,
|
||||
@ -333,6 +333,11 @@ async function summarizeChat(context) {
|
||||
}
|
||||
|
||||
async function summarizeChatMain(context, force) {
|
||||
if (extension_settings.memory.promptInterval === 0 && !force) {
|
||||
console.debug('Prompt interval is set to 0, skipping summarization');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Wait for group to finish generating
|
||||
if (selected_group) {
|
||||
@ -562,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" />
|
||||
@ -583,6 +588,7 @@ jQuery(function () {
|
||||
<label for="memory_prompt_words">Number of words in the summary (<span id="memory_prompt_words_value"></span> words)</label>
|
||||
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
|
||||
<label for="memory_prompt_interval">Update interval (<span id="memory_prompt_interval_value"></span> messages)</label>
|
||||
<small>Set to 0 to disable</small>
|
||||
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
|
||||
<label for="memory_prompt_words_force">Force update after (<span id="memory_prompt_words_force_value"></span> words)</label>
|
||||
<small>Set to 0 to disable</small>
|
||||
|
@ -164,7 +164,7 @@ function getNextIncompleteTaskRecurse(task){
|
||||
}
|
||||
|
||||
// Set a task in extensionPrompt context. Defaults to first incomplete
|
||||
function setCurrentTask(taskId = null) {
|
||||
function setCurrentTask(taskId = null, skipSave = false) {
|
||||
const context = getContext();
|
||||
|
||||
// TODO: Should probably null this rather than set empty object
|
||||
@ -202,7 +202,10 @@ function setCurrentTask(taskId = null) {
|
||||
console.info(`No current task`);
|
||||
}
|
||||
|
||||
saveState();
|
||||
// Save state if not skipping
|
||||
if (!skipSave) {
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
function getHighestTaskIdRecurse(task) {
|
||||
@ -731,7 +734,7 @@ function loadSettings() {
|
||||
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
|
||||
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
|
||||
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
|
||||
setCurrentTask()
|
||||
setCurrentTask(null, true)
|
||||
}
|
||||
|
||||
function addManualTaskCheckUi() {
|
||||
|
@ -28,7 +28,7 @@ async function updateQuickReplyPresetList() {
|
||||
if (result.ok) {
|
||||
var data = await result.json();
|
||||
presets = data.quickReplyPresets?.length ? data.quickReplyPresets : [];
|
||||
console.log(presets)
|
||||
console.debug('Quick Reply presets', presets);
|
||||
$("#quickReplyPresets").find('option[value!=""]').remove();
|
||||
|
||||
|
||||
@ -284,7 +284,7 @@ async function doQR(_, text) {
|
||||
}
|
||||
|
||||
text = Number(text)
|
||||
//use scale starting with 0
|
||||
//use scale starting with 0
|
||||
//ex: user inputs "/qr 2" >> qr with data-index 1 (but 2nd item displayed) gets triggered
|
||||
let QRnum = Number(text - 1)
|
||||
if (QRnum <= 0) { QRnum = 0 }
|
||||
|
@ -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
|
||||
|
@ -4,11 +4,12 @@ TODO:
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from "../../../script.js";
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
||||
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
|
||||
import { VoskSttProvider } from './vosk.js'
|
||||
import { WhisperSttProvider } from './whisper.js'
|
||||
import { BrowserSttProvider } from './browser.js'
|
||||
import { StreamingSttProvider } from './streaming.js'
|
||||
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'Speech Recognition';
|
||||
@ -61,10 +62,10 @@ async function moduleWorker() {
|
||||
let messageStart = -1;
|
||||
|
||||
if (extension_settings.speech_recognition.Streaming.triggerWordsEnabled) {
|
||||
|
||||
|
||||
for (const triggerWord of extension_settings.speech_recognition.Streaming.triggerWords) {
|
||||
const triggerPos = userMessageRaw.indexOf(triggerWord.toLowerCase());
|
||||
|
||||
|
||||
// Trigger word not found or not starting message and just a substring
|
||||
if (triggerPos == -1){ // | (triggerPos > 0 & userMessageFormatted[triggerPos-1] != " ")) {
|
||||
console.debug(DEBUG_PREFIX+"trigger word not found: ", triggerWord);
|
||||
@ -152,12 +153,12 @@ async function processTranscript(transcript) {
|
||||
name: context.name1,
|
||||
is_user: true,
|
||||
is_name: true,
|
||||
send_date: Date.now(),
|
||||
send_date: getMessageTimeStamp(),
|
||||
mes: messageText,
|
||||
};
|
||||
context.chat.push(message);
|
||||
context.addOneMessage(message);
|
||||
|
||||
|
||||
await context.generate();
|
||||
|
||||
$('#debug_output').text("<SST-module DEBUG>: message sent: \""+ transcriptFormatted +"\"");
|
||||
@ -191,10 +192,10 @@ async function processTranscript(transcript) {
|
||||
function loadNavigatorAudioRecording() {
|
||||
if (navigator.mediaDevices.getUserMedia) {
|
||||
console.debug(DEBUG_PREFIX+' getUserMedia supported by browser.');
|
||||
|
||||
|
||||
let onSuccess = function(stream) {
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
|
||||
|
||||
$("#microphone_button").off('click').on("click", function() {
|
||||
if (!audioRecording) {
|
||||
mediaRecorder.start();
|
||||
@ -211,30 +212,30 @@ function loadNavigatorAudioRecording() {
|
||||
$("#microphone_button").toggleClass('fa-microphone fa-microphone-slash');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
mediaRecorder.onstop = async function() {
|
||||
console.debug(DEBUG_PREFIX+"data available after MediaRecorder.stop() called: ", audioChunks.length, " chunks");
|
||||
const audioBlob = new Blob(audioChunks, { type: "audio/wav; codecs=0" });
|
||||
audioChunks = [];
|
||||
|
||||
|
||||
const transcript = await sttProvider.processAudio(audioBlob);
|
||||
|
||||
|
||||
// TODO: lock and release recording while processing?
|
||||
console.debug(DEBUG_PREFIX+"received transcript:", transcript);
|
||||
processTranscript(transcript);
|
||||
}
|
||||
|
||||
|
||||
mediaRecorder.ondataavailable = function(e) {
|
||||
audioChunks.push(e.data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let onError = function(err) {
|
||||
console.debug(DEBUG_PREFIX+"The following error occured: " + err);
|
||||
}
|
||||
|
||||
|
||||
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
|
||||
|
||||
|
||||
} else {
|
||||
console.debug(DEBUG_PREFIX+"getUserMedia not supported on your browser!");
|
||||
toastr.error("getUserMedia not supported", DEBUG_PREFIX+"not supported for your browser.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
@ -257,7 +258,7 @@ function loadSttProvider(provider) {
|
||||
console.warn(`Provider ${sttProviderName} not in Extension Settings, initiatilizing provider in settings`);
|
||||
extension_settings.speech_recognition[sttProviderName] = {};
|
||||
}
|
||||
|
||||
|
||||
$('#speech_recognition_provider').val(sttProviderName);
|
||||
|
||||
if (sttProviderName == "None") {
|
||||
@ -287,13 +288,13 @@ function loadSttProvider(provider) {
|
||||
loadNavigatorAudioRecording();
|
||||
$("#microphone_button").show();
|
||||
}
|
||||
|
||||
|
||||
if (sttProviderName == "Streaming") {
|
||||
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
|
||||
$("#microphone_button").off('click');
|
||||
$("#microphone_button").hide();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function onSttProviderChange() {
|
||||
@ -365,7 +366,7 @@ async function onMessageMappingChange() {
|
||||
console.debug(DEBUG_PREFIX+"Wrong syntax for message mapping, no '=' found in:", text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$("#speech_recognition_message_mapping_status").text("Message mapping updated to: "+JSON.stringify(extension_settings.speech_recognition.messageMapping))
|
||||
console.debug(DEBUG_PREFIX+"Updated message mapping", extension_settings.speech_recognition.messageMapping);
|
||||
extension_settings.speech_recognition.messageMappingText = $('#speech_recognition_message_mapping').val()
|
||||
@ -425,7 +426,7 @@ $(document).ready(function () {
|
||||
$('#speech_recognition_message_mode').on('change', onMessageModeChange);
|
||||
$('#speech_recognition_message_mapping').on('change', onMessageMappingChange);
|
||||
$('#speech_recognition_message_mapping_enabled').on('click', onMessageMappingEnabledClick);
|
||||
|
||||
|
||||
const $button = $('<div id="microphone_button" class="fa-solid fa-microphone speech-toggle" title="Click to speak"></div>');
|
||||
$('#send_but_sheld').prepend($button);
|
||||
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
||||
import { selected_group } from "../../group-chats.js";
|
||||
import { stringFormat, initScrollHeight, resetScrollHeight, timestampToMoment, getCharaFilename, saveBase64AsFile } from "../../utils.js";
|
||||
import { humanizedDateTime } from "../../RossAscends-mods.js";
|
||||
import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
// Wraps a string into monospace font-face span
|
||||
@ -755,11 +755,10 @@ async function sendMessage(prompt, image) {
|
||||
const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`;
|
||||
const message = {
|
||||
name: context.groupId ? systemUserName : context.name2,
|
||||
is_system: context.groupId ? true : false,
|
||||
is_user: false,
|
||||
is_system: true,
|
||||
is_name: true,
|
||||
send_date: timestampToMoment(Date.now()).format('LL LT'),
|
||||
send_date: getMessageTimeStamp(),
|
||||
mes: context.groupId ? p(messageText) : messageText,
|
||||
extra: {
|
||||
image: image,
|
||||
@ -819,7 +818,7 @@ function addSDGenButtons() {
|
||||
$(document).on('click touchend', function (e) {
|
||||
const target = $(e.target);
|
||||
if (target.is(dropdown)) return;
|
||||
if (target.is(button) && !dropdown.is(":visible") && $("#send_but").css('display') === 'flex') {
|
||||
if (target.is(button) && !dropdown.is(":visible") && $("#send_but").is(":visible")) {
|
||||
e.preventDefault();
|
||||
|
||||
dropdown.fadeIn(250);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { callPopup, main_api } from "../../../script.js";
|
||||
import { getContext } from "../../extensions.js";
|
||||
import { getTokenizerModel } from "../../openai.js";
|
||||
import { getTokenizerModel } from "../../tokenizers.js";
|
||||
|
||||
async function doTokenCounter() {
|
||||
const selectedTokenizer = main_api == 'openai'
|
||||
|
@ -217,6 +217,10 @@ async function translateProviderDeepl(text, lang) {
|
||||
|
||||
async function translate(text, lang) {
|
||||
try {
|
||||
if (text == '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (extension_settings.translate.provider) {
|
||||
case 'google':
|
||||
return await translateProviderGoogle(text, lang);
|
||||
@ -421,9 +425,9 @@ jQuery(() => {
|
||||
|
||||
loadSettings();
|
||||
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage);
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
|
||||
eventSource.on(event_types.MESSAGE_SENT, handleOutgoingMessage);
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);
|
||||
|
||||
|
@ -413,7 +413,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()
|
||||
|
66
public/scripts/extensions/variables/index.js
Normal file
66
public/scripts/extensions/variables/index.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { getContext } from "../../extensions.js";
|
||||
|
||||
/**
|
||||
* Gets a chat variable from the current chat metadata.
|
||||
* @param {string} name The name of the variable to get.
|
||||
* @returns {string} The value of the variable.
|
||||
*/
|
||||
function getChatVariable(name) {
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
return '';
|
||||
}
|
||||
|
||||
return metadata.variables[name] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a chat variable in the current chat metadata.
|
||||
* @param {string} name The name of the variable to set.
|
||||
* @param {any} value The value of the variable to set.
|
||||
*/
|
||||
function setChatVariable(name, value) {
|
||||
if (name === undefined || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
}
|
||||
|
||||
metadata.variables[name] = value;
|
||||
}
|
||||
|
||||
function listChatVariables() {
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.keys(metadata.variables).map(key => `${key}=${metadata.variables[key]}`).join(';');
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
const context = getContext();
|
||||
context.registerHelper('getvar', getChatVariable);
|
||||
context.registerHelper('setvar', setChatVariable);
|
||||
context.registerHelper('listvar', listChatVariables);
|
||||
});
|
11
public/scripts/extensions/variables/manifest.json
Normal file
11
public/scripts/extensions/variables/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Chat Variables",
|
||||
"loading_order": 100,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
Reference in New Issue
Block a user