Merge branch 'staging' of https://github.com/SillyTavern/SillyTavern into mancer-api

This commit is contained in:
50h100a
2023-08-02 23:46:03 -04:00
55 changed files with 2118 additions and 5040 deletions

View File

@ -13,6 +13,10 @@ import {
menu_type,
max_context,
saveSettingsDebounced,
active_group,
active_character,
setActiveGroup,
setActiveCharacter,
} from "../script.js";
import {
@ -239,7 +243,6 @@ $("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
$("#character_popup").on("input", function () { countTokensDebounced(); });
//function:
export function RA_CountCharTokens() {
$("#result_info").html("");
//console.log('RA_TC -- starting with this_chid = ' + this_chid);
if (menu_type === "create") { //if new char
function saveFormVariables() {
@ -331,11 +334,21 @@ export function RA_CountCharTokens() {
characterStatsHandler(characters, this_chid);
});
}
//Auto Load Last Charcter -- (fires when active_character is defined and auto_load_chat is true)
/**
* Auto load chat with the last active character or group.
* Fires when active_character is defined and auto_load_chat is true.
* The function first tries to find a character with a specific ID from the global settings.
* If it doesn't exist, it tries to find a group with a specific grid from the global settings.
* If the character list hadn't been loaded yet, it calls itself again after 100ms delay.
* The character or group is selected (clicked) if it is found.
*/
async function RA_autoloadchat() {
if (document.getElementById('CharID0') !== null) {
var charToAutoLoad = document.getElementById('CharID' + LoadLocal('ActiveChar'));
let groupToAutoLoad = document.querySelector(`.group_select[grid="${LoadLocal('ActiveGroup')}"]`);
// active character is the name, we should look it up in the character list and get the id
let active_character_id = Object.keys(characters).find(key => characters[key].avatar === active_character);
var charToAutoLoad = document.getElementById('CharID' + active_character_id);
let groupToAutoLoad = document.querySelector(`.group_select[grid="${active_group}"]`);
if (charToAutoLoad != null) {
$(charToAutoLoad).click();
}
@ -343,7 +356,7 @@ async function RA_autoloadchat() {
$(groupToAutoLoad).click();
}
// if the charcter list hadn't been loaded yet, try again.
// if the character list hadn't been loaded yet, try again.
} else { setTimeout(RA_autoloadchat, 100); }
}
@ -904,16 +917,22 @@ $("document").ready(function () {
$("#rm_button_characters").click(function () { SaveLocal('SelectedNavTab', 'rm_button_characters'); });
// when a char is selected from the list, save them as the auto-load character for next page load
// when a char is selected from the list, save their name as the auto-load character for next page load
$(document).on("click", ".character_select", function () {
SaveLocal('ActiveChar', $(this).attr('chid'));
SaveLocal('ActiveGroup', null);
setActiveCharacter($(this).find('.avatar').attr('title'));
setActiveGroup(null);
saveSettingsDebounced();
});
$(document).on("click", ".group_select", function () {
SaveLocal('ActiveChar', null);
SaveLocal('ActiveGroup', $(this).data('id'));
setActiveCharacter(null);
setActiveGroup($(this).data('id'));
saveSettingsDebounced();
});
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
$('#send_textarea').on('input', function () {
this.style.height = '40px';
@ -1029,11 +1048,10 @@ $("document").ready(function () {
}
if (event.ctrlKey && event.key == "ArrowUp") { //edits last USER message if chatbar is empty and focused
console.debug('got ctrl+uparrow input');
if (
$("#send_textarea").val() === '' &&
chatbarInFocus === true &&
$(".swipe_right:last").css('display') === 'flex' &&
($(".swipe_right:last").css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&
$("#character_popup").css("display") === "none" &&
$("#shadow_select_chat_popup").css("display") === "none"
) {
@ -1041,7 +1059,7 @@ $("document").ready(function () {
const lastIsUserMes = isUserMesList[isUserMesList.length - 1];
const editMes = lastIsUserMes.querySelector('.mes_block .mes_edit');
if (editMes !== null) {
$(editMes).click();
$(editMes).trigger('click');
}
}
}
@ -1062,5 +1080,59 @@ $("document").ready(function () {
}
}
}
if (event.key == "Escape") { //closes various panels
if ($("#curEditTextarea").is(":visible")) {
return
}
if ($("#dialogue_popup").is(":visible")) {
if ($("#dialogue_popup_cancel").is(":visible")) {
$("#dialogue_popup_cancel").trigger('click');
return
} else {
$("#dialogue_popup_ok").trigger('click')
return
}
}
if ($("#select_chat_popup").is(":visible")) {
$("#select_chat_cross").trigger('click');
return
}
if ($("#character_popup").is(":visible")) {
$("#character_cross").trigger('click');
return
}
if ($(".drawer-content")
.not('#WorldInfo')
.not('#left-nav-panel')
.not('#right-nav-panel')
.is(":visible")) {
let visibleDrawerContent = $(".drawer-content:visible")
.not('#WorldInfo')
.not('#left-nav-panel')
.not('#right-nav-panel')
$(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
return
}
if ($("#floatingPrompt").is(":visible")) {
$("#ANClose").trigger('click');
return
}
if ($("#WorldInfo").is(":visible")) {
$("#WIDrawerIcon").trigger('click');
return
}
if ($("#left-nav-panel").is(":visible")) {
$("#leftNavDrawerIcon").trigger('click');
return
}
if ($("#right-nav-panel").is(":visible")) {
$("#rightNavDrawerIcon").trigger('click');
return
}
}
}
});

View File

@ -9,6 +9,7 @@ const MODULE_NAME = 'expressions';
const UPDATE_INTERVAL = 2000;
const FALLBACK_EXPRESSION = 'joy';
const DEFAULT_EXPRESSIONS = [
"live2d",
"admiration",
"amusement",
"anger",
@ -44,6 +45,9 @@ let lastCharacter = undefined;
let lastMessage = null;
let spriteCache = {};
let inApiCall = false;
let live2d_var = false;
let previousSrc = null;
function isVisualNovelMode() {
return Boolean(!isMobile() && power_user.waifuMode && getContext().groupId);
@ -392,6 +396,94 @@ function onExpressionsShowDefaultInput() {
}
}
async function loadLiveChar() {
if (!modules.includes('live2d')) {
console.debug('live2d module is disabled');
return;
}
const context = getContext();
let spriteFolderName = context.name2;
const message = getLastCharacterMessage();
const avatarFileName = getSpriteFolderName(message);
const expressionOverride = extension_settings.expressionOverrides.find((e) =>
e.name == avatarFileName
);
if (expressionOverride && expressionOverride.path) {
spriteFolderName = expressionOverride.path;
}
const live2dPath = `/characters/${encodeURIComponent(spriteFolderName)}/live2d.png`;
try {
const spriteResponse = await fetch(live2dPath);
if (!spriteResponse.ok) {
throw new Error(spriteResponse.statusText);
}
const spriteBlob = await spriteResponse.blob();
const spriteFile = new File([spriteBlob], 'live2d.png', { type: 'image/png' });
const formData = new FormData();
formData.append('file', spriteFile);
const url = new URL(getApiUrl());
url.pathname = '/api/live2d/load';
const loadResponse = await doExtrasFetch(url, {
method: 'POST',
body: formData,
});
if (!loadResponse.ok) {
throw new Error(loadResponse.statusText);
}
const loadResponseText = await loadResponse.text();
console.log(`Load live2d response: ${loadResponseText}`);
} catch (error) {
console.error(`Error loading live2d image: ${live2dPath} - ${error}`);
}
}
function handleImageChange(isChecked) {
const imgElement = document.querySelector('img#expression-image.expression');
if (!imgElement) {
console.log("Cannot find addExpressionImage()");
return;
}
if (isChecked) {
// Method get IP of endpoint
if (imgElement.src !== getApiUrl() + '/api/live2d/result_feed') {
const expressionListItemElement = document.querySelector('#live2d');
const expressionImageElement = expressionListItemElement.querySelector('.expression_list_image');
const newSrc = expressionImageElement.src;
doExtrasFetch(newSrc, {
method: 'HEAD',
})
.then(response => {
if (response.ok) {
imgElement.src = getApiUrl() + '/api/live2d/result_feed';
}
})
.catch(error => {
console.error(error); // Log the error if necessary
});
} else if (previousSrc) {
imgElement.src = previousSrc; // Revert the src to its previous value
}
} else if (previousSrc !== null) {
imgElement.src = previousSrc; // Revert the src to its previous value
}
live2d_var = isChecked;
}
async function moduleWorker() {
const context = getContext();
@ -405,6 +497,23 @@ async function moduleWorker() {
if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) {
removeExpression();
spriteCache = {};
previousSrc = null;
//uncheck live image
let checkbox = document.getElementById('image_type_toggle');
if (checkbox.checked) {
checkbox.click();
}
//clear expression
let imgElement = document.getElementById('expression-image');
imgElement.src = "";
//Load new char
if (live2d_var) {
loadLiveChar();
}
}
const vnMode = isVisualNovelMode();
@ -654,7 +763,6 @@ async function getSpritesList(name) {
try {
const result = await fetch(`/get_sprites?name=${encodeURIComponent(name)}`);
let sprites = result.ok ? (await result.json()) : [];
return sprites;
}
@ -697,114 +805,126 @@ async function getExpressionsList() {
}
async function setExpression(character, expression, force) {
console.debug('entered setExpressions');
await validateImages(character);
const img = $('img.expression');
const prevExpressionSrc = img.attr('src');
const expressionClone = img.clone()
if (live2d_var == false) {
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
console.debug('checking for expression images to show..');
if (sprite) {
console.debug('setting expression from character images folder');
console.debug('entered setExpressions');
await validateImages(character);
const img = $('img.expression');
const prevExpressionSrc = img.attr('src');
const expressionClone = img.clone()
if (force && isVisualNovelMode()) {
const context = getContext();
const group = context.groups.find(x => x.id === context.groupId);
const sprite = (spriteCache[character] && spriteCache[character].find(x => x.label === expression));
console.debug('checking for expression images to show..');
if (sprite) {
console.debug('setting expression from character images folder');
for (const member of group.members) {
const groupMember = context.characters.find(x => x.avatar === member);
if (force && isVisualNovelMode()) {
const context = getContext();
const group = context.groups.find(x => x.id === context.groupId);
if (!groupMember) {
continue;
for (const member of group.members) {
const groupMember = context.characters.find(x => x.avatar === member);
if (!groupMember) {
continue;
}
if (groupMember.name == character) {
await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
return;
}
}
}
//only swap expressions when necessary
if (prevExpressionSrc !== sprite.path
&& !img.hasClass('expression-animating')) {
//clone expression
expressionClone.addClass('expression-clone')
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone.attr('id', '').css({ opacity: 0 });
//add new sprite path to clone src
expressionClone.attr('src', sprite.path);
//add invisible clone to html
expressionClone.appendTo($("#expression-holder"))
if (groupMember.name == character) {
await setImage($(`.expression-holder[data-avatar="${member}"] img`), sprite.path);
return;
const duration = 200;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img.addClass('expression-animating');
// Set the parent container's min width and height before running the transition
const imgWidth = img.width();
const imgHeight = img.height();
const expressionHolder = img.parent();
expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
//position absolute prevent the original from jumping around during transition
img.css('position', 'absolute');
expressionClone.addClass('expression-animating');
//fade the clone in
expressionClone.css({
opacity: 0
}).animate({
opacity: 1
}, duration)
//when finshed fading in clone, fade out the original
.promise().done(function () {
img.animate({
opacity: 0
}, duration);
//remove old expression
img.remove();
//replace ID so it becomes the new 'original' expression for next change
expressionClone.attr('id', 'expression-image');
expressionClone.removeClass('expression-animating');
// Reset the expression holder min height and width
expressionHolder.css('min-width', 100);
expressionHolder.css('min-height', 100);
});
expressionClone.removeClass('expression-clone');
expressionClone.removeClass('default');
expressionClone.off('error');
expressionClone.on('error', function () {
console.debug('Expression image error', sprite.path);
$(this).attr('src', '');
$(this).off('error');
if (force && extension_settings.expressions.showDefault) {
setDefault();
}
});
} else {
if (extension_settings.expressions.showDefault) {
setDefault();
}
}
}
//only swap expressions when necessary
if (prevExpressionSrc !== sprite.path
&& !img.hasClass('expression-animating')) {
//clone expression
expressionClone.addClass('expression-clone')
//make invisible and remove id to prevent double ids
//must be made invisible to start because they share the same Z-index
expressionClone.attr('id', '').css({ opacity: 0 });
//add new sprite path to clone src
expressionClone.attr('src', sprite.path);
//add invisible clone to html
expressionClone.appendTo($("#expression-holder"))
const duration = 200;
//add animation flags to both images
//to prevent multiple expression changes happening simultaneously
img.addClass('expression-animating');
// Set the parent container's min width and height before running the transition
const imgWidth = img.width();
const imgHeight = img.height();
const expressionHolder = img.parent();
expressionHolder.css('min-width', imgWidth > 100 ? imgWidth : 100);
expressionHolder.css('min-height', imgHeight > 100 ? imgHeight : 100);
//position absolute prevent the original from jumping around during transition
img.css('position', 'absolute');
expressionClone.addClass('expression-animating');
//fade the clone in
expressionClone.css({
opacity: 0
}).animate({
opacity: 1
}, duration)
//when finshed fading in clone, fade out the original
.promise().done(function () {
img.animate({
opacity: 0
}, duration);
//remove old expression
img.remove();
//replace ID so it becomes the new 'original' expression for next change
expressionClone.attr('id', 'expression-image');
expressionClone.removeClass('expression-animating');
// Reset the expression holder min height and width
expressionHolder.css('min-width', 100);
expressionHolder.css('min-height', 100);
});
expressionClone.removeClass('expression-clone');
expressionClone.removeClass('default');
expressionClone.off('error');
expressionClone.on('error', function () {
console.debug('Expression image error', sprite.path);
$(this).attr('src', '');
$(this).off('error');
if (force && extension_settings.expressions.showDefault) {
setDefault();
}
});
function setDefault() {
console.debug('setting default');
const defImgUrl = `/img/default-expressions/${expression}.png`;
//console.log(defImgUrl);
img.attr('src', defImgUrl);
img.addClass('default');
}
document.getElementById("expression-holder").style.display = '';
} else {
if (extension_settings.expressions.showDefault) {
setDefault();
if (live2d_var == true) {
// Find the <img> element with id="expression-image" and class="expression"
const imgElement = document.querySelector('img#expression-image.expression');
//console.log("searching");
if (imgElement) {
console.log("setting value");
imgElement.src = getApiUrl() + '/api/live2d/result_feed';
}
}
}
function setDefault() {
console.debug('setting default');
const defImgUrl = `/img/default-expressions/${expression}.png`;
console.log(defImgUrl);
img.attr('src', defImgUrl);
img.addClass('default');
}
document.getElementById("expression-holder").style.display = '';
}
function onClickExpressionImage() {
@ -1052,7 +1172,6 @@ function setExpressionOverrideHtml(forceClear = false) {
$('body').append(element);
}
function addSettings() {
const html = `
<div class="expression_settings">
<div class="inline-drawer">
@ -1060,8 +1179,16 @@ function setExpressionOverrideHtml(forceClear = false) {
<b>Character Expressions</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="offline_mode">
<!-- Toggle button for aituber/static images -->
<div class="toggle_button">
<label class="switch">
<input id="image_type_toggle" type="checkbox">
<span class="slider round"></span>
<label for="image_type_toggle">Image Type - Live2d (extras)</label>
</div>
<div class="offline_mode">
<small>You are in offline mode. Click on the image below to set the expression.</small>
</div>
<div class="flex-container flexnowrap">
@ -1090,6 +1217,7 @@ function setExpressionOverrideHtml(forceClear = false) {
</form>
</div>
`;
$('#extensions_settings').append(html);
$('#expression_override_button').on('click', onClickExpressionOverrideButton);
$('#expressions_show_default').on('input', onExpressionsShowDefaultInput);
@ -1105,6 +1233,15 @@ function setExpressionOverrideHtml(forceClear = false) {
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
$(window).on("resize", updateVisualNovelModeDebounced);
$('.expression_settings').hide();
$('#image_type_toggle').on('change', function () {
const isChecked = this.checked;
if (isChecked) {
loadLiveChar();
}
handleImageChange(isChecked);
});
}
addExpressionImage();
@ -1116,6 +1253,7 @@ function setExpressionOverrideHtml(forceClear = false) {
moduleWorker();
dragElement($("#expression-holder"))
eventSource.on(event_types.CHAT_CHANGED, () => {
//console.log("checked: " + live2d_var);
setExpressionOverrideHtml();
if (isVisualNovelMode()) {

View File

@ -544,6 +544,30 @@ async function onSelectInjectFile(e) {
}
}
// 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.
@ -558,6 +582,10 @@ function doAutoAdjust(chat, maxContext) {
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);

View File

@ -1,4 +1,4 @@
import { getStringHash, debounce, waitUntilCondition } from "../../utils.js";
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from "../../utils.js";
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from "../../extensions.js";
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js";
export { MODULE_NAME };
@ -12,7 +12,21 @@ let lastMessageHash = null;
let lastMessageId = null;
let inApiCall = false;
const formatMemoryValue = (value) => value ? `Summary: ${value.trim()}` : '';
const formatMemoryValue = function (value) {
if (!value) {
return '';
}
value = value.trim();
if (extension_settings.memory.template) {
let result = extension_settings.memory.template.replace(/{{summary}}/i, value);
return substituteParams(result);
} else {
return `Summary: ${value}`;
}
}
const saveChatDebounced = debounce(() => getContext().saveChat(), 2000);
const summary_sources = {
@ -21,6 +35,7 @@ const summary_sources = {
};
const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events that have happened in the chat so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]';
const defaultTemplate = '[Summary: {{summary}}]';
const defaultSettings = {
minLongMemory: 16,
@ -46,6 +61,9 @@ const defaultSettings = {
memoryFrozen: false,
source: summary_sources.extras,
prompt: defaultPrompt,
template: defaultTemplate,
position: extension_prompt_types.AFTER_SCENARIO,
depth: 2,
promptWords: 200,
promptMinWords: 25,
promptMaxWords: 1000,
@ -54,6 +72,10 @@ const defaultSettings = {
promptMinInterval: 1,
promptMaxInterval: 100,
promptIntervalStep: 1,
promptForceWords: 0,
promptForceWordsStep: 100,
promptMinForceWords: 0,
promptMaxForceWords: 10000,
};
function loadSettings() {
@ -61,20 +83,10 @@ function loadSettings() {
Object.assign(extension_settings.memory, defaultSettings);
}
if (extension_settings.memory.source === undefined) {
extension_settings.memory.source = defaultSettings.source;
}
if (extension_settings.memory.prompt === undefined) {
extension_settings.memory.prompt = defaultSettings.prompt;
}
if (extension_settings.memory.promptWords === undefined) {
extension_settings.memory.promptWords = defaultSettings.promptWords;
}
if (extension_settings.memory.promptInterval === undefined) {
extension_settings.memory.promptInterval = defaultSettings.promptInterval;
for (const key of Object.keys(defaultSettings)) {
if (extension_settings.memory[key] === undefined) {
extension_settings.memory[key] = defaultSettings[key];
}
}
$('#summary_source').val(extension_settings.memory.source).trigger('change');
@ -87,6 +99,10 @@ function loadSettings() {
$('#memory_prompt').val(extension_settings.memory.prompt).trigger('input');
$('#memory_prompt_words').val(extension_settings.memory.promptWords).trigger('input');
$('#memory_prompt_interval').val(extension_settings.memory.promptInterval).trigger('input');
$('#memory_template').val(extension_settings.memory.template).trigger('input');
$('#memory_depth').val(extension_settings.memory.depth).trigger('input');
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
}
function onSummarySourceChange(event) {
@ -170,6 +186,31 @@ function onMemoryPromptInput() {
saveSettingsDebounced();
}
function onMemoryTemplateInput() {
const value = $(this).val();
extension_settings.memory.template = value;
saveSettingsDebounced();
}
function onMemoryDepthInput() {
const value = $(this).val();
extension_settings.memory.depth = Number(value);
saveSettingsDebounced();
}
function onMemoryPositionChange(e) {
const value = e.target.value;
extension_settings.memory.position = value;
saveSettingsDebounced();
}
function onMemoryPromptWordsForceInput() {
const value = $(this).val();
extension_settings.memory.promptForceWords = Number(value);
$('#memory_prompt_words_force_value').text(extension_settings.memory.promptForceWords);
saveSettingsDebounced();
}
function saveLastValues() {
const context = getContext();
lastGroupId = context.groupId;
@ -293,7 +334,7 @@ async function summarizeChat(context) {
async function summarizeChatMain(context, force) {
try {
// Wait for the send button to be released
waitUntilCondition(() => is_send_press === false, 10000, 100);
waitUntilCondition(() => is_send_press === false, 30000, 100);
} catch {
console.debug('Timeout waiting for is_send_press');
return;
@ -310,19 +351,30 @@ async function summarizeChatMain(context, force) {
}
let messagesSinceLastSummary = 0;
let wordsSinceLastSummary = 0;
let conditionSatisfied = false;
for (let i = context.chat.length - 1; i >= 0; i--) {
if (context.chat[i].extra && context.chat[i].extra.memory) {
break;
}
messagesSinceLastSummary++;
wordsSinceLastSummary += extractAllWords(context.chat[i].mes).length;
}
if (messagesSinceLastSummary < extension_settings.memory.promptInterval && !force) {
console.debug(`Not enough messages since last summary (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}`);
if (messagesSinceLastSummary >= extension_settings.memory.promptInterval) {
conditionSatisfied = true;
}
if (extension_settings.memory.promptForceWords && wordsSinceLastSummary >= extension_settings.memory.promptForceWords) {
conditionSatisfied = true;
}
if (!conditionSatisfied && !force) {
console.debug(`Summary conditions not satisfied (messages: ${messagesSinceLastSummary}, interval: ${extension_settings.memory.promptInterval}, words: ${wordsSinceLastSummary}, force words: ${extension_settings.memory.promptForceWords})`);
return;
}
console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary);
console.log('Summarizing chat, messages since last summary: ' + messagesSinceLastSummary, 'words since last summary: ' + wordsSinceLastSummary);
const prompt = substituteParams(extension_settings.memory.prompt)
.replace(/{{words}}/gi, extension_settings.memory.promptWords);
@ -458,9 +510,11 @@ function onMemoryContentInput() {
function setMemoryContext(value, saveToMessage) {
const context = getContext();
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_prompt_types.AFTER_SCENARIO);
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth);
$('#memory_contents').val(value);
console.log('Memory set to: ' + value);
console.log('Summary set to: ' + value);
console.debug('Position: ' + extension_settings.memory.position);
console.debug('Depth: ' + extension_settings.memory.depth);
if (saveToMessage && context.chat.length) {
const idx = context.chat.length - 2;
@ -496,6 +550,21 @@ jQuery(function () {
<input id="memory_restore" class="menu_button" type="button" value="Restore previous state" />
<label for="memory_frozen"><input id="memory_frozen" type="checkbox" />Pause summarization</label>
</div>
<div class="memory_template">
<label for="memory_template">Injection template:</label>
<textarea id="memory_template" class="text_pole textarea_compact" rows="1" placeholder="Use {{summary}} macro to specify the position of summarized text."></textarea>
</div>
<label for="memory_position">Injection position:</label>
<div class="radio_group">
<label>
<input type="radio" name="memory_position" value="0" />
After scenario
</label>
<label>
<input type="radio" name="memory_position" value="1" />
In-chat @ Depth <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="99" />
</label>
</div>
<div data-source="main" class="memory_contents_controls">
</div>
<div data-source="main">
@ -511,6 +580,9 @@ jQuery(function () {
<input id="memory_prompt_words" type="range" value="${defaultSettings.promptWords}" min="${defaultSettings.promptMinWords}" max="${defaultSettings.promptMaxWords}" step="${defaultSettings.promptWordsStep}" />
<label for="memory_prompt_interval">Update interval (<span id="memory_prompt_interval_value"></span> messages)</label>
<input id="memory_prompt_interval" type="range" value="${defaultSettings.promptInterval}" min="${defaultSettings.promptMinInterval}" max="${defaultSettings.promptMaxInterval}" step="${defaultSettings.promptIntervalStep}" />
<label for="memory_prompt_words_force">Force update after (<span id="memory_prompt_words_force_value"></span> words)</label>
<small>Set to 0 to disable</small>
<input id="memory_prompt_words_force" type="range" value="${defaultSettings.promptForceWords}" min="${defaultSettings.promptMinForceWords}" max="${defaultSettings.promptMaxForceWords}" step="${defaultSettings.promptForceWordsStep}" />
</div>
<div data-source="extras">
<label for="memory_short_length">Chat to Summarize buffer length (<span id="memory_short_length_tokens"></span> tokens)</label>
@ -542,6 +614,10 @@ jQuery(function () {
$('#memory_prompt_interval').on('input', onMemoryPromptIntervalInput);
$('#memory_prompt').on('input', onMemoryPromptInput);
$('#memory_force_summarize').on('click', forceSummarizeChat);
$('#memory_template').on('input', onMemoryTemplateInput);
$('#memory_depth').on('input', onMemoryDepthInput);
$('input[name="memory_position"]').on('change', onMemoryPositionChange);
$('#memory_prompt_words_force').on('input', onMemoryPromptWordsForceInput);
}
addExtensionControls();

View File

@ -1,34 +1,26 @@
#memory_settings {
display: flex;
flex-direction: column;
}
#memory_settings textarea {
font-size: calc(var(--mainFontSize) * 0.9);
line-height: 1.2;
}
#memory_settings input[type="range"] {
margin-bottom: 20px;
}
#memory_settings label {
margin-bottom: 10px;
}
label[for="memory_frozen"] {
display: flex;
align-items: center;
margin: 0 !important;
}
label[for="memory_frozen"] input {
margin-right: 10px;
}
.memory_contents_controls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
#memory_settings {
display: flex;
flex-direction: column;
}
#memory_settings textarea {
font-size: calc(var(--mainFontSize) * 0.9);
line-height: 1.2;
}
label[for="memory_frozen"] {
display: flex;
align-items: center;
margin: 0 !important;
}
label[for="memory_frozen"] input {
margin-right: 10px;
}
.memory_contents_controls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

View File

@ -1,4 +1,4 @@
import { chat_metadata, callPopup, saveSettingsDebounced } from "../../../script.js";
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } from "../../../script.js";
import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js";
import {
substituteParams,
@ -7,15 +7,19 @@ import {
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 globalObjective = ""
let taskTree = null
let globalTasks = []
let currentChatId = ""
let currentObjective = null
let currentTask = null
let checkCounter = 0
let lastMessageWasSwipe = false
const defaultPrompts = {
@ -47,57 +51,59 @@ let objectivePrompts = defaultPrompts
//# Task Management #//
//###############################//
// Accepts optional index. Defaults to adding to end of list.
function addTask(description, index = null) {
index = index != null ? index: index = globalTasks.length
globalTasks.splice(index, 0, new ObjectiveTask(
{description: description}
))
saveState()
}
// Return the task and index or throw an error
function getTaskById(taskId){
if (taskId == null) {
throw `Null task id`
}
const index = globalTasks.findIndex((task) => task.id === taskId);
if (index !== -1) {
return { task: globalTasks[index], index: index };
} else {
throw `Cannot find task with ${taskId}`
}
return getTaskByIdRecurse(taskId, taskTree)
}
function deleteTask(taskId){
const { task, index } = getTaskById(taskId)
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;
}
globalTasks.splice(index, 1)
setCurrentTask()
updateUiTaskList()
function substituteParamsPrompts(content) {
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)
}
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 = substituteParams(objectivePrompts.createTask.replace(/{{objective}}/gi, globalObjective));
const prompt = substituteParamsPrompts(objectivePrompts.createTask);
console.log(`Generating tasks for objective with prompt`)
toastr.info('Generating tasks for objective', 'Please wait...');
const taskResponse = await generateQuietPrompt(prompt)
// Clear all existing global tasks when generating
globalTasks = []
// 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) {
addTask(task.replace(numberedListPattern,"").trim())
currentObjective.addTask(task.replace(numberedListPattern,"").trim())
}
}
updateUiTaskList()
console.info(`Response for Objective: '${globalObjective}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${globalTasks.length} tasks`, 'Done!');
updateUiTaskList();
setCurrentTask();
console.info(`Response for Objective: '${taskTree.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `)
toastr.success(`Generated ${taskTree.length} tasks`, 'Done!');
}
// Call Quiet Generate to check if a task is completed
@ -106,14 +112,28 @@ async function checkTaskCompleted() {
if (jQuery.isEmptyObject(currentTask)) {
return
}
checkCounter = $('#objective-check-frequency').val()
const prompt = substituteParams(objectivePrompts.checkTaskCompleted.replace(/{{task}}/gi, currentTask.description));
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);
const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
// Check response if task complete
if (taskResponse.includes("true")) {
console.info(`Character determined task '${JSON.stringify(currentTask.toSaveState())} is completed.`)
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}`)
@ -122,29 +142,57 @@ async function checkTaskCompleted() {
}
}
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) {
const context = getContext();
// TODO: Should probably null this rather than set empty object
currentTask = {};
// Set current task to either the next incomplete task, or the index
// Find the task, either next incomplete, or by provided taskId
if (taskId === null) {
currentTask = globalTasks.find(task => !task.completed) || {};
currentTask = getNextIncompleteTaskRecurse(taskTree) || {};
} else {
const { _, index } = getTaskById(taskId)
currentTask = globalTasks[index];
currentTask = getTaskById(taskId);
}
// Get the task description and add to extension prompt
// Don't just check for a current task, check if it has data
const description = currentTask.description || null;
// Now update the extension prompt
if (description) {
const extensionPromptText = objectivePrompts.currentTask.replace(/{{task}}/gi, description);
$('.objective-task').css({'border-color':'','border-width':''}) // Clear highlights
currentTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'}); // Highlight current task
const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask);
// 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 {
@ -155,22 +203,26 @@ function setCurrentTask(taskId = null) {
saveState();
}
let taskIdCounter = 0
function getNextTaskId(){
// Make sure id does not exist
while (globalTasks.find(task => task.id == taskIdCounter) != undefined) {
taskIdCounter += 1
function getHighestTaskIdRecurse(task) {
let nextId = task.id;
for (const childTask of task.children) {
const childId = getHighestTaskIdRecurse(childTask);
if (childId > nextId) {
nextId = childId;
}
}
const nextId = taskIdCounter
console.log(`TaskID assigned: ${nextId}`)
taskIdCounter += 1
return nextId
return nextId;
}
//###############################//
//# Task Class #//
//###############################//
class ObjectiveTask {
id
description
completed
parent
parentId
children
// UI Elements
@ -180,25 +232,67 @@ class ObjectiveTask {
deleteTaskButton
addTaskButton
constructor ({id=undefined, description, completed=false, parent=null}) {
constructor ({id=undefined, description, completed=false, parentId=""}) {
this.description = description
this.parent = parent
this.parentId = parentId
this.children = []
this.completed = completed
// Generate a new ID if none specified
if (id==undefined){
this.id = getNextTaskId()
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()
}
@ -211,6 +305,7 @@ class ObjectiveTask {
<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>
`;
@ -221,6 +316,15 @@ class ObjectiveTask {
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);
@ -229,49 +333,197 @@ class ObjectiveTask {
$(`#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(){
deleteTask(this.id);
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.children.splice(index, 1)
updateUiTaskList()
setCurrentTask()
}
onAddClick(){
const {_, index} = getTaskById(this.id)
addTask("", index + 1);
setCurrentTask();
const index = this.getIndex()
const parent = getTaskById(this.parentId)
parent.addTask("", index + 1);
updateUiTaskList();
setCurrentTask();
}
toSaveState() {
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,
"parent": this.parent,
"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 = {
objective: "",
tasks: [],
currentObjectiveId: null,
taskTree: null,
chatDepth: 2,
checkFrequency: 3,
hideTasks: false,
@ -280,6 +532,7 @@ const defaultSettings = {
// Convenient single call. Not much at the moment.
function resetState() {
lastMessageWasSwipe = false
loadSettings();
}
@ -291,12 +544,9 @@ function saveState() {
currentChatId = context.chatId
}
// Convert globalTasks for saving
const tasks = globalTasks.map(task => {return task.toSaveState()})
chat_metadata['objective'] = {
objective: globalObjective,
tasks: tasks,
currentObjectiveId: currentObjective.id,
taskTree: taskTree.toSaveStateRecurse(),
checkFrequency: $('#objective-check-frequency').val(),
chatDepth: $('#objective-chat-depth').val(),
hideTasks: $('#objective-hide-tasks').prop('checked'),
@ -309,9 +559,9 @@ function saveState() {
// Dump core state
function debugObjectiveExtension() {
console.log(JSON.stringify({
"currentTask": currentTask.description,
"globalObjective": globalObjective,
"globalTasks": globalTasks.map(v => {return v.toSaveState()}),
"currentTask": currentTask,
"currentObjective": currentObjective,
"taskTree": taskTree.toSaveStateRecurse(),
"chat_metadata": chat_metadata['objective'],
"extension_settings": extension_settings['objective'],
"prompts": objectivePrompts
@ -324,9 +574,20 @@ window.debugObjectiveExtension = debugObjectiveExtension
// Populate UI task list
function updateUiTaskList() {
$('#objective-tasks').empty()
// Show tasks if there are any
if (globalTasks.length > 0){
for (const task of globalTasks) {
// 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 {
@ -335,17 +596,21 @@ function updateUiTaskList() {
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
`)
$("#objective-task-add-first").on('click', () => {
addTask("")
currentObjective.addTask("")
setCurrentTask()
updateUiTaskList()
})
}
}
function onParentClick() {
currentObjective = getTaskById(currentObjective.parentId)
updateUiTaskList()
setCurrentTask()
}
// Trigger creation of new tasks with given objective.
async function onGenerateObjectiveClick() {
globalObjective = $('#objective-text').val()
await generateTasks()
saveState()
}
@ -356,6 +621,13 @@ function onChatDepthInput() {
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()
@ -368,114 +640,28 @@ function onHideTasksInput() {
saveState()
}
function onEditPromptClick() {
let popupText = ''
popupText += `
<div class="objective_prompt_modal">
<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">
<input id="objective-custom-prompt-name" style="flex-grow:2" type="text" class="flex1 heightFitContent text_pole widthNatural" maxlength="250" placeholder="Custom Prompt Name">
<input id="objective-custom-prompt-save" style="flex-grow:1" class="menu_button" type="submit" value="Save Prompt" />
</div>
<div class="objective_prompt_block">
<label for="objective-prompt-load">Load Prompt</label>
<select id="objective-prompt-load"><select>
<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()
function loadTaskChildrenRecurse(savedTask) {
let tempTaskTree = new ObjectiveTask({
id: savedTask.id,
description: savedTask.description,
completed: savedTask.completed,
parentId: savedTask.parentId,
})
$('#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 save
$('#objective-custom-prompt-save').on('click', () => {
addCustomPrompt($('#objective-custom-prompt-name').val(), objectivePrompts)
})
// Handle delete
$('#objective-custom-prompt-delete').on('click', () => {
const optionSelected = $("#objective-prompt-load").find(':selected').val()
deleteCustomPrompt(optionSelected)
})
// Handle load
$('#objective-prompt-load').on('change', loadCustomPrompt)
}
function addCustomPrompt(customPromptName, customPrompts) {
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], customPrompts)
saveSettingsDebounced()
populateCustomPrompts()
}
function deleteCustomPrompt(customPromptName){
if (customPromptName == "default"){
toastr.error("Cannot delete default prompt")
return
}
delete extension_settings.objective.customPrompts[customPromptName]
saveSettingsDebounced()
populateCustomPrompts()
loadCustomPrompt()
}
function loadCustomPrompt(){
const optionSelected = $("#objective-prompt-load").find(':selected').val()
console.log(optionSelected)
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-prompt-load').empty()
for (const customPromptName in extension_settings.objective.customPrompts){
const option = document.createElement('option');
option.innerText = customPromptName;
option.value = customPromptName;
option.selected = customPromptName
$('#objective-prompt-load').append(option)
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}})
@ -488,6 +674,7 @@ function loadSettings() {
// 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];
}
@ -496,21 +683,47 @@ function loadSettings() {
Object.assign(chat_metadata, { objective: defaultSettings });
}
// Update globals
globalObjective = chat_metadata['objective'].objective
globalTasks = chat_metadata['objective'].tasks.map(task => {
return new ObjectiveTask({
id: task.id,
description: task.description,
completed: task.completed,
parent: task.parent,
})
});
// 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(globalObjective)
$("#objective-text").text(taskTree.description)
updateUiTaskList()
$('#objective-chat-depth').val(chat_metadata['objective'].chatDepth)
$('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency)
@ -533,38 +746,44 @@ jQuery(() => {
<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>
<b>Objective</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</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 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>
<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 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>
<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);
@ -573,14 +792,20 @@ jQuery(() => {
$("#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) {
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
lastMessageWasSwipe = false
return
}
if ($("#objective-check-frequency").val() > 0) {

View File

@ -1,19 +1,48 @@
import { saveSettingsDebounced } from "../../../script.js";
import { saveSettingsDebounced, callPopup, getRequestHeaders } from "../../../script.js";
import { getContext, extension_settings } from "../../extensions.js";
import { initScrollHeight, resetScrollHeight } from "../../utils.js";
export { MODULE_NAME };
const MODULE_NAME = 'quick-reply';
const UPDATE_INTERVAL = 1000;
let presets = [];
let selected_preset = '';
const defaultSettings = {
quickReplyEnabled: false,
quickReplyEnabled: true,
numberOfSlots: 5,
quickReplySlots: [],
}
async function loadSettings() {
//method from worldinfo
async function updateQuickReplyPresetList() {
var result = await fetch("/getsettings", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({}),
});
if (result.ok) {
var data = await result.json();
presets = data.quickReplyPresets?.length ? data.quickReplyPresets : [];
console.log(presets)
$("#quickReplyPresets").find('option[value!=""]').remove();
if (presets !== undefined) {
presets.forEach((item, i) => {
$("#quickReplyPresets").append(`<option value='${item.name}'${selected_preset.includes(item.name) ? ' selected' : ''}>${item.name}</option>`);
});
}
}
}
async function loadSettings(type) {
if (type === 'init') {
await updateQuickReplyPresetList()
}
if (Object.keys(extension_settings.quickReply).length === 0) {
Object.assign(extension_settings.quickReply, defaultSettings);
}
@ -111,6 +140,51 @@ async function moduleWorker() {
if (extension_settings.quickReply.quickReplyEnabled === true) {
$('#quickReplyBar').toggle(getContext().onlineStatus !== 'no_connection');
}
if (extension_settings.quickReply.selectedPreset) {
selected_preset = extension_settings.quickReply.selectedPreset;
}
}
async function saveQuickReplyPreset() {
const name = await callPopup('Enter a name for the Quick Reply Preset:', 'input');
if (!name) {
return;
}
const quickReplyPreset = {
name: name,
quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled,
quickReplySlots: extension_settings.quickReply.quickReplySlots,
numberOfSlots: extension_settings.quickReply.numberOfSlots,
selectedPreset: name
}
const response = await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(quickReplyPreset)
});
if (response.ok) {
const quickReplyPresetIndex = presets.findIndex(x => x.name == name);
if (quickReplyPresetIndex == -1) {
presets.push(quickReplyPreset);
const option = document.createElement('option');
option.selected = true;
option.value = name;
option.innerText = name;
$('#quickReplyPresets').append(option);
}
else {
presets[quickReplyPresetIndex] = quickReplyPreset;
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
}
saveSettingsDebounced();
} else {
toastr.warning('Failed to save Quick Reply Preset.')
}
}
async function onQuickReplyNumberOfSlotsInput() {
@ -178,6 +252,27 @@ function generateQuickReplyElements() {
});
}
async function applyQuickReplyPreset(name) {
const quickReplyPreset = presets.find(x => x.name == name);
if (!quickReplyPreset) {
console.log(`error, QR preset '${name}' not found`)
return;
}
extension_settings.quickReply = quickReplyPreset;
extension_settings.quickReply.selectedPreset = name;
saveSettingsDebounced()
loadSettings('init')
addQuickReplyBar();
moduleWorker();
$(`#quickReplyPresets option[value="${name}"]`).attr('selected', true);
console.debug('QR Preset applied: ' + name);
//loadMovingUIState()
}
jQuery(async () => {
moduleWorker();
@ -190,11 +285,18 @@ jQuery(async () => {
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label class="checkbox_label marginBot10">
<input id="quickReplyEnabled" type="checkbox" />
Enable Quick Replies
</label>
<label for="quickReplyNumberOfSlots">Number of slots:</label>
<div class="flex-container ">
<label class="checkbox_label marginBot10 wide100p flexnowrap">
<input id="quickReplyEnabled" type="checkbox" />
Enable Quick Replies
</label>
<div class="flex-container flexnowrap wide100p">
<select id="quickReplyPresets" name="quickreply-preset">
</select>
<i id="quickReplyPresetSaveButton" class="fa-solid fa-save"></i>
</div>
<label for="quickReplyNumberOfSlots">Number of slots:</label>
</div>
<div class="flex-container flexGap5 flexnowrap">
<input id="quickReplyNumberOfSlots" class="text_pole" type="number" min="1" max="100" value="" />
<div class="menu_button menu_button_icon" id="quickReplyNumberOfSlotsApply">
@ -212,8 +314,17 @@ jQuery(async () => {
$('#quickReplyEnabled').on('input', onQuickReplyEnabledInput);
$('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput);
$("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset);
await loadSettings();
$("#quickReplyPresets").on('change', async function () {
const quickReplyPresetSelected = $(this).find(':selected').val();
extension_settings.quickReplyPreset = quickReplyPresetSelected;
applyQuickReplyPreset(quickReplyPresetSelected);
saveSettingsDebounced();
});
await loadSettings('init');
addQuickReplyBar();
});

View File

@ -8,16 +8,21 @@ import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper
import { VoskSttProvider } from './vosk.js'
import { WhisperSttProvider } from './whisper.js'
import { BrowserSttProvider } from './browser.js'
import { StreamingSttProvider } from './streaming.js'
export { MODULE_NAME };
const MODULE_NAME = 'Speech Recognition';
const DEBUG_PREFIX = "<Speech Recognition module> "
const UPDATE_INTERVAL = 100;
let inApiCall = false;
let sttProviders = {
None: null,
Browser: BrowserSttProvider,
Whisper: WhisperSttProvider,
Vosk: VoskSttProvider,
Streaming: StreamingSttProvider,
}
let sttProvider = null
@ -27,6 +32,82 @@ let audioRecording = false
const constraints = { audio: { sampleSize: 16, channelCount: 1, sampleRate: 16000 } };
let audioChunks = [];
async function moduleWorker() {
if (sttProviderName != "Streaming") {
return;
}
// API is busy
if (inApiCall) {
return;
}
try {
inApiCall = true;
const userMessageOriginal = await sttProvider.getUserMessage();
let userMessageFormatted = userMessageOriginal.trim();
if (userMessageFormatted.length > 0)
{
console.debug(DEBUG_PREFIX+"recorded transcript: \""+userMessageFormatted+"\"");
let userMessageLower = userMessageFormatted.toLowerCase();
// remove punctuation
let userMessageRaw = userMessageLower.replace(/[^\w\s\']|_/g, "").replace(/\s+/g, " ");
console.debug(DEBUG_PREFIX+"raw transcript:",userMessageRaw);
// Detect trigger words
let messageStart = -1;
if (extension_settings.speech_recognition.Streaming.triggerWordsEnabled) {
for (const triggerWord of extension_settings.speech_recognition.Streaming.triggerWords) {
const triggerPos = userMessageRaw.indexOf(triggerWord.toLowerCase());
// Trigger word not found or not starting message and just a substring
if (triggerPos == -1){ // | (triggerPos > 0 & userMessageFormatted[triggerPos-1] != " ")) {
console.debug(DEBUG_PREFIX+"trigger word not found: ", triggerWord);
}
else {
console.debug(DEBUG_PREFIX+"Found trigger word: ", triggerWord, " at index ", triggerPos);
if (triggerPos < messageStart | messageStart == -1) { // & (triggerPos + triggerWord.length) < userMessageFormatted.length)) {
messageStart = triggerPos; // + triggerWord.length + 1;
}
}
}
} else {
messageStart = 0;
}
if (messageStart == -1) {
console.debug(DEBUG_PREFIX+"message ignored, no trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"");
if (extension_settings.speech_recognition.Streaming.debug) {
toastr.info(
"No trigger word preceding a message. Voice transcript: \""+ userMessageOriginal +"\"",
DEBUG_PREFIX+"message ignored.",
{ timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true },
);
}
}
else{
userMessageFormatted = userMessageFormatted.substring(messageStart);
processTranscript(userMessageFormatted);
}
}
else
{
console.debug(DEBUG_PREFIX+"Received empty transcript, ignored");
}
}
catch (error) {
console.debug(error);
}
finally {
inApiCall = false;
}
}
async function processTranscript(transcript) {
try {
const transcriptOriginal = transcript;
@ -198,13 +279,21 @@ function loadSttProvider(provider) {
if (sttProviderName == "Browser") {
sttProvider.processTranscriptFunction = processTranscript;
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
}
else {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
loadNavigatorAudioRecording();
$("#microphone_button").show();
}
if (sttProviderName == "Vosk" | sttProviderName == "Whisper") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
loadNavigatorAudioRecording();
$("#microphone_button").show();
}
if (sttProviderName == "Streaming") {
sttProvider.loadSettings(extension_settings.speech_recognition[sttProviderName]);
$("#microphone_button").off('click');
$("#microphone_button").hide();
}
}
function onSttProviderChange() {
@ -231,7 +320,7 @@ const defaultSettings = {
messageMode: "append",
messageMappingText: "",
messageMapping: [],
messageMappingEnabled: false
messageMappingEnabled: false,
}
function loadSettings() {
@ -344,8 +433,7 @@ $(document).ready(function () {
addExtensionControls(); // No init dependencies
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadSttProvider(extension_settings.speech_recognition.currentProvider); // No dependencies
//const wrapper = new ModuleWorkerWrapper(moduleWorker);
//setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
//moduleWorker();
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
moduleWorker();
})

View File

@ -0,0 +1,102 @@
import { getApiUrl, doExtrasFetch, modules } from "../../extensions.js";
export { StreamingSttProvider }
const DEBUG_PREFIX = "<Speech Recognition module (streaming)> "
class StreamingSttProvider {
//########//
// Config //
//########//
settings
defaultSettings = {
triggerWordsText: "",
triggerWords : [],
triggerWordsEnabled : false,
debug : false,
}
get settingsHtml() {
let html = '\
<div id="speech_recognition_streaming_trigger_words_div">\
<span>Trigger words</span>\
<textarea id="speech_recognition_streaming_trigger_words" class="text_pole textarea_compact" type="text" rows="4" placeholder="Enter comma separated words that triggers new message, example:\nhey, hey aqua, record, listen"></textarea>\
<label class="checkbox_label" for="speech_recognition_streaming_trigger_words_enabled">\
<input type="checkbox" id="speech_recognition_streaming_trigger_words_enabled" name="speech_recognition_trigger_words_enabled">\
<small>Enable trigger words</small>\
</label>\
<label class="checkbox_label" for="speech_recognition_streaming_debug">\
<input type="checkbox" id="speech_recognition_streaming_debug" name="speech_recognition_streaming_debug">\
<small>Enable debug pop ups</small>\
</label>\
</div>\
'
return html
}
onSettingsChange() {
this.settings.triggerWordsText = $('#speech_recognition_streaming_trigger_words').val();
let array = $('#speech_recognition_streaming_trigger_words').val().split(",");
array = array.map(element => {return element.trim().toLowerCase();});
array = array.filter((str) => str !== '');
this.settings.triggerWords = array;
this.settings.triggerWordsEnabled = $("#speech_recognition_streaming_trigger_words_enabled").is(':checked');
this.settings.debug = $("#speech_recognition_streaming_debug").is(':checked');
console.debug(DEBUG_PREFIX+" Updated settings: ", this.settings);
this.loadSettings(this.settings);
}
loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.debug(DEBUG_PREFIX+"Using default Whisper STT extension settings")
}
// Only accept keys defined in defaultSettings
this.settings = this.defaultSettings
for (const key in settings){
if (key in this.settings){
this.settings[key] = settings[key]
} else {
throw `Invalid setting passed to STT extension: ${key}`
}
}
$("#speech_recognition_streaming_trigger_words").val(this.settings.triggerWordsText);
$("#speech_recognition_streaming_trigger_words_enabled").prop('checked',this.settings.triggerWordsEnabled);
$("#speech_recognition_streaming_debug").prop('checked',this.settings.debug);
console.debug(DEBUG_PREFIX+"streaming STT settings loaded")
}
async getUserMessage() {
// Return if module is not loaded
if (!modules.includes('streaming-stt')) {
console.debug(DEBUG_PREFIX+"Module streaming-stt must be activated in Sillytavern Extras for streaming user voice.")
return "";
}
const url = new URL(getApiUrl());
url.pathname = '/api/speech-recognition/streaming/record-and-transcript';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: "" }),
});
if (!apiResult.ok) {
toastr.error(apiResult.statusText, DEBUG_PREFIX+'STT Generation Failed (streaming)', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
}
const data = await apiResult.json();
return data.transcript;
}
}

View File

@ -1,5 +1,5 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, isMultigenEnabled, is_send_press, saveSettingsDebounced } from '../../../script.js'
import { ModuleWorkerWrapper, extension_settings, getContext } from '../../extensions.js'
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext } from '../../extensions.js'
import { escapeRegex, getStringHash } from '../../utils.js'
import { EdgeTtsProvider } from './edge.js'
import { ElevenLabsTtsProvider } from './elevenlabs.js'
@ -7,14 +7,13 @@ import { SileroTtsProvider } from './silerotts.js'
import { CoquiTtsProvider } from './coquitts.js'
import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js'
import { isMobile } from '../../RossAscends-mods.js'
import { power_user } from '../../power-user.js'
const UPDATE_INTERVAL = 1000
let voiceMap = {} // {charName:voiceid, charName2:voiceid2}
let audioControl
let storedvalue = false;
let lastCharacterId = null
let lastGroupId = null
let lastChatId = null
@ -164,6 +163,20 @@ async function moduleWorker() {
ttsJobQueue.push(message)
}
function talkingAnimation(switchValue) {
const apiUrl = getApiUrl();
const animationType = switchValue ? "start" : "stop";
if (switchValue !== storedvalue) {
try {
console.log(animationType + " Talking Animation");
doExtrasFetch(`${apiUrl}/api/live2d/${animationType}_talking`);
storedvalue = switchValue; // Update the storedvalue to the current switchValue
} catch (error) {
// Handle the error here or simply ignore it to prevent logging
}
}
}
function resetTtsPlayback() {
// Stop system TTS utterance
@ -291,8 +304,10 @@ function updateUiAudioPlayState() {
// Give user feedback that TTS is active by setting the stop icon if processing or playing
if (!audioElement.paused || isTtsProcessing()) {
img = 'fa-solid fa-stop-circle extensionsMenuExtensionButton'
talkingAnimation(true)
} else {
img = 'fa-solid fa-circle-play extensionsMenuExtensionButton'
talkingAnimation(false)
}
$('#tts_media_control').attr('class', img);
} else {
@ -354,6 +369,7 @@ async function processAudioJobQueue() {
audioQueueProcessorReady = false
currentAudioJob = audioJobQueue.pop()
playAudioData(currentAudioJob)
talkingAnimation(true)
} catch (error) {
console.error(error)
audioQueueProcessorReady = true

View File

@ -5,6 +5,7 @@ import {
delay,
isDataURL,
createThumbnail,
extractAllWords,
} from './utils.js';
import { RA_CountCharTokens, humanizedDateTime, dragElement } from "./RossAscends-mods.js";
import { sortCharactersList, sortGroupMembers, loadMovingUIState } from './power-user.js';
@ -782,19 +783,6 @@ function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, i
return memberIds;
}
function extractAllWords(value) {
const words = [];
if (!value) {
return words;
}
const matches = value.matchAll(/\b\w+\b/gim);
for (let match of matches) {
words.push(match[0].toLowerCase());
}
return words;
}
async function deleteGroup(id) {

View File

@ -1,7 +1,10 @@
import {
getRequestHeaders,
saveSettingsDebounced,
getStoppingStrings,
getTextTokens
} from "../script.js";
import { tokenizers } from "./power-user.js";
export {
nai_settings,
@ -62,6 +65,8 @@ function loadNovelPreset(preset) {
nai_settings.top_a = preset.top_a;
nai_settings.typical_p = preset.typical_p;
nai_settings.min_length = preset.min_length;
nai_settings.cfg_scale = preset.cfg_scale;
nai_settings.phrase_rep_pen = preset.phrase_rep_pen;
loadNovelSettingsUi(nai_settings);
}
@ -84,14 +89,42 @@ function loadNovelSettings(settings) {
nai_settings.top_a = settings.top_a;
nai_settings.typical_p = settings.typical_p;
nai_settings.min_length = settings.min_length;
nai_settings.phrase_rep_pen = settings.phrase_rep_pen;
nai_settings.cfg_scale = settings.cfg_scale;
nai_settings.streaming_novel = !!settings.streaming_novel;
loadNovelSettingsUi(nai_settings);
}
// reload the preset to migrate any new settings
for (const key of Object.keys(nai_settings)) {
if (typeof nai_settings[key] === 'number' && Number.isNaN(nai_settings[key])) {
$("#settings_perset_novel").trigger("change");
}
const phraseRepPenStrings = [
null,
"very_light",
"light",
"medium",
"aggressive",
"very_aggressive"
]
function getPhraseRepPenString(phraseRepPenCounter) {
if (phraseRepPenCounter < 1 || phraseRepPenCounter > 5) {
return null;
} else {
return phraseRepPenStrings[phraseRepPenCounter];
}
}
function getPhraseRepPenCounter(phraseRepPenString) {
if (phraseRepPenString === phraseRepPenStrings[1]) {
return 1;
} else if (phraseRepPenString === phraseRepPenStrings[2]) {
return 2;
} else if (phraseRepPenString === phraseRepPenStrings[3]) {
return 3;
} else if (phraseRepPenString === phraseRepPenStrings[4]) {
return 4;
} else if (phraseRepPenString === phraseRepPenStrings[5]) {
return 5;
} else {
return 0;
}
}
@ -118,6 +151,10 @@ function loadNovelSettingsUi(ui_settings) {
$("#top_a_counter_novel").text(Number(ui_settings.top_a).toFixed(2));
$("#typical_p_novel").val(ui_settings.typical_p);
$("#typical_p_counter_novel").text(Number(ui_settings.typical_p).toFixed(2));
$("#cfg_scale_novel").val(ui_settings.cfg_scale);
$("#cfg_scale_counter_novel").text(Number(ui_settings.cfg_scale).toFixed(2));
$("#phrase_rep_pen_novel").val(getPhraseRepPenCounter(ui_settings.phrase_rep_pen));
$("#phrase_rep_pen_counter_novel").text(getPhraseRepPenCounter(ui_settings.phrase_rep_pen));
$("#min_length_novel").val(ui_settings.min_length);
$("#min_length_counter_novel").text(Number(ui_settings.min_length).toFixed(0));
@ -191,6 +228,18 @@ const sliders = [
format: (val) => Number(val).toFixed(2),
setValue: (val) => { nai_settings.typical_p = Number(val).toFixed(2); },
},
{
sliderId: "#cfg_scale_novel",
counterId: "#cfg_scale_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.cfg_scale = Number(val).toFixed(2); },
},
{
sliderId: "#phrase_rep_pen_novel",
counterId: "#phrase_rep_pen_counter_novel",
format: (val) => `${val}`,
setValue: (val) => { nai_settings.phrase_rep_pen = getPhraseRepPenString(Number(val).toFixed(0)); },
},
{
sliderId: "#min_length_novel",
counterId: "#min_length_counter_novel",
@ -199,7 +248,17 @@ const sliders = [
},
];
export function getNovelGenerationData(finalPromt, this_settings, this_amount_gen) {
export function getNovelGenerationData(finalPromt, this_settings, this_amount_gen, isImpersonate) {
const clio = nai_settings.model_novel.includes('clio');
const kayra = nai_settings.model_novel.includes('kayra');
const isNewModel = clio || kayra;
const tokenizerType = kayra ? tokenizers.NERD2 : (clio ? tokenizers.NERD : tokenizers.NONE);
const stopSequences = (tokenizerType !== tokenizers.NONE)
? getStoppingStrings(isImpersonate, false)
.map(t => getTextTokens(tokenizerType, t))
: undefined;
return {
"input": finalPromt,
"model": nai_settings.model_novel,
@ -217,13 +276,17 @@ export function getNovelGenerationData(finalPromt, this_settings, this_amount_ge
"top_p": parseFloat(nai_settings.top_p),
"top_k": parseInt(nai_settings.top_k),
"typical_p": parseFloat(nai_settings.typical_p),
"cfg_scale": parseFloat(nai_settings.cfg_scale),
"cfg_uc": "",
"phrase_rep_pen": nai_settings.phrase_rep_pen,
//"stop_sequences": {{187}},
"stop_sequences": stopSequences,
//bad_words_ids = {{50256}, {0}, {1}};
"generate_until_sentence": true,
"use_cache": false,
"use_string": true,
"return_full_text": false,
"prefix": "vanilla",
"prefix": isNewModel ? "special_instruct" : "vanilla",
"order": this_settings.order,
"streaming": nai_settings.streaming_novel,
};

View File

@ -143,6 +143,7 @@ const default_settings = {
api_url_scale: '',
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
};
const oai_settings = {
@ -180,6 +181,7 @@ const oai_settings = {
api_url_scale: '',
show_external_models: false,
proxy_password: '',
assistant_prefill: '',
};
let openai_setting_names;
@ -775,6 +777,7 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
if (isClaude) {
generate_data['use_claude'] = true;
generate_data['top_k'] = parseFloat(oai_settings.top_k_openai);
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
}
if (isOpenRouter) {
@ -1109,6 +1112,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.api_url_scale = settings.api_url_scale ?? default_settings.api_url_scale;
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
if (settings.nsfw_toggle !== undefined) oai_settings.nsfw_toggle = !!settings.nsfw_toggle;
if (settings.keep_example_dialogue !== undefined) oai_settings.keep_example_dialogue = !!settings.keep_example_dialogue;
@ -1121,6 +1125,7 @@ function loadOpenAISettings(data, settings) {
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
$('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password);
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
$('#model_openai_select').val(oai_settings.openai_model);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
@ -1323,6 +1328,7 @@ async function saveOpenAIPreset(name, settings) {
stream_openai: settings.stream_openai,
api_url_scale: settings.api_url_scale,
show_external_models: settings.show_external_models,
assistant_prefill: settings.assistant_prefill,
};
const savePresetSettings = await fetch(`/savepreset_openai?name=${name}`, {
@ -1656,6 +1662,7 @@ function onSettingsPresetChange() {
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],
};
for (const [key, [selector, setting, isCheckbox]] of Object.entries(settingsToUpdate)) {
@ -2206,6 +2213,11 @@ $(document).ready(function () {
saveSettingsDebounced();
});
$('#claude_assistant_prefill').on('input', function () {
oai_settings.assistant_prefill = $(this).val();
saveSettingsDebounced();
});
$("#api_button_openai").on("click", onConnectButtonClick);
$("#openai_reverse_proxy").on("input", onReverseProxyInput);
$("#model_openai_select").on("change", onModelChange);

View File

@ -256,7 +256,7 @@ function fixMarkdown(text) {
// i.e. "^example * text* * harder problem *\n" -> "^example *text* *harder problem*\n"
// Find pairs of formatting characters and capture the text in between them
const format = /(\*|_|~){1,2}([\s\S]*?)\1{1,2}/gm;
const format = /([\*_]{1,2})([\s\S]*?)\1/gm;
let matches = [];
let match;
while ((match = format.exec(text)) !== null) {
@ -267,7 +267,7 @@ function fixMarkdown(text) {
let newText = text;
for (let i = matches.length - 1; i >= 0; i--) {
let matchText = matches[i][0];
let replacementText = matchText.replace(/(\*|_|~)(\s+)|(\s+)(\*|_|~)/g, '$1$4');
let replacementText = matchText.replace(/(\*|_)([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)|([\t \u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]+)(\*|_)/g, '$1$4');
newText = newText.slice(0, matches[i].index) + replacementText + newText.slice(matches[i].index + matchText.length);
}
@ -1224,12 +1224,6 @@ async function doMesCut(_, text) {
return
}
//reject attempts to delete firstmes
if (text === '0') {
toastr.error('Cannot delete the First Message')
return
}
let mesIDToCut = Number(text).toFixed(0)
let mesToCut = $("#chat").find(`.mes[mesid=${mesIDToCut}]`)

View File

@ -499,7 +499,7 @@ function onViewTagsListClick() {
$(list).append('<h3>Tags</h3><i>Click on the tag name to edit it.</i><br>');
$(list).append('<i>Click on color box to assign new color.</i><br><br>');
for (const tag of tags) {
for (const tag of tags.slice().sort((a, b) => a?.name?.localeCompare(b?.name))) {
const count = everything.filter(x => x == tag.id).length;
const template = $('#tag_view_template .tag_view_item').clone();
template.attr('id', tag.id);

View File

@ -1,154 +0,0 @@
import cloneDeep from 'lodash.clonedeep';
import userAgents from './user-agents.json';
// Normalizes the total weight to 1 and constructs a cumulative distribution.
const makeCumulativeWeightIndexPairs = (weightIndexPairs) => {
const totalWeight = weightIndexPairs.reduce((sum, [weight]) => sum + weight, 0);
let sum = 0;
return weightIndexPairs.map(([weight, index]) => {
sum += weight / totalWeight;
return [sum, index];
});
};
// Precompute these so that we can quickly generate unfiltered user agents.
const defaultWeightIndexPairs = userAgents.map(({ weight }, index) => [weight, index]);
const defaultCumulativeWeightIndexPairs = makeCumulativeWeightIndexPairs(defaultWeightIndexPairs);
// Turn the various filter formats into a single filter function that acts on raw user agents.
const constructFilter = (filters, accessor = parentObject => parentObject) => {
let childFilters;
if (typeof filters === 'function') {
childFilters = [filters];
} else if (filters instanceof RegExp) {
childFilters = [
value => (
typeof value === 'object' && value && value.userAgent
? filters.test(value.userAgent)
: filters.test(value)
),
];
} else if (filters instanceof Array) {
childFilters = filters.map(childFilter => constructFilter(childFilter));
} else if (typeof filters === 'object') {
childFilters = Object.entries(filters).map(([key, valueFilter]) => (
constructFilter(valueFilter, parentObject => parentObject[key])
));
} else {
childFilters = [
value => (
typeof value === 'object' && value && value.userAgent
? filters === value.userAgent
: filters === value
),
];
}
return (parentObject) => {
try {
const value = accessor(parentObject);
return childFilters.every(childFilter => childFilter(value));
} catch (error) {
// This happens when a user-agent lacks a nested property.
return false;
}
};
};
// Construct normalized cumulative weight index pairs given the filters.
const constructCumulativeWeightIndexPairsFromFilters = (filters) => {
if (!filters) {
return defaultCumulativeWeightIndexPairs;
}
const filter = constructFilter(filters);
const weightIndexPairs = [];
userAgents.forEach((rawUserAgent, index) => {
if (filter(rawUserAgent)) {
weightIndexPairs.push([rawUserAgent.weight, index]);
}
});
return makeCumulativeWeightIndexPairs(weightIndexPairs);
};
const setCumulativeWeightIndexPairs = (userAgent, cumulativeWeightIndexPairs) => {
Object.defineProperty(userAgent, 'cumulativeWeightIndexPairs', {
configurable: true,
enumerable: false,
writable: false,
value: cumulativeWeightIndexPairs,
});
};
export default class UserAgent extends Function {
constructor(filters) {
super();
setCumulativeWeightIndexPairs(this, constructCumulativeWeightIndexPairsFromFilters(filters));
if (this.cumulativeWeightIndexPairs.length === 0) {
throw new Error('No user agents matched your filters.');
}
this.randomize();
return new Proxy(this, {
apply: () => this.random(),
get: (target, property, receiver) => {
const dataCandidate = target.data && typeof property === 'string'
&& Object.prototype.hasOwnProperty.call(target.data, property)
&& Object.prototype.propertyIsEnumerable.call(target.data, property);
if (dataCandidate) {
const value = target.data[property];
if (value !== undefined) {
return value;
}
}
return Reflect.get(target, property, receiver);
},
});
}
static random = (filters) => {
try {
return new UserAgent(filters);
} catch (error) {
return null;
}
};
//
// Standard Object Methods
//
[Symbol.toPrimitive] = () => (
this.data.userAgent
);
toString = () => (
this.data.userAgent
);
random = () => {
const userAgent = new UserAgent();
setCumulativeWeightIndexPairs(userAgent, this.cumulativeWeightIndexPairs);
userAgent.randomize();
return userAgent;
};
randomize = () => {
// Find a random raw random user agent.
const randomNumber = Math.random();
const [, index] = this.cumulativeWeightIndexPairs
.find(([cumulativeWeight]) => cumulativeWeight > randomNumber);
const rawUserAgent = userAgents[index];
this.data = cloneDeep(rawUserAgent);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -470,6 +470,20 @@ export function getCharaFilename(chid) {
}
}
export function extractAllWords(value) {
const words = [];
if (!value) {
return words;
}
const matches = value.matchAll(/\b\w+\b/gim);
for (let match of matches) {
words.push(match[0].toLowerCase());
}
return words;
}
export function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}

View File

@ -10,6 +10,7 @@ export {
world_info_budget,
world_info_depth,
world_info_recursive,
world_info_overflow_alert,
world_info_case_sensitive,
world_info_match_whole_words,
world_info_character_strategy,
@ -32,6 +33,7 @@ let world_names;
let world_info_depth = 2;
let world_info_budget = 25;
let world_info_recursive = false;
let world_info_overflow_alert = false;
let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let world_info_character_strategy = world_info_insertion_strategy.character_first;
@ -70,6 +72,8 @@ function setWorldInfoSettings(settings, data) {
world_info_budget = Number(settings.world_info_budget);
if (settings.world_info_recursive !== undefined)
world_info_recursive = Boolean(settings.world_info_recursive);
if (settings.world_info_overflow_alert !== undefined)
world_info_overflow_alert = Boolean(settings.world_info_overflow_alert);
if (settings.world_info_case_sensitive !== undefined)
world_info_case_sensitive = Boolean(settings.world_info_case_sensitive);
if (settings.world_info_match_whole_words !== undefined)
@ -102,6 +106,7 @@ function setWorldInfoSettings(settings, data) {
$("#world_info_budget").val(world_info_budget);
$("#world_info_recursive").prop('checked', world_info_recursive);
$("#world_info_overflow_alert").prop('checked', world_info_overflow_alert);
$("#world_info_case_sensitive").prop('checked', world_info_case_sensitive);
$("#world_info_match_whole_words").prop('checked', world_info_match_whole_words);
@ -1020,6 +1025,10 @@ async function checkWorldInfo(chat, maxContext) {
if (textToScanTokens + getTokenCount(newContent) >= budget) {
console.debug(`WI budget reached, stopping`);
if (world_info_overflow_alert) {
console.log("Alerting");
toastr.warning(`World info budget reached after ${count} entries.`, 'World Info');
}
needsToScan = false;
break;
}
@ -1501,6 +1510,11 @@ jQuery(() => {
saveSettingsDebounced();
});
$('#world_info_overflow_alert').on('change', function () {
world_info_overflow_alert = $(this).val();
saveSettingsDebounced();
});
$('#world_button').on('click', async function () {
const chid = $('#set_character_world').data('chid');