mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-02-12 18:10:39 +01:00
Merge branch 'SillyTavern:staging' into staging
This commit is contained in:
commit
1dd747a24d
@ -226,4 +226,5 @@
|
||||
|
||||
.group_member .avatar {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
@ -1693,6 +1693,20 @@
|
||||
<span data-i18n="Send names in the ChatML objects.">Send names in the ChatML objects. Helps the model to associate messages with characters.</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block">
|
||||
<label for="squash_system_messages" title="Squash system messages" class="checkbox_label widthFreeExpand">
|
||||
<input id="squash_system_messages" type="checkbox" />
|
||||
<span data-i18n="Squash system messages">
|
||||
Squash system messages
|
||||
</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft">
|
||||
<span data-i18n="Combines consecutive system messages into one.">
|
||||
Combines consecutive system messages into one (excluding example dialogues).
|
||||
May improve coherence for some models.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="ai21">
|
||||
<label for="use_ai21_tokenizer" title="Use AI21 Tokenizer" class="checkbox_label widthFreeExpand">
|
||||
<input id="use_ai21_tokenizer" type="checkbox" /><span data-i18n="Use AI21 Tokenizer">Use AI21 Tokenizer</span>
|
||||
@ -2735,8 +2749,8 @@
|
||||
Alert On Overflow
|
||||
</small>
|
||||
</label>
|
||||
<div id="WIInputWidthReference" style="display:none; height:1px;">10000</div>
|
||||
</div>
|
||||
<div id="WIInputWidthReference" style="display:none; height:1px;">10000</div>
|
||||
|
||||
</div>
|
||||
|
||||
@ -3414,6 +3428,7 @@
|
||||
<input type="hidden" id="fav_checkbox" name="fav" />
|
||||
<div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div>
|
||||
<div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore" data-i18n="[title]Character Lore"></div>
|
||||
<div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
|
||||
<div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div>
|
||||
<!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
|
||||
<!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->
|
||||
@ -3529,8 +3544,11 @@
|
||||
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div name="group-metadata-controls" class="marginTopBot5">
|
||||
<input id="rm_group_chat_name" class="text_pole wide100p" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" maxlength="100" />
|
||||
<div id="group-metadata-controls" class="marginTopBot5">
|
||||
<div class="flex-container wide100p">
|
||||
<input id="rm_group_chat_name" class="text_pole flex1" type="text" name="chat_name" data-i18n="[placeholder]Chat Name (Optional)" placeholder="Chat Name (Optional)" maxlength="100" />
|
||||
<div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore" data-i18n="[title]Chat Lore"></div>
|
||||
</div>
|
||||
<div id="group_tags_div" class="wide100p">
|
||||
<div class="tag_controls">
|
||||
<input id="groupTagInput" class="text_pole tag_input flex1 margin0" data-i18n="[placeholder]Search / Create Tags" placeholder="Search / Create tags" maxlength="25" />
|
||||
@ -3597,9 +3615,9 @@
|
||||
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">
|
||||
<div id="rm_group_members_pagination" class="group_pagination"></div>
|
||||
<div id="rm_group_members" class="overflowYAuto flex-container"></div>
|
||||
<div id="currentGroupMembers" name="Current Group Members" class="flex-container flexFlowColumn overflowYAuto flex1">
|
||||
<div id="rm_group_members_pagination" class="rm_group_members_pagination group_pagination"></div>
|
||||
<div id="rm_group_members" class="rm_group_members overflowYAuto flex-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3882,6 +3900,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chat_world_template" class="template_element">
|
||||
<div class="chat_world range-block flexFlowColumn flex-container">
|
||||
<div class="range-block-title">
|
||||
<h4 data-i18n="Chat Lorebook">Chat Lorebook for <span class="chat_name"></span></h4>
|
||||
</div>
|
||||
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
||||
<span data-i18n="A selected World Info will be bound to this chat.">
|
||||
A selected World Info will be bound to this chat. When generating an AI reply,
|
||||
it will be combined with the entries from global and character lorebooks.
|
||||
</span>
|
||||
</div>
|
||||
<div class="range-block-range wide100p">
|
||||
<select class="chat_world_info_selector wide100p">
|
||||
<option value="">--- None ---</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="character_world_template" class="template_element">
|
||||
<div class="character_world range-block flexFlowColumn flex-container">
|
||||
<div class="range-block-title">
|
||||
@ -4838,4 +4875,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
@ -5729,6 +5729,7 @@ export function select_selected_character(chid) {
|
||||
checkEmbeddedWorld(chid);
|
||||
|
||||
$("#form_create").attr("actiontype", "editcharacter");
|
||||
$('.form_create_bottom_buttons_block .chat_lorebook_button').show();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
@ -5787,6 +5788,7 @@ function select_rm_create() {
|
||||
checkEmbeddedWorld();
|
||||
|
||||
$("#form_create").attr("actiontype", "createcharacter");
|
||||
$('.form_create_bottom_buttons_block .chat_lorebook_button').hide();
|
||||
}
|
||||
|
||||
function select_rm_characters() {
|
||||
@ -6970,7 +6972,7 @@ function connectAPISlash(_, text) {
|
||||
toastr.info(`API set to ${text}, trying to connect..`);
|
||||
}
|
||||
|
||||
export function processDroppedFiles(files) {
|
||||
export async function processDroppedFiles(files) {
|
||||
const allowedMimeTypes = [
|
||||
'application/json',
|
||||
'image/png',
|
||||
@ -6978,14 +6980,14 @@ export function processDroppedFiles(files) {
|
||||
|
||||
for (const file of files) {
|
||||
if (allowedMimeTypes.includes(file.type)) {
|
||||
importCharacter(file);
|
||||
await importCharacter(file);
|
||||
} else {
|
||||
toastr.warning('Unsupported file type: ' + file.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importCharacter(file) {
|
||||
async function importCharacter(file) {
|
||||
const ext = file.name.match(/\.(\w+)$/);
|
||||
if (
|
||||
!ext ||
|
||||
@ -7000,44 +7002,38 @@ function importCharacter(file) {
|
||||
formData.append('avatar', file);
|
||||
formData.append('file_type', format);
|
||||
|
||||
jQuery.ajax({
|
||||
const data = await jQuery.ajax({
|
||||
type: "POST",
|
||||
url: "/importcharacter",
|
||||
data: formData,
|
||||
async: false,
|
||||
beforeSend: function () {
|
||||
},
|
||||
async: true,
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: async function (data) {
|
||||
if (data.error) {
|
||||
toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.file_name !== undefined) {
|
||||
$('#character_search_bar').val('').trigger('input');
|
||||
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid != undefined && this_chid != "invalid-safety-id") {
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
|
||||
await getCharacters();
|
||||
select_rm_info(`char_import`, data.file_name, oldSelectedChar);
|
||||
if (power_user.import_card_tags) {
|
||||
let currentContext = getContext();
|
||||
let avatarFileName = `${data.file_name}.png`;
|
||||
let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
|
||||
await importTags(importedCharacter);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
$("#create_button").removeAttr("disabled");
|
||||
},
|
||||
});
|
||||
|
||||
if (data.error) {
|
||||
toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.file_name !== undefined) {
|
||||
$('#character_search_bar').val('').trigger('input');
|
||||
|
||||
let oldSelectedChar = null;
|
||||
if (this_chid != undefined && this_chid != "invalid-safety-id") {
|
||||
oldSelectedChar = characters[this_chid].avatar;
|
||||
}
|
||||
|
||||
await getCharacters();
|
||||
select_rm_info(`char_import`, data.file_name, oldSelectedChar);
|
||||
if (power_user.import_card_tags) {
|
||||
let currentContext = getContext();
|
||||
let avatarFileName = `${data.file_name}.png`;
|
||||
let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
|
||||
await importTags(importedCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function importFromURL(items, files) {
|
||||
@ -8966,7 +8962,7 @@ jQuery(async function () {
|
||||
|
||||
switch (customContentType) {
|
||||
case 'character':
|
||||
processDroppedFiles([file]);
|
||||
await processDroppedFiles([file]);
|
||||
break;
|
||||
case 'lorebook':
|
||||
await importWorldInfo(file);
|
||||
@ -9029,7 +9025,7 @@ jQuery(async function () {
|
||||
if (!files.length) {
|
||||
await importFromURL(event.originalEvent.dataTransfer.items, files);
|
||||
}
|
||||
processDroppedFiles(files);
|
||||
await processDroppedFiles(files);
|
||||
});
|
||||
|
||||
|
||||
|
@ -31,7 +31,7 @@ import {
|
||||
SECRET_KEYS,
|
||||
secret_state,
|
||||
} from "./secrets.js";
|
||||
import { debounce, delay, getStringHash, isUrlOrAPIKey, waitUntilCondition } from "./utils.js";
|
||||
import { debounce, delay, getStringHash, isValidUrl, waitUntilCondition } from "./utils.js";
|
||||
import { chat_completion_sources, oai_settings } from "./openai.js";
|
||||
import { getTokenCount } from "./tokenizers.js";
|
||||
|
||||
@ -394,7 +394,7 @@ function RA_autoconnect(PrevApi) {
|
||||
if (online_status === "no_connection" && LoadLocalBool('AutoConnectEnabled')) {
|
||||
switch (main_api) {
|
||||
case 'kobold':
|
||||
if (api_server && isUrlOrAPIKey(api_server)) {
|
||||
if (api_server && isValidUrl(api_server)) {
|
||||
$("#api_button").click();
|
||||
}
|
||||
break;
|
||||
@ -404,7 +404,7 @@ function RA_autoconnect(PrevApi) {
|
||||
}
|
||||
break;
|
||||
case 'textgenerationwebui':
|
||||
if (api_server_textgenerationwebui && isUrlOrAPIKey(api_server_textgenerationwebui)) {
|
||||
if (api_server_textgenerationwebui && isValidUrl(api_server_textgenerationwebui)) {
|
||||
$("#api_button_textgenerationwebui").click();
|
||||
}
|
||||
break;
|
||||
|
@ -5,10 +5,12 @@ TODO:
|
||||
|
||||
import { getRequestHeaders, callPopup } from "../../../script.js";
|
||||
import { deleteExtension, extensionNames, installExtension } from "../../extensions.js";
|
||||
import { isValidUrl } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'Assets';
|
||||
const DEBUG_PREFIX = "<Assets module> ";
|
||||
let previewAudio = null;
|
||||
let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json"
|
||||
|
||||
const extensionName = "assets";
|
||||
@ -29,7 +31,7 @@ const defaultSettings = {
|
||||
|
||||
function downloadAssetsList(url) {
|
||||
updateCurrentAssets().then(function () {
|
||||
fetch(url)
|
||||
fetch(url, { cache: "no-cache" })
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
|
||||
@ -120,13 +122,24 @@ function downloadAssetsList(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>`)
|
||||
.append(element)
|
||||
.append(`<div class="flex-container flexFlowColumn"><span>${displayName}</span><span>${description}</span></div>`)
|
||||
.append(`<div class="flex-container flexFlowColumn">
|
||||
<span class="flex-container alignitemscenter">
|
||||
<b>${displayName}</b>
|
||||
<a class="asset_preview" href="${url}" target="_blank" title="Preview in browser">
|
||||
<i class="fa-solid fa-sm ${previewIcon}"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span>${description}</span>
|
||||
</div>`)
|
||||
.appendTo(assetTypeMenu);
|
||||
}
|
||||
assetTypeMenu.appendTo("#assets_menu");
|
||||
assetTypeMenu.on('click', 'a.asset_preview', previewAsset);
|
||||
}
|
||||
|
||||
$("#assets_menu").show();
|
||||
@ -140,6 +153,28 @@ function downloadAssetsList(url) {
|
||||
});
|
||||
}
|
||||
|
||||
function previewAsset(e) {
|
||||
const href = $(this).attr('href');
|
||||
const audioExtensions = ['.mp3', '.ogg', '.wav'];
|
||||
|
||||
if (audioExtensions.some(ext => href.endsWith(ext))) {
|
||||
e.preventDefault();
|
||||
|
||||
if (previewAudio) {
|
||||
previewAudio.pause();
|
||||
|
||||
if (previewAudio.src === href) {
|
||||
previewAudio = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
previewAudio = new Audio(href);
|
||||
previewAudio.play();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function isAssetInstalled(assetType, filename) {
|
||||
let assetList = currentAssets[assetType];
|
||||
|
||||
|
@ -17,6 +17,10 @@
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.assets-list-div a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.assets-list-div i {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -1,917 +0,0 @@
|
||||
/*
|
||||
Ideas:
|
||||
- Clean design of new ui
|
||||
- change select text versus options for playing: audio
|
||||
- cross fading between bgm / start a different time
|
||||
- fading should appear before end when switching randomly
|
||||
- Background based ambient sounds
|
||||
- import option on background UI ?
|
||||
- Allow background music edition using background menu
|
||||
- https://fontawesome.com/icons/music?f=classic&s=solid
|
||||
- https://codepen.io/noirsociety/pen/rNQxQwm
|
||||
- https://codepen.io/xrocker/pen/abdKVGy
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced, getRequestHeaders } from "../../../script.js";
|
||||
import { getContext, extension_settings, ModuleWorkerWrapper } from "../../extensions.js";
|
||||
import { isDataURL } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const extensionName = "audio";
|
||||
const extensionFolderPath = `scripts/extensions/${extensionName}`;
|
||||
|
||||
const MODULE_NAME = 'Audio';
|
||||
const DEBUG_PREFIX = "<Audio module> ";
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
const ASSETS_BGM_FOLDER = "bgm";
|
||||
const ASSETS_AMBIENT_FOLDER = "ambient";
|
||||
const CHARACTER_BGM_FOLDER = "bgm"
|
||||
|
||||
const FALLBACK_EXPRESSION = "neutral";
|
||||
const DEFAULT_EXPRESSIONS = [
|
||||
//"talkinghead",
|
||||
"admiration",
|
||||
"amusement",
|
||||
"anger",
|
||||
"annoyance",
|
||||
"approval",
|
||||
"caring",
|
||||
"confusion",
|
||||
"curiosity",
|
||||
"desire",
|
||||
"disappointment",
|
||||
"disapproval",
|
||||
"disgust",
|
||||
"embarrassment",
|
||||
"excitement",
|
||||
"fear",
|
||||
"gratitude",
|
||||
"grief",
|
||||
"joy",
|
||||
"love",
|
||||
"nervousness",
|
||||
"optimism",
|
||||
"pride",
|
||||
"realization",
|
||||
"relief",
|
||||
"remorse",
|
||||
"sadness",
|
||||
"surprise",
|
||||
"neutral"
|
||||
];
|
||||
const SPRITE_DOM_ID = "#expression-image";
|
||||
|
||||
let current_chat_id = null
|
||||
|
||||
let fallback_BGMS = null; // Initialized only once with module workers
|
||||
let ambients = null; // Initialized only once with module workers
|
||||
let characterMusics = {}; // Updated with module workers
|
||||
|
||||
let currentCharacterBGM = null;
|
||||
let currentExpressionBGM = null;
|
||||
let currentBackground = null;
|
||||
|
||||
let cooldownBGM = 0;
|
||||
|
||||
let bgmEnded = true;
|
||||
|
||||
//#############################//
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
const defaultSettings = {
|
||||
enabled: false,
|
||||
dynamic_bgm_enabled: false,
|
||||
//dynamic_ambient_enabled: false,
|
||||
|
||||
bgm_locked: true,
|
||||
bgm_muted: true,
|
||||
bgm_volume: 50,
|
||||
bgm_selected: null,
|
||||
|
||||
ambient_locked: true,
|
||||
ambient_muted: true,
|
||||
ambient_volume: 50,
|
||||
ambient_selected: null,
|
||||
|
||||
bgm_cooldown: 30
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
if (extension_settings.audio === undefined)
|
||||
extension_settings.audio = {};
|
||||
|
||||
if (Object.keys(extension_settings.audio).length === 0) {
|
||||
Object.assign(extension_settings.audio, defaultSettings)
|
||||
}
|
||||
$("#audio_enabled").prop('checked', extension_settings.audio.enabled);
|
||||
$("#audio_dynamic_bgm_enabled").prop('checked', extension_settings.audio.dynamic_bgm_enabled);
|
||||
//$("#audio_dynamic_ambient_enabled").prop('checked', extension_settings.audio.dynamic_ambient_enabled);
|
||||
|
||||
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
|
||||
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
|
||||
|
||||
$("#audio_bgm_volume_slider").val(extension_settings.audio.bgm_volume);
|
||||
$("#audio_ambient_volume_slider").val(extension_settings.audio.ambient_volume);
|
||||
|
||||
if (extension_settings.audio.bgm_muted) {
|
||||
$("#audio_bgm_mute_icon").removeClass("fa-volume-high");
|
||||
$("#audio_bgm_mute_icon").addClass("fa-volume-mute");
|
||||
$("#audio_bgm_mute").addClass("redOverlayGlow");
|
||||
$("#audio_bgm").prop("muted", true);
|
||||
}
|
||||
else {
|
||||
$("#audio_bgm_mute_icon").addClass("fa-volume-high");
|
||||
$("#audio_bgm_mute_icon").removeClass("fa-volume-mute");
|
||||
$("#audio_bgm_mute").removeClass("redOverlayGlow");
|
||||
$("#audio_bgm").prop("muted", false);
|
||||
}
|
||||
|
||||
if (extension_settings.audio.bgm_locked) {
|
||||
//$("#audio_bgm_lock_icon").removeClass("fa-lock-open");
|
||||
//$("#audio_bgm_lock_icon").addClass("fa-lock");
|
||||
$("#audio_bgm").attr("loop", true);
|
||||
$("#audio_bgm_lock").addClass("redOverlayGlow");
|
||||
}
|
||||
else {
|
||||
//$("#audio_bgm_lock_icon").removeClass("fa-lock");
|
||||
//$("#audio_bgm_lock_icon").addClass("fa-lock-open");
|
||||
$("#audio_bgm").attr("loop", false);
|
||||
$("#audio_bgm_lock").removeClass("redOverlayGlow");
|
||||
}
|
||||
|
||||
/*
|
||||
if (extension_settings.audio.bgm_selected !== null) {
|
||||
$("#audio_bgm_select").append(new Option(extension_settings.audio.bgm_selected, extension_settings.audio.bgm_selected));
|
||||
$("#audio_bgm_select").val(extension_settings.audio.bgm_selected);
|
||||
}*/
|
||||
|
||||
if (extension_settings.audio.ambient_locked) {
|
||||
$("#audio_ambient_lock_icon").removeClass("fa-lock-open");
|
||||
$("#audio_ambient_lock_icon").addClass("fa-lock");
|
||||
$("#audio_ambient_lock").addClass("redOverlayGlow");
|
||||
}
|
||||
else {
|
||||
$("#audio_ambient_lock_icon").removeClass("fa-lock");
|
||||
$("#audio_ambient_lock_icon").addClass("fa-lock-open");
|
||||
}
|
||||
|
||||
/*
|
||||
if (extension_settings.audio.ambient_selected !== null) {
|
||||
$("#audio_ambient_select").append(new Option(extension_settings.audio.ambient_selected, extension_settings.audio.ambient_selected));
|
||||
$("#audio_ambient_select").val(extension_settings.audio.ambient_selected);
|
||||
}*/
|
||||
|
||||
if (extension_settings.audio.ambient_muted) {
|
||||
$("#audio_ambient_mute_icon").removeClass("fa-volume-high");
|
||||
$("#audio_ambient_mute_icon").addClass("fa-volume-mute");
|
||||
$("#audio_ambient_mute").addClass("redOverlayGlow");
|
||||
$("#audio_ambient").prop("muted", true);
|
||||
}
|
||||
else {
|
||||
$("#audio_ambient_mute_icon").addClass("fa-volume-high");
|
||||
$("#audio_ambient_mute_icon").removeClass("fa-volume-mute");
|
||||
$("#audio_ambient_mute").removeClass("redOverlayGlow");
|
||||
$("#audio_ambient").prop("muted", false);
|
||||
}
|
||||
|
||||
$("#audio_bgm_cooldown").val(extension_settings.audio.bgm_cooldown);
|
||||
|
||||
$("#audio_debug_div").hide(); // DBG: comment to see debug mode
|
||||
}
|
||||
|
||||
async function onEnabledClick() {
|
||||
extension_settings.audio.enabled = $('#audio_enabled').is(':checked');
|
||||
if (extension_settings.audio.enabled) {
|
||||
if ($("#audio_bgm").attr("src") != "")
|
||||
$("#audio_bgm")[0].play();
|
||||
if ($("#audio_ambient").attr("src") != "")
|
||||
$("#audio_ambient")[0].play();
|
||||
} else {
|
||||
$("#audio_bgm")[0].pause();
|
||||
$("#audio_ambient")[0].pause();
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onDynamicBGMEnabledClick() {
|
||||
extension_settings.audio.dynamic_bgm_enabled = $('#audio_dynamic_bgm_enabled').is(':checked');
|
||||
currentCharacterBGM = null;
|
||||
currentExpressionBGM = null;
|
||||
cooldownBGM = 0;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
/*
|
||||
async function onDynamicAmbientEnabledClick() {
|
||||
extension_settings.audio.dynamic_ambient_enabled = $('#audio_dynamic_ambient_enabled').is(':checked');
|
||||
currentBackground = null;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
*/
|
||||
async function onBGMLockClick() {
|
||||
extension_settings.audio.bgm_locked = !extension_settings.audio.bgm_locked;
|
||||
if (extension_settings.audio.bgm_locked) {
|
||||
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
|
||||
$("#audio_bgm").attr("loop", true);
|
||||
}
|
||||
else {
|
||||
$("#audio_bgm").attr("loop", false);
|
||||
}
|
||||
//$("#audio_bgm_lock_icon").toggleClass("fa-lock");
|
||||
//$("#audio_bgm_lock_icon").toggleClass("fa-lock-open");
|
||||
$("#audio_bgm_lock").toggleClass("redOverlayGlow");
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onBGMRandomClick() {
|
||||
var select = document.getElementById('audio_bgm_select');
|
||||
var items = select.getElementsByTagName('option');
|
||||
|
||||
if (items.length < 2)
|
||||
return;
|
||||
|
||||
var index;
|
||||
do {
|
||||
index = Math.floor(Math.random() * items.length);
|
||||
} while (index == select.selectedIndex);
|
||||
|
||||
select.selectedIndex = index;
|
||||
onBGMSelectChange();
|
||||
}
|
||||
|
||||
async function onBGMMuteClick() {
|
||||
extension_settings.audio.bgm_muted = !extension_settings.audio.bgm_muted;
|
||||
$("#audio_bgm_mute_icon").toggleClass("fa-volume-high");
|
||||
$("#audio_bgm_mute_icon").toggleClass("fa-volume-mute");
|
||||
$("#audio_bgm").prop("muted", !$("#audio_bgm").prop("muted"));
|
||||
$("#audio_bgm_mute").toggleClass("redOverlayGlow");
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onAmbientLockClick() {
|
||||
extension_settings.audio.ambient_locked = !extension_settings.audio.ambient_locked;
|
||||
if (extension_settings.audio.ambient_locked)
|
||||
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
|
||||
else {
|
||||
extension_settings.audio.ambient_selected = null;
|
||||
currentBackground = null;
|
||||
}
|
||||
$("#audio_ambient_lock_icon").toggleClass("fa-lock");
|
||||
$("#audio_ambient_lock_icon").toggleClass("fa-lock-open");
|
||||
$("#audio_ambient_lock").toggleClass("redOverlayGlow");
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onAmbientMuteClick() {
|
||||
extension_settings.audio.ambient_muted = !extension_settings.audio.ambient_muted;
|
||||
$("#audio_ambient_mute_icon").toggleClass("fa-volume-high");
|
||||
$("#audio_ambient_mute_icon").toggleClass("fa-volume-mute");
|
||||
$("#audio_ambient").prop("muted", !$("#audio_ambient").prop("muted"));
|
||||
$("#audio_ambient_mute").toggleClass("redOverlayGlow");
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onBGMVolumeChange() {
|
||||
extension_settings.audio.bgm_volume = ~~($("#audio_bgm_volume_slider").val());
|
||||
$("#audio_bgm").prop("volume", extension_settings.audio.bgm_volume * 0.01);
|
||||
$("#audio_bgm_volume").text(extension_settings.audio.bgm_volume);
|
||||
saveSettingsDebounced();
|
||||
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
|
||||
}
|
||||
|
||||
async function onAmbientVolumeChange() {
|
||||
extension_settings.audio.ambient_volume = ~~($("#audio_ambient_volume_slider").val());
|
||||
$("#audio_ambient").prop("volume", extension_settings.audio.ambient_volume * 0.01);
|
||||
$("#audio_ambient_volume").text(extension_settings.audio.ambient_volume);
|
||||
saveSettingsDebounced();
|
||||
//console.debug(DEBUG_PREFIX,"UPDATED Ambient MAX TO",extension_settings.audio.ambient_volume);
|
||||
}
|
||||
|
||||
async function onBGMSelectChange() {
|
||||
extension_settings.audio.bgm_selected = $("#audio_bgm_select").val();
|
||||
updateBGM(true);
|
||||
saveSettingsDebounced();
|
||||
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
|
||||
}
|
||||
|
||||
async function onAmbientSelectChange() {
|
||||
extension_settings.audio.ambient_selected = $("#audio_ambient_select").val();
|
||||
updateAmbient(true);
|
||||
saveSettingsDebounced();
|
||||
//console.debug(DEBUG_PREFIX,"UPDATED BGM MAX TO",extension_settings.audio.bgm_volume);
|
||||
}
|
||||
|
||||
async function onBGMCooldownInput() {
|
||||
extension_settings.audio.bgm_cooldown = ~~($("#audio_bgm_cooldown").val());
|
||||
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
|
||||
saveSettingsDebounced();
|
||||
console.debug(DEBUG_PREFIX, "UPDATED BGM cooldown to", extension_settings.audio.bgm_cooldown);
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// API Calls //
|
||||
//#############################//
|
||||
|
||||
async function getAssetsList(type) {
|
||||
console.debug(DEBUG_PREFIX, "getting assets of type", type);
|
||||
|
||||
try {
|
||||
const result = await fetch(`/api/assets/get`, {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
const assets = result.ok ? (await result.json()) : { type: [] };
|
||||
console.debug(DEBUG_PREFIX, "Found assets:", assets);
|
||||
return assets[type];
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getCharacterBgmList(name) {
|
||||
console.debug(DEBUG_PREFIX, "getting bgm list for", name);
|
||||
|
||||
try {
|
||||
const result = await fetch(`/api/assets/character?name=${encodeURIComponent(name)}&category=${CHARACTER_BGM_FOLDER}`, {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
let musics = result.ok ? (await result.json()) : [];
|
||||
return musics;
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// Module Worker //
|
||||
//#############################//
|
||||
|
||||
function fillBGMSelect() {
|
||||
let found_last_selected_bgm = false;
|
||||
// Update bgm list in UI
|
||||
$("#audio_bgm_select")
|
||||
.find('option')
|
||||
.remove();
|
||||
|
||||
for (const file of fallback_BGMS) {
|
||||
$('#audio_bgm_select').append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
|
||||
if (file === extension_settings.audio.bgm_selected) {
|
||||
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
|
||||
found_last_selected_bgm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update bgm list in UI
|
||||
for (const char in characterMusics)
|
||||
for (const e in characterMusics[char])
|
||||
for (const file of characterMusics[char][e]) {
|
||||
$('#audio_bgm_select').append(new Option(char + ": " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
|
||||
if (file === extension_settings.audio.bgm_selected) {
|
||||
$('#audio_bgm_select').val(extension_settings.audio.bgm_selected);
|
||||
found_last_selected_bgm = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_last_selected_bgm) {
|
||||
$('#audio_bgm_select').val($("#audio_bgm_select option:first").val());
|
||||
extension_settings.audio.bgm_selected = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
- Update ambient sound
|
||||
- Update character BGM
|
||||
- Solo dynamique expression
|
||||
- Group only neutral bgm
|
||||
*/
|
||||
async function moduleWorker() {
|
||||
const moduleEnabled = extension_settings.audio.enabled;
|
||||
|
||||
if (moduleEnabled) {
|
||||
|
||||
if (cooldownBGM > 0)
|
||||
cooldownBGM -= UPDATE_INTERVAL;
|
||||
|
||||
if (fallback_BGMS == null) {
|
||||
console.debug(DEBUG_PREFIX, "Updating audio bgm assets...");
|
||||
fallback_BGMS = await getAssetsList(ASSETS_BGM_FOLDER);
|
||||
fallback_BGMS = fallback_BGMS.filter((filename) => filename != ".placeholder")
|
||||
console.debug(DEBUG_PREFIX, "Detected assets:", fallback_BGMS);
|
||||
|
||||
fillBGMSelect();
|
||||
}
|
||||
|
||||
if (ambients == null) {
|
||||
console.debug(DEBUG_PREFIX, "Updating audio ambient assets...");
|
||||
ambients = await getAssetsList(ASSETS_AMBIENT_FOLDER);
|
||||
ambients = ambients.filter((filename) => filename != ".placeholder")
|
||||
console.debug(DEBUG_PREFIX, "Detected assets:", ambients);
|
||||
|
||||
// Update bgm list in UI
|
||||
$("#audio_ambient_select")
|
||||
.find('option')
|
||||
.remove();
|
||||
|
||||
if (extension_settings.audio.ambient_selected !== null) {
|
||||
let ambient_label = extension_settings.audio.ambient_selected;
|
||||
if (ambient_label.includes("assets"))
|
||||
ambient_label = "asset: " + ambient_label.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, "");
|
||||
else {
|
||||
ambient_label = ambient_label.substring("/characters/".length);
|
||||
ambient_label = ambient_label.substring(0, ambient_label.indexOf("/")) + ": " + ambient_label.substring(ambient_label.indexOf("/") + "/bgm/".length);
|
||||
ambient_label = ambient_label.replace(/\.[^/.]+$/, "");
|
||||
}
|
||||
$('#audio_ambient_select').append(new Option(ambient_label, extension_settings.audio.ambient_selected));
|
||||
}
|
||||
|
||||
for (const file of ambients) {
|
||||
if (file !== extension_settings.audio.ambient_selected)
|
||||
$("#audio_ambient_select").append(new Option("asset: " + file.replace(/^.*[\\\/]/, '').replace(/\.[^/.]+$/, ""), file));
|
||||
}
|
||||
}
|
||||
|
||||
// 1) Update ambient audio
|
||||
// ---------------------------
|
||||
//if (extension_settings.audio.dynamic_ambient_enabled) {
|
||||
let newBackground = $("#bg1").css("background-image");
|
||||
const custom_background = getContext()["chatMetadata"]["custom_background"];
|
||||
|
||||
if (custom_background !== undefined)
|
||||
newBackground = custom_background
|
||||
|
||||
if (!isDataURL(newBackground)) {
|
||||
newBackground = newBackground.substring(newBackground.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "").replaceAll("%20", "-").replaceAll(" ", "-"); // remove path and spaces
|
||||
|
||||
//console.debug(DEBUG_PREFIX,"Current backgroung:",newBackground);
|
||||
|
||||
if (currentBackground !== newBackground) {
|
||||
currentBackground = newBackground;
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Changing ambient audio for", currentBackground);
|
||||
updateAmbient();
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
||||
const context = getContext();
|
||||
//console.debug(DEBUG_PREFIX,context);
|
||||
|
||||
if (context.chat.length == 0)
|
||||
return;
|
||||
|
||||
let chatIsGroup = context.chat[0].is_group;
|
||||
let newCharacter = null;
|
||||
|
||||
// 1) Update BGM (single chat)
|
||||
// -----------------------------
|
||||
if (!chatIsGroup) {
|
||||
|
||||
// Reset bgm list on new chat
|
||||
if (context.chatId != current_chat_id) {
|
||||
current_chat_id = context.chatId;
|
||||
characterMusics = {};
|
||||
cooldownBGM = 0;
|
||||
}
|
||||
|
||||
newCharacter = context.name2;
|
||||
|
||||
//console.log(DEBUG_PREFIX,"SOLO CHAT MODE"); // DBG
|
||||
|
||||
// 1.1) First time loading chat
|
||||
if (characterMusics[newCharacter] === undefined) {
|
||||
await loadCharacterBGM(newCharacter);
|
||||
currentExpressionBGM = FALLBACK_EXPRESSION;
|
||||
//currentCharacterBGM = newCharacter;
|
||||
|
||||
//updateBGM();
|
||||
//cooldownBGM = BGM_UPDATE_COOLDOWN;
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.2) Switched chat
|
||||
if (currentCharacterBGM !== newCharacter) {
|
||||
currentCharacterBGM = newCharacter;
|
||||
try {
|
||||
await updateBGM(false, true);
|
||||
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(DEBUG_PREFIX, "Error while trying to update BGM character, will try again");
|
||||
currentCharacterBGM = null
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newExpression = getNewExpression();
|
||||
|
||||
// 1.3) Same character but different expression
|
||||
if (currentExpressionBGM !== newExpression) {
|
||||
|
||||
// Check cooldown
|
||||
if (cooldownBGM > 0) {
|
||||
//console.debug(DEBUG_PREFIX,"(SOLO) BGM switch on cooldown:",cooldownBGM);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentExpressionBGM = newExpression;
|
||||
await updateBGM();
|
||||
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
|
||||
console.debug(DEBUG_PREFIX, "(SOLO) Updated current character expression to", currentExpressionBGM, "cooldown", cooldownBGM);
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(DEBUG_PREFIX, "Error while trying to update BGM expression, will try again");
|
||||
currentCharacterBGM = null
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) Update BGM (group chat)
|
||||
// -----------------------------
|
||||
|
||||
// Load current chat character bgms
|
||||
// Reset bgm list on new chat
|
||||
if (context.chatId != current_chat_id) {
|
||||
current_chat_id = context.chatId;
|
||||
characterMusics = {};
|
||||
cooldownBGM = 0;
|
||||
for (const message of context.chat) {
|
||||
if (characterMusics[message.name] === undefined)
|
||||
await loadCharacterBGM(message.name);
|
||||
}
|
||||
|
||||
try {
|
||||
newCharacter = context.chat[context.chat.length - 1].name;
|
||||
currentCharacterBGM = newCharacter;
|
||||
await updateBGM(false, true);
|
||||
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
|
||||
currentCharacterBGM = newCharacter;
|
||||
currentExpressionBGM = FALLBACK_EXPRESSION;
|
||||
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
|
||||
currentCharacterBGM = null
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
newCharacter = context.chat[context.chat.length - 1].name;
|
||||
const userName = context.name1;
|
||||
|
||||
if (newCharacter !== undefined && newCharacter != userName) {
|
||||
|
||||
//console.log(DEBUG_PREFIX,"GROUP CHAT MODE"); // DBG
|
||||
|
||||
// 2.1) New character appear
|
||||
if (characterMusics[newCharacter] === undefined) {
|
||||
await loadCharacterBGM(newCharacter);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2) Switched char
|
||||
if (currentCharacterBGM !== newCharacter) {
|
||||
// Check cooldown
|
||||
if (cooldownBGM > 0) {
|
||||
console.debug(DEBUG_PREFIX, "(GROUP) BGM switch on cooldown:", cooldownBGM);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
currentCharacterBGM = newCharacter;
|
||||
await updateBGM();
|
||||
cooldownBGM = extension_settings.audio.bgm_cooldown * 1000;
|
||||
currentCharacterBGM = newCharacter;
|
||||
currentExpressionBGM = FALLBACK_EXPRESSION;
|
||||
console.debug(DEBUG_PREFIX, "(GROUP) Updated current character BGM to", currentExpressionBGM, "cooldown", cooldownBGM);
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(DEBUG_PREFIX, "Error while trying to update BGM group, will try again");
|
||||
currentCharacterBGM = null
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
const newExpression = getNewExpression();
|
||||
|
||||
// 1.3) Same character but different expression
|
||||
if (currentExpressionBGM !== newExpression) {
|
||||
|
||||
// Check cooldown
|
||||
if (cooldownBGM > 0) {
|
||||
console.debug(DEBUG_PREFIX,"BGM switch on cooldown:",cooldownBGM);
|
||||
return;
|
||||
}
|
||||
|
||||
cooldownBGM = BGM_UPDATE_COOLDOWN;
|
||||
currentExpressionBGM = newExpression;
|
||||
console.debug(DEBUG_PREFIX,"Updated current character expression to",currentExpressionBGM);
|
||||
updateBGM();
|
||||
return;
|
||||
}
|
||||
|
||||
return;*/
|
||||
|
||||
}
|
||||
|
||||
// Case 3: Same character/expression or BGM switch on cooldown keep playing same BGM
|
||||
//console.debug(DEBUG_PREFIX,"Nothing to do for",currentCharacterBGM, newCharacter, currentExpressionBGM, cooldownBGM);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCharacterBGM(newCharacter) {
|
||||
console.debug(DEBUG_PREFIX, "New character detected, loading BGM folder of", newCharacter);
|
||||
|
||||
// 1.1) First time character appear, load its music folder
|
||||
const audio_file_paths = await getCharacterBgmList(newCharacter);
|
||||
//console.debug(DEBUG_PREFIX, "Recieved", audio_file_paths);
|
||||
|
||||
// Initialise expression/files mapping
|
||||
characterMusics[newCharacter] = {};
|
||||
for (const e of DEFAULT_EXPRESSIONS)
|
||||
characterMusics[newCharacter][e] = [];
|
||||
|
||||
for (const i of audio_file_paths) {
|
||||
//console.debug(DEBUG_PREFIX,"File found:",i);
|
||||
for (const e of DEFAULT_EXPRESSIONS)
|
||||
if (i.includes(e))
|
||||
characterMusics[newCharacter][e].push(i);
|
||||
}
|
||||
console.debug(DEBUG_PREFIX, "Updated BGM map of", newCharacter, "to", characterMusics[newCharacter]);
|
||||
|
||||
fillBGMSelect();
|
||||
}
|
||||
|
||||
function getNewExpression() {
|
||||
let newExpression;
|
||||
|
||||
// HACK: use sprite file name as expression detection
|
||||
if (!$(SPRITE_DOM_ID).length) {
|
||||
console.error(DEBUG_PREFIX, "ERROR: expression sprite does not exist, cannot extract expression from ", SPRITE_DOM_ID)
|
||||
return FALLBACK_EXPRESSION;
|
||||
}
|
||||
|
||||
const spriteFile = $("#expression-image").attr("src");
|
||||
newExpression = spriteFile.substring(spriteFile.lastIndexOf("/") + 1).replace(/\.[^/.]+$/, "");
|
||||
//
|
||||
|
||||
// No sprite to detect expression
|
||||
if (newExpression == "") {
|
||||
//console.info(DEBUG_PREFIX,"Warning: no expression extracted from sprite, switch to",FALLBACK_EXPRESSION);
|
||||
newExpression = FALLBACK_EXPRESSION;
|
||||
}
|
||||
|
||||
if (!DEFAULT_EXPRESSIONS.includes(newExpression)) {
|
||||
console.info(DEBUG_PREFIX, "Warning:", newExpression, " is not a handled expression, expected one of", FALLBACK_EXPRESSION);
|
||||
return FALLBACK_EXPRESSION;
|
||||
}
|
||||
|
||||
return newExpression;
|
||||
}
|
||||
|
||||
async function updateBGM(isUserInput = false, newChat = false) {
|
||||
if (!isUserInput && !extension_settings.audio.dynamic_bgm_enabled && $("#audio_bgm").attr("src") != "" && !bgmEnded && !newChat) {
|
||||
console.debug(DEBUG_PREFIX, "BGM already playing and dynamic switch disabled, no update done");
|
||||
return;
|
||||
}
|
||||
|
||||
let audio_file_path = ""
|
||||
if (isUserInput || (extension_settings.audio.bgm_locked && extension_settings.audio.bgm_selected !== null)) {
|
||||
audio_file_path = extension_settings.audio.bgm_selected;
|
||||
|
||||
if (isUserInput)
|
||||
console.debug(DEBUG_PREFIX, "User selected BGM", audio_file_path);
|
||||
if (extension_settings.audio.bgm_locked)
|
||||
console.debug(DEBUG_PREFIX, "BGM locked keeping current audio", audio_file_path);
|
||||
}
|
||||
else {
|
||||
|
||||
let audio_files = null;
|
||||
|
||||
if (extension_settings.audio.dynamic_bgm_enabled) {
|
||||
extension_settings.audio.bgm_selected = null;
|
||||
saveSettingsDebounced();
|
||||
audio_files = characterMusics[currentCharacterBGM][currentExpressionBGM];// Try char expression BGM
|
||||
|
||||
if (audio_files === undefined || audio_files.length == 0) {
|
||||
console.debug(DEBUG_PREFIX, "No BGM for", currentCharacterBGM, currentExpressionBGM);
|
||||
audio_files = characterMusics[currentCharacterBGM][FALLBACK_EXPRESSION]; // Try char FALLBACK BGM
|
||||
if (audio_files === undefined || audio_files.length == 0) {
|
||||
console.debug(DEBUG_PREFIX, "No default BGM for", currentCharacterBGM, FALLBACK_EXPRESSION, "switch to ST BGM");
|
||||
audio_files = fallback_BGMS; // ST FALLBACK BGM
|
||||
|
||||
if (audio_files.length == 0) {
|
||||
console.debug(DEBUG_PREFIX, "No default BGM file found, bgm folder may be empty.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
audio_files = [];
|
||||
$("#audio_bgm_select option").each(function () { audio_files.push($(this).val()); });
|
||||
}
|
||||
|
||||
audio_file_path = audio_files[Math.floor(Math.random() * audio_files.length)];
|
||||
}
|
||||
|
||||
console.log(DEBUG_PREFIX, "Updating BGM");
|
||||
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
|
||||
try {
|
||||
const response = await fetch(audio_file_path);
|
||||
|
||||
if (!response.ok) {
|
||||
console.log(DEBUG_PREFIX, "File not found!")
|
||||
}
|
||||
else {
|
||||
console.log(DEBUG_PREFIX, "Switching BGM to", currentExpressionBGM);
|
||||
$("#audio_bgm_select").val(audio_file_path);
|
||||
const audio = $("#audio_bgm");
|
||||
|
||||
if (audio.attr("src") == audio_file_path && !bgmEnded) {
|
||||
console.log(DEBUG_PREFIX, "Already playing, ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
let fade_time = 2000;
|
||||
bgmEnded = false;
|
||||
|
||||
if (isUserInput || extension_settings.audio.bgm_locked) {
|
||||
audio.attr("src", audio_file_path);
|
||||
audio[0].play();
|
||||
}
|
||||
else {
|
||||
audio.animate({ volume: 0.0 }, fade_time, function () {
|
||||
audio.attr("src", audio_file_path);
|
||||
audio[0].play();
|
||||
audio.volume = extension_settings.audio.bgm_volume * 0.01;
|
||||
audio.animate({ volume: extension_settings.audio.bgm_volume * 0.01 }, fade_time);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(DEBUG_PREFIX, "Error while trying to fetch", audio_file_path, ":", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAmbient(isUserInput = false) {
|
||||
let audio_file_path = null;
|
||||
|
||||
if (isUserInput || extension_settings.audio.ambient_locked) {
|
||||
audio_file_path = extension_settings.audio.ambient_selected;
|
||||
|
||||
if (isUserInput)
|
||||
console.debug(DEBUG_PREFIX, "User selected Ambient", audio_file_path);
|
||||
if (extension_settings.audio.bgm_locked)
|
||||
console.debug(DEBUG_PREFIX, "Ambient locked keeping current audio", audio_file_path);
|
||||
}
|
||||
else {
|
||||
extension_settings.audio.ambient_selected = null;
|
||||
for (const i of ambients) {
|
||||
console.debug(i)
|
||||
if (i.includes(currentBackground)) {
|
||||
audio_file_path = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_file_path === null) {
|
||||
console.debug(DEBUG_PREFIX, "No bgm file found for background", currentBackground);
|
||||
const audio = $("#audio_ambient");
|
||||
audio.attr("src", "");
|
||||
audio[0].pause();
|
||||
return;
|
||||
}
|
||||
|
||||
//const audio_file_path = AMBIENT_FOLDER+currentBackground+".mp3";
|
||||
console.log(DEBUG_PREFIX, "Updating ambient");
|
||||
console.log(DEBUG_PREFIX, "Checking file", audio_file_path);
|
||||
$("#audio_ambient_select").val(audio_file_path);
|
||||
|
||||
let fade_time = 2000;
|
||||
if (isUserInput)
|
||||
fade_time = 0;
|
||||
|
||||
const audio = $("#audio_ambient");
|
||||
|
||||
if (audio.attr("src") == audio_file_path) {
|
||||
console.log(DEBUG_PREFIX, "Already playing, ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
audio.animate({ volume: 0.0 }, fade_time, function () {
|
||||
audio.attr("src", audio_file_path);
|
||||
audio[0].play();
|
||||
audio.volume = extension_settings.audio.ambient_volume * 0.01;
|
||||
audio.animate({ volume: extension_settings.audio.ambient_volume * 0.01 }, fade_time);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles wheel events on volume sliders.
|
||||
* @param {WheelEvent} e Event
|
||||
*/
|
||||
function onVolumeSliderWheelEvent(e) {
|
||||
const slider = $(this);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const delta = e.deltaY / 20;
|
||||
const sliderVal = Number(slider.val());
|
||||
|
||||
let newVal = sliderVal - delta;
|
||||
if (newVal < 0) {
|
||||
newVal = 0;
|
||||
} else if (newVal > 100) {
|
||||
newVal = 100;
|
||||
}
|
||||
|
||||
slider.val(newVal).trigger('input');
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// Extension load //
|
||||
//#############################//
|
||||
|
||||
// This function is called when the extension is loaded
|
||||
jQuery(async () => {
|
||||
const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`));
|
||||
|
||||
$('#extensions_settings').append(windowHtml);
|
||||
loadSettings();
|
||||
|
||||
$("#audio_enabled").on("click", onEnabledClick);
|
||||
$("#audio_dynamic_bgm_enabled").on("click", onDynamicBGMEnabledClick);
|
||||
//$("#audio_dynamic_ambient_enabled").on("click", onDynamicAmbientEnabledClick);
|
||||
|
||||
//$("#audio_bgm").attr("loop", false);
|
||||
$("#audio_ambient").attr("loop", true);
|
||||
|
||||
$("#audio_bgm").hide();
|
||||
$("#audio_bgm_lock").on("click", onBGMLockClick);
|
||||
$("#audio_bgm_mute").on("click", onBGMMuteClick);
|
||||
$("#audio_bgm_volume_slider").on("input", onBGMVolumeChange);
|
||||
$("#audio_bgm_random").on("click", onBGMRandomClick);
|
||||
|
||||
$("#audio_ambient").hide();
|
||||
$("#audio_ambient_lock").on("click", onAmbientLockClick);
|
||||
$("#audio_ambient_mute").on("click", onAmbientMuteClick);
|
||||
$("#audio_ambient_volume_slider").on("input", onAmbientVolumeChange);
|
||||
|
||||
document.getElementById('audio_ambient_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
|
||||
document.getElementById('audio_bgm_volume_slider').addEventListener('wheel', onVolumeSliderWheelEvent, { passive: false });
|
||||
|
||||
$("#audio_bgm_cooldown").on("input", onBGMCooldownInput);
|
||||
|
||||
// Reset assets container, will be redected like if ST restarted
|
||||
$("#audio_refresh_assets").on("click", function () {
|
||||
console.debug(DEBUG_PREFIX, "Refreshing audio assets");
|
||||
current_chat_id = null
|
||||
fallback_BGMS = null;
|
||||
ambients = null;
|
||||
characterMusics = {};
|
||||
currentCharacterBGM = null;
|
||||
currentExpressionBGM = null;
|
||||
currentBackground = null;
|
||||
})
|
||||
|
||||
$("#audio_bgm_select").on("change", onBGMSelectChange);
|
||||
$("#audio_ambient_select").on("change", onAmbientSelectChange);
|
||||
|
||||
// DBG
|
||||
$("#audio_debug").on("click", function () {
|
||||
if ($("#audio_debug").is(':checked')) {
|
||||
$("#audio_bgm").show();
|
||||
$("#audio_ambient").show();
|
||||
}
|
||||
else {
|
||||
$("#audio_bgm").hide();
|
||||
$("#audio_ambient").hide();
|
||||
}
|
||||
});
|
||||
//
|
||||
|
||||
$("#audio_bgm").on("ended", function () {
|
||||
console.debug(DEBUG_PREFIX, "END OF BGM")
|
||||
if (!extension_settings.audio.bgm_locked) {
|
||||
bgmEnded = true;
|
||||
updateBGM();
|
||||
}
|
||||
});
|
||||
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "Dynamic Audio",
|
||||
"loading_order": 14,
|
||||
"requires": [],
|
||||
"optional": ["classify"],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Keij#6799 and Deffcolony",
|
||||
"version": "0.1.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
.audio-ui-block {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.audio-mixer-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 5px;
|
||||
background-color: rgba(38, 38, 38, 0.5);
|
||||
border: 1px rgb(75, 75, 75) solid;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.audio-label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.audio-volume-div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.audio-lock-button {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.audio-random-button {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.audio-mute-button {
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.audio-slider {
|
||||
width: 100% !important;
|
||||
vertical-align: center;
|
||||
}
|
||||
|
||||
.audio-mute-button-muted {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#audio_refresh_assets {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.audio-mixer-mute {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.audio-mixer-lock {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.audio-mixer-random {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.audio-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.audio-container>.vol {
|
||||
width: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.audio-container>.vol>input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.audio-container>.playlist {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.audio-container>.playlist>select {
|
||||
height: 100%;
|
||||
margin: 0 !important;
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
<div id="audio_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Dynamic Audio</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div>
|
||||
<label class="checkbox_label" for="audio_enabled">
|
||||
<input type="checkbox" id="audio_enabled" name="audio_enabled">
|
||||
<small>Enabled</small>
|
||||
</label>
|
||||
<div id="audio_bgm_dynamic_enable_div">
|
||||
<label class="checkbox_label" for="audio_dynamic_bgm_enabled">
|
||||
<input type="checkbox" id="audio_dynamic_bgm_enabled" name="audio_dynamic_bgm_enabled">
|
||||
<small>Enable expression BGM switch (req. character expression)</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="audio_debug_div">
|
||||
<label class="checkbox_label" for="audio_debug">
|
||||
<input type="checkbox" id="audio_debug" name="audio_debug">
|
||||
<small>Debug</small>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label for="audio_refresh_assets">Refresh assets</label>
|
||||
<div id="audio_refresh_assets" class="menu_button">
|
||||
<i class="fa-solid fa-refresh fa-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="audio-ui-block">
|
||||
<label for="audio_bgm_volume_slider">Music</label>
|
||||
<div class="audio-mixer-div audio-container">
|
||||
<div class="audio-mixer-element audio-mixer-mute">
|
||||
<div id="audio_bgm_mute" class="menu_button audio-mute-button">
|
||||
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_bgm_mute_icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-mixer-element vol audio-mixer-volume">
|
||||
<input type="range" class ="audio-slider" id ="audio_bgm_volume_slider" value = "0" maxlength ="100">
|
||||
</div>
|
||||
<div class="audio-mixer-element playlist audio-mixer-playlist">
|
||||
<select id="audio_bgm_select">
|
||||
</select>
|
||||
</div>
|
||||
<div class="audio-mixer-element audio-mixer-lock">
|
||||
<div id="audio_bgm_lock" class="menu_button audio-lock-button">
|
||||
<i class="fa-solid fa-repeat fa-lg fa-fw" id="audio_bgm_lock_icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-mixer-element audio-mixer-random">
|
||||
<div id="audio_bgm_random" class="menu_button audio-random-button">
|
||||
<i class="fa-solid fa-random fa-lg fa-fw" id="audio_bgm_random_icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="audio_bgm" controls src="">
|
||||
</div>
|
||||
<div>
|
||||
<label for="audio_ambient_volume_slider">Ambient</label>
|
||||
<div class="audio-mixer-div audio-container">
|
||||
<div class="audio-mixer-element audio-mixer-mute">
|
||||
<div id="audio_ambient_mute" class="menu_button audio-mute-button">
|
||||
<i class="fa-solid fa-volume-high fa-lg fa-fw" id="audio_ambient_mute_icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="audio-mixer-element vol audio-mixer-volume">
|
||||
<input type="range" class ="audio-slider" id ="audio_ambient_volume_slider" value = "0" maxlength ="100">
|
||||
</div>
|
||||
<div class="audio-mixer-element playlist audio-mixer-playlist">
|
||||
<select id="audio_ambient_select">
|
||||
</select>
|
||||
</div>
|
||||
<div class="audio-mixer-element">
|
||||
<div id="audio_ambient_lock" class="menu_button audio-lock-button">
|
||||
<i class="fa-solid fa-lock-open fa-lg fa-fw" id="audio_ambient_lock_icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="audio_ambient" controls src="">
|
||||
</div>
|
||||
<div>
|
||||
<label for="audio_bgm_cooldown">Music update cooldown (in seconds)</label>
|
||||
<input id="audio_bgm_cooldown" class="text_pole wide30p">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<b>Hint:</b>
|
||||
<i>
|
||||
Create new folder in the
|
||||
<b>public/characters/</b>
|
||||
folder and name it as the name of the character.
|
||||
Create a folder name <b>bgm</b> inside of it.
|
||||
Put bgm music with expressions there. File names should follow the pattern:
|
||||
<it>[expression_label]_[number].mp3</it>
|
||||
By default one of the <it>neutral_[number].mp3</it> will play if classify module is not active.
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -21,6 +21,11 @@ export const metadataKeys = {
|
||||
// Gets the CFG guidance scale
|
||||
// If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways
|
||||
export function getGuidanceScale() {
|
||||
if (!extension_settings.cfg) {
|
||||
console.warn("CFG extension is not enabled. Skipping CFG guidance.");
|
||||
return;
|
||||
}
|
||||
|
||||
const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid));
|
||||
const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale];
|
||||
const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false;
|
||||
|
@ -1,88 +0,0 @@
|
||||
import { callPopup } from "../../../script.js";
|
||||
import { getContext } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'dice';
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
|
||||
async function doDiceRoll(customDiceFormula) {
|
||||
let value = typeof customDiceFormula === 'string' ? customDiceFormula.trim() : $(this).data('value');
|
||||
|
||||
if (value == 'custom') {
|
||||
value = await callPopup('Enter the dice formula:<br><i>(for example, <tt>2d6</tt>)</i>', 'input');
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = droll.validate(value);
|
||||
|
||||
if (isValid) {
|
||||
const result = droll.roll(value);
|
||||
const context = getContext();
|
||||
context.sendSystemMessage('generic', `${context.name1} rolls a ${value}. The result is: ${result.total} (${result.rolls})`, { isSmallSys: true });
|
||||
} else {
|
||||
toastr.warning('Invalid dice formula');
|
||||
}
|
||||
}
|
||||
|
||||
function addDiceRollButton() {
|
||||
const buttonHtml = `
|
||||
<div id="roll_dice" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-dice extensionsMenuExtensionButton" title="Roll Dice" /></div>
|
||||
Roll Dice
|
||||
</div>
|
||||
`;
|
||||
const dropdownHtml = `
|
||||
<div id="dice_dropdown">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item" data-value="d4">d4</li>
|
||||
<li class="list-group-item" data-value="d6">d6</li>
|
||||
<li class="list-group-item" data-value="d8">d8</li>
|
||||
<li class="list-group-item" data-value="d10">d10</li>
|
||||
<li class="list-group-item" data-value="d12">d12</li>
|
||||
<li class="list-group-item" data-value="d20">d20</li>
|
||||
<li class="list-group-item" data-value="d100">d100</li>
|
||||
<li class="list-group-item" data-value="custom">...</li>
|
||||
</ul>
|
||||
</div>`;
|
||||
|
||||
$('#extensionsMenu').prepend(buttonHtml);
|
||||
|
||||
$(document.body).append(dropdownHtml)
|
||||
$('#dice_dropdown li').on('click', doDiceRoll);
|
||||
const button = $('#roll_dice');
|
||||
const dropdown = $('#dice_dropdown');
|
||||
dropdown.hide();
|
||||
button.hide();
|
||||
|
||||
let popper = Popper.createPopper(button.get(0), dropdown.get(0), {
|
||||
placement: 'top',
|
||||
});
|
||||
|
||||
$(document).on('click touchend', function (e) {
|
||||
const target = $(e.target);
|
||||
if (target.is(dropdown)) return;
|
||||
if (target.is(button) && !dropdown.is(":visible")) {
|
||||
e.preventDefault();
|
||||
|
||||
dropdown.fadeIn(250);
|
||||
popper.update();
|
||||
} else {
|
||||
dropdown.fadeOut(250);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
$('#roll_dice').toggle(getContext().onlineStatus !== 'no_connection');
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
addDiceRollButton();
|
||||
moduleWorker();
|
||||
setInterval(moduleWorker, UPDATE_INTERVAL);
|
||||
registerSlashCommand('roll', (_, value) => doDiceRoll(value), ['r'], "<span class='monospace'>(dice formula)</span> – roll the dice. For example, /roll 2d6", false, true);
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "D&D Dice",
|
||||
"loading_order": 5,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
#roll_dice {
|
||||
/* order: 100; */
|
||||
/* width: 40px;
|
||||
height: 40px;
|
||||
margin: 0;
|
||||
padding: 1px; */
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* justify-content: center; */
|
||||
|
||||
}
|
||||
|
||||
#roll_dice:hover {
|
||||
opacity: 1;
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
#dice_dropdown {
|
||||
z-index: 30000;
|
||||
backdrop-filter: blur(--SmartThemeBlurStrength);
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
import { eventSource, event_types, getRequestHeaders, is_send_press, saveSettingsDebounced } from "../../../script.js";
|
||||
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
|
||||
import { SECRET_KEYS, secret_state } from "../../secrets.js";
|
||||
import { collapseNewlines } from "../../power-user.js";
|
||||
import { bufferToBase64, debounce } from "../../utils.js";
|
||||
import { decodeTextTokens, getTextTokens, tokenizers } from "../../tokenizers.js";
|
||||
|
||||
const MODULE_NAME = 'hypebot';
|
||||
const WAITING_VERBS = ['thinking', 'typing', 'brainstorming', 'cooking', 'conjuring'];
|
||||
const MAX_PROMPT = 1024;
|
||||
const MAX_LENGTH = 50;
|
||||
const MAX_STRING_LENGTH = MAX_PROMPT * 4;
|
||||
|
||||
const settings = {
|
||||
enabled: false,
|
||||
name: 'Goose',
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random waiting verb
|
||||
* @returns {string} Random waiting verb
|
||||
*/
|
||||
function getWaitingVerb() {
|
||||
return WAITING_VERBS[Math.floor(Math.random() * WAITING_VERBS.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a random verb based on the text
|
||||
* @param {string} text Text to generate a verb for
|
||||
* @returns {string} Random verb
|
||||
*/
|
||||
function getVerb(text) {
|
||||
let verbList = ['says', 'notes', 'states', 'whispers', 'murmurs', 'mumbles'];
|
||||
|
||||
if (text.endsWith('!')) {
|
||||
verbList = ['proclaims', 'declares', 'salutes', 'exclaims', 'cheers'];
|
||||
}
|
||||
|
||||
if (text.endsWith('?')) {
|
||||
verbList = ['asks', 'suggests', 'ponders', 'wonders', 'inquires', 'questions'];
|
||||
}
|
||||
|
||||
return verbList[Math.floor(Math.random() * verbList.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the HypeBot reply text
|
||||
* @param {string} text HypeBot output text
|
||||
* @returns {string} Formatted HTML text
|
||||
*/
|
||||
function formatReply(text) {
|
||||
return `<span class="hypebot_name">${settings.name} ${getVerb(text)}:</span> <span class="hypebot_text">${text}</span>`;
|
||||
}
|
||||
|
||||
let hypeBotBar;
|
||||
let abortController;
|
||||
|
||||
const generateDebounced = debounce(() => generateHypeBot(), 500);
|
||||
|
||||
/**
|
||||
* Sets the HypeBot text. Preserves scroll position of the chat.
|
||||
* @param {string} text Text to set
|
||||
*/
|
||||
function setHypeBotText(text) {
|
||||
const chatBlock = $('#chat');
|
||||
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
|
||||
hypeBotBar.html(DOMPurify.sanitize(text));
|
||||
const newScrollTop = chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom);
|
||||
chatBlock.scrollTop(newScrollTop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a chat event occurs to generate a HypeBot reply.
|
||||
* @param {boolean} clear Clear the hypebot bar.
|
||||
*/
|
||||
function onChatEvent(clear) {
|
||||
if (clear) {
|
||||
setHypeBotText('');
|
||||
}
|
||||
|
||||
abortController?.abort();
|
||||
generateDebounced();
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates a HypeBot reply.
|
||||
*/
|
||||
async function generateHypeBot() {
|
||||
if (!settings.enabled || is_send_press) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secret_state[SECRET_KEYS.NOVEL]) {
|
||||
setHypeBotText('<div class="hypebot_nokey">No API key found. Please enter your API key in the NovelAI API Settings to use the HypeBot.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Generating HypeBot reply');
|
||||
setHypeBotText(`<span class="hypebot_name">${settings.name}</span> is ${getWaitingVerb()}...`);
|
||||
|
||||
const context = getContext();
|
||||
const chat = context.chat.slice();
|
||||
let prompt = '';
|
||||
|
||||
for (let index = chat.length - 1; index >= 0; index--) {
|
||||
const message = chat[index];
|
||||
|
||||
if (message.is_system || !message.mes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
prompt = `\n${message.mes}\n${prompt}`;
|
||||
|
||||
if (prompt.length >= MAX_STRING_LENGTH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
prompt = collapseNewlines(prompt.replaceAll(/[\*\[\]\{\}]/g, ''));
|
||||
|
||||
if (!prompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sliceLength = MAX_PROMPT - MAX_LENGTH;
|
||||
const encoded = getTextTokens(tokenizers.GPT2, prompt).slice(-sliceLength);
|
||||
|
||||
// Add a stop string token to the end of the prompt
|
||||
encoded.push(49527);
|
||||
|
||||
const base64String = await bufferToBase64(new Uint16Array(encoded).buffer);
|
||||
|
||||
const parameters = {
|
||||
input: base64String,
|
||||
model: "hypebot",
|
||||
streaming: false,
|
||||
temperature: 1,
|
||||
max_length: MAX_LENGTH,
|
||||
min_length: 1,
|
||||
top_k: 0,
|
||||
top_p: 1,
|
||||
tail_free_sampling: 0.95,
|
||||
repetition_penalty: 1,
|
||||
repetition_penalty_range: 2048,
|
||||
repetition_penalty_slope: 0.18,
|
||||
repetition_penalty_frequency: 0,
|
||||
repetition_penalty_presence: 0,
|
||||
phrase_rep_pen: "off",
|
||||
bad_words_ids: [],
|
||||
stop_sequences: [[48585]],
|
||||
generate_until_sentence: true,
|
||||
use_cache: false,
|
||||
use_string: false,
|
||||
return_full_text: false,
|
||||
prefix: "vanilla",
|
||||
logit_bias_exp: [],
|
||||
order: [0, 1, 2, 3],
|
||||
};
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
const response = await fetch('/api/novelai/generate', {
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(parameters),
|
||||
method: 'POST',
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const ids = Array.from(new Uint16Array(Uint8Array.from(atob(data.output), c => c.charCodeAt(0)).buffer));
|
||||
const output = decodeTextTokens(tokenizers.GPT2, ids).replace(/<2F>/g, '').trim();
|
||||
|
||||
setHypeBotText(formatReply(output));
|
||||
} else {
|
||||
setHypeBotText('<div class="hypebot_error">Something went wrong while generating a HypeBot reply. Please try again.</div>');
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
if (!extension_settings.hypebot) {
|
||||
extension_settings.hypebot = settings;
|
||||
}
|
||||
|
||||
Object.assign(settings, extension_settings.hypebot);
|
||||
$('#extensions_settings2').append(renderExtensionTemplate(MODULE_NAME, 'settings'));
|
||||
hypeBotBar = $(`<div id="hypeBotBar"></div>`).toggle(settings.enabled);
|
||||
$('#send_form').append(hypeBotBar);
|
||||
|
||||
$('#hypebot_enabled').prop('checked', settings.enabled).on('input', () => {
|
||||
settings.enabled = $('#hypebot_enabled').prop('checked');
|
||||
hypeBotBar.toggle(settings.enabled);
|
||||
abortController?.abort();
|
||||
Object.assign(extension_settings.hypebot, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#hypebot_name').val(settings.name).on('input', () => {
|
||||
settings.name = String($('#hypebot_name').val());
|
||||
Object.assign(extension_settings.hypebot, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => onChatEvent(true));
|
||||
eventSource.on(event_types.MESSAGE_DELETED, () => onChatEvent(true));
|
||||
eventSource.on(event_types.MESSAGE_EDITED, () => onChatEvent(true));
|
||||
eventSource.on(event_types.MESSAGE_SENT, () => onChatEvent(false));
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, () => onChatEvent(false));
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, () => onChatEvent(false));
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "HypeBot",
|
||||
"loading_order": 1000,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
<div class="hypebot_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>HypeBot</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<div>Show personalized suggestions based on your recent chats using the NovelAI's HypeBot engine.</div>
|
||||
<small><i>Hint: Save an API key in the NovelAI API settings to use it here.</i></small>
|
||||
<label class="checkbox_label" for="hypebot_enabled">
|
||||
<input id="hypebot_enabled" type="checkbox" class="checkbox">
|
||||
Enabled
|
||||
</label>
|
||||
<label>Name:</label>
|
||||
<input id="hypebot_name" type="text" class="text_pole" placeholder="Goose">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,17 +0,0 @@
|
||||
#hypeBotBar {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0.5em;
|
||||
white-space: normal;
|
||||
font-size: calc(var(--mainFontSize) * 0.85);
|
||||
order: 20;
|
||||
}
|
||||
|
||||
.hypebot_nokey {
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hypebot_name {
|
||||
font-weight: 600;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
<div class="idle-settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header" title="Indicates the settings for the idle feature.">
|
||||
<b>Idle</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
|
||||
<div class="inline-drawer-content">
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_enabled" type="checkbox" title="Toggle to enable or disable the idle feature." />
|
||||
<label for="idle_enabled">Enabled</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_repeats" class="text_pole widthUnset" type="number" min="0" max="100000" step="1" title="The number of times the idle action will be prompted." />
|
||||
<label for="idle_repeats">Idle Prompt Count</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container" style="display: none;">
|
||||
<input id="idle_timer_min" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The minimum amount of time in seconds before the idle action is triggered." />
|
||||
<label for="idle_timer_min">Idle Timer Minimum (seconds)</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_timer" class="text_pole widthUnset" type="number" min="0" max="600000" step="1" title="The amount of time in seconds before the idle action is triggered." />
|
||||
<label for="idle_timer">Idle Timer (seconds)</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<label for="idle_prompts">Idle Prompts</label>
|
||||
<textarea id="idle_prompts" class="text_pole textarea_compact" rows="6" title="The prompts to be sent to initial the idle reply (newline seperated)."></textarea>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_use_continuation" type="checkbox" title="Indicates whether the idle action will just use the 'Continue' function instead of a prompt." />
|
||||
<label for="idle_use_continuation">Use Continuation</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_random_time" type="checkbox" title="Indicates if the idle time should be randomized between a min/max value." />
|
||||
<label for="idle_random_time">Randomize Time</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<input id="idle_include_prompt" type="checkbox" title="Indicates if the idle prompting should be included in context. (Sends as user)" />
|
||||
<label for="idle_include_prompt">Include Idle Prompt</label>
|
||||
</div>
|
||||
<div class="idle_block flex-container">
|
||||
<label for="idle_sendAs">Send As</label>
|
||||
<select id="idle_sendAs" class="text_pole" title="Determines how the idle message prompting is sent; as a user, character, system, or raw message.">
|
||||
<option value="user">User</option>
|
||||
<option value="char">Character</option>
|
||||
<option value="sys">System</option>
|
||||
<option value="raw">Raw</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<hr class="sysHR" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,329 +0,0 @@
|
||||
import {
|
||||
saveSettingsDebounced,
|
||||
substituteParams
|
||||
} from "../../../script.js";
|
||||
import { debounce } from "../../utils.js";
|
||||
import { promptQuietForLoudResponse, sendMessageAs, sendNarratorMessage } from "../../slash-commands.js";
|
||||
import { extension_settings, getContext, renderExtensionTemplate } from "../../extensions.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
const extensionName = "idle";
|
||||
|
||||
let idleTimer = null;
|
||||
let repeatCount = 0;
|
||||
|
||||
let defaultSettings = {
|
||||
enabled: false,
|
||||
timer: 120,
|
||||
prompts: [
|
||||
"*stands silently, looking deep in thought*",
|
||||
"*pauses, eyes wandering over the surroundings*",
|
||||
"*hesitates, appearing lost for a moment*",
|
||||
"*takes a deep breath, collecting their thoughts*",
|
||||
"*gazes into the distance, seemingly distracted*",
|
||||
"*remains still, absorbing the ambiance*",
|
||||
"*lingers in silence, a contemplative look on their face*",
|
||||
"*stops, fingers brushing against an old memory*",
|
||||
"*seems to drift into a momentary daydream*",
|
||||
"*waits quietly, allowing the weight of the moment to settle*",
|
||||
],
|
||||
useContinuation: true,
|
||||
repeats: 2, // 0 = infinite
|
||||
sendAs: "user",
|
||||
randomTime: false,
|
||||
timeMin: 60,
|
||||
includePrompt: false,
|
||||
};
|
||||
|
||||
|
||||
//TODO: Can we make this a generic function?
|
||||
/**
|
||||
* Load the extension settings and set defaults if they don't exist.
|
||||
*/
|
||||
async function loadSettings() {
|
||||
if (!extension_settings.idle) {
|
||||
console.log("Creating extension_settings.idle");
|
||||
extension_settings.idle = {};
|
||||
}
|
||||
for (const [key, value] of Object.entries(defaultSettings)) {
|
||||
if (!extension_settings.idle.hasOwnProperty(key)) {
|
||||
console.log(`Setting default for: ${key}`);
|
||||
extension_settings.idle[key] = value;
|
||||
}
|
||||
}
|
||||
populateUIWithSettings();
|
||||
}
|
||||
|
||||
//TODO: Can we make this a generic function too?
|
||||
/**
|
||||
* Populate the UI components with values from the extension settings.
|
||||
*/
|
||||
function populateUIWithSettings() {
|
||||
$("#idle_timer").val(extension_settings.idle.timer).trigger("input");
|
||||
$("#idle_prompts").val(extension_settings.idle.prompts.join("\n")).trigger("input");
|
||||
$("#idle_use_continuation").prop("checked", extension_settings.idle.useContinuation).trigger("input");
|
||||
$("#idle_enabled").prop("checked", extension_settings.idle.enabled).trigger("input");
|
||||
$("#idle_repeats").val(extension_settings.idle.repeats).trigger("input");
|
||||
$("#idle_sendAs").val(extension_settings.idle.sendAs).trigger("input");
|
||||
$("#idle_random_time").prop("checked", extension_settings.idle.randomTime).trigger("input");
|
||||
$("#idle_timer_min").val(extension_settings.idle.timerMin).trigger("input");
|
||||
$("#idle_include_prompt").prop("checked", extension_settings.idle.includePrompt).trigger("input");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset the idle timer based on the extension settings and context.
|
||||
*/
|
||||
function resetIdleTimer() {
|
||||
console.debug("Resetting idle timer");
|
||||
if (idleTimer) clearTimeout(idleTimer);
|
||||
let context = getContext();
|
||||
if (!context.characterId && !context.groupID) return;
|
||||
if (!extension_settings.idle.enabled) return;
|
||||
if (extension_settings.idle.randomTime) {
|
||||
// ensure these are ints
|
||||
let min = extension_settings.idle.timerMin;
|
||||
let max = extension_settings.idle.timer;
|
||||
min = parseInt(min);
|
||||
max = parseInt(max);
|
||||
let randomTime = (Math.random() * (max - min + 1)) + min;
|
||||
idleTimer = setTimeout(sendIdlePrompt, 1000 * randomTime);
|
||||
} else {
|
||||
idleTimer = setTimeout(sendIdlePrompt, 1000 * extension_settings.idle.timer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a random idle prompt to the AI based on the extension settings.
|
||||
* Checks conditions like if the extension is enabled and repeat conditions.
|
||||
*/
|
||||
async function sendIdlePrompt() {
|
||||
if (!extension_settings.idle.enabled) return;
|
||||
|
||||
// Check repeat conditions and waiting for a response
|
||||
if (repeatCount >= extension_settings.idle.repeats || $('#mes_stop').is(':visible')) {
|
||||
//console.debug("Not sending idle prompt due to repeat conditions or waiting for a response.");
|
||||
resetIdleTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const randomPrompt = extension_settings.idle.prompts[
|
||||
Math.floor(Math.random() * extension_settings.idle.prompts.length)
|
||||
];
|
||||
|
||||
sendPrompt(randomPrompt);
|
||||
repeatCount++;
|
||||
resetIdleTimer();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add our prompt to the chat and then send the chat to the backend.
|
||||
* @param {string} sendAs - The type of message to send. "user", "char", or "sys".
|
||||
* @param {string} prompt - The prompt text to send to the AI.
|
||||
*/
|
||||
function sendLoud(sendAs, prompt) {
|
||||
if (sendAs === "user") {
|
||||
prompt = substituteParams(prompt);
|
||||
|
||||
$("#send_textarea").val(prompt);
|
||||
|
||||
// Set the focus back to the textarea
|
||||
$("#send_textarea").focus();
|
||||
|
||||
$("#send_but").trigger('click');
|
||||
} else if (sendAs === "char") {
|
||||
sendMessageAs("", `${getContext().name2}\n${prompt}`);
|
||||
promptQuietForLoudResponse(sendAs, "");
|
||||
} else if (sendAs === "sys") {
|
||||
sendNarratorMessage("", prompt);
|
||||
promptQuietForLoudResponse(sendAs, "");
|
||||
}
|
||||
else {
|
||||
console.error(`Unknown sendAs value: ${sendAs}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the provided prompt to the AI. Determines method based on continuation setting.
|
||||
* @param {string} prompt - The prompt text to send to the AI.
|
||||
*/
|
||||
function sendPrompt(prompt) {
|
||||
clearTimeout(idleTimer);
|
||||
$("#send_textarea").off("input");
|
||||
|
||||
if (extension_settings.idle.useContinuation) {
|
||||
$('#option_continue').trigger('click');
|
||||
console.debug("Sending idle prompt with continuation");
|
||||
} else {
|
||||
console.debug("Sending idle prompt");
|
||||
console.log(extension_settings.idle);
|
||||
if (extension_settings.idle.includePrompt) {
|
||||
sendLoud(extension_settings.idle.sendAs, prompt);
|
||||
}
|
||||
else {
|
||||
promptQuietForLoudResponse(extension_settings.idle.sendAs, prompt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the settings HTML and append to the designated area.
|
||||
*/
|
||||
async function loadSettingsHTML() {
|
||||
const settingsHtml = renderExtensionTemplate(extensionName, "dropdown");
|
||||
$("#extensions_settings2").append(settingsHtml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a specific setting based on user input.
|
||||
* @param {string} elementId - The HTML element ID tied to the setting.
|
||||
* @param {string} property - The property name in the settings object.
|
||||
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
|
||||
*/
|
||||
function updateSetting(elementId, property, isCheckbox = false) {
|
||||
let value = $(`#${elementId}`).val();
|
||||
if (isCheckbox) {
|
||||
value = $(`#${elementId}`).prop('checked');
|
||||
}
|
||||
|
||||
if (property === "prompts") {
|
||||
value = value.split("\n");
|
||||
}
|
||||
|
||||
extension_settings.idle[property] = value;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an input listener to a UI component to update the corresponding setting.
|
||||
* @param {string} elementId - The HTML element ID tied to the setting.
|
||||
* @param {string} property - The property name in the settings object.
|
||||
* @param {boolean} [isCheckbox=false] - Whether the setting is a checkbox.
|
||||
*/
|
||||
function attachUpdateListener(elementId, property, isCheckbox = false) {
|
||||
$(`#${elementId}`).on('input', debounce(() => {
|
||||
updateSetting(elementId, property, isCheckbox);
|
||||
}, 250));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the enabling or disabling of the idle extension.
|
||||
* Adds or removes the idle listeners based on the checkbox's state.
|
||||
*/
|
||||
function handleIdleEnabled() {
|
||||
if (!extension_settings.idle.enabled) {
|
||||
clearTimeout(idleTimer);
|
||||
removeIdleListeners();
|
||||
} else {
|
||||
resetIdleTimer();
|
||||
attachIdleListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Setup input listeners for the various settings and actions related to the idle extension.
|
||||
*/
|
||||
function setupListeners() {
|
||||
const settingsToWatch = [
|
||||
['idle_timer', 'timer'],
|
||||
['idle_prompts', 'prompts'],
|
||||
['idle_use_continuation', 'useContinuation', true],
|
||||
['idle_enabled', 'enabled', true],
|
||||
['idle_repeats', 'repeats'],
|
||||
['idle_sendAs', 'sendAs'],
|
||||
['idle_random_time', 'randomTime', true],
|
||||
['idle_timer_min', 'timerMin'],
|
||||
['idle_include_prompt', 'includePrompt', true]
|
||||
];
|
||||
settingsToWatch.forEach(setting => {
|
||||
attachUpdateListener(...setting);
|
||||
});
|
||||
|
||||
// Idleness listeners, could be made better
|
||||
$('#idle_enabled').on('input', debounce(handleIdleEnabled, 250));
|
||||
|
||||
// Add the idle listeners initially if the idle feature is enabled
|
||||
if (extension_settings.idle.enabled) {
|
||||
attachIdleListeners();
|
||||
}
|
||||
|
||||
//show/hide timer min parent div
|
||||
$('#idle_random_time').on('input', function () {
|
||||
if ($(this).prop('checked')) {
|
||||
$('#idle_timer_min').parent().show();
|
||||
} else {
|
||||
$('#idle_timer_min').parent().hide();
|
||||
}
|
||||
|
||||
$('#idle_timer').trigger('input');
|
||||
});
|
||||
|
||||
// if we're including the prompt, hide raw from the sendAs dropdown
|
||||
$('#idle_include_prompt').on('input', function () {
|
||||
if ($(this).prop('checked')) {
|
||||
$('#idle_sendAs option[value="raw"]').hide();
|
||||
} else {
|
||||
$('#idle_sendAs option[value="raw"]').show();
|
||||
}
|
||||
});
|
||||
|
||||
//make sure timer min is less than timer
|
||||
$('#idle_timer').on('input', function () {
|
||||
if ($('#idle_random_time').prop('checked')) {
|
||||
if ($(this).val() < $('#idle_timer_min').val()) {
|
||||
$('#idle_timer_min').val($(this).val());
|
||||
$('#idle_timer_min').trigger('input');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const debouncedActivityHandler = debounce((event) => {
|
||||
// Check if the event target (or any of its parents) has the id "option_continue"
|
||||
if ($(event.target).closest('#option_continue').length) {
|
||||
return; // Do not proceed if the click was on (or inside) an element with id "option_continue"
|
||||
}
|
||||
|
||||
console.debug("Activity detected, resetting idle timer");
|
||||
resetIdleTimer();
|
||||
repeatCount = 0;
|
||||
}, 250);
|
||||
|
||||
function attachIdleListeners() {
|
||||
$(document).on("click keypress", debouncedActivityHandler);
|
||||
document.addEventListener('keydown', debouncedActivityHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove idle-specific listeners.
|
||||
*/
|
||||
function removeIdleListeners() {
|
||||
$(document).off("click keypress", debouncedActivityHandler);
|
||||
document.removeEventListener('keydown', debouncedActivityHandler);
|
||||
}
|
||||
|
||||
function toggleIdle() {
|
||||
extension_settings.idle.enabled = !extension_settings.idle.enabled;
|
||||
$('#idle_enabled').prop('checked', extension_settings.idle.enabled);
|
||||
$('#idle_enabled').trigger('input');
|
||||
toastr.info(`Idle mode ${extension_settings.idle.enabled ? "enabled" : "disabled"}.`);
|
||||
resetIdleTimer();
|
||||
}
|
||||
|
||||
|
||||
|
||||
jQuery(async () => {
|
||||
await loadSettingsHTML();
|
||||
loadSettings();
|
||||
setupListeners();
|
||||
if (extension_settings.idle.enabled) {
|
||||
resetIdleTimer();
|
||||
}
|
||||
// once the doc is ready, check if random time is checked and hide/show timer min
|
||||
if ($('#idle_random_time').prop('checked')) {
|
||||
$('#idle_timer_min').parent().show();
|
||||
}
|
||||
registerSlashCommand('idle', toggleIdle, [], '– toggles idle mode', true, true);
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"display_name": "Idle",
|
||||
"loading_order": 6,
|
||||
"requires": [],
|
||||
"optional": [
|
||||
],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "City-Unit",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.idle_block {
|
||||
align-items: center;
|
||||
}
|
@ -1,949 +0,0 @@
|
||||
import { saveSettingsDebounced, getCurrentChatId, system_message_types, extension_prompt_types, eventSource, event_types, getRequestHeaders, substituteParams, } from "../../../script.js";
|
||||
import { humanizedDateTime } from "../../RossAscends-mods.js";
|
||||
import { getApiUrl, extension_settings, getContext, doExtrasFetch } from "../../extensions.js";
|
||||
import { CHARACTERS_PER_TOKEN_RATIO } from "../../tokenizers.js";
|
||||
import { getFileText, onlyUnique, splitRecursive } from "../../utils.js";
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'chromadb';
|
||||
const dbStore = localforage.createInstance({ name: 'SillyTavern_ChromaDB' });
|
||||
|
||||
const defaultSettings = {
|
||||
strategy: 'original',
|
||||
sort_strategy: 'date',
|
||||
|
||||
keep_context: 10,
|
||||
keep_context_min: 1,
|
||||
keep_context_max: 500,
|
||||
keep_context_step: 1,
|
||||
|
||||
n_results: 20,
|
||||
n_results_min: 0,
|
||||
n_results_max: 500,
|
||||
n_results_step: 1,
|
||||
|
||||
chroma_depth: 20,
|
||||
chroma_depth_min: -1,
|
||||
chroma_depth_max: 500,
|
||||
chroma_depth_step: 1,
|
||||
chroma_default_msg: "In a past conversation: [{{memories}}]",
|
||||
chroma_default_hhaa_wrapper: "Previous messages exchanged between {{user}} and {{char}}:\n{{memories}}",
|
||||
chroma_default_hhaa_memory: "- {{name}}: {{message}}\n",
|
||||
hhaa_token_limit: 512,
|
||||
|
||||
split_length: 384,
|
||||
split_length_min: 64,
|
||||
split_length_max: 4096,
|
||||
split_length_step: 64,
|
||||
|
||||
file_split_length: 1024,
|
||||
file_split_length_min: 512,
|
||||
file_split_length_max: 4096,
|
||||
file_split_length_step: 128,
|
||||
|
||||
keep_context_proportion: 0.5,
|
||||
keep_context_proportion_min: 0.0,
|
||||
keep_context_proportion_max: 1.0,
|
||||
keep_context_proportion_step: 0.05,
|
||||
|
||||
auto_adjust: true,
|
||||
freeze: false,
|
||||
query_last_only: true,
|
||||
};
|
||||
|
||||
const postHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
'Bypass-Tunnel-Reminder': 'bypass',
|
||||
};
|
||||
|
||||
async function invalidateMessageSyncState(messageId) {
|
||||
console.log('CHROMADB: invalidating message sync state', messageId);
|
||||
const state = await getChatSyncState();
|
||||
state[messageId] = 0;
|
||||
await dbStore.setItem(getCurrentChatId(), state);
|
||||
}
|
||||
|
||||
async function getChatSyncState() {
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!checkChatId(currentChatId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = getContext();
|
||||
const chatState = (await dbStore.getItem(currentChatId)) || [];
|
||||
|
||||
// if the chat length has decreased, it means that some messages were deleted
|
||||
if (chatState.length > context.chat.length) {
|
||||
for (let i = context.chat.length; i < chatState.length; i++) {
|
||||
// if the synced message was deleted, notify the user
|
||||
if (chatState[i]) {
|
||||
toastr.warning(
|
||||
'Purge your ChromaDB to remove it from there too. See the "Smart Context" tab in the Extensions menu for more information.',
|
||||
'Message deleted from chat, but it still exists inside the ChromaDB database.',
|
||||
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatState.length = context.chat.length;
|
||||
for (let i = 0; i < chatState.length; i++) {
|
||||
if (chatState[i] === undefined) {
|
||||
chatState[i] = 0;
|
||||
}
|
||||
}
|
||||
await dbStore.setItem(currentChatId, chatState);
|
||||
|
||||
return chatState;
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
if (Object.keys(extension_settings.chromadb).length === 0) {
|
||||
Object.assign(extension_settings.chromadb, defaultSettings);
|
||||
}
|
||||
|
||||
console.debug(`loading chromadb strat:${extension_settings.chromadb.strategy}`);
|
||||
$("#chromadb_strategy option[value=" + extension_settings.chromadb.strategy + "]").attr(
|
||||
"selected",
|
||||
"true"
|
||||
);
|
||||
$("#chromadb_sort_strategy option[value=" + extension_settings.chromadb.sort_strategy + "]").attr(
|
||||
"selected",
|
||||
"true"
|
||||
);
|
||||
$('#chromadb_keep_context').val(extension_settings.chromadb.keep_context).trigger('input');
|
||||
$('#chromadb_n_results').val(extension_settings.chromadb.n_results).trigger('input');
|
||||
$('#chromadb_split_length').val(extension_settings.chromadb.split_length).trigger('input');
|
||||
$('#chromadb_file_split_length').val(extension_settings.chromadb.file_split_length).trigger('input');
|
||||
$('#chromadb_keep_context_proportion').val(extension_settings.chromadb.keep_context_proportion).trigger('input');
|
||||
$('#chromadb_custom_depth').val(extension_settings.chromadb.chroma_depth).trigger('input');
|
||||
$('#chromadb_custom_msg').val(extension_settings.chromadb.recall_msg).trigger('input');
|
||||
|
||||
$('#chromadb_hhaa_wrapperfmt').val(extension_settings.chromadb.hhaa_wrapper_msg).trigger('input');
|
||||
$('#chromadb_hhaa_memoryfmt').val(extension_settings.chromadb.hhaa_memory_msg).trigger('input');
|
||||
$('#chromadb_hhaa_token_limit').val(extension_settings.chromadb.hhaa_token_limit).trigger('input');
|
||||
|
||||
$('#chromadb_auto_adjust').prop('checked', extension_settings.chromadb.auto_adjust);
|
||||
$('#chromadb_freeze').prop('checked', extension_settings.chromadb.freeze);
|
||||
$('#chromadb_query_last_only').prop('checked', extension_settings.chromadb.query_last_only);
|
||||
enableDisableSliders();
|
||||
onStrategyChange();
|
||||
}
|
||||
|
||||
function onStrategyChange() {
|
||||
console.debug('changing chromadb strat');
|
||||
extension_settings.chromadb.strategy = $('#chromadb_strategy').val();
|
||||
if (extension_settings.chromadb.strategy === "custom") {
|
||||
$('#chromadb_custom_depth').show();
|
||||
$('label[for="chromadb_custom_depth"]').show();
|
||||
$('#chromadb_custom_msg').show();
|
||||
$('label[for="chromadb_custom_msg"]').show();
|
||||
}
|
||||
else if(extension_settings.chromadb.strategy === "hh_aa"){
|
||||
$('#chromadb_hhaa_wrapperfmt').show();
|
||||
$('label[for="chromadb_hhaa_wrapperfmt"]').show();
|
||||
$('#chromadb_hhaa_memoryfmt').show();
|
||||
$('label[for="chromadb_hhaa_memoryfmt"]').show();
|
||||
$('#chromadb_hhaa_token_limit').show();
|
||||
$('label[for="chromadb_hhaa_token_limit"]').show();
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onRecallStrategyChange() {
|
||||
console.log('changing chromadb recall strat');
|
||||
extension_settings.chromadb.recall_strategy = $('#chromadb_recall_strategy').val();
|
||||
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onSortStrategyChange() {
|
||||
console.log('changing chromadb sort strat');
|
||||
extension_settings.chromadb.sort_strategy = $('#chromadb_sort_strategy').val();
|
||||
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onKeepContextInput() {
|
||||
extension_settings.chromadb.keep_context = Number($('#chromadb_keep_context').val());
|
||||
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onNResultsInput() {
|
||||
extension_settings.chromadb.n_results = Number($('#chromadb_n_results').val());
|
||||
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onChromaDepthInput() {
|
||||
extension_settings.chromadb.chroma_depth = Number($('#chromadb_custom_depth').val());
|
||||
$('#chromadb_custom_depth_value').text(extension_settings.chromadb.chroma_depth);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onChromaMsgInput() {
|
||||
extension_settings.chromadb.recall_msg = $('#chromadb_custom_msg').val();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onChromaHHAAWrapper() {
|
||||
extension_settings.chromadb.hhaa_wrapper_msg = $('#chromadb_hhaa_wrapperfmt').val();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
function onChromaHHAAMemory() {
|
||||
extension_settings.chromadb.hhaa_memory_msg = $('#chromadb_hhaa_memoryfmt').val();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
function onChromaHHAATokens() {
|
||||
extension_settings.chromadb.hhaa_token_limit = Number($('#chromadb_hhaa_token_limit').val());
|
||||
$('#chromadb_hhaa_token_limit_value').text(extension_settings.chromadb.hhaa_token_limit);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onSplitLengthInput() {
|
||||
extension_settings.chromadb.split_length = Number($('#chromadb_split_length').val());
|
||||
$('#chromadb_split_length_value').text(extension_settings.chromadb.split_length);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onFileSplitLengthInput() {
|
||||
extension_settings.chromadb.file_split_length = Number($('#chromadb_file_split_length').val());
|
||||
$('#chromadb_file_split_length_value').text(extension_settings.chromadb.file_split_length);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onChunkNLInput() {
|
||||
let shouldSplit = $('#onChunkNLInput').is(':checked');
|
||||
if (shouldSplit) {
|
||||
extension_settings.chromadb.file_split_type = "newline";
|
||||
} else {
|
||||
extension_settings.chromadb.file_split_type = "length";
|
||||
}
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function checkChatId(chat_id) {
|
||||
if (!chat_id || chat_id.trim() === '') {
|
||||
toastr.error('Please select a character and try again.');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function addMessages(chat_id, messages) {
|
||||
if (extension_settings.chromadb.freeze) {
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb';
|
||||
|
||||
const messagesDeepCopy = JSON.parse(JSON.stringify(messages));
|
||||
let splitMessages = [];
|
||||
|
||||
let id = 0;
|
||||
messagesDeepCopy.forEach((m, index) => {
|
||||
const split = splitRecursive(m.mes, extension_settings.chromadb.split_length);
|
||||
splitMessages.push(...split.map(text => ({
|
||||
...m,
|
||||
mes: text,
|
||||
send_date: id,
|
||||
id: `msg-${id++}`,
|
||||
index: index,
|
||||
extra: undefined,
|
||||
})));
|
||||
});
|
||||
|
||||
splitMessages = await filterSyncedMessages(splitMessages);
|
||||
|
||||
// no messages to add
|
||||
if (splitMessages.length === 0) {
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
const transformedMessages = splitMessages.map((m) => ({
|
||||
id: m.id,
|
||||
role: m.is_user ? 'user' : 'assistant',
|
||||
content: m.mes,
|
||||
date: m.send_date,
|
||||
meta: JSON.stringify(m),
|
||||
}));
|
||||
|
||||
const addMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id, messages: transformedMessages }),
|
||||
});
|
||||
|
||||
if (addMessagesResult.ok) {
|
||||
const addMessagesData = await addMessagesResult.json();
|
||||
return addMessagesData; // { count: 1 }
|
||||
}
|
||||
|
||||
return { count: 0 };
|
||||
}
|
||||
|
||||
async function filterSyncedMessages(splitMessages) {
|
||||
const syncState = await getChatSyncState();
|
||||
const removeIndices = [];
|
||||
const syncedIndices = [];
|
||||
for (let i = 0; i < splitMessages.length; i++) {
|
||||
const index = splitMessages[i].index;
|
||||
|
||||
if (syncState[index]) {
|
||||
removeIndices.push(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
syncedIndices.push(index);
|
||||
}
|
||||
|
||||
for (const index of syncedIndices) {
|
||||
syncState[index] = 1;
|
||||
}
|
||||
|
||||
console.debug('CHROMADB: sync state', syncState.map((v, i) => ({ id: i, synced: v })));
|
||||
await dbStore.setItem(getCurrentChatId(), syncState);
|
||||
|
||||
// remove messages that are already synced
|
||||
return splitMessages.filter((_, i) => !removeIndices.includes(i));
|
||||
}
|
||||
|
||||
async function onPurgeClick() {
|
||||
const chat_id = getCurrentChatId();
|
||||
if (!checkChatId(chat_id)) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/purge';
|
||||
|
||||
const purgeResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id }),
|
||||
});
|
||||
|
||||
if (purgeResult.ok) {
|
||||
await dbStore.removeItem(chat_id);
|
||||
toastr.success('ChromaDB context has been successfully cleared');
|
||||
}
|
||||
}
|
||||
|
||||
async function onExportClick() {
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!checkChatId(currentChatId)) {
|
||||
return;
|
||||
}
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/export';
|
||||
|
||||
const exportResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id: currentChatId }),
|
||||
});
|
||||
|
||||
if (exportResult.ok) {
|
||||
const data = await exportResult.json();
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const href = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = href;
|
||||
link.download = currentChatId + '.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
//Show the error from the result without the html, only what's in the body paragraph
|
||||
let parser = new DOMParser();
|
||||
let error = await exportResult.text();
|
||||
let doc = parser.parseFromString(error, 'text/html');
|
||||
let errorMessage = doc.querySelector('p').textContent;
|
||||
toastr.error(`An error occurred while attempting to download the data from ChromaDB: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
function tinyhash(text) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text.length; ++i) {
|
||||
hash = ((hash<<5) - hash) + text.charCodeAt(i);
|
||||
hash = hash & hash; // Keeps it 32-bit allegedly.
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
async function onSelectImportFile(e) {
|
||||
const file = e.target.files[0];
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!checkChatId(currentChatId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('This may take some time, depending on the file size', 'Processing...');
|
||||
|
||||
const text = await getFileText(file);
|
||||
const imported = JSON.parse(text);
|
||||
|
||||
const id_salt = "-" + tinyhash(imported.chat_id).toString(36);
|
||||
for (let entry of imported.content) {
|
||||
entry.id = entry.id + id_salt;
|
||||
}
|
||||
|
||||
imported.chat_id = currentChatId;
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/import';
|
||||
|
||||
const importResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify(imported),
|
||||
});
|
||||
|
||||
if (importResult.ok) {
|
||||
const importResultData = await importResult.json();
|
||||
|
||||
toastr.success(`Number of chunks: ${importResultData.count}`, 'Injected successfully!');
|
||||
return importResultData;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
toastr.error('Something went wrong while importing the data');
|
||||
}
|
||||
finally {
|
||||
e.target.form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
async function queryMessages(chat_id, query) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/query';
|
||||
|
||||
const queryMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id, query, n_results: extension_settings.chromadb.n_results }),
|
||||
});
|
||||
|
||||
if (queryMessagesResult.ok) {
|
||||
const queryMessagesData = await queryMessagesResult.json();
|
||||
|
||||
return queryMessagesData;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function queryMultiMessages(chat_id, query) {
|
||||
const context = getContext();
|
||||
const response = await fetch("/getallchatsofcharacter", {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ avatar_url: context.characters[context.characterId].avatar }),
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
let data = await response.json();
|
||||
data = Object.values(data);
|
||||
let chat_list = data.sort((a, b) => a["file_name"].localeCompare(b["file_name"])).reverse();
|
||||
|
||||
// Extracting chat_ids from the chat_list
|
||||
chat_list = chat_list.map(chat => chat.file_name.replace(/\.[^/.]+$/, ""));
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb/multiquery';
|
||||
|
||||
const queryMessagesResult = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ chat_list, query, n_results: extension_settings.chromadb.n_results }),
|
||||
headers: postHeaders,
|
||||
});
|
||||
|
||||
if (queryMessagesResult.ok) {
|
||||
const queryMessagesData = await queryMessagesResult.json();
|
||||
return queryMessagesData;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function onSelectInjectFile(e) {
|
||||
const file = e.target.files[0];
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!checkChatId(currentChatId)) {
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toastr.info('This may take some time, depending on the file size', 'Processing...');
|
||||
const text = await getFileText(file);
|
||||
extension_settings.chromadb.file_split_type = "newline";
|
||||
//allow splitting on newlines or splitrecursively
|
||||
let split = [];
|
||||
if (extension_settings.chromadb.file_split_type == "newline") {
|
||||
split = text.split(/\r?\n/).filter(onlyUnique);
|
||||
} else {
|
||||
split = splitRecursive(text, extension_settings.chromadb.file_split_length).filter(onlyUnique);
|
||||
}
|
||||
const baseDate = Date.now();
|
||||
|
||||
const messages = split.map((m, i) => ({
|
||||
id: `${file.name}-${split.indexOf(m)}`,
|
||||
role: 'system',
|
||||
content: m,
|
||||
date: baseDate + i,
|
||||
meta: JSON.stringify({
|
||||
name: file.name,
|
||||
is_user: false,
|
||||
is_system: false,
|
||||
send_date: humanizedDateTime(),
|
||||
mes: m,
|
||||
extra: {
|
||||
type: system_message_types.NARRATOR,
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/chromadb';
|
||||
|
||||
const addMessagesResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: postHeaders,
|
||||
body: JSON.stringify({ chat_id: currentChatId, messages: messages }),
|
||||
});
|
||||
|
||||
if (addMessagesResult.ok) {
|
||||
const addMessagesData = await addMessagesResult.json();
|
||||
|
||||
toastr.success(`Number of chunks: ${addMessagesData.count}`, 'Injected successfully!');
|
||||
return addMessagesData;
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
toastr.error('Something went wrong while injecting the data');
|
||||
}
|
||||
finally {
|
||||
e.target.form.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the length of character description in the current context
|
||||
function getCharacterDataLength() {
|
||||
const context = getContext();
|
||||
const character = context.characters[context.characterId];
|
||||
|
||||
if (typeof character?.data !== 'object') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let characterDataLength = 0;
|
||||
|
||||
for (const [key, value] of Object.entries(character.data)) {
|
||||
if (typeof value !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (['description', 'personality', 'scenario'].includes(key)) {
|
||||
characterDataLength += character.data[key].length;
|
||||
}
|
||||
}
|
||||
|
||||
return characterDataLength;
|
||||
}
|
||||
|
||||
/*
|
||||
* Automatically adjusts the extension settings for the optimal number of messages to keep and query based
|
||||
* on the chat history and a specified maximum context length.
|
||||
*/
|
||||
function doAutoAdjust(chat, maxContext) {
|
||||
// Only valid for chat injections strategy
|
||||
if (extension_settings.chromadb.recall_strategy !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('CHROMADB: Auto-adjusting sliders (messages: %o, maxContext: %o)', chat.length, maxContext);
|
||||
// Get mean message length
|
||||
const meanMessageLength = chat.reduce((acc, cur) => acc + (cur?.mes?.length ?? 0), 0) / chat.length;
|
||||
|
||||
if (Number.isNaN(meanMessageLength) || meanMessageLength === 0) {
|
||||
console.debug('CHROMADB: Mean message length is zero or NaN, aborting auto-adjust');
|
||||
return;
|
||||
}
|
||||
|
||||
// Adjust max context for character defs length
|
||||
maxContext = Math.floor(maxContext - (getCharacterDataLength() / CHARACTERS_PER_TOKEN_RATIO));
|
||||
console.debug('CHROMADB: Max context adjusted for character defs: %o', maxContext);
|
||||
|
||||
console.debug('CHROMADB: Mean message length (characters): %o', meanMessageLength);
|
||||
// Convert to number of "tokens"
|
||||
const meanMessageLengthTokens = Math.ceil(meanMessageLength / CHARACTERS_PER_TOKEN_RATIO);
|
||||
console.debug('CHROMADB: Mean message length (tokens): %o', meanMessageLengthTokens);
|
||||
// Get number of messages in context
|
||||
const contextMessages = Math.max(1, Math.ceil(maxContext / meanMessageLengthTokens));
|
||||
// Round up to nearest 5
|
||||
const contextMessagesRounded = Math.ceil(contextMessages / 5) * 5;
|
||||
console.debug('CHROMADB: Estimated context messages (rounded): %o', contextMessagesRounded);
|
||||
// Messages to keep (proportional, rounded to nearest 5, minimum 5, maximum 500)
|
||||
const messagesToKeep = Math.min(defaultSettings.keep_context_max, Math.max(5, Math.floor(contextMessagesRounded * extension_settings.chromadb.keep_context_proportion / 5) * 5));
|
||||
console.debug('CHROMADB: Estimated messages to keep: %o', messagesToKeep);
|
||||
// Messages to query (rounded, maximum 500)
|
||||
const messagesToQuery = Math.min(defaultSettings.n_results_max, contextMessagesRounded - messagesToKeep);
|
||||
console.debug('CHROMADB: Estimated messages to query: %o', messagesToQuery);
|
||||
// Set extension settings
|
||||
extension_settings.chromadb.keep_context = messagesToKeep;
|
||||
extension_settings.chromadb.n_results = messagesToQuery;
|
||||
// Update sliders
|
||||
$('#chromadb_keep_context').val(messagesToKeep);
|
||||
$('#chromadb_n_results').val(messagesToQuery);
|
||||
// Update labels
|
||||
$('#chromadb_keep_context_value').text(extension_settings.chromadb.keep_context);
|
||||
$('#chromadb_n_results_value').text(extension_settings.chromadb.n_results);
|
||||
}
|
||||
|
||||
window.chromadb_interceptGeneration = async (chat, maxContext) => {
|
||||
if (extension_settings.chromadb.auto_adjust) {
|
||||
doAutoAdjust(chat, maxContext);
|
||||
}
|
||||
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (!currentChatId)
|
||||
return;
|
||||
|
||||
//log the current settings
|
||||
console.debug("CHROMADB: Current settings: %o", extension_settings.chromadb);
|
||||
|
||||
const selectedStrategy = extension_settings.chromadb.strategy;
|
||||
const recallStrategy = extension_settings.chromadb.recall_strategy;
|
||||
let recallMsg = extension_settings.chromadb.recall_msg || defaultSettings.chroma_default_msg;
|
||||
const chromaDepth = extension_settings.chromadb.chroma_depth;
|
||||
const chromaSortStrategy = extension_settings.chromadb.sort_strategy;
|
||||
const chromaQueryLastOnly = extension_settings.chromadb.query_last_only;
|
||||
const messagesToStore = chat.slice(0, -extension_settings.chromadb.keep_context);
|
||||
|
||||
if (messagesToStore.length > 0 && !extension_settings.chromadb.freeze) {
|
||||
//log the messages to store
|
||||
console.debug("CHROMADB: Messages to store: %o", messagesToStore);
|
||||
//log the messages to store length vs keep context
|
||||
console.debug("CHROMADB: Messages to store length vs keep context: %o vs %o", messagesToStore.length, extension_settings.chromadb.keep_context);
|
||||
await addMessages(currentChatId, messagesToStore);
|
||||
}
|
||||
|
||||
const lastMessage = chat[chat.length - 1];
|
||||
|
||||
let queriedMessages;
|
||||
if (lastMessage) {
|
||||
let queryBlob = "";
|
||||
if (chromaQueryLastOnly) {
|
||||
queryBlob = lastMessage.mes;
|
||||
}
|
||||
else {
|
||||
for (let msg of chat.slice(-extension_settings.chromadb.keep_context)) {
|
||||
queryBlob += `${msg.mes}\n`
|
||||
}
|
||||
}
|
||||
console.debug("CHROMADB: Query text:", queryBlob);
|
||||
|
||||
if (recallStrategy === 'multichat') {
|
||||
console.log("Utilizing multichat")
|
||||
queriedMessages = await queryMultiMessages(currentChatId, queryBlob);
|
||||
}
|
||||
else {
|
||||
queriedMessages = await queryMessages(currentChatId, queryBlob);
|
||||
}
|
||||
|
||||
if (chromaSortStrategy === "date") {
|
||||
queriedMessages.sort((a, b) => a.date - b.date);
|
||||
}
|
||||
else {
|
||||
queriedMessages.sort((a, b) => b.distance - a.distance);
|
||||
}
|
||||
console.debug("CHROMADB: Query results: %o", queriedMessages);
|
||||
|
||||
|
||||
let newChat = [];
|
||||
|
||||
if (selectedStrategy === 'ross') {
|
||||
//adds chroma to the end of chat and allows Generate() to cull old messages naturally.
|
||||
const context = getContext();
|
||||
const charname = context.name2;
|
||||
newChat.push(
|
||||
{
|
||||
is_user: false,
|
||||
mes: `[Use these past chat exchanges to inform ${charname}'s next response:`,
|
||||
name: "system",
|
||||
send_date: 0,
|
||||
}
|
||||
);
|
||||
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
|
||||
newChat.push(
|
||||
{
|
||||
is_user: false,
|
||||
mes: `]\n`,
|
||||
name: "system",
|
||||
send_date: 0,
|
||||
}
|
||||
);
|
||||
chat.splice(chat.length, 0, ...newChat);
|
||||
}
|
||||
if (selectedStrategy === 'hh_aa') {
|
||||
// Insert chroma history messages as a list at the AFTER_SCENARIO anchor point
|
||||
const context = getContext();
|
||||
const chromaTokenLimit = extension_settings.chromadb.hhaa_token_limit;
|
||||
|
||||
let wrapperMsg = extension_settings.chromadb.hhaa_wrapper_msg || defaultSettings.chroma_default_hhaa_wrapper;
|
||||
wrapperMsg = substituteParams(wrapperMsg, context.name1, context.name2);
|
||||
if (!wrapperMsg.includes("{{memories}}")) {
|
||||
wrapperMsg += " {{memories}}";
|
||||
}
|
||||
let memoryMsg = extension_settings.chromadb.hhaa_memory_msg || defaultSettings.chroma_default_hhaa_memory;
|
||||
memoryMsg = substituteParams(memoryMsg, context.name1, context.name2);
|
||||
if (!memoryMsg.includes("{{message}}")) {
|
||||
memoryMsg += " {{message}}";
|
||||
}
|
||||
|
||||
// Reversed because we want the most 'important' messages at the bottom.
|
||||
let recalledMemories = queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse).reverse();
|
||||
let tokenApprox = 0;
|
||||
let allMemoryBlob = "";
|
||||
let seenMemories = new Set(); // Why are there even duplicates in chromadb anyway?
|
||||
for (const msg of recalledMemories) {
|
||||
const memoryBlob = memoryMsg.replace('{{name}}', msg.name).replace('{{message}}', msg.mes);
|
||||
const memoryTokens = (memoryBlob.length / CHARACTERS_PER_TOKEN_RATIO);
|
||||
if (!seenMemories.has(memoryBlob) && tokenApprox + memoryTokens <= chromaTokenLimit) {
|
||||
allMemoryBlob += memoryBlob;
|
||||
tokenApprox += memoryTokens;
|
||||
seenMemories.add(memoryBlob);
|
||||
}
|
||||
}
|
||||
|
||||
// No memories? No prompt.
|
||||
const promptBlob = (tokenApprox == 0) ? "" : wrapperMsg.replace('{{memories}}', allMemoryBlob);
|
||||
console.debug("CHROMADB: prompt blob: %o", promptBlob);
|
||||
context.setExtensionPrompt(MODULE_NAME, promptBlob, extension_prompt_types.IN_PROMPT);
|
||||
}
|
||||
if (selectedStrategy === 'custom') {
|
||||
const context = getContext();
|
||||
recallMsg = substituteParams(recallMsg, context.name1, context.name2);
|
||||
if (!recallMsg.includes("{{memories}}")) {
|
||||
recallMsg += " {{memories}}";
|
||||
}
|
||||
let recallStart = recallMsg.split('{{memories}}')[0]
|
||||
let recallEnd = recallMsg.split('{{memories}}')[1]
|
||||
|
||||
newChat.push(
|
||||
{
|
||||
is_user: false,
|
||||
mes: recallStart,
|
||||
name: "system",
|
||||
send_date: 0,
|
||||
}
|
||||
);
|
||||
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
|
||||
newChat.push(
|
||||
{
|
||||
is_user: false,
|
||||
mes: recallEnd + `\n`,
|
||||
name: "system",
|
||||
send_date: 0,
|
||||
}
|
||||
);
|
||||
|
||||
//prototype chroma duplicate removal
|
||||
let chatset = new Set(chat.map(obj => obj.mes));
|
||||
newChat = newChat.filter(obj => !chatset.has(obj.mes));
|
||||
|
||||
if(chromaDepth === -1) {
|
||||
chat.splice(chat.length, 0, ...newChat);
|
||||
}
|
||||
else {
|
||||
chat.splice(chromaDepth, 0, ...newChat);
|
||||
}
|
||||
}
|
||||
if (selectedStrategy === 'original') {
|
||||
//removes .length # messages from the start of 'kept messages'
|
||||
//replaces them with chromaDB results (with no separator)
|
||||
newChat.push(...queriedMessages.map(m => m.meta).filter(onlyUnique).map(JSON.parse));
|
||||
chat.splice(0, messagesToStore.length, ...newChat);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onFreezeInput() {
|
||||
extension_settings.chromadb.freeze = $('#chromadb_freeze').is(':checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onAutoAdjustInput() {
|
||||
extension_settings.chromadb.auto_adjust = $('#chromadb_auto_adjust').is(':checked');
|
||||
enableDisableSliders();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
function onFullLogQuery() {
|
||||
extension_settings.chromadb.query_last_only = $('#chromadb_query_last_only').is(':checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function enableDisableSliders() {
|
||||
const auto_adjust = extension_settings.chromadb.auto_adjust;
|
||||
$('label[for="chromadb_keep_context"]').prop('hidden', auto_adjust);
|
||||
$('#chromadb_keep_context').prop('hidden', auto_adjust)
|
||||
$('label[for="chromadb_n_results"]').prop('hidden', auto_adjust);
|
||||
$('#chromadb_n_results').prop('hidden', auto_adjust)
|
||||
$('label[for="chromadb_keep_context_proportion"]').prop('hidden', !auto_adjust);
|
||||
$('#chromadb_keep_context_proportion').prop('hidden', !auto_adjust)
|
||||
}
|
||||
|
||||
function onKeepContextProportionInput() {
|
||||
extension_settings.chromadb.keep_context_proportion = $('#chromadb_keep_context_proportion').val();
|
||||
$('#chromadb_keep_context_proportion_value').text(Math.round(extension_settings.chromadb.keep_context_proportion * 100));
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
jQuery(async () => {
|
||||
const settingsHtml = `
|
||||
<div class="chromadb_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Smart Context</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<small>This extension rearranges the messages in the current chat to keep more relevant information in the context. Adjust the sliders below based on average amount of messages in your prompt (refer to the chat cut-off line).</small>
|
||||
<span class="wide100p marginTopBot5 displayBlock">Memory Injection Strategy</span>
|
||||
<hr>
|
||||
<select id="chromadb_strategy">
|
||||
<option value="original">Replace non-kept chat items with memories</option>
|
||||
<option value="ross">Add memories after chat with a header tag</option>
|
||||
<option value="hh_aa">Add memory list to character description</option>
|
||||
<option value="custom">Add memories at custom depth with custom msg</option>
|
||||
</select>
|
||||
<label for="chromadb_custom_msg" hidden><small>Custom injection message:</small></label>
|
||||
<textarea id="chromadb_custom_msg" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_msg}" style="height: 61px; display: none;"></textarea>
|
||||
<label for="chromadb_custom_depth" hidden><small>How deep should the memory messages be injected?: (<span id="chromadb_custom_depth_value"></span>)</small></label>
|
||||
<input id="chromadb_custom_depth" type="range" min="${defaultSettings.chroma_depth_min}" max="${defaultSettings.chroma_depth_max}" step="${defaultSettings.chroma_depth_step}" value="${defaultSettings.chroma_depth}" hidden/>
|
||||
|
||||
<label for="chromadb_hhaa_wrapperfmt" hidden><small>Custom wrapper format:</small></label>
|
||||
<textarea id="chromadb_hhaa_wrapperfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_wrapper}" style="height: 61px; display: none;"></textarea>
|
||||
<label for="chromadb_hhaa_memoryfmt" hidden><small>Custom memory format:</small></label>
|
||||
<textarea id="chromadb_hhaa_memoryfmt" hidden class="text_pole textarea_compact" rows="2" placeholder="${defaultSettings.chroma_default_hhaa_memory}" style="height: 61px; display: none;"></textarea>
|
||||
<label for="chromadb_hhaa_token_limit" hidden><small>Maximum tokens allowed for memories: (<span id="chromadb_hhaa_token_limit_value"></span>)</small></label>
|
||||
<input id="chromadb_hhaa_token_limit" type="range" min="0" max="2048" step="64" value="${defaultSettings.hhaa_token_limit}" hidden/>
|
||||
|
||||
|
||||
<span>Memory Recall Strategy</span>
|
||||
<select id="chromadb_recall_strategy">
|
||||
<option value="original">Recall only from this chat</option>
|
||||
<option value="multichat">Recall from all character chats (experimental)</option>
|
||||
</select>
|
||||
<span>Memory Sort Strategy</span>
|
||||
<select id="chromadb_sort_strategy">
|
||||
<option value="date">Sort memories by date</option>
|
||||
<option value="distance">Sort memories by relevance</option>
|
||||
</select>
|
||||
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
|
||||
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
|
||||
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>
|
||||
<input id="chromadb_n_results" type="range" min="${defaultSettings.n_results_min}" max="${defaultSettings.n_results_max}" step="${defaultSettings.n_results_step}" value="${defaultSettings.n_results}" />
|
||||
|
||||
<label for="chromadb_keep_context_proportion"><small>Keep (<span id="chromadb_keep_context_proportion_value"></span>%) of in-context chat messages; replace the rest with memories</small></label>
|
||||
<input id="chromadb_keep_context_proportion" type="range" min="${defaultSettings.keep_context_proportion_min}" max="${defaultSettings.keep_context_proportion_max}" step="${defaultSettings.keep_context_proportion_step}" value="${defaultSettings.keep_context_proportion}" />
|
||||
<label for="chromadb_split_length"><small>Max length for each 'memory' pulled from the current chat history: (<span id="chromadb_split_length_value"></span>) characters</small></label>
|
||||
<input id="chromadb_split_length" type="range" min="${defaultSettings.split_length_min}" max="${defaultSettings.split_length_max}" step="${defaultSettings.split_length_step}" value="${defaultSettings.split_length}" />
|
||||
<label for="chromadb_file_split_length"><small>Max length for each 'memory' pulled from imported text files: (<span id="chromadb_file_split_length_value"></span>) characters</small></label>
|
||||
<input id="chromadb_file_split_length" type="range" min="${defaultSettings.file_split_length_min}" max="${defaultSettings.file_split_length_max}" step="${defaultSettings.file_split_length_step}" value="${defaultSettings.file_split_length}" />
|
||||
<label class="checkbox_label" for="chromadb_freeze" title="Pauses the automatic synchronization of new messages with ChromaDB. Older messages and injections will still be pulled as usual." >
|
||||
<input type="checkbox" id="chromadb_freeze" />
|
||||
<span>Freeze ChromaDB state</span>
|
||||
</label>
|
||||
<label class="checkbox_label for="chromadb_auto_adjust" title="Automatically adjusts the number of messages to keep based on the average number of messages in the current chat and the chosen proportion.">
|
||||
<input type="checkbox" id="chromadb_auto_adjust" />
|
||||
<span>Use % strategy</span>
|
||||
</label>
|
||||
<label class="checkbox_label" for="chromadb_chunk_nl" title="Chunk injected documents on newline instead of at set character size." >
|
||||
<input type="checkbox" id="chromadb_chunk_nl" />
|
||||
<span>Chunk on Newlines</span>
|
||||
</label>
|
||||
<label class="checkbox_label for="chromadb_query_last_only" title="ChromaDB queries only use the most recent message. (Instead of using all messages still in the context.)">
|
||||
<input type="checkbox" id="chromadb_query_last_only" />
|
||||
<span>Query last message only</span>
|
||||
</label>
|
||||
<div class="flex-container spaceEvenly">
|
||||
<div id="chromadb_inject" title="Upload custom textual data to use in the context of the current chat" class="menu_button">
|
||||
<i class="fa-solid fa-file-arrow-up"></i>
|
||||
<span>Inject Data (TXT file)</span>
|
||||
</div>
|
||||
<div id="chromadb_export" title="Export all of the current chromadb data for this current chat" class="menu_button">
|
||||
<i class="fa-solid fa-file-export"></i>
|
||||
<span>Export</span>
|
||||
</div>
|
||||
<div id="chromadb_import" title="Import a full chromadb export for this current chat" class="menu_button">
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span>Import</span>
|
||||
</div>
|
||||
<div id="chromadb_purge" title="Force purge all the data related to the current chat from the database" class="menu_button">
|
||||
<i class="fa-solid fa-broom"></i>
|
||||
<span>Purge Chat from the DB</span>
|
||||
</div>
|
||||
</div>
|
||||
<small><i>Local ChromaDB now persists to disk by default. The default folder is .chroma_db, and you can set a different folder with the --chroma-folder argument. If you are using the Extras Colab notebook, you will need to inject the text data every time the Extras API server is restarted.</i></small>
|
||||
</div>
|
||||
<form><input id="chromadb_inject_file" type="file" accept="text/plain" hidden></form>
|
||||
<form><input id="chromadb_import_file" type="file" accept="application/json" hidden></form>
|
||||
</div>`;
|
||||
|
||||
$('#extensions_settings2').append(settingsHtml);
|
||||
$('#chromadb_strategy').on('change', onStrategyChange);
|
||||
$('#chromadb_recall_strategy').on('change', onRecallStrategyChange);
|
||||
$('#chromadb_sort_strategy').on('change', onSortStrategyChange);
|
||||
$('#chromadb_keep_context').on('input', onKeepContextInput);
|
||||
$('#chromadb_n_results').on('input', onNResultsInput);
|
||||
$('#chromadb_custom_depth').on('input', onChromaDepthInput);
|
||||
$('#chromadb_custom_msg').on('input', onChromaMsgInput);
|
||||
|
||||
$('#chromadb_hhaa_wrapperfmt').on('input', onChromaHHAAWrapper);
|
||||
$('#chromadb_hhaa_memoryfmt').on('input', onChromaHHAAMemory);
|
||||
$('#chromadb_hhaa_token_limit').on('input', onChromaHHAATokens);
|
||||
|
||||
$('#chromadb_split_length').on('input', onSplitLengthInput);
|
||||
$('#chromadb_file_split_length').on('input', onFileSplitLengthInput);
|
||||
$('#chromadb_inject').on('click', () => $('#chromadb_inject_file').trigger('click'));
|
||||
$('#chromadb_import').on('click', () => $('#chromadb_import_file').trigger('click'));
|
||||
$('#chromadb_inject_file').on('change', onSelectInjectFile);
|
||||
$('#chromadb_import_file').on('change', onSelectImportFile);
|
||||
$('#chromadb_purge').on('click', onPurgeClick);
|
||||
$('#chromadb_export').on('click', onExportClick);
|
||||
$('#chromadb_freeze').on('input', onFreezeInput);
|
||||
$('#chromadb_chunk_nl').on('input', onChunkNLInput);
|
||||
$('#chromadb_auto_adjust').on('input', onAutoAdjustInput);
|
||||
$('#chromadb_query_last_only').on('input', onFullLogQuery);
|
||||
$('#chromadb_keep_context_proportion').on('input', onKeepContextProportionInput);
|
||||
await loadSettings();
|
||||
|
||||
// Not sure if this is needed, but it's here just in case
|
||||
eventSource.on(event_types.MESSAGE_DELETED, getChatSyncState);
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, getChatSyncState);
|
||||
eventSource.on(event_types.MESSAGE_SENT, getChatSyncState);
|
||||
// Will make the sync state update when a message is edited or swiped
|
||||
eventSource.on(event_types.MESSAGE_EDITED, invalidateMessageSyncState);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, invalidateMessageSyncState);
|
||||
});
|
||||
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"display_name": "Smart Context",
|
||||
"loading_order": 11,
|
||||
"requires": [
|
||||
"chromadb"
|
||||
],
|
||||
"optional": [],
|
||||
"generate_interceptor": "chromadb_interceptGeneration",
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "maceter636@proton.me",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
.chromadb_settings .menu_button {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
}
|
@ -1,811 +0,0 @@
|
||||
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
|
||||
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
|
||||
import {
|
||||
substituteParams,
|
||||
eventSource,
|
||||
event_types,
|
||||
generateQuietPrompt,
|
||||
} from "../../../script.js";
|
||||
import { registerSlashCommand } from "../../slash-commands.js";
|
||||
import { waitUntilCondition } from "../../utils.js";
|
||||
import { is_group_generating, selected_group } from "../../group-chats.js";
|
||||
|
||||
const MODULE_NAME = "Objective"
|
||||
|
||||
|
||||
let taskTree = null
|
||||
let globalTasks = []
|
||||
let currentChatId = ""
|
||||
let currentObjective = null
|
||||
let currentTask = null
|
||||
let checkCounter = 0
|
||||
let lastMessageWasSwipe = false
|
||||
|
||||
|
||||
const defaultPrompts = {
|
||||
"createTask": `Pause your roleplay. Please generate a numbered list of plain text tasks to complete an objective. The objective that you must make a numbered task list for is: "{{objective}}". The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Include the objective as the final task.`,
|
||||
"checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}]. To do this, examine the most recent messages. Your response must only contain either true or false, and nothing else. Example output: true`,
|
||||
'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.`,
|
||||
}
|
||||
|
||||
let objectivePrompts = defaultPrompts
|
||||
|
||||
//###############################//
|
||||
//# Task Management #//
|
||||
//###############################//
|
||||
|
||||
// Return the task and index or throw an error
|
||||
function getTaskById(taskId){
|
||||
if (taskId == null) {
|
||||
throw `Null task id`
|
||||
}
|
||||
return getTaskByIdRecurse(taskId, taskTree)
|
||||
}
|
||||
|
||||
function getTaskByIdRecurse(taskId, task) {
|
||||
if (task.id == taskId){
|
||||
return task
|
||||
}
|
||||
for (const childTask of task.children) {
|
||||
const foundTask = getTaskByIdRecurse(taskId, childTask);
|
||||
if (foundTask != null) {
|
||||
return foundTask;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function substituteParamsPrompts(content, substituteGlobal) {
|
||||
content = content.replace(/{{objective}}/gi, currentObjective.description)
|
||||
content = content.replace(/{{task}}/gi, currentTask.description)
|
||||
if (currentTask.parent){
|
||||
content = content.replace(/{{parent}}/gi, currentTask.parent.description)
|
||||
}
|
||||
if (substituteGlobal) {
|
||||
content = substituteParams(content)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
|
||||
async function generateTasks() {
|
||||
|
||||
const prompt = substituteParamsPrompts(objectivePrompts.createTask, false);
|
||||
console.log(`Generating tasks for objective with prompt`)
|
||||
toastr.info('Generating tasks for objective', 'Please wait...');
|
||||
const taskResponse = await generateQuietPrompt(prompt)
|
||||
|
||||
// Clear all existing objective tasks when generating
|
||||
currentObjective.children = []
|
||||
const numberedListPattern = /^\d+\./
|
||||
|
||||
// Create tasks from generated task list
|
||||
for (const task of taskResponse.split('\n').map(x => x.trim())) {
|
||||
if (task.match(numberedListPattern) != null) {
|
||||
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
|
||||
}
|
||||
}
|
||||
updateUiTaskList();
|
||||
setCurrentTask();
|
||||
console.info(`Response for Objective: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
|
||||
toastr.success(`Generated ${currentObjective.children.length} tasks`, 'Done!');
|
||||
}
|
||||
|
||||
// Call Quiet Generate to check if a task is completed
|
||||
async function checkTaskCompleted() {
|
||||
// Make sure there are tasks
|
||||
if (jQuery.isEmptyObject(currentTask)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Wait for group to finish generating
|
||||
if (selected_group) {
|
||||
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
|
||||
}
|
||||
// Another extension might be doing something with the chat, so wait for it to finish
|
||||
await waitUntilCondition(() => is_send_press === false, 30000, 10);
|
||||
} catch {
|
||||
console.debug("Failed to wait for group to finish generating")
|
||||
return;
|
||||
}
|
||||
|
||||
checkCounter = $('#objective-check-frequency').val()
|
||||
toastr.info("Checking for task completion.")
|
||||
|
||||
const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted, false);
|
||||
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
|
||||
|
||||
// Check response if task complete
|
||||
if (taskResponse.includes("true")) {
|
||||
console.info(`Character determined task '${currentTask.description} is completed.`)
|
||||
currentTask.completeTask()
|
||||
} else if (!(taskResponse.includes("false"))) {
|
||||
console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`)
|
||||
} else {
|
||||
console.debug(`Checked task completion. taskResponse: ${taskResponse}`)
|
||||
}
|
||||
}
|
||||
|
||||
function getNextIncompleteTaskRecurse(task){
|
||||
if (task.completed === false // Return task if incomplete
|
||||
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
|
||||
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
|
||||
){
|
||||
return task
|
||||
}
|
||||
for (const childTask of task.children) {
|
||||
if (childTask.completed === true){ // Don't recurse into completed tasks
|
||||
continue
|
||||
}
|
||||
const foundTask = getNextIncompleteTaskRecurse(childTask);
|
||||
if (foundTask != null) {
|
||||
return foundTask;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set a task in extensionPrompt context. Defaults to first incomplete
|
||||
function setCurrentTask(taskId = null, skipSave = false) {
|
||||
const context = getContext();
|
||||
|
||||
// TODO: Should probably null this rather than set empty object
|
||||
currentTask = {};
|
||||
|
||||
// Find the task, either next incomplete, or by provided taskId
|
||||
if (taskId === null) {
|
||||
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
|
||||
} else {
|
||||
currentTask = getTaskById(taskId);
|
||||
}
|
||||
|
||||
// Don't just check for a current task, check if it has data
|
||||
const description = currentTask.description || null;
|
||||
if (description) {
|
||||
const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask, true);
|
||||
|
||||
// Remove highlights
|
||||
$('.objective-task').css({'border-color':'','border-width':''})
|
||||
// Highlight current task
|
||||
let highlightTask = currentTask
|
||||
while (highlightTask.parentId !== ""){
|
||||
if (highlightTask.descriptionSpan){
|
||||
highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'});
|
||||
}
|
||||
const parent = getTaskById(highlightTask.parentId)
|
||||
highlightTask = parent
|
||||
}
|
||||
|
||||
// Update the extension prompt
|
||||
context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val());
|
||||
console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`);
|
||||
} else {
|
||||
context.setExtensionPrompt(MODULE_NAME, '');
|
||||
console.info(`No current task`);
|
||||
}
|
||||
|
||||
// Save state if not skipping
|
||||
if (!skipSave) {
|
||||
saveState();
|
||||
}
|
||||
}
|
||||
|
||||
function getHighestTaskIdRecurse(task) {
|
||||
let nextId = task.id;
|
||||
|
||||
for (const childTask of task.children) {
|
||||
const childId = getHighestTaskIdRecurse(childTask);
|
||||
if (childId > nextId) {
|
||||
nextId = childId;
|
||||
}
|
||||
}
|
||||
return nextId;
|
||||
}
|
||||
|
||||
//###############################//
|
||||
//# Task Class #//
|
||||
//###############################//
|
||||
class ObjectiveTask {
|
||||
id
|
||||
description
|
||||
completed
|
||||
parentId
|
||||
children
|
||||
|
||||
// UI Elements
|
||||
taskHtml
|
||||
descriptionSpan
|
||||
completedCheckbox
|
||||
deleteTaskButton
|
||||
addTaskButton
|
||||
|
||||
constructor ({id=undefined, description, completed=false, parentId=""}) {
|
||||
this.description = description
|
||||
this.parentId = parentId
|
||||
this.children = []
|
||||
this.completed = completed
|
||||
|
||||
// Generate a new ID if none specified
|
||||
if (id==undefined){
|
||||
this.id = getHighestTaskIdRecurse(taskTree) + 1
|
||||
} else {
|
||||
this.id=id
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts optional index. Defaults to adding to end of list.
|
||||
addTask(description, index = null) {
|
||||
index = index != null ? index: index = this.children.length
|
||||
this.children.splice(index, 0, new ObjectiveTask(
|
||||
{description: description, parentId: this.id}
|
||||
))
|
||||
saveState()
|
||||
}
|
||||
|
||||
getIndex(){
|
||||
if (this.parentId !== null) {
|
||||
const parent = getTaskById(this.parentId)
|
||||
const index = parent.children.findIndex(task => task.id === this.id)
|
||||
if (index === -1){
|
||||
throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'`
|
||||
}
|
||||
return index
|
||||
} else {
|
||||
throw `getIndex failed: Task '${this.description}' has no parent`
|
||||
}
|
||||
}
|
||||
|
||||
// Used to set parent to complete when all child tasks are completed
|
||||
checkParentComplete() {
|
||||
let all_completed = true;
|
||||
if (this.parentId !== ""){
|
||||
const parent = getTaskById(this.parentId);
|
||||
for (const child of parent.children){
|
||||
if (!child.completed){
|
||||
all_completed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_completed){
|
||||
parent.completed = true;
|
||||
console.info(`Parent task '${parent.description}' completed after all child tasks complated.`)
|
||||
} else {
|
||||
parent.completed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the current task, setting next task to next incomplete task
|
||||
completeTask() {
|
||||
this.completed = true
|
||||
console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
|
||||
this.checkParentComplete()
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
}
|
||||
|
||||
// Add a single task to the UI and attach event listeners for user edits
|
||||
addUiElement() {
|
||||
const template = `
|
||||
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
|
||||
<input id="objective-task-complete-${this.id}" type="checkbox">
|
||||
<span class="text_pole objective-task" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
|
||||
<div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
|
||||
<div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
|
||||
<div id="objective-task-add-branch-${this.id}" class="objective-task-button fa-solid fa-code-fork fa-2x" title="Branch Task"></div>
|
||||
</div><br>
|
||||
`;
|
||||
|
||||
// Add the filled out template
|
||||
$('#objective-tasks').append(template);
|
||||
|
||||
this.completedCheckbox = $(`#objective-task-complete-${this.id}`);
|
||||
this.descriptionSpan = $(`#objective-task-description-${this.id}`);
|
||||
this.addButton = $(`#objective-task-add-${this.id}`);
|
||||
this.deleteButton = $(`#objective-task-delete-${this.id}`);
|
||||
this.taskHtml = $(`#objective-task-label-${this.id}`);
|
||||
this.branchButton = $(`#objective-task-add-branch-${this.id}`)
|
||||
|
||||
// Handle sub-task forking style
|
||||
if (this.children.length > 0){
|
||||
this.branchButton.css({'color':'#33cc33'})
|
||||
} else {
|
||||
this.branchButton.css({'color':''})
|
||||
}
|
||||
|
||||
// Add event listeners and set properties
|
||||
$(`#objective-task-complete-${this.id}`).prop('checked', this.completed);
|
||||
$(`#objective-task-complete-${this.id}`).on('click', () => (this.onCompleteClick()));
|
||||
$(`#objective-task-description-${this.id}`).on('keyup', () => (this.onDescriptionUpdate()));
|
||||
$(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout()));
|
||||
$(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick()));
|
||||
$(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick()));
|
||||
this.branchButton.on('click', () => (this.onBranchClick()))
|
||||
}
|
||||
|
||||
onBranchClick() {
|
||||
currentObjective = this
|
||||
updateUiTaskList();
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
onCompleteClick(){
|
||||
this.completed = this.completedCheckbox.prop('checked')
|
||||
this.checkParentComplete()
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
onDescriptionUpdate(){
|
||||
this.description = this.descriptionSpan.text();
|
||||
}
|
||||
|
||||
onDescriptionFocusout(){
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
onDeleteClick(){
|
||||
const index = this.getIndex()
|
||||
const parent = getTaskById(this.parentId)
|
||||
parent.children.splice(index, 1)
|
||||
updateUiTaskList()
|
||||
setCurrentTask()
|
||||
}
|
||||
|
||||
onAddClick(){
|
||||
const index = this.getIndex()
|
||||
const parent = getTaskById(this.parentId)
|
||||
parent.addTask("", index + 1);
|
||||
updateUiTaskList();
|
||||
setCurrentTask();
|
||||
}
|
||||
|
||||
toSaveStateRecurse() {
|
||||
let children = []
|
||||
if (this.children.length > 0){
|
||||
for (const child of this.children){
|
||||
children.push(child.toSaveStateRecurse())
|
||||
}
|
||||
}
|
||||
return {
|
||||
"id":this.id,
|
||||
"description":this.description,
|
||||
"completed":this.completed,
|
||||
"parentId": this.parentId,
|
||||
"children": children,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//###############################//
|
||||
//# Custom Prompts #//
|
||||
//###############################//
|
||||
|
||||
function onEditPromptClick() {
|
||||
let popupText = ''
|
||||
popupText += `
|
||||
<div class="objective_prompt_modal">
|
||||
<small>Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.</small>
|
||||
<br>
|
||||
<div>
|
||||
<label for="objective-prompt-generate">Generation Prompt</label>
|
||||
<textarea id="objective-prompt-generate" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||||
<label for="objective-prompt-check">Completion Check Prompt</label>
|
||||
<textarea id="objective-prompt-check" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||||
<label for="objective-prompt-extension-prompt">Injected Prompt</label>
|
||||
<textarea id="objective-prompt-extension-prompt" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||||
</div>
|
||||
<div class="objective_prompt_block">
|
||||
<label for="objective-custom-prompt-select">Custom Prompt Select</label>
|
||||
<select id="objective-custom-prompt-select"><select>
|
||||
</div>
|
||||
<div class="objective_prompt_block">
|
||||
<input id="objective-custom-prompt-new" class="menu_button" type="submit" value="New Prompt" />
|
||||
<input id="objective-custom-prompt-save" class="menu_button" type="submit" value="Save Prompt" />
|
||||
<input id="objective-custom-prompt-delete" class="menu_button" type="submit" value="Delete Prompt" />
|
||||
</div>
|
||||
</div>`
|
||||
callPopup(popupText, 'text')
|
||||
populateCustomPrompts()
|
||||
|
||||
// Set current values
|
||||
$('#objective-prompt-generate').val(objectivePrompts.createTask)
|
||||
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
|
||||
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
|
||||
|
||||
// Handle value updates
|
||||
$('#objective-prompt-generate').on('input', () => {
|
||||
objectivePrompts.createTask = $('#objective-prompt-generate').val()
|
||||
})
|
||||
$('#objective-prompt-check').on('input', () => {
|
||||
objectivePrompts.checkTaskCompleted = $('#objective-prompt-check').val()
|
||||
})
|
||||
$('#objective-prompt-extension-prompt').on('input', () => {
|
||||
objectivePrompts.currentTask = $('#objective-prompt-extension-prompt').val()
|
||||
})
|
||||
|
||||
// Handle new
|
||||
$('#objective-custom-prompt-new').on('click', () => {
|
||||
newCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle save
|
||||
$('#objective-custom-prompt-save').on('click', () => {
|
||||
saveCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle delete
|
||||
$('#objective-custom-prompt-delete').on('click', () => {
|
||||
deleteCustomPrompt()
|
||||
})
|
||||
|
||||
// Handle load
|
||||
$('#objective-custom-prompt-select').on('change', loadCustomPrompt)
|
||||
}
|
||||
async function newCustomPrompt() {
|
||||
const customPromptName = await callPopup('<h3>Custom Prompt name:</h3>', 'input');
|
||||
|
||||
if (customPromptName == "") {
|
||||
toastr.warning("Please set custom prompt name to save.")
|
||||
return
|
||||
}
|
||||
if (customPromptName == "default"){
|
||||
toastr.error("Cannot save over default prompt")
|
||||
return
|
||||
}
|
||||
extension_settings.objective.customPrompts[customPromptName] = {}
|
||||
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
}
|
||||
|
||||
function saveCustomPrompt() {
|
||||
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
if (customPromptName == "default"){
|
||||
toastr.error("Cannot save over default prompt")
|
||||
return
|
||||
}
|
||||
Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts)
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
}
|
||||
|
||||
function deleteCustomPrompt(){
|
||||
const customPromptName = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
|
||||
if (customPromptName == "default"){
|
||||
toastr.error("Cannot delete default prompt")
|
||||
return
|
||||
}
|
||||
delete extension_settings.objective.customPrompts[customPromptName]
|
||||
saveSettingsDebounced()
|
||||
populateCustomPrompts()
|
||||
loadCustomPrompt()
|
||||
}
|
||||
|
||||
function loadCustomPrompt(){
|
||||
const optionSelected = $("#objective-custom-prompt-select").find(':selected').val()
|
||||
Object.assign(objectivePrompts, extension_settings.objective.customPrompts[optionSelected])
|
||||
|
||||
$('#objective-prompt-generate').val(objectivePrompts.createTask)
|
||||
$('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted)
|
||||
$('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask)
|
||||
}
|
||||
|
||||
function populateCustomPrompts(){
|
||||
// Populate saved prompts
|
||||
$('#objective-custom-prompt-select').empty()
|
||||
for (const customPromptName in extension_settings.objective.customPrompts){
|
||||
const option = document.createElement('option');
|
||||
option.innerText = customPromptName;
|
||||
option.value = customPromptName;
|
||||
option.selected = customPromptName
|
||||
$('#objective-custom-prompt-select').append(option)
|
||||
}
|
||||
}
|
||||
|
||||
//###############################//
|
||||
//# UI AND Settings #//
|
||||
//###############################//
|
||||
|
||||
|
||||
const defaultSettings = {
|
||||
currentObjectiveId: null,
|
||||
taskTree: null,
|
||||
chatDepth: 2,
|
||||
checkFrequency: 3,
|
||||
hideTasks: false,
|
||||
prompts: defaultPrompts,
|
||||
}
|
||||
|
||||
// Convenient single call. Not much at the moment.
|
||||
function resetState() {
|
||||
lastMessageWasSwipe = false
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
//
|
||||
function saveState() {
|
||||
const context = getContext();
|
||||
|
||||
if (currentChatId == "") {
|
||||
currentChatId = context.chatId
|
||||
}
|
||||
|
||||
chat_metadata['objective'] = {
|
||||
currentObjectiveId: currentObjective.id,
|
||||
taskTree: taskTree.toSaveStateRecurse(),
|
||||
checkFrequency: $('#objective-check-frequency').val(),
|
||||
chatDepth: $('#objective-chat-depth').val(),
|
||||
hideTasks: $('#objective-hide-tasks').prop('checked'),
|
||||
prompts: objectivePrompts,
|
||||
}
|
||||
|
||||
saveMetadataDebounced();
|
||||
}
|
||||
|
||||
// Dump core state
|
||||
function debugObjectiveExtension() {
|
||||
console.log(JSON.stringify({
|
||||
"currentTask": currentTask,
|
||||
"currentObjective": currentObjective,
|
||||
"taskTree": taskTree.toSaveStateRecurse(),
|
||||
"chat_metadata": chat_metadata['objective'],
|
||||
"extension_settings": extension_settings['objective'],
|
||||
"prompts": objectivePrompts
|
||||
}, null, 2))
|
||||
}
|
||||
|
||||
window.debugObjectiveExtension = debugObjectiveExtension
|
||||
|
||||
|
||||
// Populate UI task list
|
||||
function updateUiTaskList() {
|
||||
$('#objective-tasks').empty()
|
||||
|
||||
// Show button to navigate back to parent objective if parent exists
|
||||
if (currentObjective){
|
||||
if (currentObjective.parentId !== "") {
|
||||
$('#objective-parent').show()
|
||||
} else {
|
||||
$('#objective-parent').hide()
|
||||
}
|
||||
}
|
||||
|
||||
$('#objective-text').val(currentObjective.description)
|
||||
if (currentObjective.children.length > 0){
|
||||
// Show tasks if there are any to show
|
||||
for (const task of currentObjective.children) {
|
||||
task.addUiElement()
|
||||
}
|
||||
} else {
|
||||
// Show button to add tasks if there are none
|
||||
$('#objective-tasks').append(`
|
||||
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
|
||||
`)
|
||||
$("#objective-task-add-first").on('click', () => {
|
||||
currentObjective.addTask("")
|
||||
setCurrentTask()
|
||||
updateUiTaskList()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onParentClick() {
|
||||
currentObjective = getTaskById(currentObjective.parentId)
|
||||
updateUiTaskList()
|
||||
setCurrentTask()
|
||||
}
|
||||
|
||||
// Trigger creation of new tasks with given objective.
|
||||
async function onGenerateObjectiveClick() {
|
||||
await generateTasks()
|
||||
saveState()
|
||||
}
|
||||
|
||||
// Update extension prompts
|
||||
function onChatDepthInput() {
|
||||
saveState()
|
||||
setCurrentTask() // Ensure extension prompt is updated
|
||||
}
|
||||
|
||||
function onObjectiveTextFocusOut(){
|
||||
if (currentObjective){
|
||||
currentObjective.description = $('#objective-text').val()
|
||||
saveState()
|
||||
}
|
||||
}
|
||||
|
||||
// Update how often we check for task completion
|
||||
function onCheckFrequencyInput() {
|
||||
checkCounter = $("#objective-check-frequency").val()
|
||||
$('#objective-counter').text(checkCounter)
|
||||
saveState()
|
||||
}
|
||||
|
||||
function onHideTasksInput() {
|
||||
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
|
||||
saveState()
|
||||
}
|
||||
|
||||
function loadTaskChildrenRecurse(savedTask) {
|
||||
let tempTaskTree = new ObjectiveTask({
|
||||
id: savedTask.id,
|
||||
description: savedTask.description,
|
||||
completed: savedTask.completed,
|
||||
parentId: savedTask.parentId,
|
||||
})
|
||||
for (const task of savedTask.children){
|
||||
const childTask = loadTaskChildrenRecurse(task)
|
||||
tempTaskTree.children.push(childTask)
|
||||
}
|
||||
return tempTaskTree
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
// Load/Init settings for chatId
|
||||
currentChatId = getContext().chatId
|
||||
|
||||
// Reset Objectives and Tasks in memory
|
||||
taskTree = null;
|
||||
currentObjective = null;
|
||||
|
||||
// Init extension settings
|
||||
if (Object.keys(extension_settings.objective).length === 0) {
|
||||
Object.assign(extension_settings.objective, { 'customPrompts': {'default':defaultPrompts}})
|
||||
}
|
||||
|
||||
// Bail on home screen
|
||||
if (currentChatId == undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate existing settings
|
||||
if (currentChatId in extension_settings.objective) {
|
||||
// TODO: Remove this soon
|
||||
chat_metadata['objective'] = extension_settings.objective[currentChatId];
|
||||
delete extension_settings.objective[currentChatId];
|
||||
}
|
||||
|
||||
if (!('objective' in chat_metadata)) {
|
||||
Object.assign(chat_metadata, { objective: defaultSettings });
|
||||
}
|
||||
|
||||
// Migrate legacy flat objective to new objectiveTree and currentObjective
|
||||
if ('objective' in chat_metadata.objective) {
|
||||
|
||||
// Create root objective from legacy objective
|
||||
taskTree = new ObjectiveTask({id:0, description: chat_metadata.objective.objective});
|
||||
currentObjective = taskTree;
|
||||
|
||||
// Populate root objective tree from legacy tasks
|
||||
if ('tasks' in chat_metadata.objective) {
|
||||
let idIncrement = 0;
|
||||
taskTree.children = chat_metadata.objective.tasks.map(task => {
|
||||
idIncrement += 1;
|
||||
return new ObjectiveTask({
|
||||
id: idIncrement,
|
||||
description: task.description,
|
||||
completed: task.completed,
|
||||
parentId: taskTree.id,
|
||||
})
|
||||
});
|
||||
}
|
||||
saveState();
|
||||
delete chat_metadata.objective.objective;
|
||||
delete chat_metadata.objective.tasks;
|
||||
} else {
|
||||
// Load Objectives and Tasks (Normal path)
|
||||
if (chat_metadata.objective.taskTree){
|
||||
taskTree = loadTaskChildrenRecurse(chat_metadata.objective.taskTree)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there's a root task
|
||||
if (!taskTree) {
|
||||
taskTree = new ObjectiveTask({id:0,description:$('#objective-text').val()})
|
||||
}
|
||||
|
||||
currentObjective = taskTree
|
||||
checkCounter = chat_metadata['objective'].checkFrequency
|
||||
|
||||
// Update UI elements
|
||||
$('#objective-counter').text(checkCounter)
|
||||
$("#objective-text").text(taskTree.description)
|
||||
updateUiTaskList()
|
||||
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
|
||||
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
|
||||
$('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks)
|
||||
$('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked'))
|
||||
setCurrentTask(null, true)
|
||||
}
|
||||
|
||||
function addManualTaskCheckUi() {
|
||||
$('#extensionsMenu').prepend(`
|
||||
<div id="objective-task-manual-check-menu-item" class="list-group-item flex-container flexGap5">
|
||||
<div id="objective-task-manual-check" class="extensionsMenuExtensionButton fa-regular fa-square-check"/></div>
|
||||
Manual Task Check
|
||||
</div>`)
|
||||
$('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted)
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
const settingsHtml = `
|
||||
<div class="objective-settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Objective</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
|
||||
<textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
|
||||
<div class="objective_block flex-container">
|
||||
<input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
|
||||
<label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
|
||||
</div>
|
||||
<div id="objective-parent" class="objective_block flex-container">
|
||||
<i class="objective-task-button fa-solid fa-circle-left fa-2x" title="Go to Parent"></i>
|
||||
<small>Go to parent task</small>
|
||||
</div>
|
||||
|
||||
<div id="objective-tasks"> </div>
|
||||
<div class="objective_block margin-bot-10px">
|
||||
<div class="objective_block objective_block_control flex1 flexFlowColumn">
|
||||
<label for="objective-chat-depth">Position in Chat</label>
|
||||
<input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
</div>
|
||||
<br>
|
||||
<div class="objective_block objective_block_control flex1">
|
||||
|
||||
<label for="objective-check-frequency">Task Check Frequency</label>
|
||||
<input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
|
||||
<small>(0 = disabled)</small>
|
||||
</div>
|
||||
</div>
|
||||
<span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
|
||||
<div class="objective_block flex-container">
|
||||
<input id="objective_prompt_edit" class="menu_button" type="submit" value="Edit Prompts" />
|
||||
</div>
|
||||
<hr class="sysHR">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
addManualTaskCheckUi()
|
||||
$('#extensions_settings').append(settingsHtml);
|
||||
$('#objective-generate').on('click', onGenerateObjectiveClick)
|
||||
$('#objective-chat-depth').on('input', onChatDepthInput)
|
||||
$("#objective-check-frequency").on('input', onCheckFrequencyInput)
|
||||
$('#objective-hide-tasks').on('click', onHideTasksInput)
|
||||
$('#objective_prompt_edit').on('click', onEditPromptClick)
|
||||
$('#objective-parent').hide()
|
||||
$('#objective-parent').on('click',onParentClick)
|
||||
$('#objective-text').on('focusout',onObjectiveTextFocusOut)
|
||||
loadSettings()
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
resetState()
|
||||
});
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, () => {
|
||||
lastMessageWasSwipe = true
|
||||
})
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
|
||||
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
|
||||
lastMessageWasSwipe = false
|
||||
return
|
||||
}
|
||||
if ($("#objective-check-frequency").val() > 0) {
|
||||
// Check only at specified interval
|
||||
if (checkCounter <= 0) {
|
||||
checkTaskCompleted();
|
||||
}
|
||||
checkCounter -= 1
|
||||
}
|
||||
setCurrentTask();
|
||||
$('#objective-counter').text(checkCounter)
|
||||
});
|
||||
|
||||
registerSlashCommand('taskcheck', checkTaskCompleted, [], '– checks if the current task is completed', true, true);
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "Objective",
|
||||
"loading_order": 5,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Ouoertheo",
|
||||
"version": "0.0.1",
|
||||
"homePage": ""
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
#objective-counter {
|
||||
font-weight: 600;
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.objective_block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.objective_prompt_block {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
column-gap: 5px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.objective_block_control {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.objective_block_control small,
|
||||
.objective_block_control label {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.objective-task-button {
|
||||
margin: 0;
|
||||
outline: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
opacity: 0.7;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
.objective-task-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[id^=objective-task-delete-] {
|
||||
color: #da3f3f;
|
||||
}
|
||||
|
||||
#objective-tasks span {
|
||||
margin: unset;
|
||||
margin-bottom: 5px !important;
|
||||
}
|
@ -1,152 +0,0 @@
|
||||
import { saveSettingsDebounced } from "../../../script.js";
|
||||
import { extension_settings } from "../../extensions.js";
|
||||
|
||||
function toggleRandomizedSetting(buttonRef, forId) {
|
||||
if (extension_settings.randomizer.controls.indexOf(forId) === -1) {
|
||||
extension_settings.randomizer.controls.push(forId);
|
||||
} else {
|
||||
extension_settings.randomizer.controls = extension_settings.randomizer.controls.filter(x => x !== forId);
|
||||
}
|
||||
|
||||
buttonRef.toggleClass('active');
|
||||
console.debug('Randomizer controls:', extension_settings.randomizer.controls);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function addRandomizeButton() {
|
||||
const counterRef = $(this);
|
||||
const labelRef = $(this).find('div[data-for]');
|
||||
const isDisabled = counterRef.data('randomization-disabled');
|
||||
|
||||
if (labelRef.length === 0 || isDisabled == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forId = labelRef.data('for');
|
||||
const buttonRef = $('<div class="randomize_button menu_button fa-solid fa-shuffle"></div>');
|
||||
buttonRef.toggleClass('active', extension_settings.randomizer.controls.indexOf(forId) !== -1);
|
||||
buttonRef.hide();
|
||||
buttonRef.on('click', () => toggleRandomizedSetting(buttonRef, forId));
|
||||
counterRef.append(buttonRef);
|
||||
}
|
||||
|
||||
function onRandomizerEnabled() {
|
||||
extension_settings.randomizer.enabled = $(this).prop('checked');
|
||||
$('.randomize_button').toggle(extension_settings.randomizer.enabled);
|
||||
console.debug('Randomizer enabled:', extension_settings.randomizer.enabled);
|
||||
}
|
||||
|
||||
window['randomizerInterceptor'] = (function () {
|
||||
if (extension_settings.randomizer.enabled === false) {
|
||||
console.debug('Randomizer skipped: disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (extension_settings.randomizer.fluctuation === 0 || extension_settings.randomizer.controls.length === 0) {
|
||||
console.debug('Randomizer skipped: nothing to do.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const control of extension_settings.randomizer.controls) {
|
||||
const controlRef = $('#' + control);
|
||||
|
||||
if (controlRef.length === 0) {
|
||||
console.debug(`Randomizer skipped: control ${control} not found.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!controlRef.is(':visible')) {
|
||||
console.debug(`Randomizer skipped: control ${control} is not visible.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let previousValue = parseFloat(controlRef.data('previous-value'));
|
||||
let originalValue = parseFloat(controlRef.data('original-value'));
|
||||
let currentValue = parseFloat(controlRef.val());
|
||||
|
||||
let value;
|
||||
|
||||
// Initialize originalValue and previousValue if they are NaN
|
||||
if (isNaN(originalValue)) {
|
||||
originalValue = currentValue;
|
||||
controlRef.data('original-value', originalValue);
|
||||
}
|
||||
if (isNaN(previousValue)) {
|
||||
previousValue = currentValue;
|
||||
controlRef.data('previous-value', previousValue);
|
||||
}
|
||||
|
||||
// If the current value hasn't changed compared to the previous value, use the original value as a base for the calculation
|
||||
if (currentValue === previousValue) {
|
||||
console.debug(`Randomizer for ${control} reusing original value: ${originalValue}`);
|
||||
value = originalValue;
|
||||
} else {
|
||||
console.debug(`Randomizer for ${control} using current value: ${currentValue}`);
|
||||
value = currentValue;
|
||||
controlRef.data('previous-value', currentValue); // Update the previous value when using the current value
|
||||
controlRef.data('original-value', currentValue); // Update the original value when using the current value
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
console.debug('Randomizer skipped: NaN.');
|
||||
continue;
|
||||
}
|
||||
|
||||
const fluctuation = extension_settings.randomizer.fluctuation;
|
||||
const min = parseFloat(controlRef.attr('min'));
|
||||
const max = parseFloat(controlRef.attr('max'));
|
||||
const delta = (Math.random() * fluctuation * 2 - fluctuation) * value;
|
||||
const newValue = Math.min(Math.max(value + delta, min), max);
|
||||
console.debug(`Randomizer for ${control}: ${value} -> ${newValue} (delta: ${delta}, min: ${min}, max: ${max})`);
|
||||
controlRef.val(newValue).trigger('input');
|
||||
controlRef.data('previous-value', parseFloat(controlRef.val()));
|
||||
}
|
||||
});
|
||||
|
||||
jQuery(() => {
|
||||
const html = `
|
||||
<div class="randomizer_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>Parameter Randomizer</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<label for="randomizer_enabled" class="checkbox_label">
|
||||
<input type="checkbox" id="randomizer_enabled" name="randomizer_enabled" >
|
||||
Enabled
|
||||
</label>
|
||||
<div class="range-block">
|
||||
<div class="range-block-title">
|
||||
Fluctuation (0-1)
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="randomizer_fluctuation" min="0" max="1" step="0.1">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<div contenteditable="true" data-for="randomizer_fluctuation" id="randomizer_fluctuation_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
$('#extensions_settings2').append(html);
|
||||
$('#ai_response_configuration .range-block-counter').each(addRandomizeButton);
|
||||
$('#randomizer_enabled').on('input', onRandomizerEnabled);
|
||||
$('#randomizer_enabled').prop('checked', extension_settings.randomizer.enabled).trigger('input');
|
||||
$('#randomizer_fluctuation').val(extension_settings.randomizer.fluctuation).trigger('input');
|
||||
$('#randomizer_fluctuation_counter').text(extension_settings.randomizer.fluctuation);
|
||||
$('#randomizer_fluctuation').on('input', function () {
|
||||
const value = parseFloat($(this).val());
|
||||
$('#randomizer_fluctuation_counter').text(value);
|
||||
extension_settings.randomizer.fluctuation = value;
|
||||
console.debug('Randomizer fluctuation:', extension_settings.randomizer.fluctuation);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"display_name": "Parameter Randomizer",
|
||||
"loading_order": 15,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"generate_interceptor": "randomizerInterceptor",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,489 +0,0 @@
|
||||
/*
|
||||
TODO:
|
||||
- load RVC models list from extras
|
||||
- Settings per characters
|
||||
*/
|
||||
|
||||
import { saveSettingsDebounced } from "../../../script.js";
|
||||
import { getContext, getApiUrl, extension_settings, doExtrasFetch, ModuleWorkerWrapper, modules } from "../../extensions.js";
|
||||
export { MODULE_NAME, rvcVoiceConversion };
|
||||
|
||||
const MODULE_NAME = 'RVC';
|
||||
const DEBUG_PREFIX = "<RVC module> "
|
||||
const UPDATE_INTERVAL = 1000
|
||||
|
||||
let charactersList = [] // Updated with module worker
|
||||
let rvcModelsList = [] // Initialized only once
|
||||
let rvcModelsReceived = false;
|
||||
|
||||
function updateVoiceMapText() {
|
||||
let voiceMapText = ""
|
||||
for (let i in extension_settings.rvc.voiceMap) {
|
||||
const voice_settings = extension_settings.rvc.voiceMap[i];
|
||||
voiceMapText += i + ":"
|
||||
+ voice_settings["modelName"] + "("
|
||||
+ voice_settings["pitchExtraction"] + ","
|
||||
+ voice_settings["pitchOffset"] + ","
|
||||
+ voice_settings["indexRate"] + ","
|
||||
+ voice_settings["filterRadius"] + ","
|
||||
+ voice_settings["rmsMixRate"] + ","
|
||||
+ voice_settings["protect"]
|
||||
+ "),\n"
|
||||
}
|
||||
|
||||
extension_settings.rvc.voiceMapText = voiceMapText;
|
||||
$('#rvc_voice_map').val(voiceMapText);
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Updated voice map debug text to\n", voiceMapText)
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
const defaultSettings = {
|
||||
enabled: false,
|
||||
model: "",
|
||||
pitchOffset: 0,
|
||||
pitchExtraction: "dio",
|
||||
indexRate: 0.88,
|
||||
filterRadius: 3,
|
||||
rmsMixRate: 1,
|
||||
protect: 0.33,
|
||||
voicMapText: "",
|
||||
voiceMap: {}
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
if (extension_settings.rvc === undefined)
|
||||
extension_settings.rvc = {};
|
||||
|
||||
if (Object.keys(extension_settings.rvc).length === 0) {
|
||||
Object.assign(extension_settings.rvc, defaultSettings)
|
||||
}
|
||||
$('#rvc_enabled').prop('checked', extension_settings.rvc.enabled);
|
||||
$('#rvc_model').val(extension_settings.rvc.model);
|
||||
|
||||
$('#rvc_pitch_extraction').val(extension_settings.rvc.pitchExtraction);
|
||||
$('#rvc_pitch_extractiont_value').text(extension_settings.rvc.pitchExtraction);
|
||||
|
||||
$('#rvc_index_rate').val(extension_settings.rvc.indexRate);
|
||||
$('#rvc_index_rate_value').text(extension_settings.rvc.indexRate);
|
||||
|
||||
$('#rvc_filter_radius').val(extension_settings.rvc.filterRadius);
|
||||
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius);
|
||||
|
||||
$('#rvc_pitch_offset').val(extension_settings.rvc.pitchOffset);
|
||||
$('#rvc_pitch_offset_value').text(extension_settings.rvc.pitchOffset);
|
||||
|
||||
$('#rvc_rms_mix_rate').val(extension_settings.rvc.rmsMixRate);
|
||||
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate);
|
||||
|
||||
$('#rvc_protect').val(extension_settings.rvc.protect);
|
||||
$("#rvc_protect_value").text(extension_settings.rvc.protect);
|
||||
|
||||
$('#rvc_voice_map').val(extension_settings.rvc.voiceMapText);
|
||||
|
||||
}
|
||||
|
||||
async function onEnabledClick() {
|
||||
extension_settings.rvc.enabled = $('#rvc_enabled').is(':checked');
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onPitchExtractionChange() {
|
||||
extension_settings.rvc.pitchExtraction = $('#rvc_pitch_extraction').val();
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onIndexRateChange() {
|
||||
extension_settings.rvc.indexRate = Number($('#rvc_index_rate').val());
|
||||
$("#rvc_index_rate_value").text(extension_settings.rvc.indexRate)
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onFilterRadiusChange() {
|
||||
extension_settings.rvc.filterRadius = Number($('#rvc_filter_radius').val());
|
||||
$("#rvc_filter_radius_value").text(extension_settings.rvc.filterRadius)
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onPitchOffsetChange() {
|
||||
extension_settings.rvc.pitchOffset = Number($('#rvc_pitch_offset').val());
|
||||
$("#rvc_pitch_offset_value").text(extension_settings.rvc.pitchOffset)
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onRmsMixRateChange() {
|
||||
extension_settings.rvc.rmsMixRate = Number($('#rvc_rms_mix_rate').val());
|
||||
$("#rvc_rms_mix_rate_value").text(extension_settings.rvc.rmsMixRate)
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onProtectChange() {
|
||||
extension_settings.rvc.protect = Number($('#rvc_protect').val());
|
||||
$("#rvc_protect_value").text(extension_settings.rvc.protect)
|
||||
saveSettingsDebounced()
|
||||
}
|
||||
|
||||
async function onApplyClick() {
|
||||
let error = false;
|
||||
const character = $("#rvc_character_select").val();
|
||||
const model_name = $("#rvc_model_select").val();
|
||||
const pitchExtraction = $("#rvc_pitch_extraction").val();
|
||||
const indexRate = $("#rvc_index_rate").val();
|
||||
const filterRadius = $("#rvc_filter_radius").val();
|
||||
const pitchOffset = $("#rvc_pitch_offset").val();
|
||||
const rmsMixRate = $("#rvc_rms_mix_rate").val();
|
||||
const protect = $("#rvc_protect").val();
|
||||
|
||||
if (character === "none") {
|
||||
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error("Model not selected.", DEBUG_PREFIX + " voice mapping apply", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.rvc.voiceMap[character] = {
|
||||
"modelName": model_name,
|
||||
"pitchExtraction": pitchExtraction,
|
||||
"indexRate": indexRate,
|
||||
"filterRadius": filterRadius,
|
||||
"pitchOffset": pitchOffset,
|
||||
"rmsMixRate": rmsMixRate,
|
||||
"protect": protect
|
||||
}
|
||||
|
||||
updateVoiceMapText();
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Updated settings of ", character, ":", extension_settings.rvc.voiceMap[character])
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onDeleteClick() {
|
||||
const character = $("#rvc_character_select").val();
|
||||
|
||||
if (character === "none") {
|
||||
toastr.error("Character not selected.", DEBUG_PREFIX + " voice mapping delete", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
delete extension_settings.rvc.voiceMap[character];
|
||||
console.debug(DEBUG_PREFIX, "Deleted settings of ", character);
|
||||
updateVoiceMapText();
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
async function onChangeUploadFiles() {
|
||||
const url = new URL(getApiUrl());
|
||||
const inputFiles = $("#rvc_model_upload_files").get(0).files;
|
||||
let formData = new FormData();
|
||||
|
||||
for (const file of inputFiles)
|
||||
formData.append(file.name, file);
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Sending files:", formData);
|
||||
url.pathname = '/api/voice-conversion/rvc/upload-models';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check extras console for errors log');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
alert('The files have been uploaded successfully.');
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
function addExtensionControls() {
|
||||
const settingsHtml = `
|
||||
<div id="rvc_settings">
|
||||
<div class="inline-drawer">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b>RVC</b>
|
||||
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content">
|
||||
<h4 class="center">Characters Voice Mapping</h4>
|
||||
<div>
|
||||
<label class="checkbox_label" for="rvc_enabled">
|
||||
<input type="checkbox" id="rvc_enabled" name="rvc_enabled">
|
||||
<small>Enabled</small>
|
||||
</label>
|
||||
<label>Voice Map (debug infos)</label>
|
||||
<textarea id="rvc_voice_map" type="text" class="text_pole textarea_compact" rows="4"
|
||||
placeholder="Voice map will appear here for debug purpose"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<div class="background_controls">
|
||||
<label for="rvc_character_select">Character:</label>
|
||||
<select id="rvc_character_select">
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
<div id="rvc_delete" class="menu_button">
|
||||
<i class="fa-solid fa-times"></i>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
<div class="background_controls">
|
||||
<label for="rvc_model_select">Voice:</label>
|
||||
<select id="rvc_model_select">
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
<div id="rvc_model_refresh_button" class="menu_button">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
<!-- Refresh -->
|
||||
</div>
|
||||
<div id="rvc_model_upload_select_button" class="menu_button">
|
||||
<i class="fa-solid fa-upload"></i>
|
||||
Upload
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
id="rvc_model_upload_files"
|
||||
accept=".zip,.rar,.7zip,.7z" multiple />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<small>
|
||||
Upload one archive per model. With .pth and .index (optional) inside.<br/>
|
||||
Supported format: .zip .rar .7zip .7z
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<h4>Model Settings</h4>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_pitch_extraction">
|
||||
Pitch Extraction
|
||||
</label>
|
||||
<select id="rvc_pitch_extraction">
|
||||
<option value="dio">dio</option>
|
||||
<option value="pm">pm</option>
|
||||
<option value="harvest">harvest</option>
|
||||
<option value="torchcrepe">torchcrepe</option>
|
||||
<option value="rmvpe">rmvpe</option>
|
||||
<option value="">None</option>
|
||||
</select>
|
||||
<small>
|
||||
Tips: dio and pm faster, harvest slower but good.<br/>
|
||||
Torchcrepe and rmvpe are good but uses GPU.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_index_rate">
|
||||
Search feature ratio (<span id="rvc_index_rate_value"></span>)
|
||||
</label>
|
||||
<input id="rvc_index_rate" type="range" min="0" max="1" step="0.01" value="0.5" />
|
||||
<small>
|
||||
Controls accent strength, too high may produce artifact.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_filter_radius">Filter radius (<span id="rvc_filter_radius_value"></span>)</label>
|
||||
<input id="rvc_filter_radius" type="range" min="0" max="7" step="1" value="3" />
|
||||
<small>
|
||||
Higher can reduce breathiness but may increase run time.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_pitch_offset">Pitch offset (<span id="rvc_pitch_offset_value"></span>)</label>
|
||||
<input id="rvc_pitch_offset" type="range" min="-20" max="20" step="1" value="0" />
|
||||
<small>
|
||||
Recommended +12 key for male to female conversion and -12 key for female to male conversion.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_rms_mix_rate">Mix rate (<span id="rvc_rms_mix_rate_value"></span>)</label>
|
||||
<input id="rvc_rms_mix_rate" type="range" min="0" max="1" step="0.01" value="1" />
|
||||
<small>
|
||||
Closer to 0 is closer to TTS and 1 is closer to trained voice.
|
||||
Can help mask noise and sound more natural when set relatively low.
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rvc_protect">Protect amount (<span id="rvc_protect_value"></span>)</label>
|
||||
<input id="rvc_protect" type="range" min="0" max="1" step="0.01" value="0.33" />
|
||||
<small>
|
||||
Avoid non voice sounds. Lower is more being ignored.
|
||||
</small>
|
||||
</div>
|
||||
<div id="rvc_status">
|
||||
</div>
|
||||
<div class="rvc_buttons">
|
||||
<input id="rvc_apply" class="menu_button" type="submit" value="Apply" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#extensions_settings').append(settingsHtml);
|
||||
$("#rvc_enabled").on("click", onEnabledClick);
|
||||
$("#rvc_voice_map").attr("disabled", "disabled");;
|
||||
$('#rvc_pitch_extraction').on('change', onPitchExtractionChange);
|
||||
$('#rvc_index_rate').on('input', onIndexRateChange);
|
||||
$('#rvc_filter_radius').on('input', onFilterRadiusChange);
|
||||
$('#rvc_pitch_offset').on('input', onPitchOffsetChange);
|
||||
$('#rvc_rms_mix_rate').on('input', onRmsMixRateChange);
|
||||
$('#rvc_protect').on('input', onProtectChange);
|
||||
$("#rvc_apply").on("click", onApplyClick);
|
||||
$("#rvc_delete").on("click", onDeleteClick);
|
||||
|
||||
$("#rvc_model_upload_files").hide();
|
||||
$("#rvc_model_upload_select_button").on("click", function() {$("#rvc_model_upload_files").click()});
|
||||
|
||||
$("#rvc_model_upload_files").on("change", onChangeUploadFiles);
|
||||
//$("#rvc_model_upload_button").on("click", onClickUpload);
|
||||
$("#rvc_model_refresh_button").on("click", refreshVoiceList);
|
||||
|
||||
}
|
||||
addExtensionControls(); // No init dependencies
|
||||
loadSettings(); // Depends on Extension Controls
|
||||
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
})
|
||||
|
||||
//#############################//
|
||||
// API Calls //
|
||||
//#############################//
|
||||
|
||||
/*
|
||||
Check model installation state, return one of ["installed", "corrupted", "absent"]
|
||||
*/
|
||||
async function get_models_list(model_id) {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/voice-conversion/rvc/get-models-list';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check model state request failed');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
}
|
||||
|
||||
/*
|
||||
Send an audio file to RVC to convert voice
|
||||
*/
|
||||
async function rvcVoiceConversion(response, character, text) {
|
||||
let apiResult
|
||||
|
||||
// Check voice map
|
||||
if (extension_settings.rvc.voiceMap[character] === undefined) {
|
||||
//toastr.error("No model is assigned to character '"+character+"', check RVC voice map in the extension menu.", DEBUG_PREFIX+'RVC Voice map error', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
console.info(DEBUG_PREFIX, "No RVC model assign in voice map for current character " + character);
|
||||
return response;
|
||||
}
|
||||
|
||||
const audioData = await response.blob()
|
||||
if (!audioData.type in ['audio/mpeg', 'audio/wav', 'audio/x-wav', 'audio/wave', 'audio/webm']) {
|
||||
throw `TTS received HTTP response with invalid data format. Expecting audio/mpeg, got ${audioData.type}`
|
||||
}
|
||||
console.log("Audio type received:", audioData.type)
|
||||
|
||||
const voice_settings = extension_settings.rvc.voiceMap[character];
|
||||
|
||||
var requestData = new FormData();
|
||||
requestData.append('AudioFile', audioData, 'record');
|
||||
requestData.append("json", JSON.stringify({
|
||||
"modelName": voice_settings["modelName"],
|
||||
"pitchExtraction": voice_settings["pitchExtraction"],
|
||||
"pitchOffset": voice_settings["pitchOffset"],
|
||||
"indexRate": voice_settings["indexRate"],
|
||||
"filterRadius": voice_settings["filterRadius"],
|
||||
"rmsMixRate": voice_settings["rmsMixRate"],
|
||||
"protect": voice_settings["protect"],
|
||||
"text": text
|
||||
}));
|
||||
|
||||
console.log("Sending tts audio data to RVC on extras server",requestData)
|
||||
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/voice-conversion/rvc/process-audio';
|
||||
|
||||
apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
body: requestData,
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX + ' RVC Voice Conversion Failed', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult;
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// Module Worker //
|
||||
//#############################//
|
||||
|
||||
async function refreshVoiceList() {
|
||||
let result = await get_models_list();
|
||||
result = await result.json();
|
||||
rvcModelsList = result["models_list"]
|
||||
|
||||
$('#rvc_model_select')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select Voice</option>')
|
||||
.val('none')
|
||||
|
||||
for (const modelName of rvcModelsList) {
|
||||
$("#rvc_model_select").append(new Option(modelName, modelName));
|
||||
}
|
||||
|
||||
rvcModelsReceived = true
|
||||
console.debug(DEBUG_PREFIX, "Updated model list to:", rvcModelsList);
|
||||
}
|
||||
|
||||
async function moduleWorker() {
|
||||
updateCharactersList();
|
||||
|
||||
if (modules.includes('rvc') && !rvcModelsReceived) {
|
||||
refreshVoiceList();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCharactersList() {
|
||||
let currentcharacters = new Set();
|
||||
const context = getContext();
|
||||
for (const i of context.characters) {
|
||||
currentcharacters.add(i.name);
|
||||
}
|
||||
|
||||
currentcharacters = Array.from(currentcharacters);
|
||||
currentcharacters.unshift(context.name1);
|
||||
|
||||
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
|
||||
charactersList = currentcharacters
|
||||
|
||||
$('#rvc_character_select')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select Character</option>')
|
||||
.val('none')
|
||||
|
||||
for (const charName of charactersList) {
|
||||
$("#rvc_character_select").append(new Option(charName, charName));
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX, "Updated character list to:", charactersList);
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "RVC",
|
||||
"loading_order": 13,
|
||||
"requires": ["rvc"],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Keij#6799",
|
||||
"version": "0.1.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
.speech-toggle {
|
||||
display: flex;
|
||||
}
|
@ -8,7 +8,6 @@ import { CoquiTtsProvider } from './coqui.js'
|
||||
import { SystemTtsProvider } from './system.js'
|
||||
import { NovelTtsProvider } from './novel.js'
|
||||
import { power_user } from '../../power-user.js'
|
||||
import { rvcVoiceConversion } from "../rvc/index.js"
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000
|
||||
@ -415,8 +414,8 @@ async function tts(text, voiceId, char) {
|
||||
let response = await ttsProvider.generateTts(text, voiceId)
|
||||
|
||||
// RVC injection
|
||||
if (extension_settings.rvc.enabled)
|
||||
response = await rvcVoiceConversion(response, char, text)
|
||||
if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
|
||||
response = await window['rvcVoiceConversion'](response, char, text)
|
||||
|
||||
addAudioJob(response)
|
||||
completeTtsJob()
|
||||
|
@ -1,66 +0,0 @@
|
||||
import { getContext } from "../../extensions.js";
|
||||
|
||||
/**
|
||||
* Gets a chat variable from the current chat metadata.
|
||||
* @param {string} name The name of the variable to get.
|
||||
* @returns {string} The value of the variable.
|
||||
*/
|
||||
function getChatVariable(name) {
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
return '';
|
||||
}
|
||||
|
||||
return metadata.variables[name] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a chat variable in the current chat metadata.
|
||||
* @param {string} name The name of the variable to set.
|
||||
* @param {any} value The value of the variable to set.
|
||||
*/
|
||||
function setChatVariable(name, value) {
|
||||
if (name === undefined || value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
}
|
||||
|
||||
metadata.variables[name] = value;
|
||||
}
|
||||
|
||||
function listChatVariables() {
|
||||
const metadata = getContext().chatMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!metadata.variables) {
|
||||
metadata.variables = {};
|
||||
return '';
|
||||
}
|
||||
|
||||
return Object.keys(metadata.variables).map(key => `${key}=${metadata.variables[key]}`).join(';');
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
const context = getContext();
|
||||
context.registerHelper('getvar', getChatVariable);
|
||||
context.registerHelper('setvar', setChatVariable);
|
||||
context.registerHelper('listvar', listChatVariables);
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"display_name": "Chat Variables",
|
||||
"loading_order": 100,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "",
|
||||
"author": "Cohee#1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
@ -993,27 +993,29 @@ function printGroupCandidates() {
|
||||
|
||||
function printGroupMembers() {
|
||||
const storageKey = 'GroupMembers_PerPage';
|
||||
$("#rm_group_members_pagination").pagination({
|
||||
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
|
||||
pageRange: 1,
|
||||
position: 'top',
|
||||
showPageNumbers: false,
|
||||
prevText: '<',
|
||||
nextText: '>',
|
||||
formatNavigator: PAGINATION_TEMPLATE,
|
||||
showNavigator: true,
|
||||
showSizeChanger: true,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || 5,
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200],
|
||||
afterSizeSelectorChange: function (e) {
|
||||
localStorage.setItem(storageKey, e.target.value);
|
||||
},
|
||||
callback: function (data) {
|
||||
$("#rm_group_members").empty();
|
||||
for (const i of data) {
|
||||
$("#rm_group_members").append(getGroupCharacterBlock(i.item));
|
||||
}
|
||||
},
|
||||
$(".rm_group_members_pagination").each(function() {
|
||||
$(this).pagination({
|
||||
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
|
||||
pageRange: 1,
|
||||
position: 'top',
|
||||
showPageNumbers: false,
|
||||
prevText: '<',
|
||||
nextText: '>',
|
||||
formatNavigator: PAGINATION_TEMPLATE,
|
||||
showNavigator: true,
|
||||
showSizeChanger: true,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || 5,
|
||||
sizeChangerOptions: [5, 10, 25, 50, 100, 200],
|
||||
afterSizeSelectorChange: function (e) {
|
||||
localStorage.setItem(storageKey, e.target.value);
|
||||
},
|
||||
callback: function (data) {
|
||||
$(".rm_group_members").empty();
|
||||
for (const i of data) {
|
||||
$(".rm_group_members").append(getGroupCharacterBlock(i.item));
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -1108,6 +1110,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
$("#rm_group_submit").hide();
|
||||
$("#rm_group_delete").show();
|
||||
$("#rm_group_scenario").show();
|
||||
$('#group-metadata-controls .chat_lorebook_button').removeClass('disabled').prop('disabled', false);
|
||||
} else {
|
||||
$("#rm_group_submit").show();
|
||||
if ($("#groupAddMemberListToggle .inline-drawer-content").css('display') !== 'block') {
|
||||
@ -1115,6 +1118,7 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
}
|
||||
$("#rm_group_delete").hide();
|
||||
$("#rm_group_scenario").hide();
|
||||
$('#group-metadata-controls .chat_lorebook_button').addClass('disabled').prop('disabled', true);
|
||||
}
|
||||
|
||||
updateFavButtonState(group?.fav ?? false);
|
||||
@ -1563,6 +1567,9 @@ function doCurMemberListPopout() {
|
||||
.append(controlBarHtml)
|
||||
.append(memberListClone)
|
||||
|
||||
// Remove pagination from popout
|
||||
newElement.find('.group_pagination').empty();
|
||||
|
||||
$('body').append(newElement);
|
||||
loadMovingUIState();
|
||||
$("#groupMemberListPopout").fadeIn(250)
|
||||
@ -1571,6 +1578,8 @@ function doCurMemberListPopout() {
|
||||
$("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() })
|
||||
})
|
||||
|
||||
// Re-add pagination not working in popout
|
||||
printGroupMembers();
|
||||
} else {
|
||||
console.debug('saw existing popout, removing')
|
||||
$("#groupMemberListPopout").fadeOut(250, () => { $("#groupMemberListPopout").remove() });
|
||||
|
@ -217,6 +217,7 @@ const default_settings = {
|
||||
use_ai21_tokenizer: false,
|
||||
exclude_assistant: false,
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
};
|
||||
|
||||
const oai_settings = {
|
||||
@ -261,6 +262,7 @@ const oai_settings = {
|
||||
use_ai21_tokenizer: false,
|
||||
exclude_assistant: false,
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
};
|
||||
|
||||
let openai_setting_names;
|
||||
@ -937,6 +939,10 @@ function prepareOpenAIMessages({
|
||||
// Pass chat completion to prompt manager for inspection
|
||||
promptManager.setChatCompletion(chatCompletion);
|
||||
|
||||
if (oai_settings.squash_system_messages) {
|
||||
chatCompletion.squashSystemMessages();
|
||||
}
|
||||
|
||||
// All information is up-to-date, render.
|
||||
if (false === dryRun) promptManager.render(false);
|
||||
}
|
||||
@ -1646,6 +1652,21 @@ class MessageCollection {
|
||||
getTokens() {
|
||||
return this.collection.reduce((tokens, message) => tokens + message.getTokens(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines message collections into a single collection.
|
||||
* @returns {Message[]} The collection of messages flattened into a single array.
|
||||
*/
|
||||
flatten() {
|
||||
return this.collection.reduce((acc, message) => {
|
||||
if (message instanceof MessageCollection) {
|
||||
acc.push(...message.flatten());
|
||||
} else {
|
||||
acc.push(message);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1660,6 +1681,36 @@ class MessageCollection {
|
||||
*/
|
||||
class ChatCompletion {
|
||||
|
||||
/**
|
||||
* Combines consecutive system messages into one if they have no name attached.
|
||||
*/
|
||||
squashSystemMessages() {
|
||||
const excludeList = ['newMainChat', 'newChat', 'groupNudge'];
|
||||
this.messages.collection = this.messages.flatten();
|
||||
|
||||
let lastMessage = null;
|
||||
let squashedMessages = [];
|
||||
|
||||
for (let message of this.messages.collection) {
|
||||
if (!excludeList.includes(message.identifier) && message.role === 'system' && !message.name) {
|
||||
if (lastMessage && lastMessage.role === 'system') {
|
||||
lastMessage.content += '\n' + message.content;
|
||||
lastMessage.tokens = tokenHandler.count({ role: lastMessage.role, content: lastMessage.content });
|
||||
}
|
||||
else {
|
||||
squashedMessages.push(message);
|
||||
lastMessage = message;
|
||||
}
|
||||
}
|
||||
else {
|
||||
squashedMessages.push(message);
|
||||
lastMessage = message;
|
||||
}
|
||||
}
|
||||
|
||||
this.messages.collection = squashedMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new instance of ChatCompletion.
|
||||
* Sets up the initial token budget and a new message collection.
|
||||
@ -1819,7 +1870,11 @@ class ChatCompletion {
|
||||
for (let item of this.messages.collection) {
|
||||
if (item instanceof MessageCollection) {
|
||||
chat.push(...item.getChat());
|
||||
} else if (item instanceof Message && item.content) {
|
||||
const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };
|
||||
chat.push(message);
|
||||
} else {
|
||||
this.log(`Item ${item} has an unknown type. Adding as-is`);
|
||||
chat.push(item);
|
||||
}
|
||||
}
|
||||
@ -1993,6 +2048,7 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.new_group_chat_prompt = settings.new_group_chat_prompt ?? default_settings.new_group_chat_prompt;
|
||||
oai_settings.new_example_chat_prompt = settings.new_example_chat_prompt ?? default_settings.new_example_chat_prompt;
|
||||
oai_settings.continue_nudge_prompt = settings.continue_nudge_prompt ?? default_settings.continue_nudge_prompt;
|
||||
oai_settings.squash_system_messages = settings.squash_system_messages ?? default_settings.squash_system_messages;
|
||||
|
||||
if (settings.wrap_in_quotes !== undefined) oai_settings.wrap_in_quotes = !!settings.wrap_in_quotes;
|
||||
if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion;
|
||||
@ -2029,6 +2085,7 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#exclude_assistant').prop('checked', oai_settings.exclude_assistant);
|
||||
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
|
||||
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
|
||||
$('#squash_system_messages').prop('checked', oai_settings.squash_system_messages);
|
||||
if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt;
|
||||
|
||||
$('#impersonation_prompt_textarea').val(oai_settings.impersonation_prompt);
|
||||
@ -2228,6 +2285,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
use_ai21_tokenizer: settings.use_ai21_tokenizer,
|
||||
exclude_assistant: settings.exclude_assistant,
|
||||
use_alt_scale: settings.use_alt_scale,
|
||||
squash_system_messages: settings.squash_system_messages,
|
||||
};
|
||||
|
||||
const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {
|
||||
@ -2572,14 +2630,14 @@ function onSettingsPresetChange() {
|
||||
stream_openai: ['#stream_toggle', 'stream_openai', true],
|
||||
prompts: ['', 'prompts', false],
|
||||
prompt_order: ['', 'prompt_order', false],
|
||||
use_openrouter: ['#use_openrouter', 'use_openrouter', true],
|
||||
api_url_scale: ['#api_url_scale', 'api_url_scale', false],
|
||||
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
|
||||
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', false],
|
||||
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', false],
|
||||
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
|
||||
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
|
||||
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
|
||||
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
|
||||
};
|
||||
|
||||
const presetName = $('#settings_perset_openai').find(":selected").text();
|
||||
@ -3286,6 +3344,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#squash_system_messages').on('input', function () {
|
||||
oai_settings.squash_system_messages = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$(document).on('input', '#openai_settings .autoSetHeight', function () {
|
||||
resetScrollHeight($(this));
|
||||
});
|
||||
|
@ -382,8 +382,10 @@ jQuery(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = file.name.replace('.json', '').replace('.settings', '');
|
||||
const fileName = file.name.replace('.json', '').replace('.settings', '');
|
||||
const data = await parseJsonFile(file);
|
||||
const name = data?.name ?? fileName;
|
||||
data['name'] = name;
|
||||
|
||||
await presetManager.savePreset(name, data);
|
||||
toastr.success('Preset imported');
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { saveSettingsDebounced } from "../script.js";
|
||||
import { power_user } from "./power-user.js";
|
||||
import { isUrlOrAPIKey } from "./utils.js";
|
||||
import { isValidUrl } from "./utils.js";
|
||||
|
||||
/**
|
||||
* @param {{ term: string; }} request
|
||||
@ -64,7 +64,7 @@ function onServerConnectClick() {
|
||||
const value = String($(`[data-server-history="${serverLabel}"]`).val()).toLowerCase().trim();
|
||||
|
||||
// Don't save empty values or invalid URLs
|
||||
if (!value || !isUrlOrAPIKey(value)) {
|
||||
if (!value || !isValidUrl(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ export function escapeHtml(str) {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
export function isUrlOrAPIKey(value) {
|
||||
export function isValidUrl(value) {
|
||||
try {
|
||||
new URL(value);
|
||||
return true;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPrompt, MAX_INJECTION_DEPTH, extension_prompt_types, getExtensionPromptByName } from "../script.js";
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPrompt, MAX_INJECTION_DEPTH, extension_prompt_types, getExtensionPromptByName, saveMetadata, getCurrentChatId } from "../script.js";
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition } from "./utils.js";
|
||||
import { extension_settings, getContext } from "./extensions.js";
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
|
||||
@ -53,6 +53,7 @@ let updateEditor = (navigation) => { navigation; };
|
||||
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
|
||||
const worldInfoFilter = new FilterHelper(() => updateEditor());
|
||||
const SORT_ORDER_KEY = 'world_info_sort_order';
|
||||
const METADATA_KEY = 'world_info';
|
||||
|
||||
const InputWidthReference = $("#WIInputWidthReference");
|
||||
|
||||
@ -167,6 +168,11 @@ function setWorldInfoSettings(settings, data) {
|
||||
|
||||
$('#world_info_sort_order').val(localStorage.getItem(SORT_ORDER_KEY) || '0');
|
||||
$("#world_editor_select").trigger("change");
|
||||
|
||||
eventSource.on(event_types.CHAT_CHANGED, () => {
|
||||
const hasWorldInfo = !!chat_metadata[METADATA_KEY] && world_names.includes(chat_metadata[METADATA_KEY]);
|
||||
$('.chat_lorebook_button').toggleClass('world_set', hasWorldInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// World Info Editor
|
||||
@ -1275,6 +1281,11 @@ async function getCharacterLore() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chat_metadata[METADATA_KEY] === worldName) {
|
||||
console.debug(`Character ${name}'s world ${worldName} is already activated in chat lore! Skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await loadWorldInfoData(worldName);
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||||
entries = entries.concat(newEntries);
|
||||
@ -1301,10 +1312,31 @@ async function getGlobalLore() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getChatLore() {
|
||||
const chatWorld = chat_metadata[METADATA_KEY];
|
||||
|
||||
if (!chatWorld) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (selected_world_info.includes(chatWorld)) {
|
||||
console.debug(`Chat world ${chatWorld} is already activated in global world info! Skipping...`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await loadWorldInfoData(chatWorld);
|
||||
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
|
||||
|
||||
console.debug(`Chat lore has ${entries.length} entries`);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getSortedEntries() {
|
||||
try {
|
||||
const globalLore = await getGlobalLore();
|
||||
const characterLore = await getCharacterLore();
|
||||
const chatLore = await getChatLore();
|
||||
|
||||
let entries;
|
||||
|
||||
@ -1327,6 +1359,9 @@ async function getSortedEntries() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Chat lore always goes first
|
||||
entries = [...chatLore.sort(sortFn), ...entries];
|
||||
|
||||
console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`);
|
||||
|
||||
// Need to deep clone the entries to avoid modifying the cached data
|
||||
@ -1911,6 +1946,39 @@ export async function importWorldInfo(file) {
|
||||
});
|
||||
}
|
||||
|
||||
function assignLorebookToChat() {
|
||||
const selectedName = chat_metadata[METADATA_KEY];
|
||||
const template = $('#chat_world_template .chat_world').clone();
|
||||
|
||||
const worldSelect = template.find('select');
|
||||
const chatName = template.find('.chat_name');
|
||||
chatName.text(getCurrentChatId());
|
||||
|
||||
for (const worldName of world_names) {
|
||||
const option = document.createElement('option');
|
||||
option.value = worldName;
|
||||
option.innerText = worldName;
|
||||
option.selected = selectedName === worldName;
|
||||
worldSelect.append(option);
|
||||
}
|
||||
|
||||
worldSelect.on('change', function () {
|
||||
const worldName = $(this).val();
|
||||
|
||||
if (worldName) {
|
||||
chat_metadata[METADATA_KEY] = worldName;
|
||||
$('.chat_lorebook_button').addClass('world_set');
|
||||
} else {
|
||||
delete chat_metadata[METADATA_KEY];
|
||||
$('.chat_lorebook_button').removeClass('world_set');
|
||||
}
|
||||
|
||||
saveMetadata();
|
||||
});
|
||||
|
||||
callPopup(template, 'text');
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
|
||||
$(document).ready(function () {
|
||||
@ -1997,7 +2065,7 @@ jQuery(() => {
|
||||
});
|
||||
|
||||
$('#world_info_character_strategy').on('change', function () {
|
||||
world_info_character_strategy = $(this).val();
|
||||
world_info_character_strategy = Number($(this).val());
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
@ -2012,19 +2080,19 @@ jQuery(() => {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
$('#world_button').on('click', async function () {
|
||||
$('#world_button').on('click', async function (event) {
|
||||
const chid = $('#set_character_world').data('chid');
|
||||
|
||||
if (chid) {
|
||||
const worldName = characters[chid]?.data?.extensions?.world;
|
||||
const hasEmbed = checkEmbeddedWorld(chid);
|
||||
if (worldName && world_names.includes(worldName)) {
|
||||
if (worldName && world_names.includes(worldName) && !event.shiftKey) {
|
||||
if (!$('#WorldInfo').is(':visible')) {
|
||||
$('#WIDrawerIcon').trigger('click');
|
||||
}
|
||||
const index = world_names.indexOf(worldName);
|
||||
$("#world_editor_select").val(index).trigger('change');
|
||||
} else if (hasEmbed) {
|
||||
} else if (hasEmbed && !event.shiftKey) {
|
||||
await importEmbeddedWorldInfo();
|
||||
saveCharacterDebounced();
|
||||
}
|
||||
@ -2051,6 +2119,8 @@ jQuery(() => {
|
||||
updateEditor(navigation_option.none);
|
||||
})
|
||||
|
||||
$(document).on('click', '.chat_lorebook_button', assignLorebookToChat);
|
||||
|
||||
// Not needed on mobile
|
||||
const deviceInfo = getDeviceInfo();
|
||||
if (deviceInfo && deviceInfo.device.type === 'desktop') {
|
||||
|
@ -1316,7 +1316,7 @@ select option:not(:checked) {
|
||||
}
|
||||
|
||||
.menu_button.disabled {
|
||||
filter: brightness(50%);
|
||||
filter: brightness(75%) grayscale(1);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@ -1911,10 +1911,10 @@ grammarly-extension {
|
||||
font-weight: bold;
|
||||
padding: 5px;
|
||||
margin: 0;
|
||||
height: 32px;
|
||||
height: 26px;
|
||||
filter: grayscale(0.5);
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-size: 17px;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
@ -2307,7 +2307,7 @@ input[type="range"]::-webkit-slider-thumb {
|
||||
|
||||
#char-management-dropdown,
|
||||
#tagInput {
|
||||
height: 32px;
|
||||
height: 26px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -3474,6 +3474,11 @@ a {
|
||||
|
||||
#groupMemberListPopout {
|
||||
padding: 0;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
#groupMemberListPopout #currentGroupMembers {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#groupMemberListPopout #rm_group_members {
|
||||
@ -3564,6 +3569,7 @@ a {
|
||||
padding: 5px;
|
||||
font-size: calc(var(--mainFontSize) * .8);
|
||||
font-weight: bold;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.onboarding {
|
||||
@ -3623,4 +3629,4 @@ a {
|
||||
height: 100vh;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
32
server.js
32
server.js
@ -858,7 +858,7 @@ function unsetFavFlag(char) {
|
||||
|
||||
function readFromV2(char) {
|
||||
if (_.isUndefined(char.data)) {
|
||||
console.warn('Spec v2 data missing');
|
||||
console.warn(`Char ${char['name']} has Spec v2 data missing`);
|
||||
return char;
|
||||
}
|
||||
|
||||
@ -893,12 +893,12 @@ function readFromV2(char) {
|
||||
//console.debug(`Spec v2 extension data missing for field: ${charField}, using default value: ${defaultValue}`);
|
||||
char[charField] = defaultValue;
|
||||
} else {
|
||||
console.debug(`Spec v2 data missing for unknown field: ${charField}`);
|
||||
console.debug(`Char ${char['name']} has Spec v2 data missing for unknown field: ${charField}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!_.isUndefined(char[charField]) && !_.isUndefined(v2Value) && String(char[charField]) !== String(v2Value)) {
|
||||
console.debug(`Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
|
||||
console.debug(`Char ${char['name']} has Spec v2 data mismatch with Spec v1 for field: ${charField}`, char[charField], v2Value);
|
||||
}
|
||||
char[charField] = v2Value;
|
||||
});
|
||||
@ -1811,15 +1811,18 @@ function convertWorldInfoToCharacterBook(name, entries) {
|
||||
}
|
||||
|
||||
function readWorldInfoFile(worldInfoName) {
|
||||
const dummyObject = { entries: {} };
|
||||
|
||||
if (!worldInfoName) {
|
||||
return { entries: {} };
|
||||
return dummyObject;
|
||||
}
|
||||
|
||||
const filename = `${worldInfoName}.json`;
|
||||
const pathToWorldInfo = path.join(DIRECTORIES.worlds, filename);
|
||||
|
||||
if (!fs.existsSync(pathToWorldInfo)) {
|
||||
throw new Error(`World info file ${filename} doesn't exist.`);
|
||||
console.log(`World info file ${filename} doesn't exist.`);
|
||||
return dummyObject;
|
||||
}
|
||||
|
||||
const worldInfoText = fs.readFileSync(pathToWorldInfo, 'utf8');
|
||||
@ -2748,7 +2751,7 @@ app.post("/getstatus_openai", jsonParser, async function (request, response_gets
|
||||
const data = await response.json();
|
||||
response_getstatus_openai.send(data);
|
||||
|
||||
if (request.body.use_openrouter) {
|
||||
if (request.body.use_openrouter && Array.isArray(data?.data)) {
|
||||
let models = [];
|
||||
|
||||
data.data.forEach(model => {
|
||||
@ -2763,8 +2766,14 @@ app.post("/getstatus_openai", jsonParser, async function (request, response_gets
|
||||
|
||||
console.log('Available OpenRouter models:', models);
|
||||
} else {
|
||||
const modelIds = data?.data?.map(x => x.id)?.sort();
|
||||
console.log('Available OpenAI models:', modelIds);
|
||||
const models = data?.data;
|
||||
|
||||
if (Array.isArray(models)) {
|
||||
const modelIds = models.filter(x => x && typeof x === 'object').map(x => x.id).sort();
|
||||
console.log('Available OpenAI models:', modelIds);
|
||||
} else {
|
||||
console.log('OpenAI endpoint did not return a list of models.')
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
@ -2773,7 +2782,12 @@ app.post("/getstatus_openai", jsonParser, async function (request, response_gets
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
response_getstatus_openai.send({ error: true });
|
||||
|
||||
if (!response_getstatus_openai.headersSent) {
|
||||
response_getstatus_openai.send({ error: true });
|
||||
} else {
|
||||
response_getstatus_openai.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -288,7 +288,8 @@ function registerEndpoints(app, jsonParser) {
|
||||
if (!req.body) return res.sendStatus(400);
|
||||
|
||||
let num_tokens = 0;
|
||||
const model = getTokenizerModel(String(req.query.model || ''));
|
||||
const queryModel = String(req.query.model || '');
|
||||
const model = getTokenizerModel(queryModel);
|
||||
|
||||
if (model == 'claude') {
|
||||
num_tokens = countClaudeTokens(claude_tokenizer, req.body);
|
||||
@ -316,6 +317,12 @@ function registerEndpoints(app, jsonParser) {
|
||||
}
|
||||
num_tokens += tokensPadding;
|
||||
|
||||
// NB: Since 2023-10-14, the GPT-3.5 Turbo 0301 model shoves in 7-9 extra tokens to every message.
|
||||
// More details: https://community.openai.com/t/gpt-3-5-turbo-0301-showing-different-behavior-suddenly/431326/14
|
||||
if (queryModel.endsWith('-0301')) {
|
||||
num_tokens += 9;
|
||||
}
|
||||
|
||||
// not needed for cached tokenizers
|
||||
//tokenizer.free();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user