mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into xtts-more-controls
This commit is contained in:
@@ -3,18 +3,16 @@ TODO:
|
||||
*/
|
||||
//const DEBUG_TONY_SAMA_FORK_MODE = true
|
||||
|
||||
import { getRequestHeaders, callPopup } from "../../../script.js";
|
||||
import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from "../../extensions.js";
|
||||
import { getStringHash, isValidUrl } from "../../utils.js";
|
||||
import { getRequestHeaders, callPopup } from '../../../script.js';
|
||||
import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from '../../extensions.js';
|
||||
import { getStringHash, isValidUrl } from '../../utils.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'assets';
|
||||
const DEBUG_PREFIX = "<Assets module> ";
|
||||
const DEBUG_PREFIX = '<Assets module> ';
|
||||
let previewAudio = null;
|
||||
let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json"
|
||||
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)
|
||||
@@ -26,35 +24,32 @@ let currentAssets = {};
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
const defaultSettings = {
|
||||
}
|
||||
|
||||
function downloadAssetsList(url) {
|
||||
updateCurrentAssets().then(function () {
|
||||
fetch(url, { cache: "no-cache" })
|
||||
fetch(url, { cache: 'no-cache' })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
|
||||
availableAssets = {};
|
||||
$("#assets_menu").empty();
|
||||
$('#assets_menu').empty();
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Received assets dictionary", json);
|
||||
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"]] = [];
|
||||
if (availableAssets[i['type']] === undefined)
|
||||
availableAssets[i['type']] = [];
|
||||
|
||||
availableAssets[i["type"]].push(i);
|
||||
availableAssets[i['type']].push(i);
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Updated available assets to", availableAssets);
|
||||
console.debug(DEBUG_PREFIX, 'Updated available assets to', availableAssets);
|
||||
// First extensions, then everything else
|
||||
const assetTypes = Object.keys(availableAssets).sort((a, b) => (a === 'extension') ? -1 : (b === 'extension') ? 1 : 0);
|
||||
|
||||
for (const assetType of assetTypes) {
|
||||
let assetTypeMenu = $('<div />', { id: "assets_audio_ambient_div", class: "assets-list-div" });
|
||||
assetTypeMenu.append(`<h3>${assetType}</h3>`)
|
||||
let assetTypeMenu = $('<div />', { id: 'assets_audio_ambient_div', class: 'assets-list-div' });
|
||||
assetTypeMenu.append(`<h3>${assetType}</h3>`);
|
||||
|
||||
if (assetType == 'extension') {
|
||||
assetTypeMenu.append(`
|
||||
@@ -66,74 +61,74 @@ function downloadAssetsList(url) {
|
||||
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-fw fa-solid fa-download fa-xl\"></i>");
|
||||
let element = $('<button />', { id: elemId, type: 'button', class: 'asset-download-button menu_button' });
|
||||
const label = $('<i class="fa-fw fa-solid fa-download fa-xl"></i>');
|
||||
element.append(label);
|
||||
|
||||
//if (DEBUG_TONY_SAMA_FORK_MODE)
|
||||
// asset["url"] = asset["url"].replace("https://github.com/SillyTavern/","https://github.com/Tony-sama/"); // DBG
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Checking asset", asset["id"], asset["url"]);
|
||||
console.debug(DEBUG_PREFIX, 'Checking asset', asset['id'], asset['url']);
|
||||
|
||||
const assetInstall = async function () {
|
||||
element.off("click");
|
||||
label.removeClass("fa-download");
|
||||
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");
|
||||
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");
|
||||
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);
|
||||
}
|
||||
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");
|
||||
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, 'not installed, unchecked');
|
||||
element.prop('checked', false);
|
||||
element.on('click', assetInstall);
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Created element for ", asset["id"])
|
||||
console.debug(DEBUG_PREFIX, 'Created element for ', asset['id']);
|
||||
|
||||
const displayName = DOMPurify.sanitize(asset["name"] || asset["id"]);
|
||||
const description = DOMPurify.sanitize(asset["description"] || "");
|
||||
const url = isValidUrl(asset["url"]) ? asset["url"] : "";
|
||||
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
|
||||
const description = DOMPurify.sanitize(asset['description'] || '');
|
||||
const url = isValidUrl(asset['url']) ? asset['url'] : '';
|
||||
const previewIcon = assetType == 'extension' ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
|
||||
|
||||
$(`<i></i>`)
|
||||
$('<i></i>')
|
||||
.append(element)
|
||||
.append(`<div class="flex-container flexFlowColumn">
|
||||
<span class="flex-container alignitemscenter">
|
||||
@@ -146,17 +141,17 @@ function downloadAssetsList(url) {
|
||||
</div>`)
|
||||
.appendTo(assetTypeMenu);
|
||||
}
|
||||
assetTypeMenu.appendTo("#assets_menu");
|
||||
assetTypeMenu.appendTo('#assets_menu');
|
||||
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
|
||||
}
|
||||
|
||||
$("#assets_menu").show();
|
||||
$('#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");
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -187,7 +182,7 @@ function isAssetInstalled(assetType, filename) {
|
||||
let assetList = currentAssets[assetType];
|
||||
|
||||
if (assetType == 'extension') {
|
||||
const thirdPartyMarker = "third-party/";
|
||||
const thirdPartyMarker = 'third-party/';
|
||||
assetList = extensionNames.filter(x => x.startsWith(thirdPartyMarker)).map(x => x.replace(thirdPartyMarker, ''));
|
||||
}
|
||||
|
||||
@@ -201,13 +196,13 @@ function isAssetInstalled(assetType, filename) {
|
||||
}
|
||||
|
||||
async function installAsset(url, assetType, filename) {
|
||||
console.debug(DEBUG_PREFIX, "Downloading ", url);
|
||||
console.debug(DEBUG_PREFIX, 'Downloading ', url);
|
||||
const category = assetType;
|
||||
try {
|
||||
if (category === 'extension') {
|
||||
console.debug(DEBUG_PREFIX, "Installing extension ", url)
|
||||
console.debug(DEBUG_PREFIX, 'Installing extension ', url);
|
||||
await installExtension(url);
|
||||
console.debug(DEBUG_PREFIX, "Extension installed.")
|
||||
console.debug(DEBUG_PREFIX, 'Extension installed.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -219,7 +214,7 @@ async function installAsset(url, assetType, filename) {
|
||||
cache: 'no-cache',
|
||||
});
|
||||
if (result.ok) {
|
||||
console.debug(DEBUG_PREFIX, "Download success.")
|
||||
console.debug(DEBUG_PREFIX, 'Download success.');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
@@ -229,13 +224,13 @@ async function installAsset(url, assetType, filename) {
|
||||
}
|
||||
|
||||
async function deleteAsset(assetType, filename) {
|
||||
console.debug(DEBUG_PREFIX, "Deleting ", assetType, filename);
|
||||
console.debug(DEBUG_PREFIX, 'Deleting ', assetType, filename);
|
||||
const category = assetType;
|
||||
try {
|
||||
if (category === 'extension') {
|
||||
console.debug(DEBUG_PREFIX, "Deleting extension ", filename)
|
||||
console.debug(DEBUG_PREFIX, 'Deleting extension ', filename);
|
||||
await deleteExtension(filename);
|
||||
console.debug(DEBUG_PREFIX, "Extension deleted.")
|
||||
console.debug(DEBUG_PREFIX, 'Extension deleted.');
|
||||
}
|
||||
|
||||
const body = { category, filename };
|
||||
@@ -246,7 +241,7 @@ async function deleteAsset(assetType, filename) {
|
||||
cache: 'no-cache',
|
||||
});
|
||||
if (result.ok) {
|
||||
console.debug(DEBUG_PREFIX, "Deletion success.")
|
||||
console.debug(DEBUG_PREFIX, 'Deletion success.');
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
@@ -260,9 +255,9 @@ async function deleteAsset(assetType, filename) {
|
||||
//#############################//
|
||||
|
||||
async function updateCurrentAssets() {
|
||||
console.debug(DEBUG_PREFIX, "Checking installed assets...")
|
||||
console.debug(DEBUG_PREFIX, 'Checking installed assets...');
|
||||
try {
|
||||
const result = await fetch(`/api/assets/get`, {
|
||||
const result = await fetch('/api/assets/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
@@ -271,7 +266,7 @@ async function updateCurrentAssets() {
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
console.debug(DEBUG_PREFIX, "Current assets found:", currentAssets)
|
||||
console.debug(DEBUG_PREFIX, 'Current assets found:', currentAssets);
|
||||
}
|
||||
|
||||
|
||||
@@ -288,7 +283,7 @@ jQuery(async () => {
|
||||
assetsJsonUrl.val(ASSETS_JSON_URL);
|
||||
|
||||
const connectButton = windowHtml.find('#assets-connect-button');
|
||||
connectButton.on("click", async function () {
|
||||
connectButton.on('click', async function () {
|
||||
const url = String(assetsJsonUrl.val());
|
||||
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
|
||||
const skipConfirm = localStorage.getItem(rememberKey) === 'true';
|
||||
@@ -303,21 +298,21 @@ jQuery(async () => {
|
||||
localStorage.setItem(rememberKey, String(rememberValue));
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Confimation, loading assets...");
|
||||
console.debug(DEBUG_PREFIX, 'Confimation, loading assets...');
|
||||
downloadAssetsList(url);
|
||||
connectButton.removeClass("fa-plug-circle-exclamation");
|
||||
connectButton.removeClass("redOverlayGlow");
|
||||
connectButton.addClass("fa-plug-circle-check");
|
||||
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 ${url}`);
|
||||
connectButton.removeClass("fa-plug-circle-check");
|
||||
connectButton.addClass("fa-plug-circle-exclamation");
|
||||
connectButton.removeClass("redOverlayGlow");
|
||||
connectButton.removeClass('fa-plug-circle-check');
|
||||
connectButton.addClass('fa-plug-circle-exclamation');
|
||||
connectButton.removeClass('redOverlayGlow');
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.debug(DEBUG_PREFIX, "Connection refused by user");
|
||||
console.debug(DEBUG_PREFIX, 'Connection refused by user');
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -1,9 +1,10 @@
|
||||
import { getBase64Async, saveBase64AsFile } from "../../utils.js";
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
|
||||
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
|
||||
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
|
||||
import { SECRET_KEYS, secret_state } from "../../secrets.js";
|
||||
import { getMultimodalCaption } from "../shared.js";
|
||||
import { getBase64Async, saveBase64AsFile } from '../../utils.js';
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from '../../extensions.js';
|
||||
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@@ -87,7 +88,7 @@ async function sendCaptionedMessage(caption, image) {
|
||||
let template = extension_settings.caption.template || TEMPLATE_DEFAULT;
|
||||
|
||||
if (!/{{caption}}/i.test(template)) {
|
||||
console.warn('Poka-yoke: Caption template does not contain {{caption}}. Appending it.')
|
||||
console.warn('Poka-yoke: Caption template does not contain {{caption}}. Appending it.');
|
||||
template += ' {{caption}}';
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ async function captionExtras(base64Img) {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
},
|
||||
body: JSON.stringify({ image: base64Img })
|
||||
body: JSON.stringify({ image: base64Img }),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -179,7 +180,7 @@ async function captionLocal(base64Img) {
|
||||
const apiResult = await fetch('/api/extra/caption', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ image: base64Img })
|
||||
body: JSON.stringify({ image: base64Img }),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -199,7 +200,7 @@ async function captionHorde(base64Img) {
|
||||
const apiResult = await fetch('/api/horde/caption-image', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ image: base64Img })
|
||||
body: JSON.stringify({ image: base64Img }),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -216,7 +217,16 @@ async function captionHorde(base64Img) {
|
||||
* @returns {Promise<{caption: string}>} Generated caption
|
||||
*/
|
||||
async function captionMultimodal(base64Img) {
|
||||
const prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
|
||||
let prompt = extension_settings.caption.prompt || PROMPT_DEFAULT;
|
||||
|
||||
if (extension_settings.caption.prompt_ask) {
|
||||
const customPrompt = await callPopup('<h3>Enter a comment or question:</h3>', 'input', prompt, { rows: 2 });
|
||||
if (!customPrompt) {
|
||||
throw new Error('User aborted the caption sending.');
|
||||
}
|
||||
prompt = String(customPrompt).trim();
|
||||
}
|
||||
|
||||
const caption = await getMultimodalCaption(base64Img, prompt);
|
||||
return { caption };
|
||||
}
|
||||
@@ -271,8 +281,13 @@ jQuery(function () {
|
||||
$(sendButton).on('click', () => {
|
||||
const hasCaptionModule =
|
||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'custom') ||
|
||||
extension_settings.caption.source === 'local' ||
|
||||
extension_settings.caption.source === 'horde';
|
||||
|
||||
@@ -285,7 +300,7 @@ jQuery(function () {
|
||||
});
|
||||
}
|
||||
function addPictureSendForm() {
|
||||
const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
|
||||
const inputHtml = '<input id="img_file" type="file" hidden accept="image/*">';
|
||||
const imgForm = document.createElement('form');
|
||||
imgForm.id = 'img_form';
|
||||
$(imgForm).append(inputHtml);
|
||||
@@ -299,7 +314,7 @@ jQuery(function () {
|
||||
$('#caption_prompt_block').toggle(isMultimodal);
|
||||
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
|
||||
$('#caption_multimodal_model').val(extension_settings.caption.multimodal_model);
|
||||
$('#caption_multimodal_model option').each(function () {
|
||||
$('#caption_multimodal_block [data-type]').each(function () {
|
||||
const type = $(this).data('type');
|
||||
$(this).toggle(type === extension_settings.caption.multimodal_api);
|
||||
});
|
||||
@@ -328,7 +343,7 @@ jQuery(function () {
|
||||
<label for="caption_source">Source</label>
|
||||
<select id="caption_source" class="text_pole">
|
||||
<option value="local">Local</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / OpenRouter)</option>
|
||||
<option value="multimodal">Multimodal (OpenAI / llama / Google)</option>
|
||||
<option value="extras">Extras</option>
|
||||
<option value="horde">Horde</option>
|
||||
</select>
|
||||
@@ -336,22 +351,45 @@ jQuery(function () {
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_api">API</label>
|
||||
<select id="caption_multimodal_api" class="flex1 text_pole">
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="ooba">Text Generation WebUI (oobabooga)</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
<option value="google">Google MakerSuite</option>
|
||||
<option value="custom">Custom (OpenAI-compatible)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_model">Model</label>
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
|
||||
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
|
||||
<option data-type="custom" value="custom_current">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
<label data-type="openai" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
|
||||
Allow reverse proxy
|
||||
</label>
|
||||
<div class="flexBasis100p m-b-1">
|
||||
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div id="caption_prompt_block">
|
||||
<label for="caption_prompt">Caption Prompt</label>
|
||||
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="< Use default >">${PROMPT_DEFAULT}</textarea>
|
||||
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
|
||||
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
|
||||
Ask every time
|
||||
</label>
|
||||
</div>
|
||||
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
|
||||
<textarea id="caption_template" class="text_pole" rows="2" placeholder="< Use default >">${TEMPLATE_DEFAULT}</textarea>
|
||||
@@ -374,6 +412,8 @@ jQuery(function () {
|
||||
switchMultimodalBlocks();
|
||||
|
||||
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
|
||||
$('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
|
||||
$('#caption_prompt_ask').prop('checked', !!(extension_settings.caption.prompt_ask));
|
||||
$('#caption_source').val(extension_settings.caption.source);
|
||||
$('#caption_prompt').val(extension_settings.caption.prompt);
|
||||
$('#caption_template').val(extension_settings.caption.template);
|
||||
@@ -391,4 +431,12 @@ jQuery(function () {
|
||||
extension_settings.caption.template = String($('#caption_template').val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_allow_reverse_proxy').on('input', () => {
|
||||
extension_settings.caption.allow_reverse_proxy = $('#caption_allow_reverse_proxy').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_prompt_ask').on('input', () => {
|
||||
extension_settings.caption.prompt_ask = $('#caption_prompt_ask').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, this_chid } from "../../../script.js";
|
||||
import { dragElement, isMobile } from "../../RossAscends-mods.js";
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js";
|
||||
import { loadMovingUIState, power_user } from "../../power-user.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from "../../utils.js";
|
||||
import { hideMutedSprites } from "../../group-chats.js";
|
||||
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from '../../../script.js';
|
||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from '../../extensions.js';
|
||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
||||
import { hideMutedSprites } from '../../group-chats.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
@@ -12,35 +12,35 @@ const UPDATE_INTERVAL = 2000;
|
||||
const STREAMING_UPDATE_INTERVAL = 6000;
|
||||
const FALLBACK_EXPRESSION = 'joy';
|
||||
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"
|
||||
'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',
|
||||
];
|
||||
|
||||
let expressionsList = null;
|
||||
@@ -49,6 +49,7 @@ let lastMessage = null;
|
||||
let spriteCache = {};
|
||||
let inApiCall = false;
|
||||
let lastServerResponseTime = 0;
|
||||
export let lastExpression = {};
|
||||
|
||||
function isVisualNovelMode() {
|
||||
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
|
||||
@@ -209,7 +210,7 @@ async function visualNovelUpdateLayers(container) {
|
||||
const containerWidth = container.width();
|
||||
const pivotalPoint = containerWidth * 0.5;
|
||||
|
||||
let images = $('.expression-holder');
|
||||
let images = $('#visual-novel-wrapper .expression-holder');
|
||||
let imagesWidth = [];
|
||||
|
||||
images.sort(sortFunction).each(function () {
|
||||
@@ -229,14 +230,14 @@ async function visualNovelUpdateLayers(container) {
|
||||
|
||||
images.sort(sortFunction).each((index, current) => {
|
||||
const element = $(current);
|
||||
const elementID = element.attr('id')
|
||||
const elementID = element.attr('id');
|
||||
|
||||
// skip repositioning of dragged elements
|
||||
if (element.data('dragged')
|
||||
|| (power_user.movingUIState[elementID]
|
||||
&& (typeof power_user.movingUIState[elementID] === 'object')
|
||||
&& Object.keys(power_user.movingUIState[elementID]).length > 0)) {
|
||||
loadMovingUIState()
|
||||
loadMovingUIState();
|
||||
//currentPosition += imagesWidth[index];
|
||||
return;
|
||||
}
|
||||
@@ -298,7 +299,7 @@ async function setImage(img, path) {
|
||||
//only swap expressions when necessary
|
||||
if (prevExpressionSrc !== path && !img.hasClass('expression-animating')) {
|
||||
//clone expression
|
||||
expressionClone.addClass('expression-clone')
|
||||
expressionClone.addClass('expression-clone');
|
||||
//make invisible and remove id to prevent double ids
|
||||
//must be made invisible to start because they share the same Z-index
|
||||
expressionClone.attr('id', '').css({ opacity: 0 });
|
||||
@@ -325,14 +326,14 @@ async function setImage(img, path) {
|
||||
expressionClone.addClass('expression-animating');
|
||||
//fade the clone in
|
||||
expressionClone.css({
|
||||
opacity: 0
|
||||
opacity: 0,
|
||||
}).animate({
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
}, duration)
|
||||
//when finshed fading in clone, fade out the original
|
||||
.promise().done(function () {
|
||||
img.animate({
|
||||
opacity: 0
|
||||
opacity: 0,
|
||||
}, duration);
|
||||
//remove old expression
|
||||
img.remove();
|
||||
@@ -393,7 +394,6 @@ async function unloadLiveChar() {
|
||||
if (!loadResponse.ok) {
|
||||
throw new Error(loadResponse.statusText);
|
||||
}
|
||||
const loadResponseText = await loadResponse.text();
|
||||
//console.log(`Response: ${loadResponseText}`);
|
||||
} catch (error) {
|
||||
//console.error(`Error unloading - ${error}`);
|
||||
@@ -446,7 +446,7 @@ function handleImageChange() {
|
||||
const imgElement = document.querySelector('img#expression-image.expression');
|
||||
|
||||
if (!imgElement || !(imgElement instanceof HTMLImageElement)) {
|
||||
console.log("Cannot find addExpressionImage()");
|
||||
console.log('Cannot find addExpressionImage()');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ function handleImageChange() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
imgElement.src = ""; //remove incase char doesnt have expressions
|
||||
imgElement.src = ''; //remove incase char doesnt have expressions
|
||||
setExpression(getContext().name2, FALLBACK_EXPRESSION, true);
|
||||
}
|
||||
}
|
||||
@@ -510,7 +510,7 @@ async function moduleWorker() {
|
||||
if (vnStateChanged) {
|
||||
lastMessage = null;
|
||||
$('#visual-novel-wrapper').empty();
|
||||
$("#expression-holder").css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
|
||||
$('#expression-holder').css({ top: '', left: '', right: '', bottom: '', height: '', width: '', margin: '' });
|
||||
}
|
||||
|
||||
const currentLastMessage = getLastCharacterMessage();
|
||||
@@ -688,11 +688,12 @@ function getFolderNameByMessage(message) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const folderName = avatarPath.replace(/\.[^/.]+$/, "");
|
||||
const folderName = avatarPath.replace(/\.[^/.]+$/, '');
|
||||
return folderName;
|
||||
}
|
||||
|
||||
async function sendExpressionCall(name, expression, force, vnMode) {
|
||||
lastExpression[name.split('/')[0]] = expression;
|
||||
if (!vnMode) {
|
||||
vnMode = isVisualNovelMode();
|
||||
}
|
||||
@@ -717,7 +718,7 @@ async function setSpriteSetCommand(_, folder) {
|
||||
folder = `${currentLastMessage.name}/${folder}`;
|
||||
}
|
||||
|
||||
$("#expression_override").val(folder.trim());
|
||||
$('#expression_override').val(folder.trim());
|
||||
onClickExpressionOverrideButton();
|
||||
removeExpression();
|
||||
moduleWorker();
|
||||
@@ -763,7 +764,7 @@ function sampleClassifyText(text) {
|
||||
}
|
||||
|
||||
// Remove asterisks and quotes
|
||||
let result = text.replace(/[\*\"]/g, '');
|
||||
let result = text.replace(/[*"]/g, '');
|
||||
|
||||
const SAMPLE_THRESHOLD = 500;
|
||||
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||
@@ -856,7 +857,7 @@ async function validateImages(character, forceRedrawCached) {
|
||||
|
||||
if (spriteCache[character]) {
|
||||
if (forceRedrawCached && $('#image_list').data('name') !== character) {
|
||||
console.debug('force redrawing character sprites list')
|
||||
console.debug('force redrawing character sprites list');
|
||||
drawSpritesList(character, labels, spriteCache[character]);
|
||||
}
|
||||
|
||||
@@ -993,8 +994,7 @@ async function getExpressionsList() {
|
||||
}
|
||||
|
||||
const result = await resolveExpressionsList();
|
||||
result.push(...extension_settings.expressions.custom);
|
||||
return result;
|
||||
return [...result, ...extension_settings.expressions.custom];
|
||||
}
|
||||
|
||||
async function setExpression(character, expression, force) {
|
||||
@@ -1003,7 +1003,7 @@ async function setExpression(character, expression, force) {
|
||||
await validateImages(character);
|
||||
const img = $('img.expression');
|
||||
const prevExpressionSrc = img.attr('src');
|
||||
const expressionClone = img.clone()
|
||||
const expressionClone = img.clone();
|
||||
|
||||
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
|
||||
console.debug('checking for expression images to show..');
|
||||
@@ -1031,14 +1031,14 @@ async function setExpression(character, expression, force) {
|
||||
if (prevExpressionSrc !== sprite.path
|
||||
&& !img.hasClass('expression-animating')) {
|
||||
//clone expression
|
||||
expressionClone.addClass('expression-clone')
|
||||
expressionClone.addClass('expression-clone');
|
||||
//make invisible and remove id to prevent double ids
|
||||
//must be made invisible to start because they share the same Z-index
|
||||
expressionClone.attr('id', '').css({ opacity: 0 });
|
||||
//add new sprite path to clone src
|
||||
expressionClone.attr('src', sprite.path);
|
||||
//add invisible clone to html
|
||||
expressionClone.appendTo($("#expression-holder"))
|
||||
expressionClone.appendTo($('#expression-holder'));
|
||||
|
||||
const duration = 200;
|
||||
|
||||
@@ -1058,14 +1058,14 @@ async function setExpression(character, expression, force) {
|
||||
expressionClone.addClass('expression-animating');
|
||||
//fade the clone in
|
||||
expressionClone.css({
|
||||
opacity: 0
|
||||
opacity: 0,
|
||||
}).animate({
|
||||
opacity: 1
|
||||
opacity: 1,
|
||||
}, duration)
|
||||
//when finshed fading in clone, fade out the original
|
||||
.promise().done(function () {
|
||||
img.animate({
|
||||
opacity: 0
|
||||
opacity: 0,
|
||||
}, duration);
|
||||
//remove old expression
|
||||
img.remove();
|
||||
@@ -1106,7 +1106,7 @@ async function setExpression(character, expression, force) {
|
||||
img.attr('src', defImgUrl);
|
||||
img.addClass('default');
|
||||
}
|
||||
document.getElementById("expression-holder").style.display = '';
|
||||
document.getElementById('expression-holder').style.display = '';
|
||||
|
||||
} else {
|
||||
|
||||
@@ -1204,7 +1204,7 @@ async function onClickExpressionRemoveCustom() {
|
||||
async function handleFileUpload(url, formData) {
|
||||
try {
|
||||
const data = await jQuery.ajax({
|
||||
type: "POST",
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: formData,
|
||||
beforeSend: function () { },
|
||||
@@ -1267,9 +1267,9 @@ async function onClickExpressionOverrideButton() {
|
||||
return;
|
||||
}
|
||||
|
||||
const overridePath = String($("#expression_override").val());
|
||||
const overridePath = String($('#expression_override').val());
|
||||
const existingOverrideIndex = extension_settings.expressionOverrides.findIndex((e) =>
|
||||
e.name == avatarFileName
|
||||
e.name == avatarFileName,
|
||||
);
|
||||
|
||||
// If the path is empty, delete the entry from overrides
|
||||
@@ -1318,7 +1318,7 @@ async function onClickExpressionOverrideRemoveAllButton() {
|
||||
extension_settings.expressionOverrides = [];
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug("All expression image overrides have been cleared.");
|
||||
console.debug('All expression image overrides have been cleared.');
|
||||
|
||||
// Refresh sprites list to use the default name if applicable
|
||||
try {
|
||||
@@ -1366,7 +1366,7 @@ async function onClickExpressionDelete(event) {
|
||||
// Prevents the expression from being set
|
||||
event.stopPropagation();
|
||||
|
||||
const confirmation = await callPopup("<h3>Are you sure?</h3>Once deleted, it's gone forever!", 'confirm');
|
||||
const confirmation = await callPopup('<h3>Are you sure?</h3>Once deleted, it\'s gone forever!', 'confirm');
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
@@ -1398,17 +1398,17 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
}
|
||||
|
||||
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
|
||||
e.name == avatarFileName
|
||||
e.name == avatarFileName,
|
||||
);
|
||||
|
||||
if (expressionOverride && expressionOverride.path) {
|
||||
$("#expression_override").val(expressionOverride.path);
|
||||
$('#expression_override').val(expressionOverride.path);
|
||||
} else if (expressionOverride) {
|
||||
delete extension_settings.expressionOverrides[expressionOverride.name];
|
||||
}
|
||||
|
||||
if (forceClear && !expressionOverride) {
|
||||
$("#expression_override").val("");
|
||||
$('#expression_override').val('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,7 +1427,7 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
function addVisualNovelMode() {
|
||||
const html = `
|
||||
<div id="visual-novel-wrapper">
|
||||
</div>`
|
||||
</div>`;
|
||||
const element = $(html);
|
||||
element.hide();
|
||||
$('body').append(element);
|
||||
@@ -1445,14 +1445,14 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
});
|
||||
$('#expression_override_cleanup_button').on('click', onClickExpressionOverrideRemoveAllButton);
|
||||
$(document).on('dragstart', '.expression', (e) => {
|
||||
e.preventDefault()
|
||||
return false
|
||||
})
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
||||
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
||||
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
||||
$(window).on("resize", updateVisualNovelModeDebounced);
|
||||
$("#open_chat_expressions").hide();
|
||||
$(window).on('resize', updateVisualNovelModeDebounced);
|
||||
$('#open_chat_expressions').hide();
|
||||
|
||||
$('#image_type_toggle').on('click', function () {
|
||||
if (this instanceof HTMLInputElement) {
|
||||
@@ -1473,16 +1473,17 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
const updateFunction = wrapper.update.bind(wrapper);
|
||||
setInterval(updateFunction, UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
dragElement($("#expression-holder"))
|
||||
dragElement($('#expression-holder'));
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
// character changed
|
||||
removeExpression();
|
||||
spriteCache = {};
|
||||
lastExpression = {};
|
||||
|
||||
//clear expression
|
||||
let imgElement = document.getElementById('expression-image');
|
||||
if (imgElement && imgElement instanceof HTMLImageElement) {
|
||||
imgElement.src = "";
|
||||
imgElement.src = '';
|
||||
}
|
||||
|
||||
//set checkbox to global var
|
||||
@@ -1503,4 +1504,5 @@ function setExpressionOverrideHtml(forceClear = false) {
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
|
||||
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
|
||||
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
|
||||
})();
|
||||
|
@@ -190,8 +190,3 @@ img.expression.default {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@media screen and (max-width:1200px) {
|
||||
div.expression {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@@ -3,14 +3,14 @@ import {
|
||||
this_chid,
|
||||
characters,
|
||||
getRequestHeaders,
|
||||
} from "../../../script.js";
|
||||
import { selected_group } from "../../group-chats.js";
|
||||
import { loadFileToDocument, delay } from "../../utils.js";
|
||||
} from '../../../script.js';
|
||||
import { selected_group } from '../../group-chats.js';
|
||||
import { loadFileToDocument, delay } from '../../utils.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
|
||||
const extensionName = "gallery";
|
||||
const extensionName = 'gallery';
|
||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||
let firstTime = true;
|
||||
|
||||
@@ -38,7 +38,7 @@ async function getGalleryItems(url) {
|
||||
const items = data.map((file) => ({
|
||||
src: `user/images/${url}/${file}`,
|
||||
srct: `user/images/${url}/${file}`,
|
||||
title: "", // Optional title for each item
|
||||
title: '', // Optional title for each item
|
||||
}));
|
||||
|
||||
return items;
|
||||
@@ -54,8 +54,8 @@ async function getGalleryItems(url) {
|
||||
* @returns {Promise<void>} - Promise representing the completion of the gallery initialization.
|
||||
*/
|
||||
async function initGallery(items, url) {
|
||||
$("#dragGallery").nanogallery2({
|
||||
"items": items,
|
||||
$('#dragGallery').nanogallery2({
|
||||
'items': items,
|
||||
thumbnailWidth: 'auto',
|
||||
thumbnailHeight: thumbnailHeight,
|
||||
paginationVisiblePages: paginationVisiblePages,
|
||||
@@ -70,15 +70,15 @@ async function initGallery(items, url) {
|
||||
navigationPagination: { background: '#111', color: '#fff', colorHover: '#ccc', borderRadius: '4px' },
|
||||
thumbnail: { background: '#444', backgroundImage: 'linear-gradient(315deg, #111 0%, #445 90%)', borderColor: '#000', borderRadius: '0px', labelOpacity: 1, labelBackground: 'rgba(34, 34, 34, 0)', titleColor: '#fff', titleBgColor: 'transparent', titleShadow: '', descriptionColor: '#ccc', descriptionBgColor: 'transparent', descriptionShadow: '', stackBackground: '#aaa' },
|
||||
thumbnailIcon: { padding: '5px', color: '#fff', shadow: '' },
|
||||
pagination: { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid var(--SmartThemeQuoteColor)', shapeColor: '#444', shapeSelectedColor: '#aaa' }
|
||||
pagination: { background: '#181818', backgroundSelected: '#666', color: '#fff', borderRadius: '2px', shapeBorder: '3px solid var(--SmartThemeQuoteColor)', shapeColor: '#444', shapeSelectedColor: '#aaa' },
|
||||
},
|
||||
galleryDisplayMode: "pagination",
|
||||
galleryDisplayMode: 'pagination',
|
||||
fnThumbnailOpen: viewWithDragbox,
|
||||
});
|
||||
|
||||
|
||||
eventSource.on('resizeUI', function (elmntName) {
|
||||
jQuery("#dragGallery").nanogallery2('resize');
|
||||
jQuery('#dragGallery').nanogallery2('resize');
|
||||
});
|
||||
|
||||
const dropZone = $('#dragGallery');
|
||||
@@ -111,11 +111,11 @@ async function initGallery(items, url) {
|
||||
});
|
||||
|
||||
//let images populate first
|
||||
await delay(100)
|
||||
await delay(100);
|
||||
//unset the height (which must be getting set by the gallery library at some point)
|
||||
$("#dragGallery").css('height', 'unset');
|
||||
$('#dragGallery').css('height', 'unset');
|
||||
//force a resize to make images display correctly
|
||||
jQuery("#dragGallery").nanogallery2('resize');
|
||||
jQuery('#dragGallery').nanogallery2('resize');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,27 +135,27 @@ async function showCharGallery() {
|
||||
if (firstTime) {
|
||||
await loadFileToDocument(
|
||||
`${extensionFolderPath}nanogallery2.woff.min.css`,
|
||||
"css"
|
||||
'css',
|
||||
);
|
||||
await loadFileToDocument(
|
||||
`${extensionFolderPath}jquery.nanogallery2.min.js`,
|
||||
"js"
|
||||
'js',
|
||||
);
|
||||
firstTime = false;
|
||||
toastr.info("Images can also be found in the folder `user/images`", "Drag and drop images onto the gallery to upload them", { timeOut: 6000 });
|
||||
toastr.info('Images can also be found in the folder `user/images`', 'Drag and drop images onto the gallery to upload them', { timeOut: 6000 });
|
||||
}
|
||||
|
||||
try {
|
||||
let url = selected_group || this_chid;
|
||||
if (!selected_group && this_chid) {
|
||||
const char = characters[this_chid];
|
||||
url = char.avatar.replace(".png", "");
|
||||
url = char.avatar.replace('.png', '');
|
||||
}
|
||||
|
||||
const items = await getGalleryItems(url);
|
||||
// if there already is a gallery, destroy it and place this one in its place
|
||||
if ($(`#dragGallery`).length) {
|
||||
$(`#dragGallery`).nanogallery2("destroy");
|
||||
if ($('#dragGallery').length) {
|
||||
$('#dragGallery').nanogallery2('destroy');
|
||||
initGallery(items, url);
|
||||
} else {
|
||||
makeMovable();
|
||||
@@ -187,7 +187,7 @@ async function uploadFile(file, url) {
|
||||
|
||||
// Create the payload
|
||||
const payload = {
|
||||
image: base64Data
|
||||
image: base64Data,
|
||||
};
|
||||
|
||||
// Add the ch_name from the provided URL (assuming it's the character name)
|
||||
@@ -198,13 +198,13 @@ async function uploadFile(file, url) {
|
||||
|
||||
// Merge headers with content-type for JSON
|
||||
Object.assign(headers, {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
const response = await fetch('/uploadimage', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -216,35 +216,35 @@ async function uploadFile(file, url) {
|
||||
toastr.success('File uploaded successfully. Saved at: ' + result.path);
|
||||
|
||||
// Refresh the gallery
|
||||
$("#dragGallery").nanogallery2("destroy"); // Destroy old gallery
|
||||
$('#dragGallery').nanogallery2('destroy'); // Destroy old gallery
|
||||
const newItems = await getGalleryItems(url); // Fetch the latest items
|
||||
initGallery(newItems, url); // Reinitialize the gallery with new items and pass 'url'
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error("There was an issue uploading the file:", error);
|
||||
console.error('There was an issue uploading the file:', error);
|
||||
|
||||
// Replacing alert with toastr error notification
|
||||
toastr.error('Failed to upload the file.');
|
||||
}
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Register an event listener
|
||||
eventSource.on("charManagementDropdown", (selectedOptionId) => {
|
||||
if (selectedOptionId === "show_char_gallery") {
|
||||
eventSource.on('charManagementDropdown', (selectedOptionId) => {
|
||||
if (selectedOptionId === 'show_char_gallery') {
|
||||
showCharGallery();
|
||||
}
|
||||
});
|
||||
|
||||
// Add an option to the dropdown
|
||||
$("#char-management-dropdown").append(
|
||||
$("<option>", {
|
||||
id: "show_char_gallery",
|
||||
text: "Show Gallery",
|
||||
})
|
||||
$('#char-management-dropdown').append(
|
||||
$('<option>', {
|
||||
id: 'show_char_gallery',
|
||||
text: 'Show Gallery',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -254,18 +254,18 @@ $(document).ready(function () {
|
||||
* The cloned element has its attributes set, a new child div appended, and is made visible on the body.
|
||||
* Additionally, it sets up the element to prevent dragging on its images.
|
||||
*/
|
||||
function makeMovable(id = "gallery") {
|
||||
function makeMovable(id = 'gallery') {
|
||||
|
||||
console.debug('making new container from template')
|
||||
console.debug('making new container from template');
|
||||
const template = $('#generic_draggable_template').html();
|
||||
const newElement = $(template);
|
||||
newElement.css('background-color', 'var(--SmartThemeBlurTintColor)');
|
||||
newElement.attr('forChar', id);
|
||||
newElement.attr('id', `${id}`);
|
||||
newElement.find('.drag-grabber').attr('id', `${id}header`);
|
||||
newElement.find('.dragTitle').text('Image Gallery')
|
||||
newElement.find('.dragTitle').text('Image Gallery');
|
||||
//add a div for the gallery
|
||||
newElement.append(`<div id="dragGallery"></div>`);
|
||||
newElement.append('<div id="dragGallery"></div>');
|
||||
// add no-scrollbar class to this element
|
||||
newElement.addClass('no-scrollbar');
|
||||
|
||||
@@ -274,7 +274,7 @@ function makeMovable(id = "gallery") {
|
||||
closeButton.attr('id', `${id}close`);
|
||||
closeButton.attr('data-related-id', `${id}`);
|
||||
|
||||
$(`#dragGallery`).css('display', 'block');
|
||||
$('#dragGallery').css('display', 'block');
|
||||
|
||||
$('body').append(newElement);
|
||||
|
||||
@@ -370,7 +370,7 @@ function makeDragImg(id, url) {
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
console.error("Failed to append the template content or retrieve the appended content.");
|
||||
console.error('Failed to append the template content or retrieve the appended content.');
|
||||
}
|
||||
|
||||
$('body').on('click', '.dragClose', function () {
|
||||
@@ -415,7 +415,7 @@ function viewWithDragbox(items) {
|
||||
|
||||
|
||||
// Registers a simple command for opening the char gallery.
|
||||
registerSlashCommand("show-gallery", showGalleryCommand, ["sg"], "– shows the gallery", true, true);
|
||||
registerSlashCommand('show-gallery', showGalleryCommand, ['sg'], '– shows the gallery', true, true);
|
||||
|
||||
function showGalleryCommand(args) {
|
||||
showCharGallery();
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from "../../utils.js";
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
|
||||
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js";
|
||||
import { is_group_generating, selected_group } from "../../group-chats.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from '../../extensions.js';
|
||||
import { animation_duration, eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
|
||||
import { is_group_generating, selected_group } from '../../group-chats.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from "../../RossAscends-mods.js";
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
import { getTextTokens, tokenizers } from '../../tokenizers.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = '1_memory';
|
||||
@@ -29,7 +30,7 @@ const formatMemoryValue = function (value) {
|
||||
} else {
|
||||
return `Summary: ${value}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveChatDebounced = debounce(() => getContext().saveChat(), 2000);
|
||||
|
||||
@@ -42,26 +43,6 @@ const defaultPrompt = '[Pause your roleplay. Summarize the most important facts
|
||||
const defaultTemplate = '[Summary: {{summary}}]';
|
||||
|
||||
const defaultSettings = {
|
||||
minLongMemory: 16,
|
||||
maxLongMemory: 1024,
|
||||
longMemoryLength: 128,
|
||||
shortMemoryLength: 512,
|
||||
minShortMemory: 128,
|
||||
maxShortMemory: 1024,
|
||||
shortMemoryStep: 16,
|
||||
longMemoryStep: 8,
|
||||
repetitionPenaltyStep: 0.05,
|
||||
repetitionPenalty: 1.2,
|
||||
maxRepetitionPenalty: 2.0,
|
||||
minRepetitionPenalty: 1.0,
|
||||
temperature: 1.0,
|
||||
minTemperature: 0.1,
|
||||
maxTemperature: 2.0,
|
||||
temperatureStep: 0.05,
|
||||
lengthPenalty: 1,
|
||||
minLengthPenalty: -4,
|
||||
maxLengthPenalty: 4,
|
||||
lengthPenaltyStep: 0.1,
|
||||
memoryFrozen: false,
|
||||
SkipWIAN: false,
|
||||
source: summary_sources.extras,
|
||||
@@ -95,11 +76,6 @@ function loadSettings() {
|
||||
}
|
||||
|
||||
$('#summary_source').val(extension_settings.memory.source).trigger('change');
|
||||
$('#memory_long_length').val(extension_settings.memory.longMemoryLength).trigger('input');
|
||||
$('#memory_short_length').val(extension_settings.memory.shortMemoryLength).trigger('input');
|
||||
$('#memory_repetition_penalty').val(extension_settings.memory.repetitionPenalty).trigger('input');
|
||||
$('#memory_temperature').val(extension_settings.memory.temperature).trigger('input');
|
||||
$('#memory_length_penalty').val(extension_settings.memory.lengthPenalty).trigger('input');
|
||||
$('#memory_frozen').prop('checked', extension_settings.memory.memoryFrozen).trigger('input');
|
||||
$('#memory_skipWIAN').prop('checked', extension_settings.memory.SkipWIAN).trigger('input');
|
||||
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
|
||||
@@ -109,61 +85,21 @@ function loadSettings() {
|
||||
$('#memory_depth').val(extension_settings.memory.depth).trigger('input');
|
||||
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
|
||||
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
|
||||
switchSourceControls(extension_settings.memory.source);
|
||||
}
|
||||
|
||||
function onSummarySourceChange(event) {
|
||||
const value = event.target.value;
|
||||
extension_settings.memory.source = value;
|
||||
switchSourceControls(value);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function switchSourceControls(value) {
|
||||
$('#memory_settings [data-source]').each((_, element) => {
|
||||
const source = $(element).data('source');
|
||||
$(element).toggle(source === value);
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryShortInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.shortMemoryLength = Number(value);
|
||||
$('#memory_short_length_tokens').text(value);
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Don't let long buffer be bigger than short
|
||||
if (extension_settings.memory.longMemoryLength > extension_settings.memory.shortMemoryLength) {
|
||||
$('#memory_long_length').val(extension_settings.memory.shortMemoryLength).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
function onMemoryLongInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.longMemoryLength = Number(value);
|
||||
$('#memory_long_length_tokens').text(value);
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Don't let long buffer be bigger than short
|
||||
if (extension_settings.memory.longMemoryLength > extension_settings.memory.shortMemoryLength) {
|
||||
$('#memory_short_length').val(extension_settings.memory.longMemoryLength).trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
function onMemoryRepetitionPenaltyInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.repetitionPenalty = Number(value);
|
||||
$('#memory_repetition_penalty_value').text(extension_settings.memory.repetitionPenalty.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryTemperatureInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.temperature = Number(value);
|
||||
$('#memory_temperature_value').text(extension_settings.memory.temperature.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryLengthPenaltyInput() {
|
||||
const value = $(this).val();
|
||||
extension_settings.memory.lengthPenalty = Number(value);
|
||||
$('#memory_length_penalty_value').text(extension_settings.memory.lengthPenalty.toFixed(2));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onMemoryFrozenInput() {
|
||||
@@ -317,10 +253,15 @@ async function onChatEvent() {
|
||||
}
|
||||
|
||||
async function forceSummarizeChat() {
|
||||
if (extension_settings.memory.source === summary_sources.extras) {
|
||||
toastr.warning('Force summarization is not supported for Extras API');
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN
|
||||
console.log(`Skipping WIAN? ${skipWIAN}`)
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN;
|
||||
console.log(`Skipping WIAN? ${skipWIAN}`);
|
||||
if (!context.chatId) {
|
||||
toastr.warning('No chat selected');
|
||||
return;
|
||||
@@ -336,7 +277,7 @@ async function forceSummarizeChat() {
|
||||
}
|
||||
|
||||
async function summarizeChat(context) {
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN
|
||||
const skipWIAN = extension_settings.memory.SkipWIAN;
|
||||
switch (extension_settings.memory.source) {
|
||||
case summary_sources.extras:
|
||||
await summarizeChatExtras(context);
|
||||
@@ -409,7 +350,7 @@ async function summarizeChatMain(context, force, skipWIAN) {
|
||||
console.debug('Summarization prompt is empty. Skipping summarization.');
|
||||
return;
|
||||
}
|
||||
console.log('sending summary prompt')
|
||||
console.log('sending summary prompt');
|
||||
const summary = await generateQuietPrompt(prompt, false, skipWIAN);
|
||||
const newContext = getContext();
|
||||
|
||||
@@ -434,33 +375,36 @@ async function summarizeChatExtras(context) {
|
||||
const longMemory = getLatestMemoryFromChat(chat);
|
||||
const reversedChat = chat.slice().reverse();
|
||||
reversedChat.shift();
|
||||
let memoryBuffer = [];
|
||||
const memoryBuffer = [];
|
||||
const CONTEXT_SIZE = 1024 - 64;
|
||||
|
||||
for (let mes of reversedChat) {
|
||||
for (const message of reversedChat) {
|
||||
// we reached the point of latest memory
|
||||
if (longMemory && mes.extra && mes.extra.memory == longMemory) {
|
||||
if (longMemory && message.extra && message.extra.memory == longMemory) {
|
||||
break;
|
||||
}
|
||||
|
||||
// don't care about system
|
||||
if (mes.is_system) {
|
||||
if (message.is_system) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// determine the sender's name
|
||||
const name = mes.is_user ? (context.name1 ?? 'You') : (mes.force_avatar ? mes.name : context.name2);
|
||||
const entry = `${name}:\n${mes['mes']}`;
|
||||
const entry = `${message.name}:\n${message.mes}`;
|
||||
memoryBuffer.push(entry);
|
||||
|
||||
// check if token limit was reached
|
||||
if (context.getTokenCount(getMemoryString()) >= extension_settings.memory.shortMemoryLength) {
|
||||
const tokens = getTextTokens(tokenizers.GPT2, getMemoryString()).length;
|
||||
if (tokens >= CONTEXT_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const resultingString = getMemoryString();
|
||||
const resultingTokens = getTextTokens(tokenizers.GPT2, resultingString).length;
|
||||
|
||||
if (context.getTokenCount(resultingString) < extension_settings.memory.shortMemoryLength) {
|
||||
if (!resultingString || resultingTokens < CONTEXT_SIZE) {
|
||||
console.debug('Not enough context to summarize');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -478,14 +422,8 @@ async function summarizeChatExtras(context) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: resultingString,
|
||||
params: {
|
||||
min_length: extension_settings.memory.longMemoryLength * 0, // testing how it behaves 0 min length
|
||||
max_length: extension_settings.memory.longMemoryLength,
|
||||
repetition_penalty: extension_settings.memory.repetitionPenalty,
|
||||
temperature: extension_settings.memory.temperature,
|
||||
length_penalty: extension_settings.memory.lengthPenalty,
|
||||
}
|
||||
})
|
||||
params: {},
|
||||
}),
|
||||
});
|
||||
|
||||
if (apiResult.ok) {
|
||||
@@ -564,48 +502,48 @@ function setMemoryContext(value, saveToMessage) {
|
||||
function doPopout(e) {
|
||||
const target = e.target;
|
||||
//repurposes the zoomed avatar template to server as a floating div
|
||||
if ($("#summaryExtensionPopout").length === 0) {
|
||||
console.debug('did not see popout yet, creating')
|
||||
const originalHTMLClone = $(target).parent().parent().parent().find('.inline-drawer-content').html()
|
||||
const originalElement = $(target).parent().parent().parent().find('.inline-drawer-content')
|
||||
if ($('#summaryExtensionPopout').length === 0) {
|
||||
console.debug('did not see popout yet, creating');
|
||||
const originalHTMLClone = $(target).parent().parent().parent().find('.inline-drawer-content').html();
|
||||
const originalElement = $(target).parent().parent().parent().find('.inline-drawer-content');
|
||||
const template = $('#zoomed_avatar_template').html();
|
||||
const controlBarHtml = `<div class="panelControlBar flex-container">
|
||||
<div id="summaryExtensionPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
|
||||
<div id="summaryExtensionPopoutClose" class="fa-solid fa-circle-xmark hoverglow dragClose"></div>
|
||||
</div>`
|
||||
</div>`;
|
||||
const newElement = $(template);
|
||||
newElement.attr('id', 'summaryExtensionPopout')
|
||||
.removeClass('zoomed_avatar')
|
||||
.addClass('draggable')
|
||||
.empty()
|
||||
.empty();
|
||||
const prevSummaryBoxContents = $('#memory_contents').val(); //copy summary box before emptying
|
||||
originalElement.empty();
|
||||
originalElement.html(`<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>`)
|
||||
newElement.append(controlBarHtml).append(originalHTMLClone)
|
||||
originalElement.html('<div class="flex-container alignitemscenter justifyCenter wide100p"><small>Currently popped out</small></div>');
|
||||
newElement.append(controlBarHtml).append(originalHTMLClone);
|
||||
$('body').append(newElement);
|
||||
$("#summaryExtensionDrawerContents").addClass('scrollableInnerFull')
|
||||
$('#summaryExtensionDrawerContents').addClass('scrollableInnerFull');
|
||||
setMemoryContext(prevSummaryBoxContents, false); //paste prev summary box contents into popout box
|
||||
setupListeners();
|
||||
loadSettings();
|
||||
loadMovingUIState();
|
||||
|
||||
$("#summaryExtensionPopout").fadeIn(250);
|
||||
$('#summaryExtensionPopout').fadeIn(animation_duration);
|
||||
dragElement(newElement);
|
||||
|
||||
//setup listener for close button to restore extensions menu
|
||||
$('#summaryExtensionPopoutClose').off('click').on('click', function () {
|
||||
$("#summaryExtensionDrawerContents").removeClass('scrollableInnerFull')
|
||||
const summaryPopoutHTML = $("#summaryExtensionDrawerContents")
|
||||
$("#summaryExtensionPopout").fadeOut(250, () => {
|
||||
$('#summaryExtensionDrawerContents').removeClass('scrollableInnerFull');
|
||||
const summaryPopoutHTML = $('#summaryExtensionDrawerContents');
|
||||
$('#summaryExtensionPopout').fadeOut(animation_duration, () => {
|
||||
originalElement.empty();
|
||||
originalElement.html(summaryPopoutHTML);
|
||||
$("#summaryExtensionPopout").remove()
|
||||
})
|
||||
$('#summaryExtensionPopout').remove();
|
||||
});
|
||||
loadSettings();
|
||||
})
|
||||
});
|
||||
} else {
|
||||
console.debug('saw existing popout, removing')
|
||||
$("#summaryExtensionPopout").fadeOut(250, () => { $("#summaryExtensionPopoutClose").trigger('click') });
|
||||
console.debug('saw existing popout, removing');
|
||||
$('#summaryExtensionPopout').fadeOut(animation_duration, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,11 +551,6 @@ function setupListeners() {
|
||||
//setup shared listeners for popout and regular ext menu
|
||||
$('#memory_restore').off('click').on('click', onMemoryRestoreClick);
|
||||
$('#memory_contents').off('click').on('input', onMemoryContentInput);
|
||||
$('#memory_long_length').off('click').on('input', onMemoryLongInput);
|
||||
$('#memory_short_length').off('click').on('input', onMemoryShortInput);
|
||||
$('#memory_repetition_penalty').off('click').on('input', onMemoryRepetitionPenaltyInput);
|
||||
$('#memory_temperature').off('click').on('input', onMemoryTemperatureInput);
|
||||
$('#memory_length_penalty').off('click').on('input', onMemoryLengthPenaltyInput);
|
||||
$('#memory_frozen').off('click').on('input', onMemoryFrozenInput);
|
||||
$('#memory_skipWIAN').off('click').on('input', onMemorySkipWIANInput);
|
||||
$('#summary_source').off('click').on('change', onSummarySourceChange);
|
||||
@@ -629,9 +562,9 @@ function setupListeners() {
|
||||
$('#memory_depth').off('click').on('input', onMemoryDepthInput);
|
||||
$('input[name="memory_position"]').off('click').on('change', onMemoryPositionChange);
|
||||
$('#memory_prompt_words_force').off('click').on('input', onMemoryPromptWordsForceInput);
|
||||
$("#summarySettingsBlockToggle").off('click').on('click', function () {
|
||||
console.log('saw settings button click')
|
||||
$("#summarySettingsBlock").slideToggle(200, "swing"); //toggleClass("hidden");
|
||||
$('#summarySettingsBlockToggle').off('click').on('click', function () {
|
||||
console.log('saw settings button click');
|
||||
$('#summarySettingsBlock').slideToggle(200, 'swing'); //toggleClass("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -659,7 +592,7 @@ jQuery(function () {
|
||||
|
||||
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
|
||||
<div class="memory_contents_controls">
|
||||
<div id="memory_force_summarize" class="menu_button menu_button_icon">
|
||||
<div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
<span>Summarize now</span>
|
||||
</div>
|
||||
@@ -710,18 +643,6 @@ jQuery(function () {
|
||||
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
|
||||
<small>If both sliders are non-zero, then both will trigger summary updates a their respective intervals.</small>
|
||||
</div>
|
||||
<div data-source="extras">
|
||||
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_short_length" type="range" value="${defaultSettings.shortMemoryLength}" min="${defaultSettings.minShortMemory}" max="${defaultSettings.maxShortMemory}" step="${defaultSettings.shortMemoryStep}" />
|
||||
<label for="memory_long_length">Summary output length (<span id="memory_long_length_tokens"></span> tokens)</label>
|
||||
<input id="memory_long_length" type="range" value="${defaultSettings.longMemoryLength}" min="${defaultSettings.minLongMemory}" max="${defaultSettings.maxLongMemory}" step="${defaultSettings.longMemoryStep}" />
|
||||
<label for="memory_temperature">Temperature (<span id="memory_temperature_value"></span>)</label>
|
||||
<input id="memory_temperature" type="range" value="${defaultSettings.temperature}" min="${defaultSettings.minTemperature}" max="${defaultSettings.maxTemperature}" step="${defaultSettings.temperatureStep}" />
|
||||
<label for="memory_repetition_penalty">Repetition penalty (<span id="memory_repetition_penalty_value"></span>)</label>
|
||||
<input id="memory_repetition_penalty" type="range" value="${defaultSettings.repetitionPenalty}" min="${defaultSettings.minRepetitionPenalty}" max="${defaultSettings.maxRepetitionPenalty}" step="${defaultSettings.repetitionPenaltyStep}" />
|
||||
<label for="memory_length_penalty">Length preference <small>[higher = longer summaries]</small> (<span id="memory_length_penalty_value"></span>)</label>
|
||||
<input id="memory_length_penalty" type="range" value="${defaultSettings.lengthPenalty}" min="${defaultSettings.minLengthPenalty}" max="${defaultSettings.maxLengthPenalty}" step="${defaultSettings.lengthPenaltyStep}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -730,7 +651,7 @@ jQuery(function () {
|
||||
`;
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
setupListeners();
|
||||
$("#summaryExtensionPopoutButton").off('click').on('click', function (e) {
|
||||
$('#summaryExtensionPopoutButton').off('click').on('click', function (e) {
|
||||
doPopout(e);
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
@@ -23,6 +23,10 @@
|
||||
<input type="checkbox" id="quickReply_hidden" >
|
||||
<span><i class="fa-solid fa-fw fa-eye-slash"></i> Invisible (auto-execute only)</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_appStartup">
|
||||
<input type="checkbox" id="quickReply_autoExecute_appStartup" >
|
||||
<span><i class="fa-solid fa-fw fa-rocket"></i> Execute on app startup</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="quickReply_autoExecute_userMessage">
|
||||
<input type="checkbox" id="quickReply_autoExecute_userMessage" >
|
||||
<span><i class="fa-solid fa-fw fa-user"></i> Execute on user message</span>
|
||||
@@ -36,5 +40,10 @@
|
||||
<span><i class="fa-solid fa-fw fa-message"></i> Execute on opening chat</span>
|
||||
</label>
|
||||
</div>
|
||||
<h3><strong>UI Options</strong></h3>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="quickReply_ui_title">Title (tooltip, leave empty to show the message or /command)</label>
|
||||
<input type="text" class="text_pole" id="quickReply_ui_title">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types } from "../../../script.js";
|
||||
import { getContext, extension_settings } from "../../extensions.js";
|
||||
import { initScrollHeight, resetScrollHeight, getSortableDelay } from "../../utils.js";
|
||||
import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js";
|
||||
import { ContextMenu } from "./src/ContextMenu.js";
|
||||
import { MenuItem } from "./src/MenuItem.js";
|
||||
import { MenuHeader } from "./src/MenuHeader.js";
|
||||
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types, animation_duration } from '../../../script.js';
|
||||
import { getContext, extension_settings } from '../../extensions.js';
|
||||
import { getSortableDelay, escapeHtml, delay } from '../../utils.js';
|
||||
import { executeSlashCommands, registerSlashCommand } from '../../slash-commands.js';
|
||||
import { ContextMenu } from './src/ContextMenu.js';
|
||||
import { MenuItem } from './src/MenuItem.js';
|
||||
import { MenuHeader } from './src/MenuHeader.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
import { dragElement } from '../../RossAscends-mods.js';
|
||||
|
||||
export { MODULE_NAME };
|
||||
|
||||
@@ -20,12 +22,12 @@ const defaultSettings = {
|
||||
placeBeforeInputEnabled: false,
|
||||
quickActionEnabled: false,
|
||||
AutoInputInject: true,
|
||||
}
|
||||
};
|
||||
|
||||
//method from worldinfo
|
||||
async function updateQuickReplyPresetList() {
|
||||
const result = await fetch("/getsettings", {
|
||||
method: "POST",
|
||||
const result = await fetch('/api/settings/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
@@ -34,7 +36,7 @@ async function updateQuickReplyPresetList() {
|
||||
var data = await result.json();
|
||||
presets = data.quickReplyPresets?.length ? data.quickReplyPresets : [];
|
||||
console.debug('Quick Reply presets', presets);
|
||||
$("#quickReplyPresets").find('option[value!=""]').remove();
|
||||
$('#quickReplyPresets').find('option[value!=""]').remove();
|
||||
|
||||
|
||||
if (presets !== undefined) {
|
||||
@@ -43,7 +45,7 @@ async function updateQuickReplyPresetList() {
|
||||
option.value = item.name;
|
||||
option.innerText = item.name;
|
||||
option.selected = selected_preset.includes(item.name);
|
||||
$("#quickReplyPresets").append(option);
|
||||
$('#quickReplyPresets').append(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -51,7 +53,7 @@ async function updateQuickReplyPresetList() {
|
||||
|
||||
async function loadSettings(type) {
|
||||
if (type === 'init') {
|
||||
await updateQuickReplyPresetList()
|
||||
await updateQuickReplyPresetList();
|
||||
}
|
||||
if (Object.keys(extension_settings.quickReply).length === 0) {
|
||||
Object.assign(extension_settings.quickReply, defaultSettings);
|
||||
@@ -96,7 +98,6 @@ async function loadSettings(type) {
|
||||
function onQuickReplyInput(id) {
|
||||
extension_settings.quickReply.quickReplySlots[id - 1].mes = $(`#quickReply${id}Mes`).val();
|
||||
$(`#quickReply${id}`).attr('title', String($(`#quickReply${id}Mes`).val()));
|
||||
resetScrollHeight($(`#quickReply${id}Mes`));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
@@ -107,13 +108,13 @@ function onQuickReplyLabelInput(id) {
|
||||
}
|
||||
|
||||
async function onQuickReplyContextMenuChange(id) {
|
||||
extension_settings.quickReply.quickReplySlots[id - 1].contextMenu = JSON.parse($(`#quickReplyContainer > [data-order="${id}"]`).attr('data-contextMenu'))
|
||||
extension_settings.quickReply.quickReplySlots[id - 1].contextMenu = JSON.parse($(`#quickReplyContainer > [data-order="${id}"]`).attr('data-contextMenu'));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onQuickReplyCtxButtonClick(id) {
|
||||
const editorHtml = $(await $.get('scripts/extensions/quick-reply/contextMenuEditor.html'));
|
||||
const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide: false, large: false, rows: 1 });
|
||||
const popupResult = callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save', wide: false, large: false, rows: 1 });
|
||||
const qr = extension_settings.quickReply.quickReplySlots[id - 1];
|
||||
if (!qr.contextMenu) {
|
||||
qr.contextMenu = [];
|
||||
@@ -139,9 +140,9 @@ async function onQuickReplyCtxButtonClick(id) {
|
||||
dom.querySelector('.quickReply_contextMenuEditor_chaining').checked = item.chain;
|
||||
$('.quickReply_contextMenuEditor_remove', ctxItem).on('click', () => ctxItem.remove());
|
||||
document.querySelector('#quickReply_contextMenuEditor_content').append(ctxItem);
|
||||
}
|
||||
};
|
||||
[...qr.contextMenu, {}].forEach((item, idx) => {
|
||||
addCtxItem(item, idx)
|
||||
addCtxItem(item, idx);
|
||||
});
|
||||
$('#quickReply_contextMenuEditor_addPreset').on('click', () => {
|
||||
addCtxItem({}, document.querySelector('#quickReply_contextMenuEditor_content').children.length);
|
||||
@@ -155,6 +156,7 @@ async function onQuickReplyCtxButtonClick(id) {
|
||||
$('#quickReply_autoExecute_userMessage').prop('checked', qr.autoExecute_userMessage ?? false);
|
||||
$('#quickReply_autoExecute_botMessage').prop('checked', qr.autoExecute_botMessage ?? false);
|
||||
$('#quickReply_autoExecute_chatLoad').prop('checked', qr.autoExecute_chatLoad ?? false);
|
||||
$('#quickReply_autoExecute_appStartup').prop('checked', qr.autoExecute_appStartup ?? false);
|
||||
$('#quickReply_hidden').prop('checked', qr.hidden ?? false);
|
||||
|
||||
$('#quickReply_hidden').on('input', () => {
|
||||
@@ -163,6 +165,12 @@ async function onQuickReplyCtxButtonClick(id) {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#quickReply_autoExecute_appStartup').on('input', () => {
|
||||
const state = !!$('#quickReply_autoExecute_appStartup').prop('checked');
|
||||
qr.autoExecute_appStartup = state;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#quickReply_autoExecute_userMessage').on('input', () => {
|
||||
const state = !!$('#quickReply_autoExecute_userMessage').prop('checked');
|
||||
qr.autoExecute_userMessage = state;
|
||||
@@ -181,6 +189,8 @@ async function onQuickReplyCtxButtonClick(id) {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#quickReply_ui_title').val(qr.title ?? '');
|
||||
|
||||
if (await popupResult) {
|
||||
qr.contextMenu = Array.from(document.querySelectorAll('#quickReply_contextMenuEditor_content > .quickReplyContextMenuEditor_item'))
|
||||
.map(item => ({
|
||||
@@ -189,17 +199,19 @@ async function onQuickReplyCtxButtonClick(id) {
|
||||
}))
|
||||
.filter(item => item.preset);
|
||||
$(`#quickReplyContainer[data-order="${id}"]`).attr('data-contextMenu', JSON.stringify(qr.contextMenu));
|
||||
qr.title = $('#quickReply_ui_title').val();
|
||||
saveSettingsDebounced();
|
||||
updateQuickReplyPreset();
|
||||
onQuickReplyLabelInput(id);
|
||||
}
|
||||
}
|
||||
|
||||
async function onQuickReplyEnabledInput() {
|
||||
let isEnabled = $(this).prop('checked')
|
||||
let isEnabled = $(this).prop('checked');
|
||||
extension_settings.quickReply.quickReplyEnabled = !!isEnabled;
|
||||
if (isEnabled === true) {
|
||||
$("#quickReplyBar").show();
|
||||
} else { $("#quickReplyBar").hide(); }
|
||||
$('#quickReplyBar').show();
|
||||
} else { $('#quickReplyBar').hide(); }
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
@@ -229,7 +241,15 @@ async function executeQuickReplyByName(name) {
|
||||
throw new Error('Quick Reply is disabled');
|
||||
}
|
||||
|
||||
const qr = extension_settings.quickReply.quickReplySlots.find(x => x.label == name);
|
||||
let qr = extension_settings.quickReply.quickReplySlots.find(x => x.label == name);
|
||||
|
||||
if (!qr && name.includes('.')) {
|
||||
const [presetName, qrName] = name.split('.');
|
||||
const preset = presets.find(x => x.name == presetName);
|
||||
if (preset) {
|
||||
qr = preset.quickReplySlots.find(x => x.label == qrName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!qr) {
|
||||
throw new Error(`Quick Reply "${name}" not found`);
|
||||
@@ -245,7 +265,7 @@ async function performQuickReply(prompt, index) {
|
||||
console.warn(`Quick reply slot ${index} is empty! Aborting.`);
|
||||
return;
|
||||
}
|
||||
const existingText = $("#send_textarea").val();
|
||||
const existingText = $('#send_textarea').val();
|
||||
|
||||
let newText;
|
||||
|
||||
@@ -263,19 +283,19 @@ async function performQuickReply(prompt, index) {
|
||||
// the prompt starts with '/' - execute slash commands natively
|
||||
if (prompt.startsWith('/')) {
|
||||
const result = await executeSlashCommands(newText);
|
||||
return result?.pipe;
|
||||
return typeof result === 'object' ? result?.pipe : '';
|
||||
}
|
||||
|
||||
newText = substituteParams(newText);
|
||||
|
||||
$("#send_textarea").val(newText);
|
||||
$('#send_textarea').val(newText);
|
||||
|
||||
// Set the focus back to the textarea
|
||||
$("#send_textarea").trigger('focus');
|
||||
$('#send_textarea').trigger('focus');
|
||||
|
||||
// Only trigger send button if quickActionEnabled is not checked or
|
||||
if (!extension_settings.quickReply.quickActionEnabled) {
|
||||
$("#send_but").trigger('click');
|
||||
$('#send_but').trigger('click');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,9 +335,109 @@ function buildContextMenu(qr, chainMes = null, hierarchy = [], labelHierarchy =
|
||||
});
|
||||
return tree;
|
||||
}
|
||||
|
||||
async function doQuickReplyBarPopout() {
|
||||
//shared elements
|
||||
const newQuickRepliesDiv = '<div id="quickReplies"></div>';
|
||||
const popoutButtonClone = $('#quickReplyPopoutButton');
|
||||
|
||||
if ($('#quickReplyBarPopout').length === 0) {
|
||||
console.debug('did not see popout yet, creating');
|
||||
const template = $('#zoomed_avatar_template').html();
|
||||
const controlBarHtml = `<div class="panelControlBar flex-container">
|
||||
<div id="quickReplyBarPopoutheader" class="fa-solid fa-grip drag-grabber hoverglow"></div>
|
||||
<div id="quickReplyBarPopoutClose" class="fa-solid fa-circle-xmark hoverglow"></div>
|
||||
</div>`;
|
||||
const newElement = $(template);
|
||||
let quickRepliesClone = $('#quickReplies').html();
|
||||
newElement.attr('id', 'quickReplyBarPopout')
|
||||
.removeClass('zoomed_avatar')
|
||||
.addClass('draggable scrollY')
|
||||
.empty()
|
||||
.append(controlBarHtml)
|
||||
.append(newQuickRepliesDiv);
|
||||
//empty original bar
|
||||
$('#quickReplyBar').empty();
|
||||
//add clone in popout
|
||||
$('body').append(newElement);
|
||||
$('#quickReplies').append(quickRepliesClone).css('margin-top', '1em');
|
||||
$('.quickReplyButton').on('click', function () {
|
||||
let index = $(this).data('index');
|
||||
sendQuickReply(index);
|
||||
});
|
||||
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
|
||||
evt.stopPropagation();
|
||||
let index = $(this.closest('.quickReplyButton')).data('index');
|
||||
const qr = extension_settings.quickReply.quickReplySlots[index];
|
||||
if (qr.contextMenu?.length) {
|
||||
evt.preventDefault();
|
||||
const tree = buildContextMenu(qr);
|
||||
const menu = new ContextMenu(tree.children);
|
||||
menu.show(evt);
|
||||
}
|
||||
});
|
||||
$('.quickReplyButton').on('contextmenu', function (evt) {
|
||||
let index = $(this).data('index');
|
||||
const qr = extension_settings.quickReply.quickReplySlots[index];
|
||||
if (qr.contextMenu?.length) {
|
||||
evt.preventDefault();
|
||||
const tree = buildContextMenu(qr);
|
||||
const menu = new ContextMenu(tree.children);
|
||||
menu.show(evt);
|
||||
}
|
||||
});
|
||||
|
||||
loadMovingUIState();
|
||||
$('#quickReplyBarPopout').fadeIn(animation_duration);
|
||||
dragElement(newElement);
|
||||
|
||||
$('#quickReplyBarPopoutClose').off('click').on('click', function () {
|
||||
console.debug('saw existing popout, removing');
|
||||
let quickRepliesClone = $('#quickReplies').html();
|
||||
$('#quickReplyBar').append(newQuickRepliesDiv);
|
||||
$('#quickReplies').prepend(quickRepliesClone);
|
||||
$('#quickReplyBar').append(popoutButtonClone).fadeIn(animation_duration);
|
||||
$('#quickReplyBarPopout').fadeOut(animation_duration, () => { $('#quickReplyBarPopout').remove(); });
|
||||
$('.quickReplyButton').on('click', function () {
|
||||
let index = $(this).data('index');
|
||||
sendQuickReply(index);
|
||||
});
|
||||
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
|
||||
evt.stopPropagation();
|
||||
let index = $(this.closest('.quickReplyButton')).data('index');
|
||||
const qr = extension_settings.quickReply.quickReplySlots[index];
|
||||
if (qr.contextMenu?.length) {
|
||||
evt.preventDefault();
|
||||
const tree = buildContextMenu(qr);
|
||||
const menu = new ContextMenu(tree.children);
|
||||
menu.show(evt);
|
||||
}
|
||||
});
|
||||
$('.quickReplyButton').on('contextmenu', function (evt) {
|
||||
let index = $(this).data('index');
|
||||
const qr = extension_settings.quickReply.quickReplySlots[index];
|
||||
if (qr.contextMenu?.length) {
|
||||
evt.preventDefault();
|
||||
const tree = buildContextMenu(qr);
|
||||
const menu = new ContextMenu(tree.children);
|
||||
menu.show(evt);
|
||||
}
|
||||
});
|
||||
$('#quickReplyPopoutButton').off('click').on('click', doQuickReplyBarPopout);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function addQuickReplyBar() {
|
||||
$('#quickReplyBar').remove();
|
||||
let quickReplyButtonHtml = '';
|
||||
var targetContainer;
|
||||
if ($('#quickReplyBarPopout').length !== 0) {
|
||||
targetContainer = 'popout';
|
||||
} else {
|
||||
targetContainer = 'bar';
|
||||
$('#quickReplyBar').remove();
|
||||
}
|
||||
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
@@ -328,7 +448,7 @@ function addQuickReplyBar() {
|
||||
if (extension_settings.quickReply.quickReplySlots[i]?.contextMenu?.length) {
|
||||
expander = '<span class="ctx-expander" title="Open context menu">⋮</span>';
|
||||
}
|
||||
quickReplyButtonHtml += `<div title="${quickReplyMes}" class="quickReplyButton ${hidden ? 'displayNone' : ''}" data-index="${i}" id="quickReply${i + 1}">${quickReplyLabel}${expander}</div>`;
|
||||
quickReplyButtonHtml += `<div title="${escapeHtml(qr.title || quickReplyMes)}" class="quickReplyButton ${hidden ? 'displayNone' : ''}" data-index="${i}" id="quickReply${i + 1}">${DOMPurify.sanitize(quickReplyLabel)}${expander}</div>`;
|
||||
}
|
||||
|
||||
const quickReplyBarFullHtml = `
|
||||
@@ -336,15 +456,22 @@ function addQuickReplyBar() {
|
||||
<div id="quickReplies">
|
||||
${quickReplyButtonHtml}
|
||||
</div>
|
||||
<div id="quickReplyPopoutButton" class="fa-solid fa-window-restore menu_button"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#send_form').prepend(quickReplyBarFullHtml);
|
||||
if (targetContainer === 'bar') {
|
||||
$('#send_form').prepend(quickReplyBarFullHtml);
|
||||
} else {
|
||||
$('#quickReplies').empty().append(quickReplyButtonHtml);
|
||||
}
|
||||
|
||||
|
||||
$('.quickReplyButton').on('click', function () {
|
||||
let index = $(this).data('index');
|
||||
sendQuickReply(index);
|
||||
});
|
||||
$('#quickReplyPopoutButton').off('click').on('click', doQuickReplyBarPopout);
|
||||
$('.quickReplyButton > .ctx-expander').on('click', function (evt) {
|
||||
evt.stopPropagation();
|
||||
let index = $(this.closest('.quickReplyButton')).data('index');
|
||||
@@ -355,7 +482,7 @@ function addQuickReplyBar() {
|
||||
const menu = new ContextMenu(tree.children);
|
||||
menu.show(evt);
|
||||
}
|
||||
})
|
||||
});
|
||||
$('.quickReplyButton').on('contextmenu', function (evt) {
|
||||
let index = $(this).data('index');
|
||||
const qr = extension_settings.quickReply.quickReplySlots[index];
|
||||
@@ -391,12 +518,12 @@ async function saveQuickReplyPreset() {
|
||||
numberOfSlots: extension_settings.quickReply.numberOfSlots,
|
||||
AutoInputInject: extension_settings.quickReply.AutoInputInject,
|
||||
selectedPreset: name,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset)
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -416,13 +543,13 @@ async function saveQuickReplyPreset() {
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
} else {
|
||||
toastr.warning('Failed to save Quick Reply Preset.')
|
||||
toastr.warning('Failed to save Quick Reply Preset.');
|
||||
}
|
||||
}
|
||||
|
||||
//just a copy of save function with the name hardcoded to currently selected preset
|
||||
async function updateQuickReplyPreset() {
|
||||
const name = $("#quickReplyPresets").val()
|
||||
const name = $('#quickReplyPresets').val();
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
@@ -435,12 +562,12 @@ async function updateQuickReplyPreset() {
|
||||
numberOfSlots: extension_settings.quickReply.numberOfSlots,
|
||||
AutoInputInject: extension_settings.quickReply.AutoInputInject,
|
||||
selectedPreset: name,
|
||||
}
|
||||
};
|
||||
|
||||
const response = await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset)
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -460,7 +587,7 @@ async function updateQuickReplyPreset() {
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
} else {
|
||||
toastr.warning('Failed to save Quick Reply Preset.')
|
||||
toastr.warning('Failed to save Quick Reply Preset.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,13 +634,13 @@ function generateQuickReplyElements() {
|
||||
let quickReplyHtml = '';
|
||||
|
||||
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
|
||||
let itemNumber = i + 1
|
||||
quickReplyHtml += `
|
||||
<div class="flex-container alignitemscenter" data-order="${i}">
|
||||
<span class="drag-handle ui-sortable-handle">☰</span>
|
||||
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
|
||||
<span class="menu_button menu_button_icon" id="quickReply${i}CtxButton" title="Additional options: context menu, auto-execution">⋮</span>
|
||||
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1 autoSetHeight" rows="2"></textarea>
|
||||
<span class="menu_button menu_button_icon editor_maximize fa-solid fa-maximize" data-tab="true" data-for="quickReply${i}Mes" id="quickReply${i}ExpandButton" title="Expand the editor"></span>
|
||||
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -521,31 +648,25 @@ function generateQuickReplyElements() {
|
||||
$('#quickReplyContainer').empty().append(quickReplyHtml);
|
||||
|
||||
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
|
||||
$(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(i); });
|
||||
$(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(i); });
|
||||
$(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(i); });
|
||||
$(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(this.closest('[data-order]').getAttribute('data-order')); });
|
||||
$(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(this.closest('[data-order]').getAttribute('data-order')); });
|
||||
$(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(this.closest('[data-order]').getAttribute('data-order')); });
|
||||
$(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i - 1]?.contextMenu ?? []));
|
||||
}
|
||||
|
||||
$('.quickReplySettings .inline-drawer-toggle').off('click').on('click', function () {
|
||||
for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) {
|
||||
initScrollHeight($(`#quickReply${i}Mes`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function applyQuickReplyPreset(name) {
|
||||
const quickReplyPreset = presets.find(x => x.name == name);
|
||||
|
||||
if (!quickReplyPreset) {
|
||||
toastr.warning(`error, QR preset '${name}' not found. Confirm you are using proper case sensitivity!`)
|
||||
toastr.warning(`error, QR preset '${name}' not found. Confirm you are using proper case sensitivity!`);
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.quickReply = quickReplyPreset;
|
||||
extension_settings.quickReply.selectedPreset = name;
|
||||
saveSettingsDebounced()
|
||||
loadSettings('init')
|
||||
saveSettingsDebounced();
|
||||
loadSettings('init');
|
||||
addQuickReplyBar();
|
||||
moduleWorker();
|
||||
|
||||
@@ -554,45 +675,268 @@ async function applyQuickReplyPreset(name) {
|
||||
}
|
||||
|
||||
async function doQRPresetSwitch(_, text) {
|
||||
text = String(text)
|
||||
applyQuickReplyPreset(text)
|
||||
text = String(text);
|
||||
applyQuickReplyPreset(text);
|
||||
}
|
||||
|
||||
async function doQR(_, text) {
|
||||
if (!text) {
|
||||
toastr.warning('must specify which QR # to use')
|
||||
return
|
||||
toastr.warning('must specify which QR # to use');
|
||||
return;
|
||||
}
|
||||
|
||||
text = Number(text)
|
||||
text = Number(text);
|
||||
//use scale starting with 0
|
||||
//ex: user inputs "/qr 2" >> qr with data-index 1 (but 2nd item displayed) gets triggered
|
||||
let QRnum = Number(text - 1)
|
||||
if (QRnum <= 0) { QRnum = 0 }
|
||||
const whichQR = $("#quickReplies").find(`[data-index='${QRnum}']`);
|
||||
whichQR.trigger('click')
|
||||
let QRnum = Number(text - 1);
|
||||
if (QRnum <= 0) { QRnum = 0; }
|
||||
const whichQR = $('#quickReplies').find(`[data-index='${QRnum}']`);
|
||||
whichQR.trigger('click');
|
||||
}
|
||||
|
||||
function saveQROrder() {
|
||||
//update html-level order data to match new sort
|
||||
let i = 1
|
||||
let i = 1;
|
||||
$('#quickReplyContainer').children().each(function () {
|
||||
$(this).attr('data-order', i)
|
||||
$(this).find('input').attr('id', `quickReply${i}Label`)
|
||||
$(this).find('textarea').attr('id', `quickReply${i}Mes`)
|
||||
i++
|
||||
const oldOrder = $(this).attr('data-order');
|
||||
$(this).attr('data-order', i);
|
||||
$(this).find('input').attr('id', `quickReply${i}Label`);
|
||||
$(this).find('textarea').attr('id', `quickReply${i}Mes`);
|
||||
$(this).find(`#quickReply${oldOrder}CtxButton`).attr('id', `quickReply${i}CtxButton`);
|
||||
$(this).find(`#quickReply${oldOrder}ExpandButton`).attr({ 'data-for': `quickReply${i}Mes`, 'id': `quickReply${i}ExpandButton` });
|
||||
i++;
|
||||
});
|
||||
|
||||
//rebuild the extension_Settings array based on new order
|
||||
i = 1
|
||||
i = 1;
|
||||
$('#quickReplyContainer').children().each(function () {
|
||||
onQuickReplyContextMenuChange(i)
|
||||
onQuickReplyLabelInput(i)
|
||||
onQuickReplyInput(i)
|
||||
i++
|
||||
onQuickReplyContextMenuChange(i);
|
||||
onQuickReplyLabelInput(i);
|
||||
onQuickReplyInput(i);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
async function qrCreateCallback(args, mes) {
|
||||
const qr = {
|
||||
label: args.label ?? '',
|
||||
mes: (mes ?? '')
|
||||
.replace(/\\\|/g, '|')
|
||||
.replace(/\\\{/g, '{')
|
||||
.replace(/\\\}/g, '}')
|
||||
,
|
||||
title: args.title ?? '',
|
||||
autoExecute_chatLoad: JSON.parse(args.load ?? false),
|
||||
autoExecute_userMessage: JSON.parse(args.user ?? false),
|
||||
autoExecute_botMessage: JSON.parse(args.bot ?? false),
|
||||
autoExecute_appStartup: JSON.parse(args.startup ?? false),
|
||||
hidden: JSON.parse(args.hidden ?? false),
|
||||
};
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
preset.quickReplySlots.push(qr);
|
||||
preset.numberOfSlots++;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrUpdateCallback(args, mes) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
const qr = {
|
||||
label: args.newlabel ?? oqr.label ?? '',
|
||||
mes: (mes ?? oqr.mes)
|
||||
.replace('\\|', '|')
|
||||
.replace('\\{', '{')
|
||||
.replace('\\}', '}')
|
||||
,
|
||||
title: args.title ?? oqr.title ?? '',
|
||||
autoExecute_chatLoad: JSON.parse(args.load ?? oqr.autoExecute_chatLoad ?? false),
|
||||
autoExecute_userMessage: JSON.parse(args.user ?? oqr.autoExecute_userMessage ?? false),
|
||||
autoExecute_botMessage: JSON.parse(args.bot ?? oqr.autoExecute_botMessage ?? false),
|
||||
autoExecute_appStartup: JSON.parse(args.startup ?? oqr.autoExecute_appStartup ?? false),
|
||||
hidden: JSON.parse(args.hidden ?? oqr.hidden ?? false),
|
||||
};
|
||||
preset.quickReplySlots[idx] = qr;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrDeleteCallback(args, label) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
|
||||
if (idx === -1) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR with label '${label}' not found`);
|
||||
return '';
|
||||
}
|
||||
preset.quickReplySlots.splice(idx, 1);
|
||||
preset.numberOfSlots--;
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function qrContextAddCallback(args, presetName) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
if (!oqr.contextMenu) {
|
||||
oqr.contextMenu = [];
|
||||
}
|
||||
let item = oqr.contextMenu.find(it => it.preset == presetName);
|
||||
if (item) {
|
||||
item.chain = JSON.parse(args.chain ?? 'null') ?? item.chain ?? false;
|
||||
} else {
|
||||
oqr.contextMenu.push({ preset: presetName, chain: JSON.parse(args.chain ?? 'false') });
|
||||
}
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrContextDeleteCallback(args, presetName) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
if (!oqr.contextMenu) return;
|
||||
const ctxIdx = oqr.contextMenu.findIndex(it => it.preset == presetName);
|
||||
if (ctxIdx > -1) {
|
||||
oqr.contextMenu.splice(ctxIdx, 1);
|
||||
}
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
async function qrContextClearCallback(args, label) {
|
||||
const setName = args.set ?? selected_preset;
|
||||
const preset = presets.find(x => x.name == setName);
|
||||
|
||||
if (!preset) {
|
||||
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
|
||||
const oqr = preset.quickReplySlots[idx];
|
||||
oqr.contextMenu = [];
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(preset),
|
||||
});
|
||||
saveSettingsDebounced();
|
||||
await delay(400);
|
||||
applyQuickReplyPreset(selected_preset);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function qrPresetAddCallback(args, name) {
|
||||
const quickReplyPreset = {
|
||||
name: name,
|
||||
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? true,
|
||||
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? false,
|
||||
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? false,
|
||||
quickReplySlots: [],
|
||||
numberOfSlots: Number(args.slots ?? '0'),
|
||||
AutoInputInject: JSON.parse(args.inject ?? 'false'),
|
||||
};
|
||||
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
await updateQuickReplyPresetList();
|
||||
}
|
||||
|
||||
async function qrPresetUpdateCallback(args, name) {
|
||||
const preset = presets.find(it => it.name == name);
|
||||
const quickReplyPreset = {
|
||||
name: preset.name,
|
||||
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? preset.quickReplyEnabled,
|
||||
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? preset.quickActionEnabled,
|
||||
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? preset.placeBeforeInputEnabled,
|
||||
quickReplySlots: preset.quickReplySlots,
|
||||
numberOfSlots: Number(args.slots ?? preset.numberOfSlots),
|
||||
AutoInputInject: JSON.parse(args.inject ?? 'null') ?? preset.AutoInputInject,
|
||||
};
|
||||
Object.assign(preset, quickReplyPreset);
|
||||
|
||||
await fetch('/savequickreply', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(quickReplyPreset),
|
||||
});
|
||||
}
|
||||
|
||||
let onMessageSentExecuting = false;
|
||||
let onMessageReceivedExecuting = false;
|
||||
let onChatChangedExecuting = false;
|
||||
|
||||
/**
|
||||
* Executes quick replies on message received.
|
||||
* @param {number} index New message index
|
||||
@@ -601,14 +945,21 @@ function saveQROrder() {
|
||||
async function onMessageReceived(index) {
|
||||
if (!extension_settings.quickReply.quickReplyEnabled) return;
|
||||
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_botMessage) {
|
||||
const message = getContext().chat[index];
|
||||
if (message?.mes && message?.mes !== '...') {
|
||||
await sendQuickReply(i);
|
||||
if (onMessageReceivedExecuting) return;
|
||||
|
||||
try {
|
||||
onMessageReceivedExecuting = true;
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_botMessage) {
|
||||
const message = getContext().chat[index];
|
||||
if (message?.mes && message?.mes !== '...') {
|
||||
await sendQuickReply(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
onMessageReceivedExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,14 +971,21 @@ async function onMessageReceived(index) {
|
||||
async function onMessageSent(index) {
|
||||
if (!extension_settings.quickReply.quickReplyEnabled) return;
|
||||
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_userMessage) {
|
||||
const message = getContext().chat[index];
|
||||
if (message?.mes && message?.mes !== '...') {
|
||||
await sendQuickReply(i);
|
||||
if (onMessageSentExecuting) return;
|
||||
|
||||
try {
|
||||
onMessageSentExecuting = true;
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_userMessage) {
|
||||
const message = getContext().chat[index];
|
||||
if (message?.mes && message?.mes !== '...') {
|
||||
await sendQuickReply(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
onMessageSentExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -639,9 +997,31 @@ async function onMessageSent(index) {
|
||||
async function onChatChanged(chatId) {
|
||||
if (!extension_settings.quickReply.quickReplyEnabled) return;
|
||||
|
||||
if (onChatChangedExecuting) return;
|
||||
|
||||
try {
|
||||
onChatChangedExecuting = true;
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_chatLoad && chatId) {
|
||||
await sendQuickReply(i);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
onChatChangedExecuting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes quick replies on app ready.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function onAppReady() {
|
||||
if (!extension_settings.quickReply.quickReplyEnabled) return;
|
||||
|
||||
for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) {
|
||||
const qr = extension_settings.quickReply.quickReplySlots[i];
|
||||
if (qr?.autoExecute_chatLoad && chatId) {
|
||||
if (qr?.autoExecute_appStartup) {
|
||||
await sendQuickReply(i);
|
||||
}
|
||||
}
|
||||
@@ -710,15 +1090,15 @@ jQuery(async () => {
|
||||
$('#AutoInputInject').on('input', onAutoInputInject);
|
||||
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
|
||||
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
|
||||
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
|
||||
$("#quickReplyPresetUpdateButton").on('click', updateQuickReplyPreset);
|
||||
$('#quickReplyPresetSaveButton').on('click', saveQuickReplyPreset);
|
||||
$('#quickReplyPresetUpdateButton').on('click', updateQuickReplyPreset);
|
||||
|
||||
$('#quickReplyContainer').sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: saveQROrder,
|
||||
});
|
||||
|
||||
$("#quickReplyPresets").on('change', async function () {
|
||||
$('#quickReplyPresets').on('change', async function () {
|
||||
const quickReplyPresetSelected = $(this).find(':selected').val();
|
||||
extension_settings.quickReplyPreset = quickReplyPresetSelected;
|
||||
applyQuickReplyPreset(quickReplyPresetSelected);
|
||||
@@ -728,12 +1108,42 @@ jQuery(async () => {
|
||||
await loadSettings('init');
|
||||
addQuickReplyBar();
|
||||
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived);
|
||||
eventSource.on(event_types.MESSAGE_SENT, onMessageSent);
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, onMessageReceived);
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, onMessageSent);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
eventSource.on(event_types.APP_READY, onAppReady);
|
||||
});
|
||||
|
||||
jQuery(() => {
|
||||
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> – activates the specified Quick Reply', true, true);
|
||||
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> – swaps to the specified Quick Reply Preset', true, true);
|
||||
})
|
||||
const qrArgs = `
|
||||
label - string - text on the button, e.g., label=MyButton
|
||||
set - string - name of the QR set, e.g., set=PresetName1
|
||||
hidden - bool - whether the button should be hidden, e.g., hidden=true
|
||||
startup - bool - auto execute on app startup, e.g., startup=true
|
||||
user - bool - auto execute on user message, e.g., user=true
|
||||
bot - bool - auto execute on AI message, e.g., bot=true
|
||||
load - bool - auto execute on chat load, e.g., load=true
|
||||
title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button"
|
||||
`.trim();
|
||||
const qrUpdateArgs = `
|
||||
newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton
|
||||
${qrArgs}
|
||||
`.trim();
|
||||
registerSlashCommand('qr-create', qrCreateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrArgs}</span> – creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-update', qrUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrUpdateArgs}</span> – updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
|
||||
registerSlashCommand('qr-delete', qrDeleteCallback, [], '<span class="monospace">(set=string [label])</span> – deletes Quick Reply', true, true);
|
||||
registerSlashCommand('qr-contextadd', qrContextAddCallback, [], '<span class="monospace">(set=string label=string chain=bool [preset name])</span> – add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextdel', qrContextDeleteCallback, [], '<span class="monospace">(set=string label=string [preset name])</span> – remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
|
||||
registerSlashCommand('qr-contextclear', qrContextClearCallback, [], '<span class="monospace">(set=string [label])</span> – remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
|
||||
const presetArgs = `
|
||||
enabled - bool - enable or disable the preset
|
||||
nosend - bool - disable send / insert in user input (invalid for slash commands)
|
||||
before - bool - place QR before user input
|
||||
slots - int - number of slots
|
||||
inject - bool - inject user input automatically (if disabled use {{input}})
|
||||
`.trim();
|
||||
registerSlashCommand('qr-presetadd', qrPresetAddCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> – create a new preset (overrides existing ones), example: <tt>/qr-presetadd slots=3 MyNewPreset</tt>`, true, true);
|
||||
registerSlashCommand('qr-presetupdate', qrPresetUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> – update an existing preset, example: <tt>/qr-presetupdate enabled=false MyPreset</tt>`, true, true);
|
||||
});
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { MenuItem } from "./MenuItem.js";
|
||||
/**
|
||||
* @typedef {import('./MenuItem.js').MenuItem} MenuItem
|
||||
*/
|
||||
|
||||
export class ContextMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { MenuItem } from "./MenuItem.js";
|
||||
import { MenuItem } from './MenuItem.js';
|
||||
|
||||
export class MenuHeader extends MenuItem {
|
||||
constructor(/**@type {String}*/label) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { SubMenu } from "./SubMenu.js";
|
||||
import { SubMenu } from './SubMenu.js';
|
||||
|
||||
export class MenuItem {
|
||||
/**@type {String}*/ label;
|
||||
|
@@ -1,4 +1,6 @@
|
||||
import { MenuItem } from "./MenuItem.js";
|
||||
/**
|
||||
* @typedef {import('./MenuItem.js').MenuItem} MenuItem
|
||||
*/
|
||||
|
||||
export class SubMenu {
|
||||
/**@type {MenuItem[]}*/ itemList = [];
|
||||
|
@@ -15,6 +15,7 @@
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
order: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#quickReplies {
|
||||
@@ -27,6 +28,12 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#quickReplyPopoutButton {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
#quickReplies div {
|
||||
color: var(--SmartThemeBodyColor);
|
||||
background-color: var(--black50a);
|
||||
@@ -34,7 +41,7 @@
|
||||
border-radius: 10px;
|
||||
padding: 3px 5px;
|
||||
margin: 3px 0;
|
||||
width: min-content;
|
||||
/* width: min-content; */
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
display: flex;
|
||||
@@ -62,7 +69,7 @@
|
||||
}
|
||||
|
||||
.ctx-menu {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
|
@@ -1,25 +1,38 @@
|
||||
import { substituteParams } from "../../../script.js";
|
||||
import { extension_settings } from "../../extensions.js";
|
||||
import { substituteParams } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
runRegexScript
|
||||
}
|
||||
runRegexScript,
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number} Where the regex script should be applied
|
||||
*/
|
||||
const regex_placement = {
|
||||
// MD Display is deprecated. Do not use.
|
||||
/**
|
||||
* @deprecated MD Display is deprecated. Do not use.
|
||||
*/
|
||||
MD_DISPLAY: 0,
|
||||
USER_INPUT: 1,
|
||||
AI_OUTPUT: 2,
|
||||
SLASH_COMMAND: 3
|
||||
}
|
||||
SLASH_COMMAND: 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum {number} How the regex script should replace the matched string
|
||||
*/
|
||||
const regex_replace_strategy = {
|
||||
REPLACE: 0,
|
||||
OVERLAY: 1
|
||||
}
|
||||
OVERLAY: 1,
|
||||
};
|
||||
|
||||
// Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
@@ -37,10 +50,23 @@ function regexFromString(input) {
|
||||
}
|
||||
}
|
||||
|
||||
// Parent function to fetch a regexed version of a raw string
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
* @param {regex_placement} placement The placement of the string
|
||||
* @param {RegexParams} params The parameters to use for the regex script
|
||||
* @returns {string} The regexed string
|
||||
* @typedef {{characterOverride?: string, isMarkdown?: boolean, isPrompt?: boolean }} RegexParams The parameters to use for the regex script
|
||||
*/
|
||||
function getRegexedString(rawString, placement, { characterOverride, isMarkdown, isPrompt } = {}) {
|
||||
// WTF have you passed me?
|
||||
if (typeof rawString !== 'string') {
|
||||
console.warn('getRegexedString: rawString is not a string. Returning empty string.');
|
||||
return '';
|
||||
}
|
||||
|
||||
let finalString = rawString;
|
||||
if (extension_settings.disabledExtensions.includes("regex") || !rawString || placement === undefined) {
|
||||
if (extension_settings.disabledExtensions.includes('regex') || !rawString || placement === undefined) {
|
||||
return finalString;
|
||||
}
|
||||
|
||||
@@ -62,14 +88,20 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
|
||||
return finalString;
|
||||
}
|
||||
|
||||
// Runs the provided regex script on the given string
|
||||
/**
|
||||
* Runs the provided regex script on the given string
|
||||
* @param {object} regexScript The regex script to run
|
||||
* @param {string} rawString The string to run the regex script on
|
||||
* @param {RegexScriptParams} params The parameters to use for the regex script
|
||||
* @returns {string} The new string
|
||||
* @typedef {{characterOverride?: string}} RegexScriptParams The parameters to use for the regex script
|
||||
*/
|
||||
function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
let newString = rawString;
|
||||
if (!regexScript || !!(regexScript.disabled) || !regexScript?.findRegex || !rawString) {
|
||||
return newString;
|
||||
}
|
||||
|
||||
let match;
|
||||
const findRegex = regexFromString(regexScript.substituteRegex ? substituteParams(regexScript.findRegex) : regexScript.findRegex);
|
||||
|
||||
// The user skill issued. Return with nothing.
|
||||
@@ -77,57 +109,49 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
return newString;
|
||||
}
|
||||
|
||||
while ((match = findRegex.exec(rawString)) !== null) {
|
||||
const fencedMatch = match[0];
|
||||
const capturedMatch = match[1];
|
||||
newString = rawString.replace(findRegex, (fencedMatch) => {
|
||||
let trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride });
|
||||
|
||||
let trimCapturedMatch;
|
||||
let trimFencedMatch;
|
||||
if (capturedMatch) {
|
||||
const tempTrimCapture = filterString(capturedMatch, regexScript.trimStrings, { characterOverride });
|
||||
trimFencedMatch = fencedMatch.replaceAll(capturedMatch, tempTrimCapture);
|
||||
trimCapturedMatch = tempTrimCapture;
|
||||
} else {
|
||||
trimFencedMatch = filterString(fencedMatch, regexScript.trimStrings, { characterOverride });
|
||||
}
|
||||
|
||||
// TODO: Use substrings for replacement. But not necessary at this time.
|
||||
// A substring is from match.index to match.index + match[0].length or fencedMatch.length
|
||||
const subReplaceString = substituteRegexParams(
|
||||
regexScript.replaceString,
|
||||
trimCapturedMatch ?? trimFencedMatch,
|
||||
trimFencedMatch,
|
||||
{
|
||||
characterOverride,
|
||||
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE
|
||||
}
|
||||
replaceStrategy: regexScript.replaceStrategy ?? regex_replace_strategy.REPLACE,
|
||||
},
|
||||
);
|
||||
if (!newString) {
|
||||
newString = rawString.replace(fencedMatch, subReplaceString);
|
||||
} else {
|
||||
newString = newString.replace(fencedMatch, subReplaceString);
|
||||
}
|
||||
|
||||
// If the regex isn't global, break out of the loop
|
||||
if (!findRegex.flags.includes('g')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return subReplaceString;
|
||||
});
|
||||
|
||||
return newString;
|
||||
}
|
||||
|
||||
// Filters anything to trim from the regex match
|
||||
/**
|
||||
* Filters anything to trim from the regex match
|
||||
* @param {string} rawString The raw string to filter
|
||||
* @param {string[]} trimStrings The strings to trim
|
||||
* @param {RegexScriptParams} params The parameters to use for the regex filter
|
||||
* @returns {string} The filtered string
|
||||
*/
|
||||
function filterString(rawString, trimStrings, { characterOverride } = {}) {
|
||||
let finalString = rawString;
|
||||
trimStrings.forEach((trimString) => {
|
||||
const subTrimString = substituteParams(trimString, undefined, characterOverride);
|
||||
finalString = finalString.replaceAll(subTrimString, "");
|
||||
finalString = finalString.replaceAll(subTrimString, '');
|
||||
});
|
||||
|
||||
return finalString;
|
||||
}
|
||||
|
||||
// Substitutes regex-specific and normal parameters
|
||||
/**
|
||||
* Substitutes regex-specific and normal parameters
|
||||
* @param {string} rawString
|
||||
* @param {string} regexMatch
|
||||
* @param {RegexSubstituteParams} params The parameters to use for the regex substitution
|
||||
* @returns {string} The substituted string
|
||||
* @typedef {{characterOverride?: string, replaceStrategy?: number}} RegexSubstituteParams The parameters to use for the regex substitution
|
||||
*/
|
||||
function substituteRegexParams(rawString, regexMatch, { characterOverride, replaceStrategy } = {}) {
|
||||
let finalString = rawString;
|
||||
finalString = substituteParams(finalString, undefined, characterOverride);
|
||||
@@ -135,7 +159,7 @@ function substituteRegexParams(rawString, regexMatch, { characterOverride, repla
|
||||
let overlaidMatch = regexMatch;
|
||||
// TODO: Maybe move the for loops into a separate function?
|
||||
if (replaceStrategy === regex_replace_strategy.OVERLAY) {
|
||||
const splitReplace = finalString.split("{{match}}");
|
||||
const splitReplace = finalString.split('{{match}}');
|
||||
|
||||
// There's a prefix
|
||||
if (splitReplace[0]) {
|
||||
@@ -177,13 +201,18 @@ function substituteRegexParams(rawString, regexMatch, { characterOverride, repla
|
||||
}
|
||||
|
||||
// Only one match is replaced. This is by design
|
||||
finalString = finalString.replace("{{match}}", overlaidMatch) || finalString.replace("{{match}}", regexMatch);
|
||||
finalString = finalString.replace('{{match}}', overlaidMatch) || finalString.replace('{{match}}', regexMatch);
|
||||
|
||||
return finalString;
|
||||
}
|
||||
|
||||
// Splices common sentence symbols and whitespace from the beginning and end of a string
|
||||
// Using a for loop due to sequential ordering
|
||||
/**
|
||||
* Splices common sentence symbols and whitespace from the beginning and end of a string.
|
||||
* Using a for loop due to sequential ordering.
|
||||
* @param {string} rawString The raw string to splice
|
||||
* @param {boolean} isSuffix String is a suffix
|
||||
* @returns {string} The spliced string
|
||||
*/
|
||||
function spliceSymbols(rawString, isSuffix) {
|
||||
let offset = 0;
|
||||
|
||||
|
@@ -1,14 +1,16 @@
|
||||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from "../../../script.js";
|
||||
import { extension_settings } from "../../extensions.js";
|
||||
import { getSortableDelay, uuidv4 } from "../../utils.js";
|
||||
import { regex_placement } from "./engine.js";
|
||||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { getSortableDelay, uuidv4 } from '../../utils.js';
|
||||
import { resolveVariable } from '../../variables.js';
|
||||
import { regex_placement, runRegexScript } from './engine.js';
|
||||
|
||||
async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
// If not editing
|
||||
|
||||
// Is the script name undefined or empty?
|
||||
if (!regexScript.scriptName) {
|
||||
toastr.error(`Could not save regex script: The script name was undefined or empty!`);
|
||||
toastr.error('Could not save regex script: The script name was undefined or empty!');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,12 +32,12 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
|
||||
|
||||
// Is a find regex present?
|
||||
if (regexScript.findRegex.length === 0) {
|
||||
toastr.warning(`This regex script will not work, but was saved anyway: A find regex isn't present.`);
|
||||
toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
|
||||
}
|
||||
|
||||
// Is there someplace to place results?
|
||||
if (regexScript.placement.length === 0) {
|
||||
toastr.warning(`This regex script will not work, but was saved anyway: One "Affects" checkbox must be selected!`);
|
||||
toastr.warning('This regex script will not work, but was saved anyway: One "Affects" checkbox must be selected!');
|
||||
}
|
||||
|
||||
if (existingScriptIndex !== -1) {
|
||||
@@ -67,45 +69,45 @@ async function deleteRegexScript({ existingId }) {
|
||||
}
|
||||
|
||||
async function loadRegexScripts() {
|
||||
$("#saved_regex_scripts").empty();
|
||||
$('#saved_regex_scripts').empty();
|
||||
|
||||
const scriptTemplate = $(await $.get("scripts/extensions/regex/scriptTemplate.html"));
|
||||
const scriptTemplate = $(await $.get('scripts/extensions/regex/scriptTemplate.html'));
|
||||
|
||||
extension_settings.regex.forEach((script) => {
|
||||
// Have to clone here
|
||||
const scriptHtml = scriptTemplate.clone();
|
||||
scriptHtml.attr('id', uuidv4());
|
||||
scriptHtml.find('.regex_script_name').text(script.scriptName);
|
||||
scriptHtml.find('.disable_regex').prop("checked", script.disabled ?? false)
|
||||
scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
|
||||
.on('input', function () {
|
||||
script.disabled = !!$(this).prop("checked");
|
||||
script.disabled = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
scriptHtml.find('.regex-toggle-on').on('click', function () {
|
||||
scriptHtml.find('.disable_regex').prop("checked", true).trigger('input');
|
||||
scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
|
||||
});
|
||||
scriptHtml.find('.regex-toggle-off').on('click', function () {
|
||||
scriptHtml.find('.disable_regex').prop("checked", false).trigger('input');
|
||||
scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
|
||||
});
|
||||
scriptHtml.find('.edit_existing_regex').on('click', async function () {
|
||||
await onRegexEditorOpenClick(scriptHtml.attr("id"));
|
||||
await onRegexEditorOpenClick(scriptHtml.attr('id'));
|
||||
});
|
||||
scriptHtml.find('.delete_regex').on('click', async function () {
|
||||
const confirm = await callPopup("Are you sure you want to delete this regex script?", "confirm");
|
||||
const confirm = await callPopup('Are you sure you want to delete this regex script?', 'confirm');
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteRegexScript({ existingId: scriptHtml.attr("id") });
|
||||
await deleteRegexScript({ existingId: scriptHtml.attr('id') });
|
||||
});
|
||||
|
||||
$("#saved_regex_scripts").append(scriptHtml);
|
||||
$('#saved_regex_scripts').append(scriptHtml);
|
||||
});
|
||||
}
|
||||
|
||||
async function onRegexEditorOpenClick(existingId) {
|
||||
const editorHtml = $(await $.get("scripts/extensions/regex/editor.html"));
|
||||
const editorHtml = $(await $.get('scripts/extensions/regex/editor.html'));
|
||||
|
||||
// If an ID exists, fill in all the values
|
||||
let existingScriptIndex = -1;
|
||||
@@ -115,93 +117,93 @@ async function onRegexEditorOpenClick(existingId) {
|
||||
if (existingScriptIndex !== -1) {
|
||||
const existingScript = extension_settings.regex[existingScriptIndex];
|
||||
if (existingScript.scriptName) {
|
||||
editorHtml.find(`.regex_script_name`).val(existingScript.scriptName);
|
||||
editorHtml.find('.regex_script_name').val(existingScript.scriptName);
|
||||
} else {
|
||||
toastr.error("This script doesn't have a name! Please delete it.")
|
||||
toastr.error('This script doesn\'t have a name! Please delete it.');
|
||||
return;
|
||||
}
|
||||
|
||||
editorHtml.find(`.find_regex`).val(existingScript.findRegex || "");
|
||||
editorHtml.find(`.regex_replace_string`).val(existingScript.replaceString || "");
|
||||
editorHtml.find(`.regex_trim_strings`).val(existingScript.trimStrings?.join("\n") || []);
|
||||
editorHtml.find('.find_regex').val(existingScript.findRegex || '');
|
||||
editorHtml.find('.regex_replace_string').val(existingScript.replaceString || '');
|
||||
editorHtml.find('.regex_trim_strings').val(existingScript.trimStrings?.join('\n') || []);
|
||||
editorHtml
|
||||
.find(`input[name="disabled"]`)
|
||||
.prop("checked", existingScript.disabled ?? false);
|
||||
.find('input[name="disabled"]')
|
||||
.prop('checked', existingScript.disabled ?? false);
|
||||
editorHtml
|
||||
.find(`input[name="only_format_display"]`)
|
||||
.prop("checked", existingScript.markdownOnly ?? false);
|
||||
.find('input[name="only_format_display"]')
|
||||
.prop('checked', existingScript.markdownOnly ?? false);
|
||||
editorHtml
|
||||
.find(`input[name="only_format_prompt"]`)
|
||||
.prop("checked", existingScript.promptOnly ?? false);
|
||||
.find('input[name="only_format_prompt"]')
|
||||
.prop('checked', existingScript.promptOnly ?? false);
|
||||
editorHtml
|
||||
.find(`input[name="run_on_edit"]`)
|
||||
.prop("checked", existingScript.runOnEdit ?? false);
|
||||
.find('input[name="run_on_edit"]')
|
||||
.prop('checked', existingScript.runOnEdit ?? false);
|
||||
editorHtml
|
||||
.find(`input[name="substitute_regex"]`)
|
||||
.prop("checked", existingScript.substituteRegex ?? false);
|
||||
.find('input[name="substitute_regex"]')
|
||||
.prop('checked', existingScript.substituteRegex ?? false);
|
||||
editorHtml
|
||||
.find(`select[name="replace_strategy_select"]`)
|
||||
.find('select[name="replace_strategy_select"]')
|
||||
.val(existingScript.replaceStrategy ?? 0);
|
||||
|
||||
existingScript.placement.forEach((element) => {
|
||||
editorHtml
|
||||
.find(`input[name="replace_position"][value="${element}"]`)
|
||||
.prop("checked", true);
|
||||
.prop('checked', true);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
editorHtml
|
||||
.find(`input[name="only_format_display"]`)
|
||||
.prop("checked", true);
|
||||
.find('input[name="only_format_display"]')
|
||||
.prop('checked', true);
|
||||
|
||||
editorHtml
|
||||
.find(`input[name="run_on_edit"]`)
|
||||
.prop("checked", true);
|
||||
.find('input[name="run_on_edit"]')
|
||||
.prop('checked', true);
|
||||
|
||||
editorHtml
|
||||
.find(`input[name="replace_position"][value="1"]`)
|
||||
.prop("checked", true);
|
||||
.find('input[name="replace_position"][value="1"]')
|
||||
.prop('checked', true);
|
||||
}
|
||||
|
||||
const popupResult = await callPopup(editorHtml, "confirm", undefined, { okButton: "Save" });
|
||||
const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
|
||||
if (popupResult) {
|
||||
const newRegexScript = {
|
||||
scriptName: editorHtml.find(".regex_script_name").val(),
|
||||
findRegex: editorHtml.find(".find_regex").val(),
|
||||
replaceString: editorHtml.find(".regex_replace_string").val(),
|
||||
trimStrings: editorHtml.find(".regex_trim_strings").val().split("\n").filter((e) => e.length !== 0) || [],
|
||||
scriptName: editorHtml.find('.regex_script_name').val(),
|
||||
findRegex: editorHtml.find('.find_regex').val(),
|
||||
replaceString: editorHtml.find('.regex_replace_string').val(),
|
||||
trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
|
||||
placement:
|
||||
editorHtml
|
||||
.find(`input[name="replace_position"]`)
|
||||
.filter(":checked")
|
||||
.map(function () { return parseInt($(this).val()) })
|
||||
.find('input[name="replace_position"]')
|
||||
.filter(':checked')
|
||||
.map(function () { return parseInt($(this).val()); })
|
||||
.get()
|
||||
.filter((e) => e !== NaN) || [],
|
||||
.filter((e) => !isNaN(e)) || [],
|
||||
disabled:
|
||||
editorHtml
|
||||
.find(`input[name="disabled"]`)
|
||||
.prop("checked"),
|
||||
.find('input[name="disabled"]')
|
||||
.prop('checked'),
|
||||
markdownOnly:
|
||||
editorHtml
|
||||
.find(`input[name="only_format_display"]`)
|
||||
.prop("checked"),
|
||||
.find('input[name="only_format_display"]')
|
||||
.prop('checked'),
|
||||
promptOnly:
|
||||
editorHtml
|
||||
.find(`input[name="only_format_prompt"]`)
|
||||
.prop("checked"),
|
||||
.find('input[name="only_format_prompt"]')
|
||||
.prop('checked'),
|
||||
runOnEdit:
|
||||
editorHtml
|
||||
.find(`input[name="run_on_edit"]`)
|
||||
.prop("checked"),
|
||||
.find('input[name="run_on_edit"]')
|
||||
.prop('checked'),
|
||||
substituteRegex:
|
||||
editorHtml
|
||||
.find(`input[name="substitute_regex"]`)
|
||||
.prop("checked"),
|
||||
.find('input[name="substitute_regex"]')
|
||||
.prop('checked'),
|
||||
replaceStrategy:
|
||||
parseInt(editorHtml
|
||||
.find(`select[name="replace_strategy_select"]`)
|
||||
.find(`:selected`)
|
||||
.val()) ?? 0
|
||||
.find('select[name="replace_strategy_select"]')
|
||||
.find(':selected')
|
||||
.val()) ?? 0,
|
||||
};
|
||||
|
||||
saveRegexScript(newRegexScript, existingScriptIndex);
|
||||
@@ -220,8 +222,8 @@ function migrateSettings() {
|
||||
Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
|
||||
script.placement = script.placement.filter((e) => e !== regex_placement.MD_DISPLAY);
|
||||
|
||||
script.markdownOnly = true
|
||||
script.promptOnly = true
|
||||
script.markdownOnly = true;
|
||||
script.promptOnly = true;
|
||||
|
||||
performSave = true;
|
||||
}
|
||||
@@ -242,6 +244,36 @@ function migrateSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /regex slash command callback
|
||||
* @param {object} args Named arguments
|
||||
* @param {string} value Unnamed argument
|
||||
* @returns {string} The regexed string
|
||||
*/
|
||||
function runRegexCallback(args, value) {
|
||||
if (!args.name) {
|
||||
toastr.warning('No regex script name provided.');
|
||||
return value;
|
||||
}
|
||||
|
||||
const scriptName = String(resolveVariable(args.name));
|
||||
|
||||
for (const script of extension_settings.regex) {
|
||||
if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
|
||||
if (script.disabled) {
|
||||
toastr.warning(`Regex script "${scriptName}" is disabled.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
console.debug(`Running regex callback for ${scriptName}`);
|
||||
return runRegexScript(script, value);
|
||||
}
|
||||
}
|
||||
|
||||
toastr.warning(`Regex script "${scriptName}" not found.`);
|
||||
return value;
|
||||
}
|
||||
|
||||
// Workaround for loading in sequence with other extensions
|
||||
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
|
||||
jQuery(async () => {
|
||||
@@ -250,13 +282,13 @@ jQuery(async () => {
|
||||
}
|
||||
|
||||
// Manually disable the extension since static imports auto-import the JS file
|
||||
if (extension_settings.disabledExtensions.includes("regex")) {
|
||||
if (extension_settings.disabledExtensions.includes('regex')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settingsHtml = await $.get("scripts/extensions/regex/dropdown.html");
|
||||
$("#extensions_settings2").append(settingsHtml);
|
||||
$("#open_regex_editor").on("click", function () {
|
||||
const settingsHtml = await $.get('scripts/extensions/regex/dropdown.html');
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#open_regex_editor').on('click', function () {
|
||||
onRegexEditorOpenClick(false);
|
||||
});
|
||||
|
||||
@@ -265,7 +297,7 @@ jQuery(async () => {
|
||||
stop: function () {
|
||||
let newScripts = [];
|
||||
$('#saved_regex_scripts').children().each(function () {
|
||||
const scriptName = $(this).find(".regex_script_name").text();
|
||||
const scriptName = $(this).find('.regex_script_name').text();
|
||||
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
|
||||
if (existingScript) {
|
||||
newScripts.push(existingScript);
|
||||
@@ -275,11 +307,13 @@ jQuery(async () => {
|
||||
extension_settings.regex = newScripts;
|
||||
saveSettingsDebounced();
|
||||
|
||||
console.debug("Regex scripts reordered");
|
||||
console.debug('Regex scripts reordered');
|
||||
// TODO: Maybe reload regex scripts after move
|
||||
},
|
||||
});
|
||||
|
||||
await loadRegexScripts();
|
||||
$("#saved_regex_scripts").sortable("enable");
|
||||
$('#saved_regex_scripts').sortable('enable');
|
||||
|
||||
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) – runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
|
||||
});
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import { getRequestHeaders } from "../../script.js";
|
||||
import { extension_settings } from "../extensions.js";
|
||||
import { SECRET_KEYS, secret_state } from "../secrets.js";
|
||||
import { createThumbnail } from "../utils.js";
|
||||
import { getRequestHeaders } from '../../script.js';
|
||||
import { extension_settings } from '../extensions.js';
|
||||
import { oai_settings } from '../openai.js';
|
||||
import { SECRET_KEYS, secret_state } from '../secrets.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
|
||||
import { createThumbnail, isValidUrl } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generates a caption for an image using a multimodal model.
|
||||
@@ -10,6 +12,105 @@ import { createThumbnail } from "../utils.js";
|
||||
* @returns {Promise<string>} Generated caption
|
||||
*/
|
||||
export async function getMultimodalCaption(base64Img, prompt) {
|
||||
throwIfInvalidModel();
|
||||
|
||||
const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
|
||||
|
||||
if (noPrefix && base64Img.startsWith('data:image/')) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
}
|
||||
|
||||
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
|
||||
// Ooba requires all images to be JPEGs.
|
||||
const isGoogle = extension_settings.caption.multimodal_api === 'google';
|
||||
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
|
||||
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
|
||||
const isCustom = extension_settings.caption.multimodal_api === 'custom';
|
||||
const isOoba = extension_settings.caption.multimodal_api === 'ooba';
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba) {
|
||||
const maxSide = 1024;
|
||||
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
|
||||
|
||||
if (isGoogle) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
}
|
||||
}
|
||||
|
||||
const useReverseProxy =
|
||||
extension_settings.caption.multimodal_api === 'openai'
|
||||
&& extension_settings.caption.allow_reverse_proxy
|
||||
&& oai_settings.reverse_proxy
|
||||
&& isValidUrl(oai_settings.reverse_proxy);
|
||||
|
||||
const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
|
||||
const proxyPassword = useReverseProxy ? oai_settings.proxy_password : '';
|
||||
|
||||
const requestBody = {
|
||||
image: base64Img,
|
||||
prompt: prompt,
|
||||
};
|
||||
|
||||
if (!isGoogle) {
|
||||
requestBody.api = extension_settings.caption.multimodal_api || 'openai';
|
||||
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-vision-preview';
|
||||
requestBody.reverse_proxy = proxyUrl;
|
||||
requestBody.proxy_password = proxyPassword;
|
||||
}
|
||||
|
||||
if (isOllama) {
|
||||
if (extension_settings.caption.multimodal_model === 'ollama_current') {
|
||||
requestBody.model = textgenerationwebui_settings.ollama_model;
|
||||
}
|
||||
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
|
||||
}
|
||||
|
||||
if (isLlamaCpp) {
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
|
||||
}
|
||||
|
||||
if (isOoba) {
|
||||
requestBody.server_url = textgenerationwebui_settings.server_urls[textgen_types.OOBA];
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
requestBody.server_url = oai_settings.custom_url;
|
||||
requestBody.model = oai_settings.custom_model || 'gpt-4-vision-preview';
|
||||
requestBody.custom_include_headers = oai_settings.custom_include_headers;
|
||||
requestBody.custom_include_body = oai_settings.custom_include_body;
|
||||
requestBody.custom_exclude_body = oai_settings.custom_exclude_body;
|
||||
}
|
||||
|
||||
function getEndpointUrl() {
|
||||
switch (extension_settings.caption.multimodal_api) {
|
||||
case 'google':
|
||||
return '/api/google/caption-image';
|
||||
case 'llamacpp':
|
||||
return '/api/backends/text-completions/llamacpp/caption-image';
|
||||
case 'ollama':
|
||||
return '/api/backends/text-completions/ollama/caption-image';
|
||||
default:
|
||||
return '/api/openai/caption-image';
|
||||
}
|
||||
}
|
||||
|
||||
const apiResult = await fetch(getEndpointUrl(), {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error('Failed to caption image via Multimodal API.');
|
||||
}
|
||||
|
||||
const { caption } = await apiResult.json();
|
||||
return String(caption).trim();
|
||||
}
|
||||
|
||||
function throwIfInvalidModel() {
|
||||
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) {
|
||||
throw new Error('OpenAI API key is not set.');
|
||||
}
|
||||
@@ -18,29 +119,27 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
throw new Error('OpenRouter API key is not set.');
|
||||
}
|
||||
|
||||
// OpenRouter has a payload limit of ~2MB
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if (extension_settings.caption.multimodal_api === 'openrouter' && base64Bytes > compressionLimit) {
|
||||
const maxSide = 1024;
|
||||
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
|
||||
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE]) {
|
||||
throw new Error('MakerSuite API key is not set.');
|
||||
}
|
||||
|
||||
const apiResult = await fetch('/api/openai/caption-image', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
image: base64Img,
|
||||
prompt: prompt,
|
||||
api: extension_settings.caption.multimodal_api || 'openai',
|
||||
model: extension_settings.caption.multimodal_model || 'gpt-4-vision-preview',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
throw new Error('Failed to caption image via OpenAI.');
|
||||
if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
|
||||
throw new Error('Ollama server URL is not set.');
|
||||
}
|
||||
|
||||
const { caption } = await apiResult.json();
|
||||
return caption;
|
||||
if (extension_settings.caption.multimodal_api === 'ollama' && extension_settings.caption.multimodal_model === 'ollama_current' && !textgenerationwebui_settings.ollama_model) {
|
||||
throw new Error('Ollama model is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
|
||||
throw new Error('LlamaCPP server URL is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'ooba' && !textgenerationwebui_settings.server_urls[textgen_types.OOBA]) {
|
||||
throw new Error('Text Generation WebUI server URL is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'custom' && !oai_settings.custom_url) {
|
||||
throw new Error('Custom API URL is not set.');
|
||||
}
|
||||
}
|
||||
|
@@ -25,6 +25,12 @@
|
||||
<a href="javascript:;" class="notes-link"><span class="note-link-span" title="Will generate a new random seed in SillyTavern that is then used in the ComfyUI workflow.">?</span></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div>Custom</div>
|
||||
<div class="sd_comfy_workflow_editor_placeholder_actions">
|
||||
<span id="sd_comfy_workflow_editor_placeholder_add" title="Add custom placeholder">+</span>
|
||||
</div>
|
||||
<ul class="sd_comfy_workflow_editor_placeholder_list" id="sd_comfy_workflow_editor_placeholder_list_custom">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -35,6 +35,7 @@
|
||||
<option value="novel">NovelAI Diffusion</option>
|
||||
<option value="openai">OpenAI (DALL-E)</option>
|
||||
<option value="comfy">ComfyUI</option>
|
||||
<option value="togetherai">TogetherAI</option>
|
||||
</select>
|
||||
<div data-sd-source="auto">
|
||||
<label for="sd_auto_url">SD Web UI URL</label>
|
||||
@@ -155,6 +156,8 @@
|
||||
<select id="sd_model"></select>
|
||||
<label for="sd_sampler">Sampling method</label>
|
||||
<select id="sd_sampler"></select>
|
||||
<label for="sd_resolution">Resolution</label>
|
||||
<select id="sd_resolution"><!-- Populated in JS --></select>
|
||||
<div data-sd-source="comfy">
|
||||
<label for="sd_scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
@@ -205,6 +208,9 @@
|
||||
<label for="sd_character_prompt">Character-specific prompt prefix</label>
|
||||
<small>Won't be used in groups.</small>
|
||||
<textarea id="sd_character_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prefix. Example: female, green eyes, brown hair, pink shirt"></textarea>
|
||||
<label for="sd_character_negative_prompt">Character-specific negative prompt prefix</label>
|
||||
<small>Won't be used in groups.</small>
|
||||
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prefix. Example: jewellery, shoes, glasses"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -82,3 +82,17 @@
|
||||
.sd_comfy_workflow_editor_placeholder_list>li>.notes-link {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.sd_comfy_workflow_editor_placeholder_list input {
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
.sd_comfy_workflow_editor_custom_remove, #sd_comfy_workflow_editor_placeholder_add {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
width: 1em;
|
||||
opacity: 0.5;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import { callPopup, main_api } from "../../../script.js";
|
||||
import { getContext } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { getFriendlyTokenizerName, getTextTokens, getTokenCount, tokenizers } from "../../tokenizers.js";
|
||||
import { resetScrollHeight } from "../../utils.js";
|
||||
import { callPopup, main_api } from '../../../script.js';
|
||||
import { getContext } from '../../extensions.js';
|
||||
import { registerSlashCommand } from '../../slash-commands.js';
|
||||
import { getFriendlyTokenizerName, getTextTokens, getTokenCount, tokenizers } from '../../tokenizers.js';
|
||||
import { resetScrollHeight } from '../../utils.js';
|
||||
|
||||
function rgb2hex(rgb) {
|
||||
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||
return (rgb && rgb.length === 4) ? "#" +
|
||||
("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : '';
|
||||
return (rgb && rgb.length === 4) ? '#' +
|
||||
('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) +
|
||||
('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) +
|
||||
('0' + parseInt(rgb[3], 10).toString(16)).slice(-2) : '';
|
||||
}
|
||||
|
||||
$('button').click(function () {
|
||||
@@ -71,16 +71,6 @@ async function doTokenCounter() {
|
||||
* @param {number[]} ids
|
||||
*/
|
||||
function drawChunks(chunks, ids) {
|
||||
const main_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBodyColor').trim()))
|
||||
const italics_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeEmColor').trim()))
|
||||
const quote_text_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeQuoteColor').trim()))
|
||||
const blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBlurTintColor').trim()))
|
||||
const chat_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeChatTintColor').trim()))
|
||||
const user_mes_blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeUserMesBlurTintColor').trim()))
|
||||
const bot_mes_blur_tint_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBotMesBlurTintColor').trim()))
|
||||
const shadow_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeShadowColor').trim()))
|
||||
const border_color = rgb2hex((getComputedStyle(document.documentElement).getPropertyValue('--SmartThemeBorderColor').trim()))
|
||||
|
||||
const pastelRainbow = [
|
||||
//main_text_color,
|
||||
//italics_text_color,
|
||||
|
@@ -1,4 +1,4 @@
|
||||
export {translate};
|
||||
export { translate };
|
||||
|
||||
import {
|
||||
callPopup,
|
||||
@@ -9,9 +9,10 @@ import {
|
||||
saveSettingsDebounced,
|
||||
substituteParams,
|
||||
updateMessageBlock,
|
||||
} from "../../../script.js";
|
||||
import { extension_settings, getContext } from "../../extensions.js";
|
||||
import { secret_state, writeSecret } from "../../secrets.js";
|
||||
} from '../../../script.js';
|
||||
import { extension_settings, getContext } from '../../extensions.js';
|
||||
import { secret_state, writeSecret } from '../../secrets.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
|
||||
export const autoModeOptions = {
|
||||
NONE: 'none',
|
||||
@@ -143,15 +144,15 @@ const LOCAL_URL = ['libre', 'oneringtranslator', 'deeplx'];
|
||||
function showKeysButton() {
|
||||
const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
|
||||
const providerOptionalUrl = LOCAL_URL.includes(extension_settings.translate.provider);
|
||||
$("#translate_key_button").toggle(providerRequiresKey);
|
||||
$("#translate_key_button").toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
|
||||
$("#translate_url_button").toggle(providerOptionalUrl);
|
||||
$("#translate_url_button").toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + "_url"]));
|
||||
$('#translate_key_button').toggle(providerRequiresKey);
|
||||
$('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
|
||||
$('#translate_url_button').toggle(providerOptionalUrl);
|
||||
$('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
for (const key in defaultSettings) {
|
||||
if (!extension_settings.translate.hasOwnProperty(key)) {
|
||||
if (!Object.hasOwn(extension_settings.translate, key)) {
|
||||
extension_settings.translate[key] = defaultSettings[key];
|
||||
}
|
||||
}
|
||||
@@ -164,7 +165,7 @@ function loadSettings() {
|
||||
|
||||
async function translateImpersonate(text) {
|
||||
const translatedText = await translate(text, extension_settings.translate.target_language);
|
||||
$("#send_textarea").val(translatedText);
|
||||
$('#send_textarea').val(translatedText);
|
||||
}
|
||||
|
||||
async function translateIncomingMessage(messageId) {
|
||||
@@ -315,6 +316,28 @@ async function translateProviderBing(text, lang) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits text into chunks and translates each chunk separately
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {(text: string, lang: string) => Promise<string>} translateFn Function to translate a single chunk (must return a Promise)
|
||||
* @param {number} chunkSize Maximum chunk size
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
|
||||
if (text.length <= chunkSize) {
|
||||
return await translateFn(text, lang);
|
||||
}
|
||||
|
||||
const chunks = splitRecursive(text, chunkSize);
|
||||
|
||||
let result = '';
|
||||
for (const chunk of chunks) {
|
||||
result += await translateFn(chunk, lang);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates text using the selected translation provider
|
||||
* @param {string} text Text to translate
|
||||
@@ -331,15 +354,15 @@ async function translate(text, lang) {
|
||||
case 'libre':
|
||||
return await translateProviderLibre(text, lang);
|
||||
case 'google':
|
||||
return await translateProviderGoogle(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderGoogle, 5000);
|
||||
case 'deepl':
|
||||
return await translateProviderDeepl(text, lang);
|
||||
case 'deeplx':
|
||||
return await translateProviderDeepLX(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderDeepLX, 1500);
|
||||
case 'oneringtranslator':
|
||||
return await translateProviderOneRing(text, lang);
|
||||
case 'bing':
|
||||
return await translateProviderBing(text, lang);
|
||||
return await chunkedTranslate(text, lang, translateProviderBing, 1000);
|
||||
default:
|
||||
console.error('Unknown translation provider', extension_settings.translate.provider);
|
||||
return text;
|
||||
@@ -540,7 +563,7 @@ jQuery(() => {
|
||||
|
||||
await writeSecret(extension_settings.translate.provider, key);
|
||||
toastr.success('API Key saved');
|
||||
$("#translate_key_button").addClass('success');
|
||||
$('#translate_key_button').addClass('success');
|
||||
});
|
||||
$('#translate_url_button').on('click', async () => {
|
||||
const optionText = $('#translation_provider option:selected').text();
|
||||
@@ -556,9 +579,9 @@ jQuery(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
await writeSecret(extension_settings.translate.provider + "_url", url);
|
||||
await writeSecret(extension_settings.translate.provider + '_url', url);
|
||||
toastr.success('API URL saved');
|
||||
$("#translate_url_button").addClass('success');
|
||||
$('#translate_url_button').addClass('success');
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
|
@@ -4,17 +4,15 @@ TODO:
|
||||
- Delete useless call
|
||||
*/
|
||||
|
||||
import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js"
|
||||
import { callPopup } from "../../../script.js"
|
||||
import { initVoiceMap } from "./index.js"
|
||||
import { doExtrasFetch, extension_settings, getApiUrl, modules } from '../../extensions.js';
|
||||
import { callPopup } from '../../../script.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
|
||||
export { CoquiTtsProvider }
|
||||
export { CoquiTtsProvider };
|
||||
|
||||
const DEBUG_PREFIX = "<Coqui TTS module> ";
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const DEBUG_PREFIX = '<Coqui TTS module> ';
|
||||
|
||||
let inApiCall = false;
|
||||
let voiceIdList = []; // Updated with module worker
|
||||
let coquiApiModels = {}; // Initialized only once
|
||||
let coquiApiModelsFull = {}; // Initialized only once
|
||||
let coquiLocalModels = []; // Initialized only once
|
||||
@@ -35,24 +33,24 @@ coquiApiModels format [language][dataset][name]:coqui-api-model-id, example:
|
||||
}
|
||||
*/
|
||||
const languageLabels = {
|
||||
"multilingual": "Multilingual",
|
||||
"en": "English",
|
||||
"fr": "French",
|
||||
"es": "Spanish",
|
||||
"ja": "Japanese"
|
||||
}
|
||||
'multilingual': 'Multilingual',
|
||||
'en': 'English',
|
||||
'fr': 'French',
|
||||
'es': 'Spanish',
|
||||
'ja': 'Japanese',
|
||||
};
|
||||
|
||||
function throwIfModuleMissing() {
|
||||
if (!modules.includes('coqui-tts')) {
|
||||
const message = `Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.`
|
||||
const message = 'Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.';
|
||||
// toastr.error(message, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
throw new Error(DEBUG_PREFIX, message);
|
||||
}
|
||||
}
|
||||
|
||||
function resetModelSettings() {
|
||||
$("#coqui_api_model_settings_language").val("none");
|
||||
$("#coqui_api_model_settings_speaker").val("none");
|
||||
$('#coqui_api_model_settings_language').val('none');
|
||||
$('#coqui_api_model_settings_speaker').val('none');
|
||||
}
|
||||
|
||||
class CoquiTtsProvider {
|
||||
@@ -60,14 +58,14 @@ class CoquiTtsProvider {
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
settings
|
||||
settings;
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
customVoices: {},
|
||||
voiceIds: [],
|
||||
voiceMapDict: {}
|
||||
}
|
||||
voiceMapDict: {},
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
@@ -121,48 +119,48 @@ class CoquiTtsProvider {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw DEBUG_PREFIX + `Invalid setting passed to extension: ${key}`
|
||||
throw DEBUG_PREFIX + `Invalid setting passed to extension: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
await initLocalModels();
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_local_model_div").hide();
|
||||
$('#coqui_api_model_div').hide();
|
||||
$('#coqui_local_model_div').hide();
|
||||
|
||||
$("#coqui_api_language").show();
|
||||
$("#coqui_api_model_name").hide();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
$("#coqui_api_model_install_status").hide();
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
$('#coqui_api_language').show();
|
||||
$('#coqui_api_model_name').hide();
|
||||
$('#coqui_api_model_settings').hide();
|
||||
$('#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() });
|
||||
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_remove_voiceId_mapping").on("click", function () { that.onRemoveClick() });
|
||||
$("#coqui_add_voiceId_mapping").on("click", function () { that.onAddClick() });
|
||||
$('#coqui_remove_voiceId_mapping').on('click', function () { that.onRemoveClick(); });
|
||||
$('#coqui_add_voiceId_mapping').on('click', function () { that.onAddClick(); });
|
||||
|
||||
// Load coqui-api settings from json file
|
||||
await fetch("/scripts/extensions/tts/coqui_api_models_settings.json")
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
coquiApiModels = json;
|
||||
console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels);
|
||||
await fetch('/scripts/extensions/tts/coqui_api_models_settings.json')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
coquiApiModels = json;
|
||||
console.debug(DEBUG_PREFIX,'initialized coqui-api model list to', coquiApiModels);
|
||||
/*
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
@@ -175,14 +173,14 @@ class CoquiTtsProvider {
|
||||
$("#coqui_api_language").append(new Option(languageLabels[language],language));
|
||||
console.log(DEBUG_PREFIX,"added language",language);
|
||||
}*/
|
||||
});
|
||||
});
|
||||
|
||||
// Load coqui-api FULL settings from json file
|
||||
await fetch("/scripts/extensions/tts/coqui_api_models_settings_full.json")
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
coquiApiModelsFull = json;
|
||||
console.debug(DEBUG_PREFIX,"initialized coqui-api full model list to", coquiApiModelsFull);
|
||||
await fetch('/scripts/extensions/tts/coqui_api_models_settings_full.json')
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
coquiApiModelsFull = json;
|
||||
console.debug(DEBUG_PREFIX,'initialized coqui-api full model list to', coquiApiModelsFull);
|
||||
/*
|
||||
$('#coqui_api_full_language')
|
||||
.find('option')
|
||||
@@ -195,13 +193,13 @@ class CoquiTtsProvider {
|
||||
$("#coqui_api_full_language").append(new Option(languageLabels[language],language));
|
||||
console.log(DEBUG_PREFIX,"added language",language);
|
||||
}*/
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady(){
|
||||
throwIfModuleMissing()
|
||||
await this.fetchTtsVoiceObjects()
|
||||
throwIfModuleMissing();
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
updateCustomVoices() {
|
||||
@@ -209,37 +207,37 @@ class CoquiTtsProvider {
|
||||
this.settings.customVoices = {};
|
||||
for (let voiceName in this.settings.voiceMapDict) {
|
||||
const voiceId = this.settings.voiceMapDict[voiceName];
|
||||
this.settings.customVoices[voiceName] = voiceId["model_id"];
|
||||
this.settings.customVoices[voiceName] = voiceId['model_id'];
|
||||
|
||||
if (voiceId["model_language"] != null)
|
||||
this.settings.customVoices[voiceName] += "[" + voiceId["model_language"] + "]";
|
||||
if (voiceId['model_language'] != null)
|
||||
this.settings.customVoices[voiceName] += '[' + voiceId['model_language'] + ']';
|
||||
|
||||
if (voiceId["model_speaker"] != null)
|
||||
this.settings.customVoices[voiceName] += "[" + voiceId["model_speaker"] + "]";
|
||||
if (voiceId['model_speaker'] != null)
|
||||
this.settings.customVoices[voiceName] += '[' + voiceId['model_speaker'] + ']';
|
||||
}
|
||||
|
||||
// Update UI select list with voices
|
||||
$("#coqui_voicename_select").empty()
|
||||
$('#coqui_voicename_select').empty();
|
||||
$('#coqui_voicename_select')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select Voice</option>')
|
||||
.val('none')
|
||||
.val('none');
|
||||
for (const voiceName in this.settings.voiceMapDict) {
|
||||
$("#coqui_voicename_select").append(new Option(voiceName, voiceName));
|
||||
$('#coqui_voicename_select').append(new Option(voiceName, voiceName));
|
||||
}
|
||||
|
||||
this.onSettingsChange()
|
||||
this.onSettingsChange();
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
console.debug(DEBUG_PREFIX, "Settings changes", this.settings);
|
||||
console.debug(DEBUG_PREFIX, 'Settings changes', this.settings);
|
||||
extension_settings.tts.Coqui = this.settings;
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
this.checkReady()
|
||||
this.checkReady();
|
||||
}
|
||||
|
||||
async onAddClick() {
|
||||
@@ -248,136 +246,136 @@ class CoquiTtsProvider {
|
||||
}
|
||||
|
||||
// Ask user for voiceId name to save voice
|
||||
const voiceName = await callPopup('<h3>Name of Coqui voice to add to voice select dropdown:</h3>', 'input')
|
||||
const voiceName = await callPopup('<h3>Name of Coqui voice to add to voice select dropdown:</h3>', 'input');
|
||||
|
||||
const model_origin = $("#coqui_model_origin").val();
|
||||
const model_language = $("#coqui_api_language").val();
|
||||
const model_name = $("#coqui_api_model_name").val();
|
||||
let model_setting_language = $("#coqui_api_model_settings_language").val();
|
||||
let model_setting_speaker = $("#coqui_api_model_settings_speaker").val();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
const model_name = $('#coqui_api_model_name').val();
|
||||
let model_setting_language = $('#coqui_api_model_settings_language').val();
|
||||
let model_setting_speaker = $('#coqui_api_model_settings_speaker').val();
|
||||
|
||||
|
||||
if (!voiceName) {
|
||||
toastr.error(`Voice name empty, please enter one.`, DEBUG_PREFIX + " voice mapping voice name", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
toastr.error('Voice name empty, please enter one.', DEBUG_PREFIX + ' voice mapping voice name', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_origin == "none") {
|
||||
toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX + " voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_origin == 'none') {
|
||||
toastr.error('Origin not selected, please select one.', DEBUG_PREFIX + ' voice mapping origin', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_origin == "local") {
|
||||
const model_id = $("#coqui_local_model_name").val();
|
||||
if (model_origin == 'local') {
|
||||
const model_id = $('#coqui_local_model_name').val();
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_name == 'none') {
|
||||
toastr.error('Model not selected, please select one.', DEBUG_PREFIX + ' voice mapping model', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
this.settings.voiceMapDict[voiceName] = { model_type: "local", model_id: "local/" + model_id };
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]);
|
||||
this.settings.voiceMapDict[voiceName] = { model_type: 'local', model_id: 'local/' + model_id };
|
||||
console.debug(DEBUG_PREFIX, 'Registered new voice map: ', voiceName, ':', this.settings.voiceMapDict[voiceName]);
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_language == "none") {
|
||||
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX + " voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_language == 'none') {
|
||||
toastr.error('Language not selected, please select one.', DEBUG_PREFIX + ' voice mapping language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX + " voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_name == 'none') {
|
||||
toastr.error('Model not selected, please select one.', DEBUG_PREFIX + ' voice mapping model', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
this.updateCustomVoices(); // Overide any manual modification
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_setting_language == "none")
|
||||
if (model_setting_language == 'none')
|
||||
model_setting_language = null;
|
||||
|
||||
if (model_setting_speaker == "none")
|
||||
if (model_setting_speaker == 'none')
|
||||
model_setting_speaker = null;
|
||||
|
||||
const tokens = $('#coqui_api_model_name').val().split("/");
|
||||
const tokens = $('#coqui_api_model_name').val().split('/');
|
||||
const model_dataset = tokens[0];
|
||||
const model_label = tokens[1];
|
||||
const model_id = "tts_models/" + model_language + "/" + model_dataset + "/" + model_label
|
||||
const model_id = 'tts_models/' + model_language + '/' + model_dataset + '/' + model_label;
|
||||
|
||||
let modelDict = coquiApiModels
|
||||
if (model_origin == "coqui-api-full")
|
||||
modelDict = coquiApiModelsFull
|
||||
let modelDict = coquiApiModels;
|
||||
if (model_origin == 'coqui-api-full')
|
||||
modelDict = coquiApiModelsFull;
|
||||
|
||||
if (model_setting_language == null & "languages" in modelDict[model_language][model_dataset][model_label]) {
|
||||
toastr.error(`Model language not selected, please select one.`, DEBUG_PREFIX+" voice mapping model language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_setting_language == null & 'languages' in modelDict[model_language][model_dataset][model_label]) {
|
||||
toastr.error('Model language not selected, please select one.', DEBUG_PREFIX + ' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_setting_speaker == null & "speakers" in modelDict[model_language][model_dataset][model_label]) {
|
||||
toastr.error(`Model speaker not selected, please select one.`, DEBUG_PREFIX+" voice mapping model speaker", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (model_setting_speaker == null & 'speakers' in modelDict[model_language][model_dataset][model_label]) {
|
||||
toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX + ' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Current custom voices: ", this.settings.customVoices);
|
||||
console.debug(DEBUG_PREFIX, 'Current custom voices: ', this.settings.customVoices);
|
||||
|
||||
this.settings.voiceMapDict[voiceName] = { model_type: "coqui-api", model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
|
||||
this.settings.voiceMapDict[voiceName] = { model_type: 'coqui-api', model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker };
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Registered new voice map: ", voiceName, ":", this.settings.voiceMapDict[voiceName]);
|
||||
console.debug(DEBUG_PREFIX, 'Registered new voice map: ', voiceName, ':', this.settings.voiceMapDict[voiceName]);
|
||||
|
||||
this.updateCustomVoices();
|
||||
initVoiceMap() // Update TTS extension voiceMap
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
|
||||
let successMsg = voiceName + ":" + model_id;
|
||||
let successMsg = voiceName + ':' + model_id;
|
||||
if (model_setting_language != null)
|
||||
successMsg += "[" + model_setting_language + "]";
|
||||
successMsg += '[' + model_setting_language + ']';
|
||||
if (model_setting_speaker != null)
|
||||
successMsg += "[" + model_setting_speaker + "]";
|
||||
toastr.info(successMsg, DEBUG_PREFIX + " voice map updated", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
successMsg += '[' + model_setting_speaker + ']';
|
||||
toastr.info(successMsg, DEBUG_PREFIX + ' voice map updated', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
async getVoice(voiceName) {
|
||||
let match = await this.fetchTtsVoiceObjects()
|
||||
let match = await this.fetchTtsVoiceObjects();
|
||||
match = match.filter(
|
||||
voice => voice.name == voiceName
|
||||
)[0]
|
||||
voice => voice.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found in CoquiTTS Provider voice list`
|
||||
throw `TTS Voice name ${voiceName} not found in CoquiTTS Provider voice list`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
async onRemoveClick() {
|
||||
const voiceName = $("#coqui_voicename_select").val();
|
||||
const voiceName = $('#coqui_voicename_select').val();
|
||||
|
||||
if (voiceName === "none") {
|
||||
toastr.error(`Voice not selected, please select one.`, DEBUG_PREFIX + " voice mapping voiceId", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
if (voiceName === 'none') {
|
||||
toastr.error('Voice not selected, please select one.', DEBUG_PREFIX + ' voice mapping voiceId', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo erase from voicemap
|
||||
delete (this.settings.voiceMapDict[voiceName]);
|
||||
this.updateCustomVoices();
|
||||
initVoiceMap() // Update TTS extension voiceMap
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
}
|
||||
|
||||
async onModelOriginChange() {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
|
||||
if (model_origin == "none") {
|
||||
$("#coqui_local_model_div").hide();
|
||||
$("#coqui_api_model_div").hide();
|
||||
if (model_origin == 'none') {
|
||||
$('#coqui_local_model_div').hide();
|
||||
$('#coqui_api_model_div').hide();
|
||||
}
|
||||
|
||||
// show coqui model selected list (SAFE)
|
||||
if (model_origin == "coqui-api") {
|
||||
$("#coqui_local_model_div").hide();
|
||||
if (model_origin == 'coqui-api') {
|
||||
$('#coqui_local_model_div').hide();
|
||||
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
@@ -387,19 +385,19 @@ class CoquiTtsProvider {
|
||||
.val('none');
|
||||
|
||||
for(let language in coquiApiModels) {
|
||||
let languageLabel = language
|
||||
let languageLabel = language;
|
||||
if (language in languageLabels)
|
||||
languageLabel = languageLabels[language]
|
||||
$("#coqui_api_language").append(new Option(languageLabel,language));
|
||||
console.log(DEBUG_PREFIX,"added language",languageLabel,"(",language,")");
|
||||
languageLabel = languageLabels[language];
|
||||
$('#coqui_api_language').append(new Option(languageLabel,language));
|
||||
console.log(DEBUG_PREFIX,'added language',languageLabel,'(',language,')');
|
||||
}
|
||||
|
||||
$("#coqui_api_model_div").show();
|
||||
$('#coqui_api_model_div').show();
|
||||
}
|
||||
|
||||
// show coqui model full list (UNSAFE)
|
||||
if (model_origin == "coqui-api-full") {
|
||||
$("#coqui_local_model_div").hide();
|
||||
if (model_origin == 'coqui-api-full') {
|
||||
$('#coqui_local_model_div').hide();
|
||||
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
@@ -409,38 +407,38 @@ class CoquiTtsProvider {
|
||||
.val('none');
|
||||
|
||||
for(let language in coquiApiModelsFull) {
|
||||
let languageLabel = language
|
||||
let languageLabel = language;
|
||||
if (language in languageLabels)
|
||||
languageLabel = languageLabels[language]
|
||||
$("#coqui_api_language").append(new Option(languageLabel,language));
|
||||
console.log(DEBUG_PREFIX,"added language",languageLabel,"(",language,")");
|
||||
languageLabel = languageLabels[language];
|
||||
$('#coqui_api_language').append(new Option(languageLabel,language));
|
||||
console.log(DEBUG_PREFIX,'added language',languageLabel,'(',language,')');
|
||||
}
|
||||
|
||||
$("#coqui_api_model_div").show();
|
||||
$('#coqui_api_model_div').show();
|
||||
}
|
||||
|
||||
|
||||
// show local model list
|
||||
if (model_origin == "local") {
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_local_model_div").show();
|
||||
if (model_origin == 'local') {
|
||||
$('#coqui_api_model_div').hide();
|
||||
$('#coqui_local_model_div').show();
|
||||
}
|
||||
}
|
||||
|
||||
async onModelLanguageChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
$('#coqui_api_model_settings').hide();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
console.debug(model_language);
|
||||
|
||||
if (model_language == "none") {
|
||||
$("#coqui_api_model_name").hide();
|
||||
if (model_language == 'none') {
|
||||
$('#coqui_api_model_name').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$("#coqui_api_model_name").show();
|
||||
$('#coqui_api_model_name').show();
|
||||
$('#coqui_api_model_name')
|
||||
.find('option')
|
||||
.remove()
|
||||
@@ -448,46 +446,46 @@ class CoquiTtsProvider {
|
||||
.append('<option value="none">Select model</option>')
|
||||
.val('none');
|
||||
|
||||
let modelDict = coquiApiModels
|
||||
if (model_origin == "coqui-api-full")
|
||||
modelDict = coquiApiModelsFull
|
||||
let modelDict = coquiApiModels;
|
||||
if (model_origin == 'coqui-api-full')
|
||||
modelDict = coquiApiModelsFull;
|
||||
|
||||
for(let model_dataset in modelDict[model_language])
|
||||
for(let model_name in modelDict[model_language][model_dataset]) {
|
||||
const model_id = model_dataset + "/" + model_name
|
||||
const model_label = model_name + " (" + model_dataset + " dataset)"
|
||||
$("#coqui_api_model_name").append(new Option(model_label, model_id));
|
||||
const model_id = model_dataset + '/' + model_name;
|
||||
const model_label = model_name + ' (' + model_dataset + ' dataset)';
|
||||
$('#coqui_api_model_name').append(new Option(model_label, model_id));
|
||||
}
|
||||
}
|
||||
|
||||
async onModelNameChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
$('#coqui_api_model_settings').hide();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
|
||||
// No model selected
|
||||
if ($('#coqui_api_model_name').val() == "none") {
|
||||
$("#coqui_api_model_install_button").off('click');
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
if ($('#coqui_api_model_name').val() == 'none') {
|
||||
$('#coqui_api_model_install_button').off('click');
|
||||
$('#coqui_api_model_install_button').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get languages and speakers options
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
const tokens = $('#coqui_api_model_name').val().split("/");
|
||||
const tokens = $('#coqui_api_model_name').val().split('/');
|
||||
const model_dataset = tokens[0];
|
||||
const model_name = tokens[1];
|
||||
|
||||
let modelDict = coquiApiModels
|
||||
if (model_origin == "coqui-api-full")
|
||||
modelDict = coquiApiModelsFull
|
||||
let modelDict = coquiApiModels;
|
||||
if (model_origin == 'coqui-api-full')
|
||||
modelDict = coquiApiModelsFull;
|
||||
|
||||
const model_settings = modelDict[model_language][model_dataset][model_name]
|
||||
const model_settings = modelDict[model_language][model_dataset][model_name];
|
||||
|
||||
if ("languages" in model_settings) {
|
||||
$("#coqui_api_model_settings").show();
|
||||
$("#coqui_api_model_settings_language").show();
|
||||
if ('languages' in model_settings) {
|
||||
$('#coqui_api_model_settings').show();
|
||||
$('#coqui_api_model_settings_language').show();
|
||||
$('#coqui_api_model_settings_language')
|
||||
.find('option')
|
||||
.remove()
|
||||
@@ -495,18 +493,18 @@ class CoquiTtsProvider {
|
||||
.append('<option value="none">Select language</option>')
|
||||
.val('none');
|
||||
|
||||
for (var i = 0; i < model_settings["languages"].length; i++) {
|
||||
const language_label = JSON.stringify(model_settings["languages"][i]).replaceAll("\"", "");
|
||||
$("#coqui_api_model_settings_language").append(new Option(language_label, i));
|
||||
for (let i = 0; i < model_settings['languages'].length; i++) {
|
||||
const language_label = JSON.stringify(model_settings['languages'][i]).replaceAll('"', '');
|
||||
$('#coqui_api_model_settings_language').append(new Option(language_label, i));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$("#coqui_api_model_settings_language").hide();
|
||||
$('#coqui_api_model_settings_language').hide();
|
||||
}
|
||||
|
||||
if ("speakers" in model_settings) {
|
||||
$("#coqui_api_model_settings").show();
|
||||
$("#coqui_api_model_settings_speaker").show();
|
||||
if ('speakers' in model_settings) {
|
||||
$('#coqui_api_model_settings').show();
|
||||
$('#coqui_api_model_settings_speaker').show();
|
||||
$('#coqui_api_model_settings_speaker')
|
||||
.find('option')
|
||||
.remove()
|
||||
@@ -514,75 +512,75 @@ class CoquiTtsProvider {
|
||||
.append('<option value="none">Select speaker</option>')
|
||||
.val('none');
|
||||
|
||||
for (var i = 0; i < model_settings["speakers"].length; i++) {
|
||||
const speaker_label = JSON.stringify(model_settings["speakers"][i]).replaceAll("\"", "");
|
||||
$("#coqui_api_model_settings_speaker").append(new Option(speaker_label, i));
|
||||
for (let i = 0; i < model_settings['speakers'].length; i++) {
|
||||
const speaker_label = JSON.stringify(model_settings['speakers'][i]).replaceAll('"', '');
|
||||
$('#coqui_api_model_settings_speaker').append(new Option(speaker_label, i));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$("#coqui_api_model_settings_speaker").hide();
|
||||
$('#coqui_api_model_settings_speaker').hide();
|
||||
}
|
||||
|
||||
$("#coqui_api_model_install_status").text("Requesting model to extras server...");
|
||||
$("#coqui_api_model_install_status").show();
|
||||
$('#coqui_api_model_install_status').text('Requesting model to extras server...');
|
||||
$('#coqui_api_model_install_status').show();
|
||||
|
||||
// Check if already installed and propose to do it otherwise
|
||||
const model_id = modelDict[model_language][model_dataset][model_name]["id"]
|
||||
console.debug(DEBUG_PREFIX,"Check if model is already installed",model_id);
|
||||
const model_id = modelDict[model_language][model_dataset][model_name]['id'];
|
||||
console.debug(DEBUG_PREFIX,'Check if model is already installed',model_id);
|
||||
let result = await CoquiTtsProvider.checkmodel_state(model_id);
|
||||
result = await result.json();
|
||||
const model_state = result["model_state"];
|
||||
const model_state = result['model_state'];
|
||||
|
||||
console.debug(DEBUG_PREFIX, " Model state:", model_state)
|
||||
console.debug(DEBUG_PREFIX, ' Model state:', model_state);
|
||||
|
||||
if (model_state == "installed") {
|
||||
$("#coqui_api_model_install_status").text("Model already installed on extras server");
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
if (model_state == 'installed') {
|
||||
$('#coqui_api_model_install_status').text('Model already installed on extras server');
|
||||
$('#coqui_api_model_install_button').hide();
|
||||
}
|
||||
else {
|
||||
let action = "download"
|
||||
if (model_state == "corrupted") {
|
||||
action = "repare"
|
||||
let action = 'download';
|
||||
if (model_state == 'corrupted') {
|
||||
action = 'repare';
|
||||
//toastr.error("Click install button to reinstall the model "+$("#coqui_api_model_name").find(":selected").text(), DEBUG_PREFIX+" corrupted model install", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$("#coqui_api_model_install_status").text("Model found but incomplete try install again (maybe still downloading)"); // (remove and download again)
|
||||
$('#coqui_api_model_install_status').text('Model found but incomplete try install again (maybe still downloading)'); // (remove and download again)
|
||||
}
|
||||
else {
|
||||
toastr.info("Click download button to install the model " + $("#coqui_api_model_name").find(":selected").text(), DEBUG_PREFIX + " model not installed", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$("#coqui_api_model_install_status").text("Model not found on extras server");
|
||||
toastr.info('Click download button to install the model ' + $('#coqui_api_model_name').find(':selected').text(), DEBUG_PREFIX + ' model not installed', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$('#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 () {
|
||||
$('#coqui_api_model_install_button').off('click').on('click', async function () {
|
||||
try {
|
||||
$("#coqui_api_model_install_status").text("Downloading model...");
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
$('#coqui_api_model_install_status').text('Downloading model...');
|
||||
$('#coqui_api_model_install_button').hide();
|
||||
//toastr.info("For model "+model_id, DEBUG_PREFIX+" Started "+action, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
let apiResult = await CoquiTtsProvider.installModel(model_id, action);
|
||||
apiResult = await apiResult.json();
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Response:", apiResult);
|
||||
console.debug(DEBUG_PREFIX, 'Response:', apiResult);
|
||||
|
||||
if (apiResult["status"] == "done") {
|
||||
$("#coqui_api_model_install_status").text("Model installed and ready to use!");
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
if (apiResult['status'] == 'done') {
|
||||
$('#coqui_api_model_install_status').text('Model installed and ready to use!');
|
||||
$('#coqui_api_model_install_button').hide();
|
||||
onModelNameChange_pointer();
|
||||
}
|
||||
|
||||
if (apiResult["status"] == "downloading") {
|
||||
toastr.error("Check extras console for progress", DEBUG_PREFIX + " already downloading", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$("#coqui_api_model_install_status").text("Already downloading a model, check extras console!");
|
||||
$("#coqui_api_model_install_button").show();
|
||||
if (apiResult['status'] == 'downloading') {
|
||||
toastr.error('Check extras console for progress', DEBUG_PREFIX + ' already downloading', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$('#coqui_api_model_install_status').text('Already downloading a model, check extras console!');
|
||||
$('#coqui_api_model_install_button').show();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
toastr.error(error, DEBUG_PREFIX + " error with model download", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
console.error(error);
|
||||
toastr.error(error, DEBUG_PREFIX + ' error with model download', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
onModelNameChange_pointer();
|
||||
}
|
||||
// will refresh model status
|
||||
});
|
||||
|
||||
$("#coqui_api_model_install_button").show();
|
||||
$('#coqui_api_model_install_button').show();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -597,7 +595,7 @@ class CoquiTtsProvider {
|
||||
Check model installation state, return one of ["installed", "corrupted", "absent"]
|
||||
*/
|
||||
static async checkmodel_state(model_id) {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/check-model-state';
|
||||
|
||||
@@ -605,11 +603,11 @@ class CoquiTtsProvider {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": model_id,
|
||||
})
|
||||
'model_id': model_id,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -617,11 +615,11 @@ class CoquiTtsProvider {
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
static async installModel(model_id, action) {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/install-model';
|
||||
|
||||
@@ -629,12 +627,12 @@ class CoquiTtsProvider {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": model_id,
|
||||
"action": action
|
||||
})
|
||||
'model_id': model_id,
|
||||
'action': action,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -642,14 +640,14 @@ class CoquiTtsProvider {
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
/*
|
||||
Retrieve user custom models
|
||||
*/
|
||||
static async getLocalModelList() {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/local/get-models';
|
||||
|
||||
@@ -657,20 +655,20 @@ class CoquiTtsProvider {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": "model_id",
|
||||
"action": "action"
|
||||
})
|
||||
})
|
||||
'model_id': 'model_id',
|
||||
'action': 'action',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Get local model list request failed');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
|
||||
@@ -679,27 +677,27 @@ class CoquiTtsProvider {
|
||||
// tts_models/en/ljspeech/glow-tts
|
||||
// ts_models/ja/kokoro/tacotron2-DDC
|
||||
async generateTts(text, voiceId) {
|
||||
throwIfModuleMissing()
|
||||
voiceId = this.settings.customVoices[voiceId]
|
||||
throwIfModuleMissing();
|
||||
voiceId = this.settings.customVoices[voiceId];
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/generate-tts';
|
||||
|
||||
let language = "none"
|
||||
let speaker = "none"
|
||||
const tokens = voiceId.replaceAll("]", "").replaceAll("\"", "").split("[");
|
||||
const model_id = tokens[0]
|
||||
let language = 'none';
|
||||
let speaker = 'none';
|
||||
const tokens = voiceId.replaceAll(']', '').replaceAll('"', '').split('[');
|
||||
const model_id = tokens[0];
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Preparing TTS request for", tokens)
|
||||
console.debug(DEBUG_PREFIX, 'Preparing TTS request for', tokens);
|
||||
|
||||
// First option
|
||||
if (tokens.length > 1) {
|
||||
const option1 = tokens[1]
|
||||
const option1 = tokens[1];
|
||||
|
||||
if (model_id.includes("multilingual"))
|
||||
language = option1
|
||||
if (model_id.includes('multilingual'))
|
||||
language = option1;
|
||||
else
|
||||
speaker = option1
|
||||
speaker = option1;
|
||||
}
|
||||
|
||||
// Second option
|
||||
@@ -710,14 +708,14 @@ class CoquiTtsProvider {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"text": text,
|
||||
"model_id": model_id,
|
||||
"language_id": parseInt(language),
|
||||
"speaker_id": parseInt(speaker)
|
||||
})
|
||||
'text': text,
|
||||
'model_id': model_id,
|
||||
'language_id': parseInt(language),
|
||||
'speaker_id': parseInt(speaker),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
@@ -725,7 +723,7 @@ class CoquiTtsProvider {
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
// Dirty hack to say not implemented
|
||||
@@ -733,12 +731,12 @@ class CoquiTtsProvider {
|
||||
const voiceIds = Object
|
||||
.keys(this.settings.voiceMapDict)
|
||||
.map(voice => ({ name: voice, voice_id: voice, preview_url: false }));
|
||||
return voiceIds
|
||||
return voiceIds;
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
previewTtsVoice(id) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
async fetchTtsFromHistory(history_item_id) {
|
||||
@@ -748,16 +746,16 @@ class CoquiTtsProvider {
|
||||
|
||||
async function initLocalModels() {
|
||||
if (!modules.includes('coqui-tts'))
|
||||
return
|
||||
return;
|
||||
|
||||
// Initialized local model once
|
||||
if (!coquiLocalModelsReceived) {
|
||||
let result = await CoquiTtsProvider.getLocalModelList();
|
||||
result = await result.json();
|
||||
|
||||
coquiLocalModels = result["models_list"];
|
||||
coquiLocalModels = result['models_list'];
|
||||
|
||||
$("#coqui_local_model_name").show();
|
||||
$('#coqui_local_model_name').show();
|
||||
$('#coqui_local_model_name')
|
||||
.find('option')
|
||||
.remove()
|
||||
@@ -766,7 +764,7 @@ async function initLocalModels() {
|
||||
.val('none');
|
||||
|
||||
for (const model_dataset of coquiLocalModels)
|
||||
$("#coqui_local_model_name").append(new Option(model_dataset, model_dataset));
|
||||
$('#coqui_local_model_name').append(new Option(model_dataset, model_dataset));
|
||||
|
||||
coquiLocalModelsReceived = true;
|
||||
}
|
||||
|
@@ -1,73 +1,73 @@
|
||||
import { getRequestHeaders } from "../../../script.js"
|
||||
import { getApiUrl } from "../../extensions.js"
|
||||
import { doExtrasFetch, modules } from "../../extensions.js"
|
||||
import { getPreviewString } from "./index.js"
|
||||
import { saveTtsProviderSettings } from "./index.js"
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { getApiUrl } from '../../extensions.js';
|
||||
import { doExtrasFetch, modules } from '../../extensions.js';
|
||||
import { getPreviewString } from './index.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { EdgeTtsProvider }
|
||||
export { EdgeTtsProvider };
|
||||
|
||||
class EdgeTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' . '
|
||||
audioElement = document.createElement('audio')
|
||||
settings;
|
||||
voices = [];
|
||||
separator = ' . ';
|
||||
audioElement = document.createElement('audio');
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
rate: 0,
|
||||
}
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `Microsoft Edge TTS Provider<br>
|
||||
<label for="edge_tts_rate">Rate: <span id="edge_tts_rate_output"></span></label>
|
||||
<input id="edge_tts_rate" type="range" value="${this.defaultSettings.rate}" min="-100" max="100" step="1" />`
|
||||
return html
|
||||
<input id="edge_tts_rate" type="range" value="${this.defaultSettings.rate}" min="-100" max="100" step="1" />`;
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
this.settings.rate = Number($('#edge_tts_rate').val());
|
||||
$('#edge_tts_rate_output').text(this.settings.rate);
|
||||
saveTtsProviderSettings()
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
$('#edge_tts_rate').val(this.settings.rate || 0);
|
||||
$('#edge_tts_rate_output').text(this.settings.rate || 0);
|
||||
$('#edge_tts_rate').on("input", () => {this.onSettingsChange()})
|
||||
await this.checkReady()
|
||||
$('#edge_tts_rate').on('input', () => {this.onSettingsChange();});
|
||||
await this.checkReady();
|
||||
|
||||
console.debug("EdgeTTS: Settings loaded")
|
||||
console.debug('EdgeTTS: Settings loaded');
|
||||
}
|
||||
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady(){
|
||||
throwIfModuleMissing()
|
||||
await this.fetchTtsVoiceObjects()
|
||||
throwIfModuleMissing();
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -76,39 +76,39 @@ class EdgeTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects()
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
voice => voice.name == voiceName
|
||||
)[0]
|
||||
voice => voice.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
return match
|
||||
return match;
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = `/api/edge-tts/list`
|
||||
const response = await doExtrasFetch(url)
|
||||
url.pathname = '/api/edge-tts/list';
|
||||
const response = await doExtrasFetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
let responseJson = await response.json()
|
||||
let responseJson = await response.json();
|
||||
responseJson = responseJson
|
||||
.sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
|
||||
.map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
|
||||
return responseJson
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,9 @@ class EdgeTtsProvider {
|
||||
this.audioElement.currentTime = 0;
|
||||
const voice = await this.getVoice(id);
|
||||
const text = getPreviewString(voice.lang);
|
||||
const response = await this.fetchTtsGeneration(text, id)
|
||||
const response = await this.fetchTtsGeneration(text, id);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const audio = await response.blob();
|
||||
@@ -129,34 +129,34 @@ class EdgeTtsProvider {
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
throwIfModuleMissing()
|
||||
throwIfModuleMissing();
|
||||
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = `/api/edge-tts/generate`;
|
||||
url.pathname = '/api/edge-tts/generate';
|
||||
const response = await doExtrasFetch(url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"voice": voiceId,
|
||||
"rate": Number(this.settings.rate),
|
||||
})
|
||||
}
|
||||
)
|
||||
'text': inputText,
|
||||
'voice': voiceId,
|
||||
'rate': Number(this.settings.rate),
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
}
|
||||
function throwIfModuleMissing() {
|
||||
if (!modules.includes('edge-tts')) {
|
||||
const message = `Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.`
|
||||
const message = 'Edge TTS module not loaded. Add edge-tts to enable-modules and restart the Extras API.';
|
||||
// toastr.error(message)
|
||||
throw new Error(message)
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import { saveTtsProviderSettings } from "./index.js"
|
||||
export { ElevenLabsTtsProvider }
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
export { ElevenLabsTtsProvider };
|
||||
|
||||
class ElevenLabsTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' ... ... ... '
|
||||
settings;
|
||||
voices = [];
|
||||
separator = ' ... ... ... ';
|
||||
|
||||
|
||||
defaultSettings = {
|
||||
stability: 0.75,
|
||||
similarity_boost: 0.75,
|
||||
apiKey: "",
|
||||
apiKey: '',
|
||||
model: 'eleven_monolingual_v1',
|
||||
voiceMap: {}
|
||||
}
|
||||
voiceMap: {},
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
@@ -36,28 +36,28 @@ class ElevenLabsTtsProvider {
|
||||
<label for="elevenlabs_tts_similarity_boost">Similarity Boost: <span id="elevenlabs_tts_similarity_boost_output"></span></label>
|
||||
<input id="elevenlabs_tts_similarity_boost" type="range" value="${this.defaultSettings.similarity_boost}" min="0" max="1" step="0.05" />
|
||||
</div>
|
||||
`
|
||||
return html
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
// Update dynamically
|
||||
this.settings.stability = $('#elevenlabs_tts_stability').val()
|
||||
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
|
||||
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val()
|
||||
this.settings.stability = $('#elevenlabs_tts_stability').val();
|
||||
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val();
|
||||
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val();
|
||||
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
|
||||
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
|
||||
saveTtsProviderSettings()
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
// Migrate old settings
|
||||
if (settings['multilingual'] !== undefined) {
|
||||
@@ -67,34 +67,34 @@ class ElevenLabsTtsProvider {
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
$('#elevenlabs_tts_stability').val(this.settings.stability)
|
||||
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost)
|
||||
$('#elevenlabs_tts_api_key').val(this.settings.apiKey)
|
||||
$('#elevenlabs_tts_stability').val(this.settings.stability);
|
||||
$('#elevenlabs_tts_similarity_boost').val(this.settings.similarity_boost);
|
||||
$('#elevenlabs_tts_api_key').val(this.settings.apiKey);
|
||||
$('#elevenlabs_tts_model').val(this.settings.model);
|
||||
$('#eleven_labs_connect').on('click', () => { this.onConnectClick() })
|
||||
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this))
|
||||
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this))
|
||||
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this))
|
||||
$('#eleven_labs_connect').on('click', () => { this.onConnectClick(); });
|
||||
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this));
|
||||
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this));
|
||||
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this));
|
||||
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
|
||||
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
|
||||
|
||||
try {
|
||||
await this.checkReady()
|
||||
console.debug("ElevenLabs: Settings loaded")
|
||||
await this.checkReady();
|
||||
console.debug('ElevenLabs: Settings loaded');
|
||||
} catch {
|
||||
console.debug("ElevenLabs: Settings loaded, but not ready")
|
||||
console.debug('ElevenLabs: Settings loaded, but not ready');
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady() {
|
||||
await this.fetchTtsVoiceObjects()
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
@@ -103,22 +103,21 @@ class ElevenLabsTtsProvider {
|
||||
async onConnectClick() {
|
||||
// Update on Apply click
|
||||
return await this.updateApiKey().catch((error) => {
|
||||
toastr.error(`ElevenLabs: ${error}`)
|
||||
})
|
||||
toastr.error(`ElevenLabs: ${error}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async updateApiKey() {
|
||||
// Using this call to validate API key
|
||||
this.settings.apiKey = $('#elevenlabs_tts_api_key').val()
|
||||
this.settings.apiKey = $('#elevenlabs_tts_api_key').val();
|
||||
|
||||
await this.fetchTtsVoiceObjects().catch(error => {
|
||||
throw `TTS API key validation failed`
|
||||
})
|
||||
this.settings.apiKey = this.settings.apiKey
|
||||
console.debug(`Saved new API_KEY: ${this.settings.apiKey}`)
|
||||
$('#tts_status').text('')
|
||||
this.onSettingsChange()
|
||||
throw 'TTS API key validation failed';
|
||||
});
|
||||
console.debug(`Saved new API_KEY: ${this.settings.apiKey}`);
|
||||
$('#tts_status').text('');
|
||||
this.onSettingsChange();
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -127,30 +126,30 @@ class ElevenLabsTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects()
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
elevenVoice => elevenVoice.name == voiceName
|
||||
)[0]
|
||||
elevenVoice => elevenVoice.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found in ElevenLabs account`
|
||||
throw `TTS Voice name ${voiceName} not found in ElevenLabs account`;
|
||||
}
|
||||
return match
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const historyId = await this.findTtsGenerationInHistory(text, voiceId)
|
||||
const historyId = await this.findTtsGenerationInHistory(text, voiceId);
|
||||
|
||||
let response
|
||||
let response;
|
||||
if (historyId) {
|
||||
console.debug(`Found existing TTS generation with id ${historyId}`)
|
||||
response = await this.fetchTtsFromHistory(historyId)
|
||||
console.debug(`Found existing TTS generation with id ${historyId}`);
|
||||
response = await this.fetchTtsFromHistory(historyId);
|
||||
} else {
|
||||
console.debug(`No existing TTS generation found, requesting new generation`)
|
||||
response = await this.fetchTtsGeneration(text, voiceId)
|
||||
console.debug('No existing TTS generation found, requesting new generation');
|
||||
response = await this.fetchTtsGeneration(text, voiceId);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
//###################//
|
||||
@@ -158,16 +157,16 @@ class ElevenLabsTtsProvider {
|
||||
//###################//
|
||||
|
||||
async findTtsGenerationInHistory(message, voiceId) {
|
||||
const ttsHistory = await this.fetchTtsHistory()
|
||||
const ttsHistory = await this.fetchTtsHistory();
|
||||
for (const history of ttsHistory) {
|
||||
const text = history.text
|
||||
const itemId = history.history_item_id
|
||||
const text = history.text;
|
||||
const itemId = history.history_item_id;
|
||||
if (message === text && history.voice_id == voiceId) {
|
||||
console.info(`Existing TTS history item ${itemId} found: ${text} `)
|
||||
return itemId
|
||||
console.info(`Existing TTS history item ${itemId} found: ${text} `);
|
||||
return itemId;
|
||||
}
|
||||
}
|
||||
return ''
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@@ -176,44 +175,44 @@ class ElevenLabsTtsProvider {
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
const headers = {
|
||||
'xi-api-key': this.settings.apiKey
|
||||
}
|
||||
const response = await fetch(`https://api.elevenlabs.io/v1/voices`, {
|
||||
headers: headers
|
||||
})
|
||||
'xi-api-key': this.settings.apiKey,
|
||||
};
|
||||
const response = await fetch('https://api.elevenlabs.io/v1/voices', {
|
||||
headers: headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson.voices
|
||||
const responseJson = await response.json();
|
||||
return responseJson.voices;
|
||||
}
|
||||
|
||||
async fetchTtsVoiceSettings() {
|
||||
const headers = {
|
||||
'xi-api-key': this.settings.apiKey
|
||||
}
|
||||
'xi-api-key': this.settings.apiKey,
|
||||
};
|
||||
const response = await fetch(
|
||||
`https://api.elevenlabs.io/v1/voices/settings/default`,
|
||||
'https://api.elevenlabs.io/v1/voices/settings/default',
|
||||
{
|
||||
headers: headers
|
||||
}
|
||||
)
|
||||
headers: headers,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response.json()
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(text, voiceId) {
|
||||
let model = this.settings.model ?? "eleven_monolingual_v1";
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`)
|
||||
let model = this.settings.model ?? 'eleven_monolingual_v1';
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}, model ${model}`);
|
||||
const response = await fetch(
|
||||
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'xi-api-key': this.settings.apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model_id: model,
|
||||
@@ -222,43 +221,43 @@ class ElevenLabsTtsProvider {
|
||||
stability: Number(this.settings.stability),
|
||||
similarity_boost: Number(this.settings.similarity_boost),
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchTtsFromHistory(history_item_id) {
|
||||
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`)
|
||||
console.info(`Fetched existing TTS with history_item_id ${history_item_id}`);
|
||||
const response = await fetch(
|
||||
`https://api.elevenlabs.io/v1/history/${history_item_id}/audio`,
|
||||
{
|
||||
headers: {
|
||||
'xi-api-key': this.settings.apiKey
|
||||
}
|
||||
}
|
||||
)
|
||||
'xi-api-key': this.settings.apiKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchTtsHistory() {
|
||||
const headers = {
|
||||
'xi-api-key': this.settings.apiKey
|
||||
}
|
||||
const response = await fetch(`https://api.elevenlabs.io/v1/history`, {
|
||||
headers: headers
|
||||
})
|
||||
'xi-api-key': this.settings.apiKey,
|
||||
};
|
||||
const response = await fetch('https://api.elevenlabs.io/v1/history', {
|
||||
headers: headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson.history
|
||||
const responseJson = await response.json();
|
||||
return responseJson.history;
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,34 @@
|
||||
import { getRequestHeaders, callPopup } from "../../../script.js"
|
||||
import { getPreviewString, saveTtsProviderSettings } from "./index.js"
|
||||
import { initVoiceMap } from "./index.js"
|
||||
import { getRequestHeaders, callPopup } from '../../../script.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
import { initVoiceMap } from './index.js';
|
||||
|
||||
export { NovelTtsProvider }
|
||||
export { NovelTtsProvider };
|
||||
|
||||
class NovelTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' . '
|
||||
audioElement = document.createElement('audio')
|
||||
settings;
|
||||
voices = [];
|
||||
separator = ' . ';
|
||||
audioElement = document.createElement('audio');
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
customVoices: []
|
||||
customVoices: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform any text processing before passing to TTS engine.
|
||||
* @param {string} text Input text
|
||||
* @returns {string} Processed text
|
||||
*/
|
||||
processText(text) {
|
||||
// Novel reads tilde as a word. Replace with full stop
|
||||
text = text.replace(/~/g, '.');
|
||||
return text;
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
@@ -41,68 +53,68 @@ class NovelTtsProvider {
|
||||
|
||||
|
||||
// Add a new Novel custom voice to provider
|
||||
async addCustomVoice(){
|
||||
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input')
|
||||
this.settings.customVoices.push(voiceName)
|
||||
this.populateCustomVoices()
|
||||
initVoiceMap() // Update TTS extension voiceMap
|
||||
saveTtsProviderSettings()
|
||||
async addCustomVoice() {
|
||||
const voiceName = await callPopup('<h3>Custom Voice name:</h3>', 'input');
|
||||
this.settings.customVoices.push(voiceName);
|
||||
this.populateCustomVoices();
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
// Delete selected custom voice from provider
|
||||
deleteCustomVoice() {
|
||||
const selected = $("#tts-novel-custom-voices-select").find(':selected').val();
|
||||
const selected = $('#tts-novel-custom-voices-select').find(':selected').val();
|
||||
const voiceIndex = this.settings.customVoices.indexOf(selected);
|
||||
|
||||
if (voiceIndex !== -1) {
|
||||
this.settings.customVoices.splice(voiceIndex, 1);
|
||||
}
|
||||
this.populateCustomVoices()
|
||||
initVoiceMap() // Update TTS extension voiceMap
|
||||
saveTtsProviderSettings()
|
||||
this.populateCustomVoices();
|
||||
initVoiceMap(); // Update TTS extension voiceMap
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
// Create the UI dropdown list of voices in provider
|
||||
populateCustomVoices(){
|
||||
let voiceSelect = $("#tts-novel-custom-voices-select")
|
||||
voiceSelect.empty()
|
||||
populateCustomVoices() {
|
||||
let voiceSelect = $('#tts-novel-custom-voices-select');
|
||||
voiceSelect.empty();
|
||||
this.settings.customVoices.forEach(voice => {
|
||||
voiceSelect.append(`<option>${voice}</option>`)
|
||||
})
|
||||
voiceSelect.append(`<option>${voice}</option>`);
|
||||
});
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Populate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
$("#tts-novel-custom-voices-add").on('click', () => (this.addCustomVoice()))
|
||||
$("#tts-novel-custom-voices-delete").on('click',() => (this.deleteCustomVoice()))
|
||||
$('#tts-novel-custom-voices-add').on('click', () => (this.addCustomVoice()));
|
||||
$('#tts-novel-custom-voices-delete').on('click', () => (this.deleteCustomVoice()));
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
this.populateCustomVoices()
|
||||
await this.checkReady()
|
||||
console.debug("NovelTTS: Settings loaded")
|
||||
this.populateCustomVoices();
|
||||
await this.checkReady();
|
||||
console.debug('NovelTTS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
// Doesnt really do much for Novel, not seeing a good way to test this at the moment.
|
||||
async checkReady(){
|
||||
await this.fetchTtsVoiceObjects()
|
||||
async checkReady() {
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -111,15 +123,15 @@ class NovelTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (!voiceName) {
|
||||
throw `TTS Voice name not provided`
|
||||
throw 'TTS Voice name not provided';
|
||||
}
|
||||
|
||||
return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false}
|
||||
return { name: voiceName, voice_id: voiceName, lang: 'en-US', preview_url: false };
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
@@ -144,9 +156,9 @@ class NovelTtsProvider {
|
||||
|
||||
// Add in custom voices to the map
|
||||
let addVoices = this.settings.customVoices.map(voice =>
|
||||
({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false })
|
||||
)
|
||||
voices = voices.concat(addVoices)
|
||||
({ name: voice, voice_id: voice, lang: 'en-US', preview_url: false }),
|
||||
);
|
||||
voices = voices.concat(addVoices);
|
||||
|
||||
return voices;
|
||||
}
|
||||
@@ -156,10 +168,10 @@ class NovelTtsProvider {
|
||||
this.audioElement.pause();
|
||||
this.audioElement.currentTime = 0;
|
||||
|
||||
const text = getPreviewString('en-US')
|
||||
const response = await this.fetchTtsGeneration(text, id)
|
||||
const text = getPreviewString('en-US');
|
||||
const response = await this.fetchTtsGeneration(text, id);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const audio = await response.blob();
|
||||
@@ -168,22 +180,26 @@ class NovelTtsProvider {
|
||||
this.audioElement.play();
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
const response = await fetch(`/api/novelai/generate-voice`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"voice": voiceId,
|
||||
})
|
||||
async* fetchTtsGeneration(inputText, voiceId) {
|
||||
const MAX_LENGTH = 1000;
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
const chunks = splitRecursive(inputText, MAX_LENGTH);
|
||||
for (const chunk of chunks) {
|
||||
const response = await fetch('/api/novelai/generate-voice',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
'text': chunk,
|
||||
'voice': voiceId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
)
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
yield response;
|
||||
}
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { getRequestHeaders } from "../../../script.js"
|
||||
import { saveTtsProviderSettings } from "./index.js";
|
||||
import { getRequestHeaders } from '../../../script.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { OpenAITtsProvider }
|
||||
export { OpenAITtsProvider };
|
||||
|
||||
class OpenAITtsProvider {
|
||||
static voices = [
|
||||
@@ -13,17 +13,17 @@ class OpenAITtsProvider {
|
||||
{ name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' },
|
||||
];
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' . '
|
||||
audioElement = document.createElement('audio')
|
||||
settings;
|
||||
voices = [];
|
||||
separator = ' . ';
|
||||
audioElement = document.createElement('audio');
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
customVoices: [],
|
||||
model: 'tts-1',
|
||||
speed: 1,
|
||||
}
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
@@ -44,7 +44,7 @@ class OpenAITtsProvider {
|
||||
</div>
|
||||
<div>
|
||||
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
|
||||
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.05">
|
||||
</div>`;
|
||||
return html;
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class OpenAITtsProvider {
|
||||
async loadSettings(settings) {
|
||||
// Populate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
@@ -79,7 +79,7 @@ class OpenAITtsProvider {
|
||||
$('#openai-tts-speed-output').text(this.settings.speed);
|
||||
|
||||
await this.checkReady();
|
||||
console.debug("OpenAI TTS: Settings loaded");
|
||||
console.debug('OpenAI TTS: Settings loaded');
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
@@ -100,21 +100,21 @@ class OpenAITtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (!voiceName) {
|
||||
throw `TTS Voice name not provided`
|
||||
throw 'TTS Voice name not provided';
|
||||
}
|
||||
|
||||
const voice = OpenAITtsProvider.voices.find(voice => voice.voice_id === voiceName || voice.name === voiceName);
|
||||
|
||||
if (!voice) {
|
||||
throw `TTS Voice not found: ${voiceName}`
|
||||
throw `TTS Voice not found: ${voiceName}`;
|
||||
}
|
||||
|
||||
return voice;
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchTtsVoiceObjects() {
|
||||
@@ -126,15 +126,15 @@ class OpenAITtsProvider {
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
const response = await fetch(`/api/openai/generate-voice`, {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
const response = await fetch('/api/openai/generate-voice', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"voice": voiceId,
|
||||
"model": this.settings.model,
|
||||
"speed": this.settings.speed,
|
||||
'text': inputText,
|
||||
'voice': voiceId,
|
||||
'model': this.settings.model,
|
||||
'speed': this.settings.speed,
|
||||
}),
|
||||
});
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# Provider Requirements.
|
||||
# Provider Requirements.
|
||||
Because I don't know how, or if you can, and/or maybe I am just too lazy to implement interfaces in JS, here's the requirements of a provider that the extension needs to operate.
|
||||
|
||||
### class YourTtsProvider
|
||||
#### Required
|
||||
#### Required
|
||||
Exported for use in extension index.js, and added to providers list in index.js
|
||||
1. generateTts(text, voiceId)
|
||||
2. fetchTtsVoiceObjects()
|
||||
@@ -13,8 +13,9 @@ Exported for use in extension index.js, and added to providers list in index.js
|
||||
7. settingsHtml field
|
||||
|
||||
#### Optional
|
||||
1. previewTtsVoice()
|
||||
1. previewTtsVoice()
|
||||
2. separator field
|
||||
3. processText(text)
|
||||
|
||||
# Requirement Descriptions
|
||||
### generateTts(text, voiceId)
|
||||
@@ -49,14 +50,14 @@ Return without error to let TTS extension know that the provider is ready.
|
||||
Return an error to block the main TTS extension for initializing the provider and UI. The error will be put in the TTS extension UI directly.
|
||||
|
||||
### loadSettings(settingsObject)
|
||||
Required.
|
||||
Required.
|
||||
Handle the input settings from the TTS extension on provider load.
|
||||
Put code in here to load your provider settings.
|
||||
|
||||
### settings field
|
||||
Required, used for storing any provider state that needs to be saved.
|
||||
Anything stored in this field is automatically persisted under extension_settings[providerName] by the main extension in `saveTtsProviderSettings()`, as well as loaded when the provider is selected in `loadTtsProvider(provider)`.
|
||||
TTS extension doesn't expect any specific contents.
|
||||
TTS extension doesn't expect any specific contents.
|
||||
|
||||
### settingsHtml field
|
||||
Required, injected into the TTS extension UI. Besides adding it, not relied on by TTS extension directly.
|
||||
@@ -68,4 +69,8 @@ Function to handle playing previews of voice samples if no direct preview_url is
|
||||
### separator field
|
||||
Optional.
|
||||
Used when narrate quoted text is enabled.
|
||||
Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...`
|
||||
Defines the string of characters used to introduce separation between between the groups of extracted quoted text sent to the provider. The provider will use this to introduce pauses by default using `...`
|
||||
|
||||
### processText(text)
|
||||
Optional.
|
||||
A function applied to the input text before passing it to the TTS generator. Can be async.
|
||||
|
@@ -1,22 +1,22 @@
|
||||
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
|
||||
import { saveTtsProviderSettings } from "./index.js"
|
||||
import { doExtrasFetch, getApiUrl, modules } from '../../extensions.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { SileroTtsProvider }
|
||||
export { SileroTtsProvider };
|
||||
|
||||
class SileroTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
ready = false
|
||||
voices = []
|
||||
separator = ' .. '
|
||||
settings;
|
||||
ready = false;
|
||||
voices = [];
|
||||
separator = ' .. ';
|
||||
|
||||
defaultSettings = {
|
||||
provider_endpoint: "http://localhost:8001/tts",
|
||||
voiceMap: {}
|
||||
}
|
||||
provider_endpoint: 'http://localhost:8001/tts',
|
||||
voiceMap: {},
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
@@ -24,30 +24,31 @@ class SileroTtsProvider {
|
||||
<input id="silero_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
|
||||
<span>
|
||||
<span>Use <a target="_blank" href="https://github.com/SillyTavern/SillyTavern-extras">SillyTavern Extras API</a> or <a target="_blank" href="https://github.com/ouoertheo/silero-api-server">Silero TTS Server</a>.</span>
|
||||
`
|
||||
return html
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
// Used when provider settings are updated from UI
|
||||
this.settings.provider_endpoint = $('#silero_tts_endpoint').val()
|
||||
saveTtsProviderSettings()
|
||||
this.settings.provider_endpoint = $('#silero_tts_endpoint').val();
|
||||
saveTtsProviderSettings();
|
||||
this.refreshSession();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings){
|
||||
if (key in this.settings){
|
||||
this.settings[key] = settings[key]
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,21 +63,26 @@ class SileroTtsProvider {
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
$('#silero_tts_endpoint').val(this.settings.provider_endpoint)
|
||||
$('#silero_tts_endpoint').on("input", () => {this.onSettingsChange()})
|
||||
$('#silero_tts_endpoint').val(this.settings.provider_endpoint);
|
||||
$('#silero_tts_endpoint').on('input', () => { this.onSettingsChange(); });
|
||||
this.refreshSession();
|
||||
|
||||
await this.checkReady()
|
||||
await this.checkReady();
|
||||
|
||||
console.debug("SileroTTS: Settings loaded")
|
||||
console.debug('SileroTTS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady(){
|
||||
await this.fetchTtsVoiceObjects()
|
||||
async checkReady() {
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
async refreshSession() {
|
||||
await this.initSession();
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -85,55 +91,81 @@ class SileroTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects()
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
sileroVoice => sileroVoice.name == voiceName
|
||||
)[0]
|
||||
sileroVoice => sileroVoice.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
return match
|
||||
return match;
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId){
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
|
||||
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson
|
||||
const responseJson = await response.json();
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(inputText, voiceId) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`)
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
const response = await doExtrasFetch(
|
||||
`${this.settings.provider_endpoint}/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23
|
||||
'Cache-Control': 'no-cache', // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"speaker": voiceId
|
||||
})
|
||||
}
|
||||
)
|
||||
'text': inputText,
|
||||
'speaker': voiceId,
|
||||
'session': 'sillytavern',
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
async initSession() {
|
||||
console.info('Silero TTS: requesting new session');
|
||||
try {
|
||||
const response = await doExtrasFetch(
|
||||
`${this.settings.provider_endpoint}/session`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
'path': 'sillytavern',
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok && response.status !== 404) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.info('Silero TTS: endpoint not available', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Interface not used by Silero TTS
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { isMobile } from "../../RossAscends-mods.js";
|
||||
import { getPreviewString } from "./index.js";
|
||||
import { isMobile } from '../../RossAscends-mods.js';
|
||||
import { getPreviewString } from './index.js';
|
||||
import { talkingAnimation } from './index.js';
|
||||
import { saveTtsProviderSettings } from "./index.js"
|
||||
export { SystemTtsProvider }
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
export { SystemTtsProvider };
|
||||
|
||||
/**
|
||||
* Chunkify
|
||||
@@ -46,7 +46,7 @@ var speechUtteranceChunker = function (utt, settings, callback) {
|
||||
newUtt = new SpeechSynthesisUtterance(chunk);
|
||||
var x;
|
||||
for (x in utt) {
|
||||
if (utt.hasOwnProperty(x) && x !== 'text') {
|
||||
if (Object.hasOwn(utt, x) && x !== 'text') {
|
||||
newUtt[x] = utt[x];
|
||||
}
|
||||
}
|
||||
@@ -79,20 +79,20 @@ class SystemTtsProvider {
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
ready = false
|
||||
voices = []
|
||||
separator = ' ... '
|
||||
settings;
|
||||
ready = false;
|
||||
voices = [];
|
||||
separator = ' ... ';
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
rate: 1,
|
||||
pitch: 1,
|
||||
}
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return "Your browser or operating system doesn't support speech synthesis";
|
||||
return 'Your browser or operating system doesn\'t support speech synthesis';
|
||||
}
|
||||
|
||||
return `<p>Uses the voices provided by your operating system</p>
|
||||
@@ -107,13 +107,13 @@ class SystemTtsProvider {
|
||||
this.settings.pitch = Number($('#system_tts_pitch').val());
|
||||
$('#system_tts_pitch_output').text(this.settings.pitch);
|
||||
$('#system_tts_rate_output').text(this.settings.rate);
|
||||
saveTtsProviderSettings()
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Populate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings");
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// iOS should only allows speech synthesis trigged by user interaction
|
||||
@@ -146,21 +146,21 @@ class SystemTtsProvider {
|
||||
$('#system_tts_pitch').val(this.settings.pitch || this.defaultSettings.pitch);
|
||||
|
||||
// Trigger updates
|
||||
$('#system_tts_rate').on("input", () => { this.onSettingsChange() })
|
||||
$('#system_tts_rate').on("input", () => { this.onSettingsChange() })
|
||||
$('#system_tts_rate').on('input', () => { this.onSettingsChange(); });
|
||||
$('#system_tts_rate').on('input', () => { this.onSettingsChange(); });
|
||||
|
||||
$('#system_tts_pitch_output').text(this.settings.pitch);
|
||||
$('#system_tts_rate_output').text(this.settings.rate);
|
||||
console.debug("SystemTTS: Settings loaded");
|
||||
console.debug('SystemTTS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady() {
|
||||
await this.fetchTtsVoiceObjects()
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -191,7 +191,7 @@ class SystemTtsProvider {
|
||||
const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);
|
||||
|
||||
if (!voice) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
throw `TTS Voice id ${voiceId} not found`;
|
||||
}
|
||||
|
||||
speechSynthesis.cancel();
|
||||
@@ -205,14 +205,14 @@ class SystemTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (!('speechSynthesis' in window)) {
|
||||
return { voice_id: null }
|
||||
return { voice_id: null };
|
||||
}
|
||||
|
||||
const voices = speechSynthesis.getVoices();
|
||||
const match = voices.find(x => x.name == voiceName);
|
||||
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
|
||||
return { voice_id: match.voiceURI, name: match.name };
|
||||
|
@@ -1,17 +1,32 @@
|
||||
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
|
||||
import { saveTtsProviderSettings } from "./index.js"
|
||||
import { doExtrasFetch, getApiUrl, modules } from '../../extensions.js';
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { XTTSTtsProvider }
|
||||
export { XTTSTtsProvider };
|
||||
|
||||
class XTTSTtsProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings
|
||||
ready = false
|
||||
voices = []
|
||||
separator = '. '
|
||||
settings;
|
||||
ready = false;
|
||||
voices = [];
|
||||
separator = '. ';
|
||||
|
||||
/**
|
||||
* Perform any text processing before passing to TTS engine.
|
||||
* @param {string} text Input text
|
||||
* @returns {string} Processed text
|
||||
*/
|
||||
processText(text) {
|
||||
// Replace fancy ellipsis with "..."
|
||||
text = text.replace(/…/g, '...');
|
||||
// Remove quotes
|
||||
text = text.replace(/["“”‘’]/g, '');
|
||||
// Replace multiple "." with single "."
|
||||
text = text.replace(/\.+/g, '.');
|
||||
return text;
|
||||
}
|
||||
|
||||
languageLabels = {
|
||||
"Arabic": "ar",
|
||||
@@ -32,7 +47,7 @@ class XTTSTtsProvider {
|
||||
"Hungarian": "hu",
|
||||
"Hindi": "hi",
|
||||
}
|
||||
|
||||
|
||||
defaultSettings = {
|
||||
provider_endpoint: "http://localhost:8020",
|
||||
language: "en",
|
||||
@@ -58,7 +73,7 @@ class XTTSTtsProvider {
|
||||
|
||||
if (this.languageLabels[language] == this.settings?.language) {
|
||||
html += `<option value="${this.languageLabels[language]}" selected="selected">${language}</option>`;
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
html += `<option value="${this.languageLabels[language]}">${language}</option>`;
|
||||
@@ -94,7 +109,6 @@ class XTTSTtsProvider {
|
||||
|
||||
<label for="xtts_tts_endpoint">Provider Endpoint:</label>
|
||||
<input id="xtts_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
|
||||
|
||||
<label for="xtts_tts_streaming" class="checkbox_label">
|
||||
<input id="xtts_tts_streaming" type="checkbox" />
|
||||
<span>Streaming <small>(RVC not supported)</small></span>
|
||||
@@ -124,7 +138,7 @@ class XTTSTtsProvider {
|
||||
this.settings.stream_chunk_size = $('#xtts_stream_chunk_size').val();
|
||||
this.settings.enable_text_splitting = $('#xtts_enable_text_splitting').is(':checked');
|
||||
this.settings.streaming = $('#xtts_tts_streaming').is(':checked');
|
||||
|
||||
|
||||
// Update the UI to reflect changes
|
||||
$('#xtts_tts_speed_output').text(this.settings.speed);
|
||||
$('#xtts_tts_temperature_output').text(this.settings.temperature);
|
||||
@@ -133,7 +147,7 @@ class XTTSTtsProvider {
|
||||
$('#xtts_top_k_output').text(this.settings.top_k);
|
||||
$('#xtts_top_p_output').text(this.settings.top_p);
|
||||
$('#xtts_stream_chunk_size_output').text(this.settings.stream_chunk_size);
|
||||
|
||||
|
||||
saveTtsProviderSettings()
|
||||
this.changeTTSSetting()
|
||||
}
|
||||
@@ -142,17 +156,17 @@ class XTTSTtsProvider {
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info("Using default TTS Provider settings")
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key]
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`
|
||||
throw `Invalid setting passed to TTS Provider: ${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,10 +181,12 @@ class XTTSTtsProvider {
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
$('#xtts_tts_endpoint').val(this.settings.provider_endpoint)
|
||||
$('#xtts_tts_endpoint').on("input", () => { this.onSettingsChange() })
|
||||
$('#xtts_api_language').val(this.settings.language)
|
||||
$('#xtts_api_language').on("change", () => { this.onSettingsChange() })
|
||||
$('#xtts_tts_endpoint').val(this.settings.provider_endpoint);
|
||||
$('#xtts_tts_endpoint').on('input', () => { this.onSettingsChange(); });
|
||||
$('#xtts_api_language').val(this.settings.language);
|
||||
$('#xtts_api_language').on('change', () => { this.onSettingsChange(); });
|
||||
$('#xtts_tts_streaming').prop('checked', this.settings.streaming);
|
||||
$('#xtts_tts_streaming').on('change', () => { this.onSettingsChange(); });
|
||||
|
||||
// Set initial values from the settings
|
||||
$('#xtts_speed').val(this.settings.speed);
|
||||
@@ -194,19 +210,18 @@ class XTTSTtsProvider {
|
||||
$('#xtts_tts_streaming').prop('checked', this.settings.streaming);
|
||||
$('#xtts_tts_streaming').on('change', () => { this.onSettingsChange(); });
|
||||
|
||||
await this.checkReady()
|
||||
await this.checkReady();
|
||||
|
||||
console.debug("XTTS: Settings loaded")
|
||||
console.debug('XTTS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady() {
|
||||
|
||||
const response = await this.fetchTtsVoiceObjects()
|
||||
await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
@@ -215,32 +230,32 @@ class XTTSTtsProvider {
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects()
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
const match = this.voices.filter(
|
||||
XTTSVoice => XTTSVoice.name == voiceName
|
||||
)[0]
|
||||
XTTSVoice => XTTSVoice.name == voiceName,
|
||||
)[0];
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
return match
|
||||
return match;
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId)
|
||||
return response
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`)
|
||||
const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`)
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
|
||||
}
|
||||
const responseJson = await response.json()
|
||||
return responseJson
|
||||
const responseJson = await response.json();
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
// Each time a parameter is changed, we change the configuration
|
||||
@@ -285,20 +300,20 @@ class XTTSTtsProvider {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23
|
||||
'Cache-Control': 'no-cache', // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"text": inputText,
|
||||
"speaker_wav": voiceId,
|
||||
"language": this.settings.language
|
||||
})
|
||||
}
|
||||
)
|
||||
'text': inputText,
|
||||
'speaker_wav': voiceId,
|
||||
'language': this.settings.language,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response
|
||||
return response;
|
||||
}
|
||||
|
||||
// Interface not used by XTTS TTS
|
||||
|
@@ -1,29 +1,40 @@
|
||||
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from "../../../script.js";
|
||||
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
|
||||
import { collapseNewlines, power_user, ui_mode } from "../../power-user.js";
|
||||
import { SECRET_KEYS, secret_state } from "../../secrets.js";
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique } from "../../utils.js";
|
||||
import { eventSource, event_types, extension_prompt_types, getCurrentChatId, getRequestHeaders, is_send_press, saveSettingsDebounced, setExtensionPrompt, substituteParams } from '../../../script.js';
|
||||
import { ModuleWorkerWrapper, extension_settings, getContext, renderExtensionTemplate } from '../../extensions.js';
|
||||
import { collapseNewlines } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
|
||||
|
||||
const MODULE_NAME = 'vectors';
|
||||
|
||||
export const EXTENSION_PROMPT_TAG = '3_vectors';
|
||||
|
||||
const settings = {
|
||||
enabled: false,
|
||||
// For both
|
||||
source: 'transformers',
|
||||
template: `Past events: {{text}}`,
|
||||
include_wi: false,
|
||||
|
||||
// For chats
|
||||
enabled_chats: false,
|
||||
template: 'Past events: {{text}}',
|
||||
depth: 2,
|
||||
position: extension_prompt_types.IN_PROMPT,
|
||||
protect: 5,
|
||||
insert: 3,
|
||||
query: 2,
|
||||
message_chunk_size: 400,
|
||||
|
||||
// For files
|
||||
enabled_files: false,
|
||||
size_threshold: 10,
|
||||
chunk_size: 5000,
|
||||
chunk_count: 2,
|
||||
};
|
||||
|
||||
const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
|
||||
|
||||
async function onVectorizeAllClick() {
|
||||
try {
|
||||
if (!settings.enabled) {
|
||||
if (!settings.enabled_chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,8 +88,31 @@ async function onVectorizeAllClick() {
|
||||
|
||||
let syncBlocked = false;
|
||||
|
||||
/**
|
||||
* Splits messages into chunks before inserting them into the vector index.
|
||||
* @param {object[]} items Array of vector items
|
||||
* @returns {object[]} Array of vector items (possibly chunked)
|
||||
*/
|
||||
function splitByChunks(items) {
|
||||
if (settings.message_chunk_size <= 0) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const chunkedItems = [];
|
||||
|
||||
for (const item of items) {
|
||||
const chunks = splitRecursive(item.text, settings.message_chunk_size);
|
||||
for (const chunk of chunks) {
|
||||
const chunkedItem = { ...item, text: chunk };
|
||||
chunkedItems.push(chunkedItem);
|
||||
}
|
||||
}
|
||||
|
||||
return chunkedItems;
|
||||
}
|
||||
|
||||
async function synchronizeChat(batchSize = 5) {
|
||||
if (!settings.enabled) {
|
||||
if (!settings.enabled_chats) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -99,15 +133,16 @@ async function synchronizeChat(batchSize = 5) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const hashedMessages = context.chat.filter(x => !x.is_system).map(x => ({ text: String(x.mes), hash: getStringHash(x.mes) }));
|
||||
const hashedMessages = context.chat.filter(x => !x.is_system).map(x => ({ text: String(x.mes), hash: getStringHash(x.mes), index: context.chat.indexOf(x) }));
|
||||
const hashesInCollection = await getSavedHashes(chatId);
|
||||
|
||||
const newVectorItems = hashedMessages.filter(x => !hashesInCollection.includes(x.hash));
|
||||
const deletedHashes = hashesInCollection.filter(x => !hashedMessages.some(y => y.hash === x));
|
||||
|
||||
if (newVectorItems.length > 0) {
|
||||
const chunkedBatch = splitByChunks(newVectorItems.slice(0, batchSize));
|
||||
console.log(`Vectors: Found ${newVectorItems.length} new items. Processing ${batchSize}...`);
|
||||
await insertVectorItems(chatId, newVectorItems.slice(0, batchSize));
|
||||
await insertVectorItems(chatId, chunkedBatch);
|
||||
}
|
||||
|
||||
if (deletedHashes.length > 0) {
|
||||
@@ -136,7 +171,7 @@ const hashCache = {};
|
||||
*/
|
||||
function getStringHash(str) {
|
||||
// Check if the hash is already in the cache
|
||||
if (hashCache.hasOwnProperty(str)) {
|
||||
if (Object.hasOwn(hashCache, str)) {
|
||||
return hashCache[str];
|
||||
}
|
||||
|
||||
@@ -149,6 +184,95 @@ function getStringHash(str) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves files from the chat and inserts them into the vector index.
|
||||
* @param {object[]} chat Array of chat messages
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function processFiles(chat) {
|
||||
try {
|
||||
if (!settings.enabled_files) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const message of chat) {
|
||||
// Message has no file
|
||||
if (!message?.extra?.file) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trim file inserted by the script
|
||||
const fileText = String(message.mes)
|
||||
.substring(0, message.extra.fileLength).trim()
|
||||
.replace(/^```/, '').replace(/```$/, '').trim();
|
||||
|
||||
// Convert kilobytes to string length
|
||||
const thresholdLength = settings.size_threshold * 1024;
|
||||
|
||||
// File is too small
|
||||
if (fileText.length < thresholdLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
message.mes = message.mes.substring(message.extra.fileLength);
|
||||
|
||||
const fileName = message.extra.file.name;
|
||||
const collectionId = `file_${getStringHash(fileName)}`;
|
||||
const hashesInCollection = await getSavedHashes(collectionId);
|
||||
|
||||
// File is already in the collection
|
||||
if (!hashesInCollection.length) {
|
||||
await vectorizeFile(fileText, fileName, collectionId);
|
||||
}
|
||||
|
||||
const queryText = getQueryText(chat);
|
||||
const fileChunks = await retrieveFileChunks(queryText, collectionId);
|
||||
|
||||
// Wrap it back in a code block
|
||||
message.mes = `\`\`\`\n${fileChunks}\n\`\`\`\n\n${message.mes}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to retrieve files', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves file chunks from the vector index and inserts them into the chat.
|
||||
* @param {string} queryText Text to query
|
||||
* @param {string} collectionId File collection ID
|
||||
* @returns {Promise<string>} Retrieved file text
|
||||
*/
|
||||
async function retrieveFileChunks(queryText, collectionId) {
|
||||
console.debug(`Vectors: Retrieving file chunks for collection ${collectionId}`, queryText);
|
||||
const queryResults = await queryCollection(collectionId, queryText, settings.chunk_count);
|
||||
console.debug(`Vectors: Retrieved ${queryResults.hashes.length} file chunks for collection ${collectionId}`, queryResults);
|
||||
const metadata = queryResults.metadata.filter(x => x.text).sort((a, b) => a.index - b.index).map(x => x.text);
|
||||
const fileText = metadata.join('\n');
|
||||
|
||||
return fileText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vectorizes a file and inserts it into the vector index.
|
||||
* @param {string} fileText File text
|
||||
* @param {string} fileName File name
|
||||
* @param {string} collectionId File collection ID
|
||||
*/
|
||||
async function vectorizeFile(fileText, fileName, collectionId) {
|
||||
try {
|
||||
toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
|
||||
const chunks = splitRecursive(fileText, settings.chunk_size);
|
||||
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks);
|
||||
|
||||
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
|
||||
await insertVectorItems(collectionId, items);
|
||||
|
||||
console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to vectorize file', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the most relevant messages from the chat and displays them in the extension prompt
|
||||
* @param {object[]} chat Array of chat messages
|
||||
@@ -156,9 +280,13 @@ function getStringHash(str) {
|
||||
async function rearrangeChat(chat) {
|
||||
try {
|
||||
// Clear the extension prompt
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0, settings.include_wi);
|
||||
|
||||
if (!settings.enabled) {
|
||||
if (settings.enabled_files) {
|
||||
await processFiles(chat);
|
||||
}
|
||||
|
||||
if (!settings.enabled_chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,7 +310,8 @@ async function rearrangeChat(chat) {
|
||||
}
|
||||
|
||||
// Get the most relevant messages, excluding the last few
|
||||
const queryHashes = (await queryCollection(chatId, queryText, settings.insert)).filter(onlyUnique);
|
||||
const queryResults = await queryCollection(chatId, queryText, settings.query);
|
||||
const queryHashes = queryResults.hashes.filter(onlyUnique);
|
||||
const queriedMessages = [];
|
||||
const insertedHashes = new Set();
|
||||
const retainMessages = chat.slice(-settings.protect);
|
||||
@@ -216,7 +345,7 @@ async function rearrangeChat(chat) {
|
||||
|
||||
// Format queried messages into a single string
|
||||
const insertedText = getPromptText(queriedMessages);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth);
|
||||
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth, settings.include_wi);
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to rearrange chat', error);
|
||||
}
|
||||
@@ -290,7 +419,8 @@ async function getSavedHashes(collectionId) {
|
||||
*/
|
||||
async function insertVectorItems(collectionId, items) {
|
||||
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.PALM]) {
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI]) {
|
||||
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
|
||||
}
|
||||
|
||||
@@ -335,7 +465,7 @@ async function deleteVectorItems(collectionId, hashes) {
|
||||
* @param {string} collectionId - The collection to query
|
||||
* @param {string} searchText - The text to query
|
||||
* @param {number} topK - The number of results to return
|
||||
* @returns {Promise<number[]>} - Hashes of the results
|
||||
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
|
||||
*/
|
||||
async function queryCollection(collectionId, searchText, topK) {
|
||||
const response = await fetch('/api/vector/query', {
|
||||
@@ -359,7 +489,7 @@ async function queryCollection(collectionId, searchText, topK) {
|
||||
|
||||
async function purgeVectorIndex(collectionId) {
|
||||
try {
|
||||
if (!settings.enabled) {
|
||||
if (!settings.enabled_chats) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -382,19 +512,73 @@ async function purgeVectorIndex(collectionId) {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
$('#vectors_files_settings').toggle(!!settings.enabled_files);
|
||||
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
|
||||
}
|
||||
|
||||
async function onPurgeClick() {
|
||||
const chatId = getCurrentChatId();
|
||||
if (!chatId) {
|
||||
toastr.info('No chat selected', 'Purge aborted');
|
||||
return;
|
||||
}
|
||||
await purgeVectorIndex(chatId);
|
||||
toastr.success('Vector index purged', 'Purge successful');
|
||||
}
|
||||
|
||||
async function onViewStatsClick() {
|
||||
const chatId = getCurrentChatId();
|
||||
if (!chatId) {
|
||||
toastr.info('No chat selected');
|
||||
return;
|
||||
}
|
||||
|
||||
const hashesInCollection = await getSavedHashes(chatId);
|
||||
const totalHashes = hashesInCollection.length;
|
||||
const uniqueHashes = hashesInCollection.filter(onlyUnique).length;
|
||||
|
||||
toastr.info(`Total hashes: <b>${totalHashes}</b><br>
|
||||
Unique hashes: <b>${uniqueHashes}</b><br><br>
|
||||
I'll mark collected messages with a green circle.`,
|
||||
`Stats for chat ${chatId}`,
|
||||
{ timeOut: 10000, escapeHtml: false });
|
||||
|
||||
const chat = getContext().chat;
|
||||
for (const message of chat) {
|
||||
if (hashesInCollection.includes(getStringHash(message.mes))) {
|
||||
const messageElement = $(`.mes[mesid="${chat.indexOf(message)}"]`);
|
||||
messageElement.addClass('vectorized');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
if (!extension_settings.vectors) {
|
||||
extension_settings.vectors = settings;
|
||||
}
|
||||
|
||||
// Migrate from old settings
|
||||
if (settings['enabled']) {
|
||||
settings.enabled_chats = true;
|
||||
}
|
||||
|
||||
Object.assign(settings, extension_settings.vectors);
|
||||
// Migrate from TensorFlow to Transformers
|
||||
settings.source = settings.source !== 'local' ? settings.source : 'transformers';
|
||||
$('#extensions_settings2').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
|
||||
$('#vectors_enabled').prop('checked', settings.enabled).on('input', () => {
|
||||
settings.enabled = $('#vectors_enabled').prop('checked');
|
||||
$('#vectors_enabled_chats').prop('checked', settings.enabled_chats).on('input', () => {
|
||||
settings.enabled_chats = $('#vectors_enabled_chats').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
toggleSettings();
|
||||
});
|
||||
$('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => {
|
||||
settings.enabled_files = $('#vectors_enabled_files').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
toggleSettings();
|
||||
});
|
||||
$('#vectors_source').val(settings.source).on('change', () => {
|
||||
settings.source = String($('#vectors_source').val());
|
||||
@@ -432,10 +616,41 @@ jQuery(async () => {
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_advanced_settings').toggleClass('displayNone', power_user.ui_mode === ui_mode.SIMPLE);
|
||||
|
||||
$('#vectors_vectorize_all').on('click', onVectorizeAllClick);
|
||||
$('#vectors_purge').on('click', onPurgeClick);
|
||||
$('#vectors_view_stats').on('click', onViewStatsClick);
|
||||
|
||||
$('#vectors_size_threshold').val(settings.size_threshold).on('input', () => {
|
||||
settings.size_threshold = Number($('#vectors_size_threshold').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_chunk_size').val(settings.chunk_size).on('input', () => {
|
||||
settings.chunk_size = Number($('#vectors_chunk_size').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_chunk_count').val(settings.chunk_count).on('input', () => {
|
||||
settings.chunk_count = Number($('#vectors_chunk_count').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_include_wi').prop('checked', settings.include_wi).on('input', () => {
|
||||
settings.include_wi = !!$('#vectors_include_wi').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_message_chunk_size').val(settings.message_chunk_size).on('input', () => {
|
||||
settings.message_chunk_size = Number($('#vectors_message_chunk_size').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
toggleSettings();
|
||||
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
||||
eventSource.on(event_types.MESSAGE_SENT, onChatEvent);
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"optional": [],
|
||||
"generate_interceptor": "vectors_rearrangeChat",
|
||||
"js": "index.js",
|
||||
"css": "",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
|
@@ -5,72 +5,139 @@
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label class="checkbox_label" for="vectors_enabled">
|
||||
<input id="vectors_enabled" type="checkbox" class="checkbox">
|
||||
Enabled
|
||||
</label>
|
||||
<label for="vectors_source">
|
||||
Vectorization Source
|
||||
</label>
|
||||
<select id="vectors_source" class="select">
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||
</select>
|
||||
<div id="vectors_advanced_settings" data-newbie-hidden>
|
||||
<label for="vectors_template">
|
||||
Insertion Template
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="vectors_source">
|
||||
Vectorization Source
|
||||
</label>
|
||||
<textarea id="vectors_template" class="text_pole textarea_compact autoSetHeight" rows="2" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
|
||||
<label for="vectors_position">Injection Position</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="2" />
|
||||
Before Main Prompt / Story String
|
||||
</label>
|
||||
<!--Keep these as 0 and 1 to interface with the setExtensionPrompt function-->
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="0" />
|
||||
After Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="1" />
|
||||
In-chat @ Depth <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
|
||||
</label>
|
||||
</div>
|
||||
<select id="vectors_source" class="text_pole">
|
||||
<option value="transformers">Local (Transformers)</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="palm">Google MakerSuite (PaLM)</option>
|
||||
<option value="mistral">MistralAI</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex-container flexFlowColumn" title="How many last messages will be matched for relevance.">
|
||||
<label for="vectors_query">
|
||||
<span>Query messages</span>
|
||||
</label>
|
||||
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
|
||||
</div>
|
||||
|
||||
<label class="checkbox_label" for="vectors_include_wi" title="Query results can activate World Info entries.">
|
||||
<input id="vectors_include_wi" type="checkbox" class="checkbox">
|
||||
Include in World Info Scanning
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>
|
||||
File vectorization settings
|
||||
</h4>
|
||||
|
||||
<label class="checkbox_label" for="vectors_enabled_files">
|
||||
<input id="vectors_enabled_files" type="checkbox" class="checkbox">
|
||||
Enabled for files
|
||||
</label>
|
||||
|
||||
<div id="vectors_files_settings">
|
||||
|
||||
<div class="flex-container">
|
||||
<div class="flex1" title="Prevents last N messages from being placed out of order.">
|
||||
<label for="vectors_protect">
|
||||
<small>Retain#</small>
|
||||
<div class="flex1" title="Only files past this size will be vectorized.">
|
||||
<label for="vectors_size_threshold">
|
||||
<small>Size threshold (KB)</small>
|
||||
</label>
|
||||
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="99" />
|
||||
<input id="vectors_size_threshold" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
<div class="flex1" title="How many last messages will be matched for relevance.">
|
||||
<label for="vectors_query">
|
||||
<small>Query#</small>
|
||||
<div class="flex1" title="Chunk size for file splitting.">
|
||||
<label for="vectors_chunk_size">
|
||||
<small>Chunk size (chars)</small>
|
||||
</label>
|
||||
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
|
||||
<input id="vectors_chunk_size" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
<div class="flex1" title="How many past messages to insert as memories.">
|
||||
<label for="vectors_insert">
|
||||
<small>Insert#</small>
|
||||
<div class="flex1" title="How many chunks to retrieve when querying.">
|
||||
<label for="vectors_chunk_count">
|
||||
<small>Retrieve chunks</small>
|
||||
</label>
|
||||
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="99" />
|
||||
<input id="vectors_chunk_count" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small>
|
||||
Old messages are vectorized gradually as you chat.
|
||||
To process all previous messages, click the button below.
|
||||
</small>
|
||||
<div id="vectors_vectorize_all" class="menu_button menu_button_icon">
|
||||
Vectorize All
|
||||
</div>
|
||||
<div id="vectorize_progress" style="display: none;">
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>
|
||||
Chat vectorization settings
|
||||
</h4>
|
||||
<label class="checkbox_label" for="vectors_enabled_chats">
|
||||
<input id="vectors_enabled_chats" type="checkbox" class="checkbox">
|
||||
Enabled for chat messages
|
||||
</label>
|
||||
|
||||
<div id="vectors_chats_settings">
|
||||
<div id="vectors_advanced_settings">
|
||||
<label for="vectors_template">
|
||||
Insertion Template
|
||||
</label>
|
||||
<textarea id="vectors_template" class="text_pole textarea_compact" rows="3" placeholder="Use {{text}} macro to specify the position of retrieved text."></textarea>
|
||||
<label for="vectors_position">Injection Position</label>
|
||||
<div class="radio_group">
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="2" />
|
||||
Before Main Prompt / Story String
|
||||
</label>
|
||||
<!--Keep these as 0 and 1 to interface with the setExtensionPrompt function-->
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="0" />
|
||||
After Main Prompt / Story String
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="vectors_position" value="1" />
|
||||
In-chat @ Depth <input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<div class="flex1" title="Can increase the retrieval quality for the cost of processing. 0 = disabled.">
|
||||
<label for="vectors_message_chunk_size">
|
||||
<small>Chunk size (chars)</small>
|
||||
</label>
|
||||
<input id="vectors_message_chunk_size" type="number" class="text_pole widthUnset" min="0" max="9999" />
|
||||
</div>
|
||||
<div class="flex1" title="Prevents last N messages from being placed out of order.">
|
||||
<label for="vectors_protect">
|
||||
<small>Retain#</small>
|
||||
</label>
|
||||
<input type="number" id="vectors_protect" class="text_pole widthUnset" min="1" max="9999" />
|
||||
</div>
|
||||
<div class="flex1" title="How many past messages to insert as memories.">
|
||||
<label for="vectors_insert">
|
||||
<small>Insert#</small>
|
||||
</label>
|
||||
<input type="number" id="vectors_insert" class="text_pole widthUnset" min="1" max="9999" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small>
|
||||
Processed <span id="vectorize_progress_percent">0</span>% of messages.
|
||||
ETA: <span id="vectorize_progress_eta">...</span> seconds.
|
||||
Old messages are vectorized gradually as you chat.
|
||||
To process all previous messages, click the button below.
|
||||
</small>
|
||||
<div class="flex-container">
|
||||
<div id="vectors_vectorize_all" class="menu_button menu_button_icon">
|
||||
Vectorize All
|
||||
</div>
|
||||
<div id="vectors_purge" class="menu_button menu_button_icon">
|
||||
Purge Vectors
|
||||
</div>
|
||||
<div id="vectors_view_stats" class="menu_button menu_button_icon">
|
||||
View Stats
|
||||
</div>
|
||||
</div>
|
||||
<div id="vectorize_progress" style="display: none;">
|
||||
<small>
|
||||
Processed <span id="vectorize_progress_percent">0</span>% of messages.
|
||||
ETA: <span id="vectorize_progress_eta">...</span> seconds.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
4
public/scripts/extensions/vectors/style.css
Normal file
4
public/scripts/extensions/vectors/style.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.mes.vectorized .name_text::after {
|
||||
content: '🟢';
|
||||
margin-left: 5px;
|
||||
}
|
Reference in New Issue
Block a user