mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into improved-instruct-mode-sequences
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -32,3 +32,4 @@ public/movingUI/
|
||||
public/QuickReplies/
|
||||
content.log
|
||||
cloudflared.exe
|
||||
public/assets/
|
1
public/assets/ambient/.placeholder
Normal file
1
public/assets/ambient/.placeholder
Normal file
@ -0,0 +1 @@
|
||||
Put ambient audio files here.
|
1
public/assets/bgm/.placeholder
Normal file
1
public/assets/bgm/.placeholder
Normal file
@ -0,0 +1 @@
|
||||
Put bgm audio files here
|
0
public/assets/temp/.placeholder
Normal file
0
public/assets/temp/.placeholder
Normal file
6
public/context/Minimalist.json
Normal file
6
public/context/Minimalist.json
Normal file
@ -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": "###"
|
||||
}
|
129
public/i18n.json
129
public/i18n.json
@ -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",
|
||||
|
@ -1022,7 +1022,7 @@
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="typical_p_novel" name="volume" min="0" max="1" step="0.01">
|
||||
<input type="range" id="typical_p_novel" name="volume" min="0" max="1" step="0.001">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="typical_p_novel" id="typical_p_counter_novel">
|
||||
@ -2344,9 +2344,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<h4>
|
||||
<div class="range-block-title justifyLeft">
|
||||
<span data-i18n="Custom Stopping Strings">
|
||||
Custom Stopping Strings (KoboldAI/TextGen/NovelAI)
|
||||
Custom Stopping Strings
|
||||
</span>
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/advancedformatting/#custom-stopping-strings" class="notes-link" target="_blank">
|
||||
<span class="note-link-span">?</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
<span data-i18n="JSON serialized array of strings">JSON serialized array of strings, for example:</span><br>
|
||||
@ -4151,7 +4156,7 @@
|
||||
<div class="floating_prompt_radio_group">
|
||||
<label>
|
||||
<input type="radio" name="extension_floating_position" value="0" />
|
||||
After scenario
|
||||
After Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="extension_floating_position" value="1" />
|
||||
@ -4219,7 +4224,7 @@
|
||||
<div class="floating_prompt_radio_group">
|
||||
<label>
|
||||
<input type="radio" name="extension_default_position" value="0" />
|
||||
After scenario
|
||||
After Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="extension_default_position" value="1" />
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
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>
|
@ -992,7 +992,6 @@ async function setExpression(character, expression, force) {
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -567,7 +567,7 @@ jQuery(function () {
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="0" />
|
||||
After scenario
|
||||
After Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="memory_position" value="1" />
|
||||
|
@ -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,24 +222,48 @@ $(document).ready(function () {
|
||||
placeholder="Voice map will appear here for debug purpose"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<label for="rvc_model_upload_file">Select models to upload (zip files)</label>
|
||||
<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_file"
|
||||
id="rvc_model_upload_files"
|
||||
accept=".zip,.rar,.7zip,.7z" multiple />
|
||||
<button id="rvc_model_upload_button"> Upload </button>
|
||||
<button id="rvc_model_refresh_button"> Refresh Voices </button>
|
||||
</div>
|
||||
<span>Select Pitch Extraction</span> </br>
|
||||
</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>
|
||||
@ -244,28 +272,53 @@ $(document).ready(function () {
|
||||
<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">
|
||||
Index rate for feature retrieval (<span id="rvc_index_rate_value"></span>)
|
||||
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="-100" max="100" step="1" value="0" />
|
||||
|
||||
<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>
|
||||
<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
|
||||
|
@ -11,7 +11,6 @@ export { CoquiTtsProvider }
|
||||
const DEBUG_PREFIX = "<Coqui TTS module> ";
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
let inApiCall = false;
|
||||
let charactersList = []; // Updated with module worker
|
||||
let coquiApiModels = {}; // Initialized only once
|
||||
let coquiApiModelsFull = {}; // Initialized only once
|
||||
@ -40,6 +39,12 @@ const languageLabels = {
|
||||
"ja": "Japanese"
|
||||
}
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
voiceMap: "",
|
||||
voiceMapDict: {}
|
||||
}
|
||||
|
||||
function throwIfModuleMissing() {
|
||||
if (!modules.includes('coqui-tts')) {
|
||||
toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
@ -54,11 +59,13 @@ function resetModelSettings() {
|
||||
|
||||
function updateCharactersList() {
|
||||
let currentcharacters = new Set();
|
||||
for (const i of getContext().characters) {
|
||||
const context = getContext();
|
||||
for (const i of context.characters) {
|
||||
currentcharacters.add(i.name);
|
||||
}
|
||||
|
||||
currentcharacters = Array.from(currentcharacters)
|
||||
currentcharacters = Array.from(currentcharacters);
|
||||
currentcharacters.unshift(context.name1);
|
||||
|
||||
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
|
||||
charactersList = currentcharacters
|
||||
@ -83,11 +90,13 @@ class CoquiTtsProvider {
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
settings
|
||||
static instance;
|
||||
settings = {};
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: "",
|
||||
voiceMapDict: {}
|
||||
// Singleton to allow acces to instance in event functions
|
||||
constructor() {
|
||||
if (CoquiTtsProvider.instance === undefined)
|
||||
CoquiTtsProvider.instance = this;
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
@ -145,8 +154,12 @@ class CoquiTtsProvider {
|
||||
}
|
||||
|
||||
loadSettings(settings) {
|
||||
if (Object.keys(this.settings).length === 0) {
|
||||
Object.assign(this.settings, defaultSettings)
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
@ -156,7 +169,7 @@ class CoquiTtsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_local_model_div").hide();
|
||||
@ -167,24 +180,12 @@ class CoquiTtsProvider {
|
||||
$("#coqui_api_model_install_status").hide();
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
|
||||
let that = this
|
||||
$("#coqui_model_origin").on("change", function () { that.onModelOriginChange() });
|
||||
$("#coqui_api_language").on("change", function () { that.onModelLanguageChange() });
|
||||
$("#coqui_api_model_name").on("change", function () { that.onModelNameChange() });
|
||||
$("#coqui_model_origin").on("change", CoquiTtsProvider.onModelOriginChange);
|
||||
$("#coqui_api_language").on("change", CoquiTtsProvider.onModelLanguageChange);
|
||||
$("#coqui_api_model_name").on("change", CoquiTtsProvider.onModelNameChange);
|
||||
$("#coqui_remove_char_mapping").on("click", CoquiTtsProvider.onRemoveClick);
|
||||
|
||||
$("#coqui_remove_char_mapping").on("click", function () { that.onRemoveClick() });
|
||||
|
||||
// Load characters list
|
||||
$('#coqui_character_select')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select Character</option>')
|
||||
.val('none')
|
||||
|
||||
for (const charName of charactersList) {
|
||||
$("#coqui_character_select").append(new Option(charName, charName));
|
||||
}
|
||||
updateCharactersList();
|
||||
|
||||
// Load coqui-api settings from json file
|
||||
fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
|
||||
@ -192,18 +193,6 @@ class CoquiTtsProvider {
|
||||
.then(json => {
|
||||
coquiApiModels = json;
|
||||
console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels);
|
||||
/*
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select model language</option>')
|
||||
.val('none');
|
||||
|
||||
for(let language in coquiApiModels) {
|
||||
$("#coqui_api_language").append(new Option(languageLabels[language],language));
|
||||
console.log(DEBUG_PREFIX,"added language",language);
|
||||
}*/
|
||||
});
|
||||
|
||||
// Load coqui-api FULL settings from json file
|
||||
@ -212,49 +201,33 @@ class CoquiTtsProvider {
|
||||
.then(json => {
|
||||
coquiApiModelsFull = json;
|
||||
console.debug(DEBUG_PREFIX,"initialized coqui-api full model list to", coquiApiModelsFull);
|
||||
/*
|
||||
$('#coqui_api_full_language')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select model language</option>')
|
||||
.val('none');
|
||||
|
||||
for(let language in coquiApiModelsFull) {
|
||||
$("#coqui_api_full_language").append(new Option(languageLabels[language],language));
|
||||
console.log(DEBUG_PREFIX,"added language",language);
|
||||
}*/
|
||||
});
|
||||
}
|
||||
|
||||
updateVoiceMap() {
|
||||
this.settings.voiceMap = "";
|
||||
for (let i in this.settings.voiceMapDict) {
|
||||
const voice_settings = this.settings.voiceMapDict[i];
|
||||
this.settings.voiceMap += i + ":" + voice_settings["model_id"];
|
||||
static updateVoiceMap() {
|
||||
CoquiTtsProvider.instance.settings.voiceMap = "";
|
||||
for (let i in CoquiTtsProvider.instance.settings.voiceMapDict) {
|
||||
const voice_settings = CoquiTtsProvider.instance.settings.voiceMapDict[i];
|
||||
CoquiTtsProvider.instance.settings.voiceMap += i + ":" + voice_settings["model_id"];
|
||||
|
||||
if (voice_settings["model_language"] != null)
|
||||
this.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
|
||||
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
|
||||
|
||||
if (voice_settings["model_speaker"] != null)
|
||||
this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
|
||||
CoquiTtsProvider.instance.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
|
||||
|
||||
this.settings.voiceMap += ",";
|
||||
CoquiTtsProvider.instance.settings.voiceMap += ",";
|
||||
}
|
||||
$("#tts_voice_map").val(this.settings.voiceMap);
|
||||
extension_settings.tts.Coqui = this.settings;
|
||||
$("#tts_voice_map").val(CoquiTtsProvider.instance.settings.voiceMap);
|
||||
//extension_settings.tts.Coqui = extension_settings.tts.Coqui;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
console.debug(DEBUG_PREFIX, "Settings changes", this.settings);
|
||||
extension_settings.tts.Coqui = this.settings;
|
||||
//console.debug(DEBUG_PREFIX, "Settings changes", CoquiTtsProvider.instance.settings);
|
||||
CoquiTtsProvider.updateVoiceMap();
|
||||
}
|
||||
|
||||
async onApplyClick() {
|
||||
if (inApiCall) {
|
||||
return; // TOdo block dropdown
|
||||
}
|
||||
|
||||
const character = $("#coqui_character_select").val();
|
||||
const model_origin = $("#coqui_model_origin").val();
|
||||
const model_language = $("#coqui_api_language").val();
|
||||
@ -262,16 +235,15 @@ class CoquiTtsProvider {
|
||||
let model_setting_language = $("#coqui_api_model_settings_language").val();
|
||||
let model_setting_speaker = $("#coqui_api_model_settings_speaker").val();
|
||||
|
||||
|
||||
if (character === "none") {
|
||||
toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX + " voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_origin == "none") {
|
||||
toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
@ -280,25 +252,25 @@ class CoquiTtsProvider {
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
this.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]);
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "local", model_id: "local/" + model_id };
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_language == "none") {
|
||||
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateVoiceMap(); // Overide any manual modification
|
||||
CoquiTtsProvider.updateVoiceMap(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
@ -327,13 +299,13 @@ class CoquiTtsProvider {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Current voice map: ", this.settings.voiceMap);
|
||||
console.debug(DEBUG_PREFIX, "Current voice map: ", CoquiTtsProvider.instance.settings.voiceMap);
|
||||
|
||||
this.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
|
||||
CoquiTtsProvider.instance.settings.voiceMapDict[character] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", this.settings.voiceMapDict[character]);
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", character, ":", CoquiTtsProvider.instance.settings.voiceMapDict[character]);
|
||||
|
||||
this.updateVoiceMap();
|
||||
CoquiTtsProvider.updateVoiceMap();
|
||||
|
||||
let successMsg = character + ":" + model_id;
|
||||
if (model_setting_language != null)
|
||||
@ -352,7 +324,7 @@ class CoquiTtsProvider {
|
||||
return output;
|
||||
}
|
||||
|
||||
async onRemoveClick() {
|
||||
static async onRemoveClick() {
|
||||
const character = $("#coqui_character_select").val();
|
||||
|
||||
if (character === "none") {
|
||||
@ -361,11 +333,11 @@ class CoquiTtsProvider {
|
||||
}
|
||||
|
||||
// Todo erase from voicemap
|
||||
delete (this.settings.voiceMapDict[character]);
|
||||
this.updateVoiceMap(); // TODO
|
||||
delete (CoquiTtsProvider.instance.settings.voiceMapDict[character]);
|
||||
CoquiTtsProvider.updateVoiceMap(); // TODO
|
||||
}
|
||||
|
||||
async onModelOriginChange() {
|
||||
static async onModelOriginChange() {
|
||||
throwIfModuleMissing()
|
||||
resetModelSettings();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
@ -378,6 +350,9 @@ class CoquiTtsProvider {
|
||||
// show coqui model selected list (SAFE)
|
||||
if (model_origin == "coqui-api") {
|
||||
$("#coqui_local_model_div").hide();
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_api_model_name").hide();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
@ -400,6 +375,9 @@ class CoquiTtsProvider {
|
||||
// show coqui model full list (UNSAFE)
|
||||
if (model_origin == "coqui-api-full") {
|
||||
$("#coqui_local_model_div").hide();
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_api_model_name").hide();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
@ -427,7 +405,7 @@ class CoquiTtsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async onModelLanguageChange() {
|
||||
static async onModelLanguageChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
@ -460,7 +438,7 @@ class CoquiTtsProvider {
|
||||
}
|
||||
}
|
||||
|
||||
async onModelNameChange() {
|
||||
static async onModelNameChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
@ -551,8 +529,6 @@ class CoquiTtsProvider {
|
||||
$("#coqui_api_model_install_status").text("Model not found on extras server");
|
||||
}
|
||||
|
||||
const onModelNameChange_pointer = this.onModelNameChange;
|
||||
|
||||
$("#coqui_api_model_install_button").off("click").on("click", async function () {
|
||||
try {
|
||||
$("#coqui_api_model_install_status").text("Downloading model...");
|
||||
@ -566,7 +542,7 @@ class CoquiTtsProvider {
|
||||
if (apiResult["status"] == "done") {
|
||||
$("#coqui_api_model_install_status").text("Model installed and ready to use!");
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
onModelNameChange_pointer();
|
||||
CoquiTtsProvider.onModelNameChange();
|
||||
}
|
||||
|
||||
if (apiResult["status"] == "downloading") {
|
||||
@ -577,7 +553,7 @@ class CoquiTtsProvider {
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
onModelNameChange_pointer();
|
||||
CoquiTtsProvider.onModelNameChange();
|
||||
}
|
||||
// will refresh model status
|
||||
});
|
||||
|
@ -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()
|
||||
|
@ -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",
|
||||
|
@ -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'
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
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%;
|
||||
|
242
server.js
242
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);
|
||||
}
|
||||
});
|
||||
|
@ -5,6 +5,83 @@ 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,
|
||||
}
|
||||
|
Reference in New Issue
Block a user