diff --git a/.gitignore b/.gitignore
index 8ac21adf0..2464b8036 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,4 @@ public/movingUI/
public/QuickReplies/
content.log
cloudflared.exe
+public/assets/
\ No newline at end of file
diff --git a/public/assets/ambient/.placeholder b/public/assets/ambient/.placeholder
new file mode 100644
index 000000000..a4faa1166
--- /dev/null
+++ b/public/assets/ambient/.placeholder
@@ -0,0 +1 @@
+Put ambient audio files here.
\ No newline at end of file
diff --git a/public/assets/bgm/.placeholder b/public/assets/bgm/.placeholder
new file mode 100644
index 000000000..95839f44e
--- /dev/null
+++ b/public/assets/bgm/.placeholder
@@ -0,0 +1 @@
+Put bgm audio files here
diff --git a/public/assets/temp/.placeholder b/public/assets/temp/.placeholder
new file mode 100644
index 000000000..e69de29bb
diff --git a/public/context/Minimalist.json b/public/context/Minimalist.json
new file mode 100644
index 000000000..e454e3934
--- /dev/null
+++ b/public/context/Minimalist.json
@@ -0,0 +1,6 @@
+{
+ "name": "Minimalist",
+ "story_string": "{{#if system}}{{system}}\n{{/if}}{{#if wiBefore}}{{wiBefore}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{personality}}\n{{/if}}{{#if scenario}}{{scenario}}\n{{/if}}{{#if wiAfter}}{{wiAfter}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}",
+ "chat_start": "###",
+ "example_separator": "###"
+}
diff --git a/public/i18n.json b/public/i18n.json
index 2e1aedb09..6abab8c31 100644
--- a/public/i18n.json
+++ b/public/i18n.json
@@ -4,7 +4,7 @@
"ja-jp",
"ko-kr",
"ru-ru",
- "it-it",
+ "it-it",
"nl-nl"
],
"zh-cn": {
@@ -2798,7 +2798,134 @@
"Bind user name to that avatar": "Lega il nome utente a questo avatar",
"Select this as default persona for the new chats.": "Seleziona questo alterego come predefinito per tutte le nuove chat",
"Change persona image": "Cambia l'immagine del tuo alterego",
- "Delete persona": "Elimina il tuo alterego"
+ "Delete persona": "Elimina il tuo alterego",
+ "--- Pick to Edit ---": "--- Scegli per modificare ---",
+ "Add text here that would make the AI generate things you don't want in your outputs.": "Scrivi qui ciò che non vuoi l'IA generi nel suo output.",
+ "write short replies, write replies using past tense": "Scrivi risposte brevi, scrivi risposte usando il passato",
+ "Alert if your world info is greater than the allocated budget.": "Questo avvisa nel momento in cui le 'Info Mondo' consumano più di quanto allocato nel budget.",
+ "Clear your cookie": "Cancella i cookie",
+ "Restore new group chat prompt": "Ripristina il prompt della nuova chat di gruppo",
+ "Save movingUI changes to a new file": "Salva i cambiamenti apportati alla posizione dei pannelli dell'UI (MovingUI) in un nuovo file",
+ "Export all": "Esporta tutto",
+ "Import": "Importa",
+ "Insert": "Inserisci",
+ "New": "Nuovo",
+ "Prompts": "Prompt",
+ "Tokens": "Token",
+ "Reset current character": "Ripristina il personaggio attuale",
+ "(0 = disabled)": "(0 = disabilitato)",
+ "1 = disabled": "1 = disabilitato",
+ "Activation Regex": "Attivazione Regex",
+ "Active World(s) for all chats": "Attiva i Mondi per tutte le chat",
+ "Add character names": "Aggiungi i nomi dei personaggi",
+ "Add Memo": "Aggiungi note",
+ "Advanced Character Search": "Ricerca dei personaggi avanzata",
+ "Aggressive": "Aggressivo",
+ "AI21 Model": "Modello AI21",
+ "Alert On Overflow": "Avviso in caso di Overflow",
+ "Allow fallback routes": "Permetti fallback routes",
+ "Allow fallback routes Description": "Permetti la descrizione di fallback routes",
+ "Alt Method": "Metodo Alt",
+ "Alternate Greetings": "Alterna i saluti",
+ "Alternate Greetings Hint": "Suggerimenti per i saluti alternati",
+ "Alternate Greetings Subtitle": "Sottotitoli per i saluti alternati",
+ "Assistant Prefill": "Assistant Prefill",
+ "Banned Tokens": "Token banditi",
+ "Blank": "In bianco",
+ "Browser default": "Predefinito del browser",
+ "Budget Cap": "Limite budget",
+ "CFG": "CFG",
+ "CFG Scale": "CFG Scale",
+ "Changes the style of the generated text.": "Cambia lo stile del testo generato.",
+ "Character Negatives": "Character Negatives",
+ "Chat Negatives": "Chat Negatives",
+ "Chat Scenario Override": "Sovrascrittura dello scenario della chat",
+ "Chat Start": "Avvio della chat",
+ "Claude Model": "Modello Claude",
+ "Close chat": "Chiudi chat",
+ "Context %": "Context %",
+ "Context Template": "Context Template",
+ "Count Penalty": "Count Penalty",
+ "Example Separator": "Separatore d'esempio",
+ "Exclude Assistant suffix": "Escludi il suffisso assistente",
+ "Exclude the assistant suffix from being added to the end of prompt.": "Esclude il suffisso assistente dall'essere aggiunto alla fine del prompt.",
+ "Force for Groups and Personas": "Forzalo per gruppi e alterego",
+ "Global Negatives": "Global Negatives",
+ "In Story String / Chat Completion: After Character Card": "Nella stringa narrativa / Chat Completion: Dopo la 'Carta Personaggio'",
+ "In Story String / Chat Completion: Before Character Card": "Nella stringa narrativa / Chat Completion: Prima della 'Carta Personaggio",
+ "Instruct": "Instruct",
+ "Instruct Mode": "Modalità Instruct",
+ "Last Sequence": "Ultima sequenza",
+ "Lazy Chat Loading": "Caricamento svogliato della chat",
+ "Least tokens": "Token minimi",
+ "Light": "Leggero",
+ "Load koboldcpp order": "Ripristina l'ordine di koboldcpp",
+ "Main": "Principale",
+ "Mancer API key": "Chiave API di Mancer",
+ "Mancer API url": "Url API di Mancer",
+ "May help the model to understand context. Names must only contain letters or numbers.": "Può aiutare il modello a comprendere meglio il contesto. I nomi devono contenere solo numeri e lettere.",
+ "Medium": "Medium",
+ "Mirostat": "Mirostat",
+ "Mirostat (mode=1 is only for llama.cpp)": "Mirostat (mode=1 è valido solo per llama.cpp)",
+ "Mirostat Eta": "Mirostat Eta",
+ "Mirostat LR": "Mirostat LR",
+ "Mirostat Mode": "Mirostat Mode",
+ "Mirostat Tau": "Mirostat Tau",
+ "Model Icon": "Icona del modello",
+ "Most tokens": "Token massimi",
+ "MovingUI Preset": "Preset MovingUI",
+ "Negative Prompt": "Prompt negativo",
+ "No Module": "Nessun modulo",
+ "NSFW": "NSFW",
+ "Nucleus Sampling": "Nucleus Sampling",
+ "Off": "Spento",
+ "OpenRouter API Key": "Chiave API di OpenRouter",
+ "OpenRouter Model": "Modello OpenRouter",
+ "or": "o",
+ "Phrase Repetition Penalty": "Phrase Repetition Penalty",
+ "Positive Prompt": "Prompt positivo",
+ "Preamble": "Premessa",
+ "Prompt Overrides (For OpenAI/Claude/Scale APIs, Window/OpenRouter, and Instruct Mode)": "Sovrascrittura del prompt (Per le API di OpenAI/Claude/Scale, Window/OpenRouter, e la Modalità Instruct)",
+ "Prompt that is used when the NSFW toggle is O": "Prompt utilizzato quando l'interruttore NSFW è disattivato.",
+ "Prose Augmenter": "Prose Augmenter",
+ "Proxy Password": "Password proxy",
+ "Quick Edit": "Editing rapido",
+ "Random": "Casuale",
+ "Relaxed API URLS": "URL API sciatto",
+ "Replace Macro in Custom Stopping Strings": "Rimpiazza le macro nelle stringe d'arresto personalizzate",
+ "Scale": "Scale",
+ "Scale": "Scale",
+ "Sequences you don't want to appear in the output. One per line.": "Sequenze che non vuoi appaiano nell'output. Una per linea.",
+ "Set at the beginning of Dialogue examples to indicate that a new example chat is about to start.": "Impostato all'inizio degli Esempi di dialogo per indicare che un nuovo esempio di chat sta per iniziare.",
+ "Set at the beginning of the chat history to indicate that a new chat is about to start.": "Impostato all'inizio degli esempi di dialogo per indicare che una nuova chat sta per iniziare.",
+ "Set at the beginning of the chat history to indicate that a new chat is about to start.": "Impostato all'inizio della cronologia chat per indicare che una nuova chat sta per iniziare.",
+ "Set at the beginning of the chat history to indicate that a new group chat is about to start.": "Impostato all'inizio della cronologia chat per indicare che un nuova chat di gruppo sta per iniziare.",
+ "Show External models (provided by API)": "Mostra modelli esterni (Forniti dall'API)",
+ "Show Notifications Show notifications on switching personas": "Mostra una notifica quando l'alterego viene cambiato",
+ "Show tags in responses": "Mostra i tag nelle risposte",
+ "Story String": "Stringa narrativa",
+ "Text Adventure": "Avventura testuale",
+ "Text Gen WebUI (ooba/Mancer) presets": "Preset Text Gen WebUI (ooba/Mancer)",
+ "Toggle Panels": "Interruttore pannelli",
+ "Top A Sampling": "Top A Sampling",
+ "Top K Sampling": "Top K Sampling",
+ "UI Language": "Linguaggio interfaccia grafica",
+ "Unlocked Context Size": "Sblocca dimensione contesto",
+ "Usage Stats": "Statistiche di utilizzo",
+ "Use AI21 Tokenizer": "Utilizza il Tokenizer di AI21",
+ "Use API key (Only required for Mancer)": "Utilizza la chiave API (Necessario soltanto per Mancer)",
+ "Use character author's note": "Utilizza le note d'autore del personaggio",
+ "Use character CFG scales": "Utilizza CFG scales del personaggio",
+ "Use Proxy password field instead. This input will be ignored.": "Utilizza il campo del password proxy al suo posto. Questo input verrà ignorato.",
+ "Use style tags to modify the writing style of the output": "Utilizza lo stile delle tag per modificare lo stile di scrittura in output",
+ "Use the appropriate tokenizer for Jurassic models, which is more efficient than GPT's.": "Utilizza il tokenizer appropiato per i modelli giurassici, visto che è più efficente di quello di GPT.",
+ "Used if CFG Scale is unset globally, per chat or character": "Usato se CFG Scale non è settato globalmente, per le chat o per i personaggi",
+ "Very aggressive": "Esageratamente aggressivo",
+ "Very light": "Esageratamente leggero",
+ "Welcome to SillyTavern!": "Benvenuto in SillyTavern!",
+ "Will be used as a password for the proxy instead of API key.": "Verrà usato come password per il proxy invece che la chiave API.",
+ "Window AI Model": "Modello Window AI",
+ "Your Persona": "Il tuo alterego"
},
"nl-nl": {
"clickslidertips": "klikregel tips",
diff --git a/public/index.html b/public/index.html
index b9a4cbd89..2a673a337 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1022,7 +1022,7 @@
-
+
-
- Custom Stopping Strings (KoboldAI/TextGen/NovelAI)
-
+
+
+ Custom Stopping Strings
+
+
+ ?
+
+
JSON serialized array of strings, for example:
@@ -4151,7 +4156,7 @@
- After scenario
+ After Main Prompt / Story String
@@ -4219,7 +4224,7 @@
- After scenario
+ After Main Prompt / Story String
diff --git a/public/script.js b/public/script.js
index f5929381a..bfda94a6c 100644
--- a/public/script.js
+++ b/public/script.js
@@ -375,6 +375,9 @@ const system_message_types = {
};
const extension_prompt_types = {
+ /**
+ * @deprecated Outdated term. In reality it's "after main prompt or story string"
+ */
AFTER_SCENARIO: 0,
IN_CHAT: 1
};
@@ -1886,9 +1889,10 @@ function cleanGroupMessage(getMessage) {
continue;
}
- const indexOfMember = getMessage.indexOf(`${name}:`);
- if (indexOfMember != -1) {
- getMessage = getMessage.substr(0, indexOfMember);
+ const regex = new RegExp(`(^|\n)${escapeRegex(name)}:`);
+ const nameMatch = getMessage.match(regex);
+ if (nameMatch) {
+ getMessage = getMessage.substring(nameMatch.index + nameMatch[0].length);
}
}
}
@@ -2257,8 +2261,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
return;
}
- // Hide swipes on either multigen or real streaming
- if ((isStreamingEnabled() || isMultigenEnabled()) && !dryRun) {
+ // Hide swipes if not in a dry run.
+ if (!dryRun) {
hideSwipeButtons();
}
diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js
new file mode 100644
index 000000000..8bab38633
--- /dev/null
+++ b/public/scripts/extensions/assets/index.js
@@ -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 = " ";
+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 = $('
', { id: "assets_audio_ambient_div", class: "assets-list-div" });
+ assetTypeMenu.append(`${assetType} `)
+ for (const i in availableAssets[assetType]) {
+ const asset = availableAssets[assetType][i];
+ const elemId = `assets_install_${assetType}_${i}`;
+ let element = $(' ', { id: elemId, type: "button", class: "asset-download-button menu_button" })
+ const label = $(" ");
+ 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"])
+
+ $(` `)
+ .append(element)
+ .append(`${asset["id"]} `)
+ .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);
+});
diff --git a/public/scripts/extensions/assets/manifest.json b/public/scripts/extensions/assets/manifest.json
new file mode 100644
index 000000000..39ba8af1d
--- /dev/null
+++ b/public/scripts/extensions/assets/manifest.json
@@ -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"
+}
diff --git a/public/scripts/extensions/assets/style.css b/public/scripts/extensions/assets/style.css
new file mode 100644
index 000000000..af55c7681
--- /dev/null
+++ b/public/scripts/extensions/assets/style.css
@@ -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);
+}
+}
+
+
\ No newline at end of file
diff --git a/public/scripts/extensions/assets/window.html b/public/scripts/extensions/assets/window.html
new file mode 100644
index 000000000..a85cfe5e8
--- /dev/null
+++ b/public/scripts/extensions/assets/window.html
@@ -0,0 +1,17 @@
+
diff --git a/public/scripts/extensions/audio/index.js b/public/scripts/extensions/audio/index.js
new file mode 100644
index 000000000..d082f1c3d
--- /dev/null
+++ b/public/scripts/extensions/audio/index.js
@@ -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 = " ";
+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();
+});
diff --git a/public/scripts/extensions/audio/manifest.json b/public/scripts/extensions/audio/manifest.json
new file mode 100644
index 000000000..f272ebb54
--- /dev/null
+++ b/public/scripts/extensions/audio/manifest.json
@@ -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"
+}
diff --git a/public/scripts/extensions/audio/style.css b/public/scripts/extensions/audio/style.css
new file mode 100644
index 000000000..97b3d42b4
--- /dev/null
+++ b/public/scripts/extensions/audio/style.css
@@ -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;
+}
\ No newline at end of file
diff --git a/public/scripts/extensions/audio/window.html b/public/scripts/extensions/audio/window.html
new file mode 100644
index 000000000..7d5425aa7
--- /dev/null
+++ b/public/scripts/extensions/audio/window.html
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+ Enabled
+
+
+
+
+ Debug
+
+
+
+ Refresh assets
+
+
+
+
+
+
+
+ Music update cooldown (in seconds)
+
+
+
+
+ Hint:
+
+ Create new folder in the
+ public/characters/
+ folder and name it as the name of the character.
+ Create a folder name bgm inside of it.
+ Put bgm music with expressions there. File names should follow the pattern:
+ [expression_label]_[number].mp3
+ By default one of the neutral_[number].mp3 will play if classify module is not active.
+
+
+
+
+
\ No newline at end of file
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index fa00042a8..1add1170d 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -991,8 +991,7 @@ async function setExpression(character, expression, force) {
}
});
-
-
+
}
}
diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js
index c37c330bb..8dcf841d5 100644
--- a/public/scripts/extensions/memory/index.js
+++ b/public/scripts/extensions/memory/index.js
@@ -567,7 +567,7 @@ jQuery(function () {
+
Characters Voice Mapping
@@ -218,54 +222,103 @@ $(document).ready(function () {
placeholder="Voice map will appear here for debug purpose">
-
Character:
-
-
-
-
Voice:
-
-
-
-
-
Select models to upload (zip files)
-
-
Upload
-
Refresh Voices
+
+ Character:
+
+
+
+
+
+
+ Voice:
+
+
+
+
+
+
+
+
+
+
+ Upload one archive per model. With .pth and .index (optional) inside.
+ Supported format: .zip .rar .7zip .7z
+
+
+
+
Model Settings
+
+
+
+ Pitch Extraction
+
+
+
+ Tips: dio and pm faster, harvest slower but good.
+ Torchcrepe and rmvpe are good but uses GPU.
+
+
+
+
+ Search feature ratio ( )
+
+
+
+ Controls accent strength, too high may produce artifact.
+
+
+
+ Filter radius ( )
+
+
+ Higher can reduce breathiness but may increase run time.
+
+
+
+ Pitch offset ( )
+
+
+ Recommended +12 key for male to female conversion and -12 key for female to male conversion.
+
+
+
+ Mix rate ( )
+
+
+ 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.
+
+
+
+ Protect amount ( )
+
+
+ Avoid non voice sounds. Lower is more being ignored.
+
-
Select Pitch Extraction
-
-
- Index rate for feature retrieval ( )
-
-
-
-
Filter radius ( )
-
-
-
Pitch offset ( )
-
-
-
Mix rate ( )
-
-
-
Protect amount ( )
-
-
-
@@ -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
diff --git a/public/scripts/extensions/tts/coqui.js b/public/scripts/extensions/tts/coqui.js
index cfaf83fec..7f006c8b0 100644
--- a/public/scripts/extensions/tts/coqui.js
+++ b/public/scripts/extensions/tts/coqui.js
@@ -11,7 +11,6 @@ export { CoquiTtsProvider }
const DEBUG_PREFIX = " ";
const UPDATE_INTERVAL = 1000;
-let inApiCall = false;
let charactersList = []; // Updated with module worker
let coquiApiModels = {}; // Initialized only once
let coquiApiModelsFull = {}; // Initialized only once
@@ -40,6 +39,12 @@ const languageLabels = {
"ja": "Japanese"
}
+
+const defaultSettings = {
+ voiceMap: "",
+ voiceMapDict: {}
+}
+
function throwIfModuleMissing() {
if (!modules.includes('coqui-tts')) {
toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
@@ -54,11 +59,13 @@ function resetModelSettings() {
function updateCharactersList() {
let currentcharacters = new Set();
- for (const i of getContext().characters) {
+ const context = getContext();
+ for (const i of context.characters) {
currentcharacters.add(i.name);
}
- currentcharacters = Array.from(currentcharacters)
+ currentcharacters = Array.from(currentcharacters);
+ currentcharacters.unshift(context.name1);
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
charactersList = currentcharacters
@@ -83,11 +90,13 @@ class CoquiTtsProvider {
// Extension UI and Settings //
//#############################//
- settings
+ static instance;
+ settings = {};
- defaultSettings = {
- voiceMap: "",
- voiceMapDict: {}
+ // Singleton to allow acces to instance in event functions
+ constructor() {
+ if (CoquiTtsProvider.instance === undefined)
+ CoquiTtsProvider.instance = this;
}
get settingsHtml() {
@@ -145,8 +154,12 @@ class CoquiTtsProvider {
}
loadSettings(settings) {
+ if (Object.keys(this.settings).length === 0) {
+ Object.assign(this.settings, defaultSettings)
+ }
+
// Only accept keys defined in defaultSettings
- this.settings = this.defaultSettings
+ this.settings = defaultSettings;
for (const key in settings) {
if (key in this.settings) {
@@ -156,7 +169,7 @@ class CoquiTtsProvider {
}
}
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
$("#coqui_api_model_div").hide();
$("#coqui_local_model_div").hide();
@@ -167,24 +180,12 @@ class CoquiTtsProvider {
$("#coqui_api_model_install_status").hide();
$("#coqui_api_model_install_button").hide();
- let that = this
- $("#coqui_model_origin").on("change", function () { that.onModelOriginChange() });
- $("#coqui_api_language").on("change", function () { that.onModelLanguageChange() });
- $("#coqui_api_model_name").on("change", function () { that.onModelNameChange() });
+ $("#coqui_model_origin").on("change", CoquiTtsProvider.onModelOriginChange);
+ $("#coqui_api_language").on("change", CoquiTtsProvider.onModelLanguageChange);
+ $("#coqui_api_model_name").on("change", CoquiTtsProvider.onModelNameChange);
+ $("#coqui_remove_char_mapping").on("click", CoquiTtsProvider.onRemoveClick);
- $("#coqui_remove_char_mapping").on("click", function () { that.onRemoveClick() });
-
- // Load characters list
- $('#coqui_character_select')
- .find('option')
- .remove()
- .end()
- .append('Select Character ')
- .val('none')
-
- for (const charName of charactersList) {
- $("#coqui_character_select").append(new Option(charName, charName));
- }
+ updateCharactersList();
// Load coqui-api settings from json file
fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
@@ -192,18 +193,6 @@ class CoquiTtsProvider {
.then(json => {
coquiApiModels = json;
console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels);
- /*
- $('#coqui_api_language')
- .find('option')
- .remove()
- .end()
- .append('Select model language ')
- .val('none');
-
- for(let language in coquiApiModels) {
- $("#coqui_api_language").append(new Option(languageLabels[language],language));
- console.log(DEBUG_PREFIX,"added language",language);
- }*/
});
// Load coqui-api FULL settings from json file
@@ -212,49 +201,33 @@ class CoquiTtsProvider {
.then(json => {
coquiApiModelsFull = json;
console.debug(DEBUG_PREFIX,"initialized coqui-api full model list to", coquiApiModelsFull);
- /*
- $('#coqui_api_full_language')
- .find('option')
- .remove()
- .end()
- .append('Select model language ')
- .val('none');
-
- for(let language in coquiApiModelsFull) {
- $("#coqui_api_full_language").append(new Option(languageLabels[language],language));
- console.log(DEBUG_PREFIX,"added language",language);
- }*/
});
}
- updateVoiceMap() {
- this.settings.voiceMap = "";
- for (let i in this.settings.voiceMapDict) {
- const voice_settings = this.settings.voiceMapDict[i];
- this.settings.voiceMap += i + ":" + voice_settings["model_id"];
+ static updateVoiceMap() {
+ CoquiTtsProvider.instance.settings.voiceMap = "";
+ for (let i in CoquiTtsProvider.instance.settings.voiceMapDict) {
+ const voice_settings = CoquiTtsProvider.instance.settings.voiceMapDict[i];
+ CoquiTtsProvider.instance.settings.voiceMap += i + ":" + voice_settings["model_id"];
if (voice_settings["model_language"] != null)
- this.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
+ CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
if (voice_settings["model_speaker"] != null)
- this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
+ CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
- this.settings.voiceMap += ",";
+ CoquiTtsProvider.instance.settings.voiceMap += ",";
}
- $("#tts_voice_map").val(this.settings.voiceMap);
- extension_settings.tts.Coqui = this.settings;
+ $("#tts_voice_map").val(CoquiTtsProvider.instance.settings.voiceMap);
+ //extension_settings.tts.Coqui = extension_settings.tts.Coqui;
}
onSettingsChange() {
- console.debug(DEBUG_PREFIX, "Settings changes", this.settings);
- extension_settings.tts.Coqui = this.settings;
+ //console.debug(DEBUG_PREFIX, "Settings changes", CoquiTtsProvider.instance.settings);
+ CoquiTtsProvider.updateVoiceMap();
}
async onApplyClick() {
- if (inApiCall) {
- return; // TOdo block dropdown
- }
-
const character = $("#coqui_character_select").val();
const model_origin = $("#coqui_model_origin").val();
const model_language = $("#coqui_api_language").val();
@@ -262,16 +235,15 @@ class CoquiTtsProvider {
let model_setting_language = $("#coqui_api_model_settings_language").val();
let model_setting_speaker = $("#coqui_api_model_settings_speaker").val();
-
if (character === "none") {
toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX + " voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
if (model_origin == "none") {
toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
@@ -280,25 +252,25 @@ class CoquiTtsProvider {
if (model_name == "none") {
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
- this.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
- console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]);
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
+ console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
if (model_language == "none") {
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
if (model_name == "none") {
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- this.updateVoiceMap(); // Overide any manual modification
+ CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
return;
}
@@ -327,13 +299,13 @@ class CoquiTtsProvider {
return;
}
- console.debug(DEBUG_PREFIX, "Current voice map: ", this.settings.voiceMap);
+ console.debug(DEBUG_PREFIX, "Current voice map: ", CoquiTtsProvider.instance.settings.voiceMap);
- this.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
+ CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
- console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]);
+ console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
- this.updateVoiceMap();
+ CoquiTtsProvider.updateVoiceMap();
let successMsg = character + ":" + model_id;
if (model_setting_language != null)
@@ -352,7 +324,7 @@ class CoquiTtsProvider {
return output;
}
- async onRemoveClick() {
+ static async onRemoveClick() {
const character = $("#coqui_character_select").val();
if (character === "none") {
@@ -361,11 +333,11 @@ class CoquiTtsProvider {
}
// Todo erase from voicemap
- delete (this.settings.voiceMapDict[character]);
- this.updateVoiceMap(); // TODO
+ delete (CoquiTtsProvider.instance.settings.voiceMapDict[character]);
+ CoquiTtsProvider.updateVoiceMap(); // TODO
}
- async onModelOriginChange() {
+ static async onModelOriginChange() {
throwIfModuleMissing()
resetModelSettings();
const model_origin = $('#coqui_model_origin').val();
@@ -378,6 +350,9 @@ class CoquiTtsProvider {
// show coqui model selected list (SAFE)
if (model_origin == "coqui-api") {
$("#coqui_local_model_div").hide();
+ $("#coqui_api_model_div").hide();
+ $("#coqui_api_model_name").hide();
+ $("#coqui_api_model_settings").hide();
$('#coqui_api_language')
.find('option')
@@ -400,6 +375,9 @@ class CoquiTtsProvider {
// show coqui model full list (UNSAFE)
if (model_origin == "coqui-api-full") {
$("#coqui_local_model_div").hide();
+ $("#coqui_api_model_div").hide();
+ $("#coqui_api_model_name").hide();
+ $("#coqui_api_model_settings").hide();
$('#coqui_api_language')
.find('option')
@@ -427,7 +405,7 @@ class CoquiTtsProvider {
}
}
- async onModelLanguageChange() {
+ static async onModelLanguageChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@@ -460,7 +438,7 @@ class CoquiTtsProvider {
}
}
- async onModelNameChange() {
+ static async onModelNameChange() {
throwIfModuleMissing();
resetModelSettings();
$("#coqui_api_model_settings").hide();
@@ -551,8 +529,6 @@ class CoquiTtsProvider {
$("#coqui_api_model_install_status").text("Model not found on extras server");
}
- const onModelNameChange_pointer = this.onModelNameChange;
-
$("#coqui_api_model_install_button").off("click").on("click", async function () {
try {
$("#coqui_api_model_install_status").text("Downloading model...");
@@ -566,7 +542,7 @@ class CoquiTtsProvider {
if (apiResult["status"] == "done") {
$("#coqui_api_model_install_status").text("Model installed and ready to use!");
$("#coqui_api_model_install_button").hide();
- onModelNameChange_pointer();
+ CoquiTtsProvider.onModelNameChange();
}
if (apiResult["status"] == "downloading") {
@@ -577,7 +553,7 @@ class CoquiTtsProvider {
} catch (error) {
console.error(error)
toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
- onModelNameChange_pointer();
+ CoquiTtsProvider.onModelNameChange();
}
// will refresh model status
});
diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js
index 65e3e5a4a..d45542380 100644
--- a/public/scripts/extensions/tts/index.js
+++ b/public/scripts/extensions/tts/index.js
@@ -412,7 +412,7 @@ async function tts(text, voiceId, char) {
// RVC injection
if (extension_settings.rvc.enabled)
- response = await rvcVoiceConversion(response, char)
+ response = await rvcVoiceConversion(response, char, text)
addAudioJob(response)
completeTtsJob()
diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js
index 3ebc435e9..953402c92 100644
--- a/public/scripts/nai-settings.js
+++ b/public/scripts/nai-settings.js
@@ -185,7 +185,7 @@ function loadNovelSettingsUi(ui_settings) {
$("#top_a_novel").val(ui_settings.top_a);
$("#top_a_counter_novel").text(Number(ui_settings.top_a).toFixed(2));
$("#typical_p_novel").val(ui_settings.typical_p);
- $("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(2));
+ $("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(3));
$("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").text(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(ui_settings.phrase_rep_pen || "off");
@@ -269,8 +269,8 @@ const sliders = [
{
sliderId: "#typical_p_novel",
counterId: "#typical_p_counter_novel",
- format: (val) => Number(val).toFixed(2),
- setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
+ format: (val) => Number(val).toFixed(3),
+ setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(3); },
},
{
sliderId: "#mirostat_tau_novel",
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 959037eda..4b3af67fd 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -683,9 +683,9 @@ function preparePromptsForChatCompletion({Scenario, charPersonality, name2, worl
// Tavern Extras - Summary
const summary = extensionPrompts['1_memory'];
- if (summary && summary.content) systemPrompts.push({
+ if (summary && summary.value) systemPrompts.push({
role: 'system',
- content: summary.content,
+ content: summary.value,
identifier: 'summary'
});
diff --git a/public/style.css b/public/style.css
index 2d5e30ca7..08d3edd5c 100644
--- a/public/style.css
+++ b/public/style.css
@@ -916,12 +916,22 @@ select {
white-space: nowrap;
}
+@media screen and (max-width: 1000px) {
+ #form_create textarea {
+ flex-grow: 1;
+ }
+}
+@media screen and (min-width: 1001px) {
+ #description_textarea {
+ height: 33vh;
+ height: 33svh;
+ }
-#description_textarea,
-#firstmessage_textarea {
- height: -webkit-fill-available;
- width: -moz-available;
+ #firstmessage_textarea {
+ resize: none;
+ flex: 1;
+ }
}
#character_name_pole {
@@ -1550,10 +1560,6 @@ input[type=search]:focus::-webkit-search-cancel-button {
overflow-y: auto;
}
-#form_create textarea {
- flex-grow: 1;
-}
-
.avatar_div {
display: flex;
width: 100%;
@@ -3520,4 +3526,4 @@ a {
z-index: 10;
margin-left: 10px;
/* Give some space between the button and search box */
-}
\ No newline at end of file
+}
diff --git a/server.js b/server.js
index 50e4dcfb3..a0f818baa 100644
--- a/server.js
+++ b/server.js
@@ -317,7 +317,8 @@ const directories = {
instruct: 'public/instruct',
context: 'public/context',
backups: 'backups/',
- quickreplies: 'public/QuickReplies'
+ quickreplies: 'public/QuickReplies',
+ assets: 'public/assets',
};
// CSRF Protection //
@@ -5011,3 +5012,242 @@ app.post('/delete_extension', jsonParser, async (request, response) => {
return response.status(500).send(`Server Error: ${error.message}`);
}
});
+
+
+/**
+ * HTTP POST handler function to retrieve name of all files of a given folder path.
+ *
+ * @param {Object} request - HTTP Request object. Require folder path in query
+ * @param {Object} response - HTTP Response object will contain a list of file path.
+ *
+ * @returns {void}
+ */
+app.post('/get_assets', jsonParser, async (request, response) => {
+ const folderPath = path.join(directories.assets);
+ let output = {}
+ //console.info("Checking files into",folderPath);
+
+ try {
+ if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
+ const folders = fs.readdirSync(folderPath)
+ .filter(filename => {
+ return fs.statSync(path.join(folderPath, filename)).isDirectory();
+ });
+
+ for (const folder of folders) {
+ if (folder == "temp")
+ continue;
+ const files = fs.readdirSync(path.join(folderPath, folder))
+ .filter(filename => {
+ return filename != ".placeholder";
+ });
+ output[folder] = [];
+ for (const file of files) {
+ output[folder].push(path.join("assets", folder, file));
+ }
+ }
+ }
+ }
+ catch (err) {
+ console.log(err);
+ }
+ finally {
+ return response.send(output);
+ }
+});
+
+
+function checkAssetFileName(inputFilename) {
+ // Sanitize filename
+ if (inputFilename.indexOf('\0') !== -1) {
+ console.debug("Bad request: poisong null bytes in filename.");
+ return '';
+ }
+
+ if (!/^[a-zA-Z0-9_\-\.]+$/.test(inputFilename)) {
+ console.debug("Bad request: illegal character in filename, only alphanumeric, '_', '-' are accepted.");
+ return '';
+ }
+
+ if (contentManager.unsafeExtensions.some(ext => inputFilename.toLowerCase().endsWith(ext))) {
+ console.debug("Bad request: forbidden file extension.");
+ return '';
+ }
+
+ if (inputFilename.startsWith('.')) {
+ console.debug("Bad request: filename cannot start with '.'");
+ return '';
+ }
+
+ return path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');;
+}
+
+/**
+ * HTTP POST handler function to download the requested asset.
+ *
+ * @param {Object} request - HTTP Request object, expects a url, a category and a filename.
+ * @param {Object} response - HTTP Response only gives status.
+ *
+ * @returns {void}
+ */
+app.post('/asset_download', jsonParser, async (request, response) => {
+ const { Readable } = require('stream');
+ const { finished } = require('stream/promises');
+ const url = request.body.url;
+ const inputCategory = request.body.category;
+ const inputFilename = sanitize(request.body.filename);
+ const validCategories = ["bgm", "ambient"];
+
+ // Check category
+ let category = null;
+ for (i of validCategories)
+ if (i == inputCategory)
+ category = i;
+
+ if (category === null) {
+ console.debug("Bad request: unsuported asset category.");
+ return response.sendStatus(400);
+ }
+
+ // Sanitize filename
+ const safe_input = checkAssetFileName(inputFilename);
+ if (safe_input == '')
+ return response.sendFile(400);
+
+ const temp_path = path.join(directories.assets, "temp", safe_input)
+ const file_path = path.join(directories.assets, category, safe_input)
+ console.debug("Request received to download", url, "to", file_path);
+
+ try {
+ // Download to temp
+ const downloadFile = (async (url, temp_path) => {
+ const res = await fetch(url);
+ if (!res.ok) {
+ throw new Error(`Unexpected response ${res.statusText}`);
+ }
+ const destination = path.resolve(temp_path);
+ // Delete if previous download failed
+ if (fs.existsSync(temp_path)) {
+ fs.unlink(temp_path, (err) => {
+ if (err) throw err;
+ });
+ }
+ const fileStream = fs.createWriteStream(destination, { flags: 'wx' });
+ await finished(Readable.fromWeb(res.body).pipe(fileStream));
+ });
+
+ await downloadFile(url, temp_path);
+
+ // Move into asset place
+ console.debug("Download finished, moving file from", temp_path, "to", file_path);
+ fs.renameSync(temp_path, file_path);
+ response.sendStatus(200);
+ }
+ catch (error) {
+ console.log(error);
+ response.sendStatus(500);
+ }
+});
+
+/**
+ * HTTP POST handler function to delete the requested asset.
+ *
+ * @param {Object} request - HTTP Request object, expects a category and a filename
+ * @param {Object} response - HTTP Response only gives stats.
+ *
+ * @returns {void}
+ */
+app.post('/asset_delete', jsonParser, async (request, response) => {
+ const { Readable } = require('stream');
+ const { finished } = require('stream/promises');
+ const inputCategory = request.body.category;
+ const inputFilename = sanitize(request.body.filename);
+ const validCategories = ["bgm", "ambient"];
+
+ // Check category
+ let category = null;
+ for (i of validCategories)
+ if (i == inputCategory)
+ category = i;
+
+ if (category === null) {
+ console.debug("Bad request: unsuported asset category.");
+ return response.sendStatus(400);
+ }
+
+ // Sanitize filename
+ const safe_input = checkAssetFileName(inputFilename);
+ if (safe_input == '')
+ return response.sendFile(400);
+
+ const file_path = path.join(directories.assets, category, safe_input)
+ console.debug("Request received to delete", category, file_path);
+
+ try {
+ // Delete if previous download failed
+ if (fs.existsSync(file_path)) {
+ fs.unlink(file_path, (err) => {
+ if (err) throw err;
+ });
+ console.debug("Asset deleted.");
+ }
+ else {
+ console.debug("Asset not found.");
+ response.sendStatus(400);
+ }
+ // Move into asset place
+ response.sendStatus(200);
+ }
+ catch (error) {
+ console.log(error);
+ response.sendStatus(500);
+ }
+});
+
+
+///////////////////////////////
+/**
+ * HTTP POST handler function to retrieve a character background music list.
+ *
+ * @param {Object} request - HTTP Request object, expects a character name in the query.
+ * @param {Object} response - HTTP Response object will contain a list of audio file path.
+ *
+ * @returns {void}
+ */
+app.post('/get_character_assets_list', jsonParser, async (request, response) => {
+ const name = sanitize(request.query.name);
+ const inputCategory = request.query.category;
+ const validCategories = ["bgm", "ambient"]
+
+ // Check category
+ let category = null
+ for (i of validCategories)
+ if (i == inputCategory)
+ category = i
+
+ if (category === null) {
+ console.debug("Bad request: unsuported asset category.");
+ return response.sendStatus(400);
+ }
+
+ const folderPath = path.join(directories.characters, name, category);
+
+ let output = [];
+ try {
+ if (fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory()) {
+ const files = fs.readdirSync(folderPath)
+ .filter(filename => {
+ return filename != ".placeholder";
+ });
+
+ for (i of files)
+ output.push(`/characters/${name}/${category}/${i}`);
+
+ }
+ return response.send(output);
+ }
+ catch (err) {
+ console.log(err);
+ return response.sendStatus(500);
+ }
+});
diff --git a/src/content-manager.js b/src/content-manager.js
index e48eab877..e453d5f4a 100644
--- a/src/content-manager.js
+++ b/src/content-manager.js
@@ -1,10 +1,87 @@
const fs = require('fs');
-const path= require('path');
+const path = require('path');
const config = require(path.join(process.cwd(), './config.conf'));
const contentDirectory = path.join(process.cwd(), 'default/content');
const contentLogPath = path.join(contentDirectory, 'content.log');
const contentIndexPath = path.join(contentDirectory, 'index.json');
+const unsafeExtensions = [
+ ".php",
+ ".exe",
+ ".com",
+ ".dll",
+ ".pif",
+ ".application",
+ ".gadget",
+ ".msi",
+ ".jar",
+ ".cmd",
+ ".bat",
+ ".reg",
+ ".sh",
+ ".py",
+ ".js",
+ ".jse",
+ ".jsp",
+ ".pdf",
+ ".html",
+ ".htm",
+ ".hta",
+ ".vb",
+ ".vbs",
+ ".vbe",
+ ".cpl",
+ ".msc",
+ ".scr",
+ ".sql",
+ ".iso",
+ ".img",
+ ".dmg",
+ ".ps1",
+ ".ps1xml",
+ ".ps2",
+ ".ps2xml",
+ ".psc1",
+ ".psc2",
+ ".msh",
+ ".msh1",
+ ".msh2",
+ ".mshxml",
+ ".msh1xml",
+ ".msh2xml",
+ ".scf",
+ ".lnk",
+ ".inf",
+ ".reg",
+ ".doc",
+ ".docm",
+ ".docx",
+ ".dot",
+ ".dotm",
+ ".dotx",
+ ".xls",
+ ".xlsm",
+ ".xlsx",
+ ".xlt",
+ ".xltm",
+ ".xltx",
+ ".xlam",
+ ".ppt",
+ ".pptm",
+ ".pptx",
+ ".pot",
+ ".potm",
+ ".potx",
+ ".ppam",
+ ".ppsx",
+ ".ppsm",
+ ".pps",
+ ".ppam",
+ ".sldx",
+ ".sldm",
+ ".ws",
+];
+
function checkForNewContent() {
try {
if (config.skipContentCheck) {
@@ -85,4 +162,5 @@ function getContentLog() {
module.exports = {
checkForNewContent,
+ unsafeExtensions,
}