Merge branch 'staging' of https://github.com/Cohee1207/SillyTavern into staging

This commit is contained in:
RossAscends 2023-11-12 17:23:33 +09:00
commit 583de0d0e7
24 changed files with 703 additions and 124 deletions

View File

@ -163,6 +163,8 @@
"custom_stopping_strings_macro": true, "custom_stopping_strings_macro": true,
"fuzzy_search": true, "fuzzy_search": true,
"encode_tags": false, "encode_tags": false,
"enableLabMode": false,
"enableZenSliders": false,
"ui_mode": 1 "ui_mode": 1
}, },
"extension_settings": { "extension_settings": {

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "sillytavern", "name": "sillytavern",
"version": "1.10.8", "version": "1.10.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sillytavern", "name": "sillytavern",
"version": "1.10.8", "version": "1.10.9",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

View File

@ -50,7 +50,7 @@
"type": "git", "type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git" "url": "https://github.com/SillyTavern/SillyTavern.git"
}, },
"version": "1.10.8", "version": "1.10.9",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"start-multi": "node server.js --disableCsrf", "start-multi": "node server.js --disableCsrf",

View File

@ -229,6 +229,10 @@
display: flex; display: flex;
} }
.flexBasis100p {
flex-basis: 100%;
}
.flexBasis50p { .flexBasis50p {
flex-basis: 50% flex-basis: 50%
} }
@ -263,6 +267,10 @@
flex-shrink: 1 flex-shrink: 1
} }
.flexWrap {
flex-wrap: wrap;
}
.flexnowrap { .flexnowrap {
flex-wrap: nowrap; flex-wrap: nowrap;
} }

View File

@ -13,7 +13,7 @@
.tag_view_item { .tag_view_item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: baseline; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }

View File

@ -358,3 +358,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtons {
body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
display: none !important; display: none !important;
} }
#openai_image_inlining:not(:checked) ~ #image_inlining_hint {
display: none;
}
#openai_image_inlining:checked ~ #image_inlining_hint {
display: block;
}

View File

@ -715,8 +715,8 @@
Temperature Temperature
<div class="fa-solid fa-circle-info opacity50p" title="Temperature controls the randomness in token selection:&#13;- low temperature (<1.0) leads to more predictable text, favoring higher probability tokens.&#13;- high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance.&#13;Set to 1.0 for the original probabilities."></div> <div class="fa-solid fa-circle-info opacity50p" title="Temperature controls the randomness in token selection:&#13;- low temperature (<1.0) leads to more predictable text, favoring higher probability tokens.&#13;- high temperature (>1.0) increases creativity and diversity in the output by giving lower probability tokens a better chance.&#13;Set to 1.0 for the original probabilities."></div>
</small> </small>
<input class="neo-range-slider" type="range" id="temp" name="volume" min="0.0" max="2.0" step="0.01"> <input class="neo-range-slider" type="range" id="temp" name="volume" min="0.0" max="4.0" step="0.01">
<input class="neo-range-input" type="number" min="0.0" max="2.0" step="0.01" data-for="temp" id="temp_counter"> <input class="neo-range-input" type="number" min="0.0" max="4.0" step="0.01" data-for="temp" id="temp_counter">
</div> </div>
<div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0"> <div data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0">
<small data-i18n="Top K"> <small data-i18n="Top K">
@ -1723,6 +1723,7 @@
</optgroup> </optgroup>
<optgroup label="GPT-4"> <optgroup label="GPT-4">
<option value="gpt-4">gpt-4</option> <option value="gpt-4">gpt-4</option>
<option value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option value="gpt-4-1106-preview">gpt-4-1106-preview</option> <option value="gpt-4-1106-preview">gpt-4-1106-preview</option>
<option value="gpt-4-0613">gpt-4-0613</option> <option value="gpt-4-0613">gpt-4-0613</option>
<option value="gpt-4-0314">gpt-4-0314</option> <option value="gpt-4-0314">gpt-4-0314</option>
@ -1745,6 +1746,17 @@
<input id="openai_show_external_models" type="checkbox" /> <input id="openai_show_external_models" type="checkbox" />
<span data-i18n="Show External models (provided by API)">Show "External" models (provided by API)</span> <span data-i18n="Show External models (provided by API)">Show "External" models (provided by API)</span>
</label> </label>
<label for="openai_image_inlining" class="checkbox_label flexWrap">
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images (only GPT-4V model)</span>
<div id="image_inlining_hint" class="flexBasis100p">
<small>
Natively replaces captioning if the model supports it.<br>
Use the <code><i class="fa-solid fa-image"></i></code> action on any message or the
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image to the chat.
</small>
</div>
</label>
</div> </div>
</form> </form>
<form id="claude_form" data-source="claude" action="javascript:void(null);" method="post" enctype="multipart/form-data"> <form id="claude_form" data-source="claude" action="javascript:void(null);" method="post" enctype="multipart/form-data">
@ -2502,6 +2514,7 @@
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search..."> <input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
<select id="world_info_sort_order" class="margin0"> <select id="world_info_sort_order" class="margin0">
<option data-rule="priority" value="0">Priority</option> <option data-rule="priority" value="0">Priority</option>
<option data-rule="custom" value="13">Custom</option>
<option data-order="asc" data-field="comment" value="1">Title A-Z</option> <option data-order="asc" data-field="comment" value="1">Title A-Z</option>
<option data-order="desc" data-field="comment" value="2">Title Z-A</option> <option data-order="desc" data-field="comment" value="2">Title Z-A</option>
<option data-order="asc" data-field="content" data-rule="length" value="3">Tokens ↗</option> <option data-order="asc" data-field="content" data-rule="length" value="3">Tokens ↗</option>
@ -2680,7 +2693,7 @@
</div> </div>
</div> </div>
</div> </div>
<div name="UserSettingsSecondColumn" id="UI-Customization" class="flex-container flexFlowColumn wide100p"> <div name="UserSettingsSecondColumn" id="UI-Customization" class="flex-container flexFlowColumn wide100p flexNoGap">
<div name="themeToggles"> <div name="themeToggles">
<h4 data-i18n="Theme Toggles">Theme Toggles</h4> <h4 data-i18n="Theme Toggles">Theme Toggles</h4>
<label data-newbie-hidden for="fast_ui_mode" class="checkbox_label" title="removes blur from window backgrounds" data-i18n="[title]removes blur from window backgrounds"> <label data-newbie-hidden for="fast_ui_mode" class="checkbox_label" title="removes blur from window backgrounds" data-i18n="[title]removes blur from window backgrounds">
@ -2737,6 +2750,13 @@
</label> </label>
</div> </div>
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4> <h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field" data-i18n="Aux List Field">Aux List Field</label>
<select id="aux_field">
<option data-i18n="Character Version" value="character_version">Character Version</option>
<option data-i18n="Created by" value="creator">Created by</option>
</select>
</div>
<div> <div>
<label for="play_message_sound" class="checkbox_label"> <label for="play_message_sound" class="checkbox_label">
<input id="play_message_sound" type="checkbox" /> <input id="play_message_sound" type="checkbox" />
@ -3088,7 +3108,7 @@
</div> </div>
<div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona" data-i18n="[title]Create a dummy persona"> <div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona" data-i18n="[title]Create a dummy persona">
<i class="fa-solid fa-person-circle-question fa-fw"></i> <i class="fa-solid fa-person-circle-question fa-fw"></i>
<span data-i18n="Blank">Blank</span> <span data-i18n="Create">Create</span>
</div> </div>
</h4> </h4>
<div id="user_avatar_block"> <div id="user_avatar_block">
@ -3752,7 +3772,7 @@
<form class="world_entry_form wi-card-entry"> <form class="world_entry_form wi-card-entry">
<div class="inline-drawer wide100p"> <div class="inline-drawer wide100p">
<div class="inline-drawer-toggle inline-drawer-header gap5px padding0"> <div class="inline-drawer-toggle inline-drawer-header gap5px padding0">
<!-- <span class="drag-handle">&#9776;</span> --> <span class="drag-handle">&#9776;</span>
<div class="gap5px world_entry_thin_controls wide100p alignitemscenter"> <div class="gap5px world_entry_thin_controls wide100p alignitemscenter">
<div class="fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-fw fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
<span class="flex-container alignitemscenter wide100p"> <span class="flex-container alignitemscenter wide100p">
@ -3854,7 +3874,7 @@
</span> </span>
</small> </small>
</label> </label>
<textarea class="text_pole" name="content" rows="4" data-i18n="[placeholder]What this keyword should mean to the AI, sent verbatim" placeholder="What this keyword should mean to the AI, sent verbatim"></textarea> <textarea class="text_pole" name="content" rows="8" data-i18n="[placeholder]What this keyword should mean to the AI, sent verbatim" placeholder="What this keyword should mean to the AI, sent verbatim"></textarea>
</div> </div>
</div> </div>
<div class="world_entry_thin_controls commentContainer"> <div class="world_entry_thin_controls commentContainer">
@ -4049,6 +4069,7 @@
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt"></div> <div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt"></div>
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div> <div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div> <div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
<div title="Embed image" class="mes_embed fa-solid fa-image" data-i18n="[title]Embed image"></div>
<div title="Create bookmark" class="mes_create_bookmark fa-regular fa-solid fa-book-bookmark" data-i18n="[title]Create Bookmark"></div> <div title="Create bookmark" class="mes_create_bookmark fa-regular fa-solid fa-book-bookmark" data-i18n="[title]Create Bookmark"></div>
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div> <div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div> <div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
@ -4167,7 +4188,7 @@
<div id="bogus_folder_template" class="template_element"> <div id="bogus_folder_template" class="template_element">
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart"> <div class="bogus_folder_select flex-container wide100p alignitemsflexstart">
<div class="avatar flex alignitemscenter textAlignCenter"> <div class="avatar flex alignitemscenter textAlignCenter">
<i class="fa-solid fa-folder-open fa-xl"></i> <i class="bogus_folder_icon fa-solid fa-folder-open fa-xl"></i>
</div> </div>
<div class="flex-container wide100pLess70px character_select_container"> <div class="flex-container wide100pLess70px character_select_container">
<div class="wide100p character_name_block"> <div class="wide100p character_name_block">
@ -4180,6 +4201,18 @@
</div> </div>
</div> </div>
</div> </div>
<div id="bogus_folder_back_template" class="template_element">
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart" id="BogusFolderBack" tagid="back">
<div class="avatar flex alignitemscenter textAlignCenter">
<i class="bogus_folder_icon fa-solid fa-xl fa-right-from-bracket fa-flip-horizontal"></i>
</div>
<div class="flex-container wide100pLess70px character_select_container height100p alignitemscenter">
<div class="wide100p character_name_block">
<span class="ch_name">Go back</span>
</div>
</div>
</div>
</div>
<div id="hotswap_template" class="template_element"> <div id="hotswap_template" class="template_element">
<div class="hotswapAvatar" title="Add a character/group to favorites to display it here!"> <div class="hotswapAvatar" title="Add a character/group to favorites to display it here!">
<img src="/img/ai4.png"> <img src="/img/ai4.png">

View File

@ -989,8 +989,15 @@ export async function selectCharacterById(id) {
} }
} }
function getTagBlock(item) { function getTagBlock(item, entities) {
const count = Object.values(tag_map).flat().filter(x => x == item.id).length; let count = 0;
for (const entity of entities) {
if (entitiesFilter.isElementTagged(entity, item.id)) {
count++;
}
}
const template = $('#bogus_folder_template .bogus_folder_select').clone(); const template = $('#bogus_folder_template .bogus_folder_select').clone();
template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` }); template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` });
template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }); template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 });
@ -999,6 +1006,11 @@ function getTagBlock(item) {
return template; return template;
} }
function getBackBlock() {
const template = $('#bogus_folder_back_template .bogus_folder_select').clone();
return template;
}
function getEmptyBlock() { function getEmptyBlock() {
const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog']; const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog'];
const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it']; const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it'];
@ -1038,9 +1050,10 @@ function getCharacterBlock(item, id) {
template.find('.ch_description').hide(); template.find('.ch_description').hide();
} }
const version = item.data?.character_version || ''; const auxFieldName = power_user.aux_field || 'character_version';
if (version) { const auxFieldValue = (item.data && item.data[auxFieldName]) || '';
template.find('.character_version').text(version); if (auxFieldValue) {
template.find('.character_version').text(auxFieldValue);
} }
else { else {
template.find('.character_version').hide(); template.find('.character_version').hide();
@ -1060,10 +1073,9 @@ async function printCharacters(fullRefresh = false) {
saveCharactersPage = 0; saveCharactersPage = 0;
printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member); printTagFilters(tag_filter_types.group_member);
const isBogusFolderOpen = !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus;
// Return to main list // Return to main list
if (isBogusFolderOpen) { if (isBogusFolderOpen()) {
entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] }); entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] });
} }
@ -1072,8 +1084,11 @@ async function printCharacters(fullRefresh = false) {
} }
const storageKey = 'Characters_PerPage'; const storageKey = 'Characters_PerPage';
const listId = '#rm_print_characters_block';
const entities = getEntitiesList({ doFilter: true });
$("#rm_print_characters_pagination").pagination({ $("#rm_print_characters_pagination").pagination({
dataSource: getEntitiesList({ doFilter: true }), dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default, pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000], sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
pageRange: 1, pageRange: 1,
@ -1086,20 +1101,25 @@ async function printCharacters(fullRefresh = false) {
formatNavigator: PAGINATION_TEMPLATE, formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true, showNavigator: true,
callback: function (data) { callback: function (data) {
$("#rm_print_characters_block").empty(); $(listId).empty();
for (const i of data) { if (isBogusFolderOpen()) {
if (i.type === 'character') { $(listId).append(getBackBlock());
$("#rm_print_characters_block").append(getCharacterBlock(i.item, i.id));
}
if (i.type === 'group') {
$("#rm_print_characters_block").append(getGroupBlock(i.item));
}
if (i.type === 'tag') {
$("#rm_print_characters_block").append(getTagBlock(i.item));
}
} }
if (!data.length) { if (!data.length) {
$("#rm_print_characters_block").append(getEmptyBlock()); $(listId).append(getEmptyBlock());
}
for (const i of data) {
switch (i.type) {
case 'character':
$(listId).append(getCharacterBlock(i.item, i.id));
break;
case 'group':
$(listId).append(getGroupBlock(i.item));
break;
case 'tag':
$(listId).append(getTagBlock(i.item, entities));
break;
}
} }
eventSource.emit(event_types.CHARACTER_PAGE_LOADED); eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
}, },
@ -1110,26 +1130,60 @@ async function printCharacters(fullRefresh = false) {
saveCharactersPage = e; saveCharactersPage = e;
}, },
afterRender: function () { afterRender: function () {
$('#rm_print_characters_block').scrollTop(0); $(listId).scrollTop(0);
}, },
}); });
favsToHotswap(); favsToHotswap();
} }
export function getEntitiesList({ doFilter } = {}) { /**
let entities = []; * Indicates whether a user is currently in a bogus folder.
entities.push(...characters.map((item, index) => ({ item, id: index, type: 'character' }))); * @returns {boolean} If currently viewing a folder
entities.push(...groups.map((item) => ({ item, id: item.id, type: 'group' }))); */
function isBogusFolderOpen() {
return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus;
}
if (power_user.bogus_folders) { export function getEntitiesList({ doFilter } = {}) {
entities.push(...tags.map((item) => ({ item, id: item.id, type: 'tag' }))); function characterToEntity(character, id) {
return { item: character, id, type: 'character' };
} }
function groupToEntity(group) {
return { item: group, id: group.id, type: 'group' };
}
function tagToEntity(tag) {
return { item: structuredClone(tag), id: tag.id, type: 'tag' };
}
let entities = [
...characters.map((item, index) => characterToEntity(item, index)),
...groups.map(item => groupToEntity(item)),
...(power_user.bogus_folders ? tags.map(item => tagToEntity(item)) : []),
];
if (doFilter) { if (doFilter) {
entities = entitiesFilter.applyFilters(entities); entities = entitiesFilter.applyFilters(entities);
} }
if (isBogusFolderOpen()) {
// Get tags of entities within the bogus folder
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
entities = entities.filter(x => x.type !== 'tag');
const otherTags = tags.filter(x => !filterData.selected.includes(x.id));
const bogusTags = [];
for (const entity of entities) {
for (const tag of otherTags) {
if (!bogusTags.includes(tag) && entitiesFilter.isElementTagged(entity, tag.id)) {
bogusTags.push(tag);
}
}
}
entities.push(...bogusTags.map(item => tagToEntity(item)));
}
sortEntitiesList(entities); sortEntitiesList(entities);
return entities; return entities;
} }
@ -3355,7 +3409,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues); generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues);
} }
else if (main_api == 'openai') { else if (main_api == 'openai') {
let [prompt, counts] = prepareOpenAIMessages({ let [prompt, counts] = await prepareOpenAIMessages({
name2: name2, name2: name2,
charDescription: description, charDescription: description,
charPersonality: personality, charPersonality: personality,
@ -7523,8 +7577,25 @@ jQuery(async function () {
$(document).on("click", ".bogus_folder_select", function () { $(document).on("click", ".bogus_folder_select", function () {
const tagId = $(this).attr('tagid'); const tagId = $(this).attr('tagid');
console.log('Bogus folder clicked', tagId); console.log('Bogus folder clicked', tagId);
entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [tagId], bogus: true, });
}) const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
if (!Array.isArray(filterData.selected)) {
filterData.selected = [];
filterData.excluded = [];
filterData.bogus = false;
}
if (tagId === 'back') {
filterData.selected.pop();
filterData.bogus = filterData.selected.length > 0;
} else {
filterData.selected.push(tagId);
filterData.bogus = true;
}
entitiesFilter.setFilterData(FILTER_TYPES.TAG, filterData);
});
$(document).on("input", ".edit_textarea", function () { $(document).on("input", ".edit_textarea", function () {
scroll_holder = $("#chat").scrollTop(); scroll_holder = $("#chat").scrollTop();

View File

@ -371,7 +371,7 @@ function RA_checkOnlineStatus() {
connection_made = false; connection_made = false;
} else { } else {
if (online_status !== undefined && online_status !== "no_connection") { if (online_status !== undefined && online_status !== "no_connection") {
$("#send_textarea").attr("placeholder", `Type a message, or /? for command list`); //on connect, placeholder tells user to type message $("#send_textarea").attr("placeholder", `Type a message, or /? for help`); //on connect, placeholder tells user to type message
$('#send_form').removeClass("no-connection"); $('#send_form').removeClass("no-connection");
$("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow"); $("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow");
$("#API-status-top").addClass("fa-plug"); $("#API-status-top").addClass("fa-plug");

View File

@ -1,8 +1,9 @@
import { getBase64Async, saveBase64AsFile } from "../../utils.js"; import { getBase64Async, saveBase64AsFile } from "../../utils.js";
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js"; import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js"; import { appendImageToMessage, callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
import { getMessageTimeStamp } from "../../RossAscends-mods.js"; import { getMessageTimeStamp } from "../../RossAscends-mods.js";
import { SECRET_KEYS, secret_state } from "../../secrets.js"; import { SECRET_KEYS, secret_state } from "../../secrets.js";
import { isImageInliningSupported } from "../../openai.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'caption'; const MODULE_NAME = 'caption';
@ -223,6 +224,83 @@ function onRefineModeInput() {
saveSettingsDebounced(); saveSettingsDebounced();
} }
async function sendEmbeddedImage(e) {
const file = e.target.files[0];
if (!file || !(file instanceof File)) {
return;
}
try {
const context = getContext();
const fileData = await getBase64Async(file);
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1];
const caption = await callPopup('<h3>Enter a comment or question (optional)</h3>', 'input', 'What is this?', { okButton: 'Send', rows: 2 });
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
const message = {
name: context.name1,
is_user: true,
send_date: getMessageTimeStamp(),
mes: caption || `[${context.name1} sends ${context.name2} a picture]`,
extra: {
image: imagePath,
inline_image: !!caption,
title: caption || '',
},
};
context.chat.push(message);
context.addOneMessage(message);
await context.generate('caption');
}
catch (error) {
console.log(error);
}
finally {
e.target.form.reset();
setImageIcon();
}
}
function onImageEmbedClicked() {
const context = getContext();
const messageElement = $(this).closest('.mes');
const messageId = messageElement.attr('mesid');
const message = context.chat[messageId];
if (!message) {
console.warn('Failed to find message with id', messageId);
return;
}
$('#embed_img_file')
.off('change')
.on('change', parseAndUploadEmbed)
.trigger('click');
async function parseAndUploadEmbed(e) {
const file = e.target.files[0];
if (!file || !(file instanceof File)) {
return;
}
const fileData = await getBase64Async(file);
const base64Data = fileData.split(',')[1];
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
if (!message.extra) {
message.extra = {};
}
message.extra.image = imagePath;
message.extra.inline_image = true;
message.extra.title = '';
appendImageToMessage(message, messageElement);
await context.saveChat();
}
}
jQuery(function () { jQuery(function () {
function addSendPictureButton() { function addSendPictureButton() {
const sendButton = $(` const sendButton = $(`
@ -234,6 +312,12 @@ jQuery(function () {
$('#extensionsMenu').prepend(sendButton); $('#extensionsMenu').prepend(sendButton);
$(sendButton).hide(); $(sendButton).hide();
$(sendButton).on('click', () => { $(sendButton).on('click', () => {
if (isImageInliningSupported()) {
console.log('Native image inlining is supported. Skipping captioning.');
$('#embed_img_file').off('change').on('change', sendEmbeddedImage).trigger('click');
return;
}
const hasCaptionModule = const hasCaptionModule =
(modules.includes('caption') && extension_settings.caption.source === 'extras') || (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'openai' && secret_state[SECRET_KEYS.OPENAI]) || (extension_settings.caption.source === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
@ -249,10 +333,12 @@ jQuery(function () {
}); });
} }
function addPictureSendForm() { function addPictureSendForm() {
const inputHtml = `<input id="img_file" type="file" accept="image/*">`; const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
const embedInputHtml = `<input id="embed_img_file" type="file" hidden accept="image/*">`;
const imgForm = document.createElement('form'); const imgForm = document.createElement('form');
imgForm.id = 'img_form'; imgForm.id = 'img_form';
$(imgForm).append(inputHtml); $(imgForm).append(inputHtml);
$(imgForm).append(embedInputHtml);
$(imgForm).hide(); $(imgForm).hide();
$('#form_sheld').append(imgForm); $('#form_sheld').append(imgForm);
$('#img_file').on('change', onSelectImage); $('#img_file').on('change', onSelectImage);
@ -312,5 +398,6 @@ jQuery(function () {
extension_settings.caption.template = String($('#caption_template').val()); extension_settings.caption.template = String($('#caption_template').val());
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$(document).on('click', '.mes_embed', onImageEmbedClicked);
setInterval(moduleWorker, UPDATE_INTERVAL); setInterval(moduleWorker, UPDATE_INTERVAL);
}); });

View File

@ -71,7 +71,7 @@ const triggerWords = {
} }
const messageTrigger = { const messageTrigger = {
activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the)?)?(.+)/i, activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i,
specialCases: { specialCases: {
[generationMode.CHARACTER]: ['you', 'yourself'], [generationMode.CHARACTER]: ['you', 'yourself'],
[generationMode.USER]: ['me', 'myself'], [generationMode.USER]: ['me', 'myself'],
@ -251,12 +251,12 @@ function processTriggers(chat, _, abort) {
console.log(`SD: Triggered by "${message}", detected subject: ${subject}"`); console.log(`SD: Triggered by "${message}", detected subject: ${subject}"`);
for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) { outer: for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) {
for (const trigger of triggers) { for (const trigger of triggers) {
if (subject === trigger) { if (subject === trigger) {
subject = triggerWords[specialMode][0]; subject = triggerWords[specialMode][0];
console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`); console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`);
break; break outer;
} }
} }
} }

View File

@ -45,6 +45,8 @@ class ElevenLabsTtsProvider {
this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.stability = $('#elevenlabs_tts_stability').val()
this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val()
this.settings.model = $('#elevenlabs_tts_model').find(':selected').val() this.settings.model = $('#elevenlabs_tts_model').find(':selected').val()
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
saveTtsProviderSettings() saveTtsProviderSettings()
} }
@ -79,6 +81,8 @@ class ElevenLabsTtsProvider {
$('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this))
$('#elevenlabs_tts_stability_output').text(this.settings.stability);
$('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost);
try { try {
await this.checkReady() await this.checkReady()

View File

@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js'
import { NovelTtsProvider } from './novel.js' import { NovelTtsProvider } from './novel.js'
import { power_user } from '../../power-user.js' import { power_user } from '../../power-user.js'
import { registerSlashCommand } from '../../slash-commands.js' import { registerSlashCommand } from '../../slash-commands.js'
import { OpenAITtsProvider } from './openai.js'
export { talkingAnimation }; export { talkingAnimation };
const UPDATE_INTERVAL = 1000 const UPDATE_INTERVAL = 1000
@ -73,6 +74,7 @@ let ttsProviders = {
Coqui: CoquiTtsProvider, Coqui: CoquiTtsProvider,
Edge: EdgeTtsProvider, Edge: EdgeTtsProvider,
Novel: NovelTtsProvider, Novel: NovelTtsProvider,
OpenAI: OpenAITtsProvider,
} }
let ttsProvider let ttsProvider
let ttsProviderName let ttsProviderName

View File

@ -0,0 +1,148 @@
import { getRequestHeaders } from "../../../script.js"
import { saveTtsProviderSettings } from "./index.js";
export { OpenAITtsProvider }
class OpenAITtsProvider {
static voices = [
{ name: 'Alloy', voice_id: 'alloy', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/alloy.wav' },
{ name: 'Echo', voice_id: 'echo', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/echo.wav' },
{ name: 'Fable', voice_id: 'fable', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/fable.wav' },
{ name: 'Onyx', voice_id: 'onyx', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/onyx.wav' },
{ name: 'Nova', voice_id: 'nova', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/nova.wav' },
{ name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' },
];
settings
voices = []
separator = ' . '
audioElement = document.createElement('audio')
defaultSettings = {
voiceMap: {},
customVoices: [],
model: 'tts-1',
speed: 1,
}
get settingsHtml() {
let html = `
<div>Use OpenAI's TTS engine.</div>
<small>Hint: Save an API key in the OpenAI API settings to use it here.</small>
<div>
<label for="openai-tts-model">Model:</label>
<select id="openai-tts-model">
<optgroup label="Latest">
<option value="tts-1">tts-1</option>
<option value="tts-1-hd">tts-1-hd</option>
</optgroup>
<optgroup label="Snapshots">
<option value="tts-1-1106">tts-1-1106</option>
<option value="tts-1-hd-1106">tts-1-hd-1106</option>
</optgroup>
<select>
</div>
<div>
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
</div>`;
return html;
}
async loadSettings(settings) {
// Populate Provider UI given input settings
if (Object.keys(settings).length == 0) {
console.info("Using default TTS Provider 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 TTS Provider: ${key}`;
}
}
$('#openai-tts-model').val(this.settings.model);
$('#openai-tts-model').on('change', () => {
this.onSettingsChange();
});
$('#openai-tts-speed').val(this.settings.speed);
$('#openai-tts-speed').on('input', () => {
this.onSettingsChange();
});
$('#openai-tts-speed-output').text(this.settings.speed);
await this.checkReady();
console.debug("OpenAI TTS: Settings loaded");
}
onSettingsChange() {
// Update dynamically
this.settings.model = String($('#openai-tts-model').find(':selected').val());
this.settings.speed = Number($('#openai-tts-speed').val());
$('#openai-tts-speed-output').text(this.settings.speed);
saveTtsProviderSettings();
}
async checkReady() {
await this.fetchTtsVoiceObjects();
}
async onRefreshClick() {
return;
}
async getVoice(voiceName) {
if (!voiceName) {
throw `TTS Voice name not provided`
}
const voice = OpenAITtsProvider.voices.find(voice => voice.voice_id === voiceName || voice.name === voiceName);
if (!voice) {
throw `TTS Voice not found: ${voiceName}`
}
return voice;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId)
return response
}
async fetchTtsVoiceObjects() {
return OpenAITtsProvider.voices;
}
async previewTtsVoice(_) {
return;
}
async fetchTtsGeneration(inputText, voiceId) {
console.info(`Generating new TTS for voice_id ${voiceId}`)
const response = await fetch(`/api/openai/generate-voice`, {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
"text": inputText,
"voice": voiceId,
"model": this.settings.model,
"speed": this.settings.speed,
}),
});
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
}

View File

@ -69,6 +69,20 @@ export class FilterHelper {
return data.filter(entity => fuzzySearchResults.includes(entity.uid)); return data.filter(entity => fuzzySearchResults.includes(entity.uid));
} }
/**
* Checks if the given entity is tagged with the given tag ID.
* @param {object} entity Searchable entity
* @param {string} tagId Tag ID to check
* @returns {boolean} Whether the entity is tagged with the given tag ID
*/
isElementTagged(entity, tagId) {
const isCharacter = entity.type === 'character';
const lookupValue = isCharacter ? entity.item.avatar : String(entity.id);
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
/** /**
* Applies a tag filter to the data. * Applies a tag filter to the data.
* @param {any[]} data The data to filter. * @param {any[]} data The data to filter.
@ -82,19 +96,12 @@ export class FilterHelper {
return data; return data;
} }
function isElementTagged(entity, tagId) { const getIsTagged = (entity) => {
const isCharacter = entity.type === 'character'; const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId));
const lookupValue = isCharacter ? entity.item.avatar : String(entity.id);
const isTagged = Array.isArray(tag_map[lookupValue]) && tag_map[lookupValue].includes(tagId);
return isTagged;
}
function getIsTagged(entity) {
const tagFlags = selected.map(tagId => isElementTagged(entity, tagId));
const trueFlags = tagFlags.filter(x => x); const trueFlags = tagFlags.filter(x => x);
const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0; const isTagged = TAG_LOGIC_AND ? tagFlags.length === trueFlags.length : trueFlags.length > 0;
const excludedTagFlags = excluded.map(tagId => isElementTagged(entity, tagId)); const excludedTagFlags = excluded.map(tagId => this.isElementTagged(entity, tagId));
const isExcluded = excludedTagFlags.includes(true); const isExcluded = excludedTagFlags.includes(true);
if (isExcluded) { if (isExcluded) {

View File

@ -31,6 +31,11 @@ export const kai_settings = {
seed: -1, seed: -1,
}; };
/**
* Stable version of KoboldAI has a nasty payload validation.
* It will reject any payload that has a key that is not in the whitelist.
* @typedef {Object.<string, boolean>} kai_flags
*/
export const kai_flags = { export const kai_flags = {
can_use_tokenization: false, can_use_tokenization: false,
can_use_stop_sequence: false, can_use_stop_sequence: false,
@ -38,6 +43,7 @@ export const kai_flags = {
can_use_default_badwordsids: false, can_use_default_badwordsids: false,
can_use_mirostat: false, can_use_mirostat: false,
can_use_grammar: false, can_use_grammar: false,
can_use_min_p: false,
}; };
const defaultValues = Object.freeze(structuredClone(kai_settings)); const defaultValues = Object.freeze(structuredClone(kai_settings));
@ -48,6 +54,7 @@ const MIN_STREAMING_KCPPVERSION = '1.30';
const MIN_TOKENIZATION_KCPPVERSION = '1.41'; const MIN_TOKENIZATION_KCPPVERSION = '1.41';
const MIN_MIROSTAT_KCPPVERSION = '1.35'; const MIN_MIROSTAT_KCPPVERSION = '1.35';
const MIN_GRAMMAR_KCPPVERSION = '1.44'; const MIN_GRAMMAR_KCPPVERSION = '1.44';
const MIN_MIN_P_KCPPVERSION = '1.48';
const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5];
export function formatKoboldUrl(value) { export function formatKoboldUrl(value) {
@ -114,7 +121,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
top_a: kai_settings.top_a, top_a: kai_settings.top_a,
top_k: kai_settings.top_k, top_k: kai_settings.top_k,
top_p: kai_settings.top_p, top_p: kai_settings.top_p,
min_p: kai_settings.min_p, min_p: (kai_flags.can_use_min_p || isHorde) ? kai_settings.min_p : undefined,
typical: kai_settings.typical, typical: kai_settings.typical,
s1: sampler_order[0], s1: sampler_order[0],
s2: sampler_order[1], s2: sampler_order[1],
@ -128,11 +135,11 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined, stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined,
streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet', streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet',
can_abort: kai_flags.can_use_streaming, can_abort: kai_flags.can_use_streaming,
mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined, mirostat: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat : undefined,
mirostat_tau: kai_flags.can_use_mirostat ? kai_settings.mirostat_tau : undefined, mirostat_tau: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_tau : undefined,
mirostat_eta: kai_flags.can_use_mirostat ? kai_settings.mirostat_eta : undefined, mirostat_eta: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_eta : undefined,
use_default_badwordsids: kai_flags.can_use_default_badwordsids ? kai_settings.use_default_badwordsids : undefined, use_default_badwordsids: (kai_flags.can_use_default_badwordsids || isHorde) ? kai_settings.use_default_badwordsids : undefined,
grammar: kai_flags.can_use_grammar ? substituteParams(kai_settings.grammar) : undefined, grammar: (kai_flags.can_use_grammar || isHorde) ? substituteParams(kai_settings.grammar) : undefined,
sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined, sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined,
}; };
return generate_data; return generate_data;
@ -302,6 +309,7 @@ export function setKoboldFlags(version, koboldVersion) {
kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version); kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version);
kai_flags.can_use_mirostat = canUseMirostat(koboldVersion); kai_flags.can_use_mirostat = canUseMirostat(koboldVersion);
kai_flags.can_use_grammar = canUseGrammar(koboldVersion); kai_flags.can_use_grammar = canUseGrammar(koboldVersion);
kai_flags.can_use_min_p = canUseMinP(koboldVersion);
} }
/** /**
@ -366,6 +374,17 @@ function canUseGrammar(koboldVersion) {
} else return false; } else return false;
} }
/**
* Determines if the Kobold min_p can be used with the given version.
* @param {{result:string, version:string;}} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold min_p can be used, false otherwise.
*/
function canUseMinP(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_MIN_P_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/** /**
* Sorts the sampler items by the given order. * Sorts the sampler items by the given order.
* @param {any[]} orderArray Sampler order array. * @param {any[]} orderArray Sampler order array.

View File

@ -54,7 +54,9 @@ import {
import { import {
delay, delay,
download, download,
getBase64Async,
getFileText, getSortableDelay, getFileText, getSortableDelay,
isDataURL,
parseJsonFile, parseJsonFile,
resetScrollHeight, resetScrollHeight,
stringFormat, stringFormat,
@ -70,7 +72,6 @@ export {
setOpenAIMessages, setOpenAIMessages,
setOpenAIMessageExamples, setOpenAIMessageExamples,
setupChatCompletionPromptManager, setupChatCompletionPromptManager,
prepareOpenAIMessages,
sendOpenAIRequest, sendOpenAIRequest,
getChatCompletionModel, getChatCompletionModel,
TokenHandler, TokenHandler,
@ -221,6 +222,7 @@ const default_settings = {
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false, use_alt_scale: false,
squash_system_messages: false, squash_system_messages: false,
image_inlining: false,
}; };
const oai_settings = { const oai_settings = {
@ -267,6 +269,7 @@ const oai_settings = {
exclude_assistant: false, exclude_assistant: false,
use_alt_scale: false, use_alt_scale: false,
squash_system_messages: false, squash_system_messages: false,
image_inlining: false,
}; };
let openai_setting_names; let openai_setting_names;
@ -409,7 +412,8 @@ function setOpenAIMessages(chat) {
// Apply the "wrap in quotes" option // Apply the "wrap in quotes" option
if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
const name = chat[j]['name']; const name = chat[j]['name'];
openai_msgs[i] = { "role": role, "content": content, name: name }; const image = chat[j]?.extra?.image;
openai_msgs[i] = { "role": role, "content": content, name: name, "image": image };
j++; j++;
} }
} }
@ -592,7 +596,7 @@ export function isOpenRouterWithInstruct() {
* @param type * @param type
* @param cyclePrompt * @param cyclePrompt
*/ */
function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) { async function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) {
chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory')); chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory'));
let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || ''; let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || '';
@ -629,8 +633,13 @@ function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt =
chatCompletion.insert(message, 'chatHistory'); chatCompletion.insert(message, 'chatHistory');
} }
const imageInlining = isImageInliningSupported();
// Insert chat messages as long as there is budget available // Insert chat messages as long as there is budget available
[...openai_msgs].reverse().every((chatPrompt, index) => { const chatPool = [...openai_msgs].reverse();
for (let index = 0; index < chatPool.length; index++) {
const chatPrompt = chatPool[index];
// We do not want to mutate the prompt // We do not want to mutate the prompt
const prompt = new Prompt(chatPrompt); const prompt = new Prompt(chatPrompt);
prompt.identifier = `chatHistory-${openai_msgs.length - index}`; prompt.identifier = `chatHistory-${openai_msgs.length - index}`;
@ -641,10 +650,16 @@ function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt =
chatMessage.setName(messageName); chatMessage.setName(messageName);
} }
if (chatCompletion.canAfford(chatMessage)) chatCompletion.insertAtStart(chatMessage, 'chatHistory'); if (imageInlining && chatPrompt.image) {
else return false; await chatMessage.addImage(chatPrompt.image);
return true; }
});
if (chatCompletion.canAfford(chatMessage)) {
chatCompletion.insertAtStart(chatMessage, 'chatHistory');
} else {
break;
}
}
// Insert and free new chat // Insert and free new chat
chatCompletion.freeBudget(newChatMessage); chatCompletion.freeBudget(newChatMessage);
@ -724,7 +739,7 @@ function getPromptPosition(position) {
* @param {string} options.quietPrompt - Instruction prompt for extras * @param {string} options.quietPrompt - Instruction prompt for extras
* @param {string} options.type - The type of the chat, can be 'impersonate'. * @param {string} options.type - The type of the chat, can be 'impersonate'.
*/ */
function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt } = {}) { async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt } = {}) {
// Helper function for preparing a prompt, that already exists within the prompt collection, for completion // Helper function for preparing a prompt, that already exists within the prompt collection, for completion
const addToChatCompletion = (source, target = null) => { const addToChatCompletion = (source, target = null) => {
// We need the prompts array to determine a position for the source. // We need the prompts array to determine a position for the source.
@ -825,9 +840,9 @@ function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, ty
// Decide whether dialogue examples should always be added // Decide whether dialogue examples should always be added
if (power_user.pin_examples) { if (power_user.pin_examples) {
populateDialogueExamples(prompts, chatCompletion); populateDialogueExamples(prompts, chatCompletion);
populateChatHistory(prompts, chatCompletion, type, cyclePrompt); await populateChatHistory(prompts, chatCompletion, type, cyclePrompt);
} else { } else {
populateChatHistory(prompts, chatCompletion, type, cyclePrompt); await populateChatHistory(prompts, chatCompletion, type, cyclePrompt);
populateDialogueExamples(prompts, chatCompletion); populateDialogueExamples(prompts, chatCompletion);
} }
@ -969,7 +984,7 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
* @param dryRun - Whether this is a live call or not. * @param dryRun - Whether this is a live call or not.
* @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag. * @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag.
*/ */
function prepareOpenAIMessages({ export async function prepareOpenAIMessages({
name2, name2,
charDescription, charDescription,
charPersonality, charPersonality,
@ -1012,7 +1027,7 @@ function prepareOpenAIMessages({
}); });
// Fill the chat completion with as much context as the budget allows // Fill the chat completion with as much context as the budget allows
populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt }); await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt });
} catch (error) { } catch (error) {
if (error instanceof TokenBudgetExceededError) { if (error instanceof TokenBudgetExceededError) {
toastr.error('An error occurred while counting tokens: Token budget exceeded.') toastr.error('An error occurred while counting tokens: Token budget exceeded.')
@ -1372,6 +1387,16 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) {
"stop": getCustomStoppingStrings(openai_max_stop_strings), "stop": getCustomStoppingStrings(openai_max_stop_strings),
}; };
// Empty array will produce a validation error
if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) {
delete generate_data.stop;
}
// Vision models don't support logit bias
if (isImageInliningSupported()) {
delete generate_data.logit_bias;
}
// Proxy is only supported for Claude and OpenAI // Proxy is only supported for Claude and OpenAI
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) { if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) {
validateReverseProxy(); validateReverseProxy();
@ -1640,7 +1665,18 @@ class InvalidCharacterNameError extends Error {
* Used for creating, managing, and interacting with a specific message object. * Used for creating, managing, and interacting with a specific message object.
*/ */
class Message { class Message {
tokens; identifier; role; content; name; static tokensPerImage = 85;
/** @type {number} */
tokens;
/** @type {string} */
identifier;
/** @type {string} */
role;
/** @type {string|any[]} */
content;
/** @type {string} */
name;
/** /**
* @constructor * @constructor
@ -1665,6 +1701,30 @@ class Message {
this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name }); this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
} }
async addImage(image) {
const textContent = this.content;
const isDataUrl = isDataURL(image);
if (!isDataUrl) {
try {
const response = await fetch(image, { method: 'GET', cache: 'force-cache' });
if (!response.ok) throw new Error('Failed to fetch image');
const blob = await response.blob();
image = await getBase64Async(blob);
} catch (error) {
console.error('Image adding skipped', error);
return;
}
}
this.content = [
{ type: "text", text: textContent },
{ type: "image_url", image_url: { "url": image, "detail": "low" } },
];
this.tokens += Message.tokensPerImage;
}
/** /**
* Create a new Message instance from a prompt. * Create a new Message instance from a prompt.
* @static * @static
@ -2148,6 +2208,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models; 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.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill; oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
oai_settings.prompts = settings.prompts ?? default_settings.prompts; oai_settings.prompts = settings.prompts ?? default_settings.prompts;
oai_settings.prompt_order = settings.prompt_order ?? default_settings.prompt_order; oai_settings.prompt_order = settings.prompt_order ?? default_settings.prompt_order;
@ -2168,6 +2229,7 @@ function loadOpenAISettings(data, settings) {
$('#api_url_scale').val(oai_settings.api_url_scale); $('#api_url_scale').val(oai_settings.api_url_scale);
$('#openai_proxy_password').val(oai_settings.proxy_password); $('#openai_proxy_password').val(oai_settings.proxy_password);
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill); $('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
$('#model_openai_select').val(oai_settings.openai_model); $('#model_openai_select').val(oai_settings.openai_model);
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true); $(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
@ -2388,6 +2450,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
exclude_assistant: settings.exclude_assistant, exclude_assistant: settings.exclude_assistant,
use_alt_scale: settings.use_alt_scale, use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages, squash_system_messages: settings.squash_system_messages,
image_inlining: settings.image_inlining,
}; };
const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, { const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, {
@ -2741,6 +2804,7 @@ function onSettingsPresetChange() {
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true], exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true], squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
}; };
const presetName = $('#settings_preset_openai').find(":selected").text(); const presetName = $('#settings_preset_openai').find(":selected").text();
@ -2785,6 +2849,9 @@ function getMaxContextOpenAI(value) {
else if (value.includes('gpt-4-1106')) { else if (value.includes('gpt-4-1106')) {
return max_128k; return max_128k;
} }
else if (value.includes('gpt-4-vision')) {
return max_128k;
}
else if (value.includes('gpt-3.5-turbo-1106')) { else if (value.includes('gpt-3.5-turbo-1106')) {
return max_16k; return max_16k;
} }
@ -2831,6 +2898,9 @@ function getMaxContextWindowAI(value) {
else if (value.includes('gpt-4-1106')) { else if (value.includes('gpt-4-1106')) {
return max_128k; return max_128k;
} }
else if (value.includes('gpt-4-vision')) {
return max_128k;
}
else if (value.includes('gpt-4-32k')) { else if (value.includes('gpt-4-32k')) {
return max_32k; return max_32k;
} }
@ -3217,6 +3287,31 @@ function updateScaleForm() {
} }
} }
/**
* Check if the model supports image inlining
* @returns {boolean} True if the model supports image inlining
*/
export function isImageInliningSupported() {
if (main_api !== 'openai') {
return false;
}
const modelId = 'gpt-4-vision';
if (!oai_settings.image_inlining) {
return false;
}
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
return oai_settings.openai_model.includes(modelId);
case chat_completion_sources.OPENROUTER:
return oai_settings.openrouter_model.includes(modelId);
default:
return false;
}
}
$(document).ready(async function () { $(document).ready(async function () {
$('#test_api_button').on('click', testApiConnection); $('#test_api_button').on('click', testApiConnection);
@ -3463,6 +3558,11 @@ $(document).ready(async function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#openai_image_inlining').on('input', function () {
oai_settings.image_inlining = !!$(this).prop('checked');
saveSettingsDebounced();
});
$(document).on('input', '#openai_settings .autoSetHeight', function () { $(document).on('input', '#openai_settings .autoSetHeight', function () {
resetScrollHeight($(this)); resetScrollHeight($(this));
}); });

View File

@ -39,7 +39,23 @@ async function uploadUserAvatar(url, name) {
} }
async function createDummyPersona() { async function createDummyPersona() {
await uploadUserAvatar(default_avatar); const personaName = await callPopup('<h3>Enter a name for this persona:</h3>', 'input', '');
if (!personaName) {
console.debug('User cancelled creating dummy persona');
return;
}
// Date + name (only ASCII) to make it unique
const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`;
power_user.personas[avatarId] = personaName;
power_user.persona_descriptions[avatarId] = {
description: '',
position: persona_description_positions.IN_PROMPT,
};
await uploadUserAvatar(default_avatar, avatarId);
saveSettingsDebounced();
} }
export async function convertCharacterToPersona(characterId = null) { export async function convertCharacterToPersona(characterId = null) {

View File

@ -220,6 +220,7 @@ let power_user = {
encode_tags: false, encode_tags: false,
servers: [], servers: [],
bogus_folders: false, bogus_folders: false,
aux_field: 'character_version',
}; };
let themes = []; let themes = [];
@ -1257,6 +1258,7 @@ function loadPowerUserSettings(settings, data) {
$(`#chat_display option[value=${power_user.chat_display}]`).attr("selected", true).trigger('change'); $(`#chat_display option[value=${power_user.chat_display}]`).attr("selected", true).trigger('change');
$('#chat_width_slider').val(power_user.chat_width); $('#chat_width_slider').val(power_user.chat_width);
$("#token_padding").val(power_user.token_padding); $("#token_padding").val(power_user.token_padding);
$("#aux_field").val(power_user.aux_field);
$("#font_scale").val(power_user.font_scale); $("#font_scale").val(power_user.font_scale);
$("#font_scale_counter").val(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale);
@ -1357,7 +1359,7 @@ function loadMaxContextUnlocked() {
} }
function switchMaxContextSize() { function switchMaxContextSize() {
const elements = [$('#max_context'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')]; const elements = [$('#max_context'), $('#max_context_counter'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')];
const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT; const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT;
const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin; const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin;
const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep; const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep;
@ -1366,7 +1368,7 @@ function switchMaxContextSize() {
element.attr('max', maxValue); element.attr('max', maxValue);
element.attr('step', steps); element.attr('step', steps);
if (element.attr('id') == 'max_context') { if (element.attr('id').indexOf('max_context') !== -1) {
element.attr('min', minValue); element.attr('min', minValue);
} }
const value = Number(element.val()); const value = Number(element.val());
@ -1651,7 +1653,17 @@ function sortEntitiesList(entities) {
return; return;
} }
entities.sort((a, b) => sortFunc(a.item, b.item)); entities.sort((a, b) => {
if (a.type === 'tag' && b.type !== 'tag') {
return -1;
}
if (a.type !== 'tag' && b.type === 'tag') {
return 1;
}
return sortFunc(a.item, b.item);
});
} }
async function saveTheme() { async function saveTheme() {
@ -2827,6 +2839,13 @@ $(document).ready(() => {
printCharacters(true); printCharacters(true);
}); });
$('#aux_field').on('change', function() {
const value = $(this).find(':selected').val();
power_user.aux_field = String(value);
saveSettingsDebounced();
printCharacters(false);
});
$(document).on('click', '#debug_table [data-debug-function]', function () { $(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function'); const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId); const functionRecord = debug_functions.find(f => f.functionId === functionId);

View File

@ -26,7 +26,7 @@ import {
setCharacterName, setCharacterName,
} from "../script.js"; } from "../script.js";
import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js";
import { groups, resetSelectedGroup, selected_group } from "./group-chats.js"; import { groups, is_group_generating, resetSelectedGroup, selected_group } from "./group-chats.js";
import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { getRegexedString, regex_placement } from "./extensions/regex/engine.js";
import { chat_styles, power_user } from "./power-user.js"; import { chat_styles, power_user } from "./power-user.js";
import { autoSelectPersona } from "./personas.js"; import { autoSelectPersona } from "./personas.js";
@ -270,7 +270,12 @@ async function unhideMessageCallback(_, arg) {
async function triggerGroupMessageCallback(_, arg) { async function triggerGroupMessageCallback(_, arg) {
if (!selected_group) { if (!selected_group) {
toastr.warning("Cannot run this command outside of a group chat."); toastr.warning("Cannot run trigger command outside of a group chat.");
return;
}
if (is_group_generating) {
toastr.warning("Cannot run trigger command while the group reply is generating.");
return; return;
} }

View File

@ -38,14 +38,13 @@ export const tag_filter_types = {
}; };
const ACTIONABLE_TAGS = { const ACTIONABLE_TAGS = {
FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star', class: 'filterByFavorites' }, FAV: { id: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: applyFavFilter, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' }, GROUP: { id: 0, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' }, HINT: { id: 3, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
} }
const InListActionable = { const InListActionable = {
VIEW: { id: 2, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear' },
} }
const DEFAULT_TAGS = [ const DEFAULT_TAGS = [
@ -321,9 +320,9 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe
tagElement.on('click', () => action.bind(tagElement)(filter)); tagElement.on('click', () => action.bind(tagElement)(filter));
tagElement.addClass('actionable'); tagElement.addClass('actionable');
} }
if (action && tag.id === 2) { /*if (action && tag.id === 2) {
tagElement.addClass('innerActionable hidden'); tagElement.addClass('innerActionable hidden');
} }*/
$(listElement).append(tagElement); $(listElement).append(tagElement);
} }

View File

@ -1,26 +1,26 @@
System-wide Replacement Macros (in order of evaluation): System-wide Replacement Macros (in order of evaluation):
<ul> <ul>
<li><tt>&lcub;&lcub;original&rcub;&rcub;</tt> - global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.</li> <li><tt>&lcub;&lcub;original&rcub;&rcub;</tt> global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.</li>
<li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> - the user input</li> <li><tt>&lcub;&lcub;input&rcub;&rcub;</tt> the user input</li>
<li><tt>&lcub;&lcub;description&rcub;&rcub;</tt> - the Character's Description</li> <li><tt>&lcub;&lcub;description&rcub;&rcub;</tt> the Character's Description</li>
<li><tt>&lcub;&lcub;personality&rcub;&rcub;</tt> - the Character's Personality</li> <li><tt>&lcub;&lcub;personality&rcub;&rcub;</tt> the Character's Personality</li>
<li><tt>&lcub;&lcub;scenario&rcub;&rcub;</tt> - the Character's Scenario</li> <li><tt>&lcub;&lcub;scenario&rcub;&rcub;</tt> the Character's Scenario</li>
<li><tt>&lcub;&lcub;persona&rcub;&rcub;</tt> - your current Persona Description</li> <li><tt>&lcub;&lcub;persona&rcub;&rcub;</tt> your current Persona Description</li>
<li><tt>&lcub;&lcub;mesExamples&rcub;&rcub;</tt> - the Character's Dialogue Examples</li> <li><tt>&lcub;&lcub;mesExamples&rcub;&rcub;</tt> the Character's Dialogue Examples</li>
<li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> - your current Persona username</li> <li><tt>&lcub;&lcub;user&rcub;&rcub;</tt> your current Persona username</li>
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> - the Character's name</li> <li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> the Character's name</li>
<li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> - index # of the latest chat message. Useful for slash command batching.</li> <li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> index # of the latest chat message. Useful for slash command batching.</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> - you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li> <li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>
<li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> - the current time</li> <li><tt>&lcub;&lcub;time&rcub;&rcub;</tt> the current time</li>
<li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> - the current date</li> <li><tt>&lcub;&lcub;date&rcub;&rcub;</tt> the current date</li>
<li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> - the current weekday</li> <li><tt>&lcub;&lcub;weekday&rcub;&rcub;</tt> the current weekday</li>
<li><tt>&lcub;&lcub;isotime&rcub;&rcub;</tt> - the current ISO date (YYYY-MM-DD)</li> <li><tt>&lcub;&lcub;isotime&rcub;&rcub;</tt> the current ISO date (YYYY-MM-DD)</li>
<li><tt>&lcub;&lcub;isodate&rcub;&rcub;</tt> - the current ISO time (24-hour clock)</li> <li><tt>&lcub;&lcub;isodate&rcub;&rcub;</tt> the current ISO time (24-hour clock)</li>
<li><tt>&lcub;&lcub;datetimeformat &hellip;&rcub;&rcub;</tt> - the current date/time in the specified format, e. g. for German date/time: <tt>&lcub;&lcub;datetimeformat DD.MM.YYYY HH:mm&rcub;&rcub;</tt></li> <li><tt>&lcub;&lcub;datetimeformat &hellip;&rcub;&rcub;</tt> the current date/time in the specified format, e. g. for German date/time: <tt>&lcub;&lcub;datetimeformat DD.MM.YYYY HH:mm&rcub;&rcub;</tt></li>
<li><tt>&lcub;&lcub;time_UTC±#&rcub;&rcub;</tt> - the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</li> <li><tt>&lcub;&lcub;time_UTC±#&rcub;&rcub;</tt> the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2</li>
<li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> - the time since the last user message was sent</li> <li><tt>&lcub;&lcub;idle_duration&rcub;&rcub;</tt> the time since the last user message was sent</li>
<li><tt>&lcub;&lcub;bias "text here"&rcub;&rcub;</tt> - sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li> <li><tt>&lcub;&lcub;bias "text here"&rcub;&rcub;</tt> sets a behavioral bias for the AI until the next user input. Quotes around the text are important.</li>
<li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> - returns a random item from the list. (ex: &lcub;&lcub;random:1,2,3,4&rcub;&rcub; will return 1 of the 4 numbers at random. Works with text lists too.</li> <li><tt>&lcub;&lcub;random:(args)&rcub;&rcub;</tt> returns a random item from the list. (ex: &lcub;&lcub;random:1,2,3,4&rcub;&rcub; will return 1 of the 4 numbers at random. Works with text lists too.</li>
<li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> - rolls a dice. (ex: &lcub;&lcub;roll:1d6&rcub;&rcub; will roll a 6-sided dice and return a number between 1 and 6)</li> <li><tt>&lcub;&lcub;roll:(formula)&rcub;&rcub;</tt> rolls a dice. (ex: &lcub;&lcub;roll:1d6&rcub;&rcub; will roll a 6- sided dice and return a number between 1 and 6)</li>
<li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> - dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li> <li><tt>&lcub;&lcub;banned "text here"&rcub;&rcub;</tt> dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</li>
</ul> </ul>

View File

@ -269,7 +269,15 @@ function sortEntries(data) {
const sortRule = option.data('rule'); const sortRule = option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1; const orderSign = sortOrder === 'asc' ? 1 : -1;
if (sortRule === 'priority') { if (sortRule === 'custom') {
// First by display index, then by order, then by uid
data.sort((a, b) => {
const aValue = a.displayIndex;
const bValue = b.displayIndex;
return (aValue - bValue || b.order - a.order || a.uid - b.uid);
});
} else if (sortRule === 'priority') {
// First constant, then normal, then disabled. Then sort by order // First constant, then normal, then disabled. Then sort by order
data.sort((a, b) => { data.sort((a, b) => {
const aValue = a.constant ? 0 : a.disable ? 2 : 1; const aValue = a.constant ? 0 : a.disable ? 2 : 1;
@ -375,7 +383,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
nextText: '>', nextText: '>',
formatNavigator: PAGINATION_TEMPLATE, formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true, showNavigator: true,
callback: function (page) { callback: function (/** @type {object[]} */ page) {
$("#world_popup_entries_list").empty(); $("#world_popup_entries_list").empty();
const keywordHeaders = ` const keywordHeaders = `
<div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;"> <div id="WIEntryHeaderTitlesPC" class="flex-container wide100p spaceBetween justifyCenter textAlignCenter" style="padding:0 2.5em;">
@ -399,6 +407,12 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
</small> </small>
</div>` </div>`
const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x); const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x);
const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom';
if (!isCustomOrder) {
blocks.forEach(block => {
block.find('.drag-handle').remove();
});
}
$("#world_popup_entries_list").append(keywordHeaders); $("#world_popup_entries_list").append(keywordHeaders);
$("#world_popup_entries_list").append(blocks); $("#world_popup_entries_list").append(blocks);
}, },
@ -500,6 +514,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
delay: getSortableDelay(), delay: getSortableDelay(),
handle: ".drag-handle", handle: ".drag-handle",
stop: async function (event, ui) { stop: async function (event, ui) {
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
$('#world_popup_entries_list .world_entry').each(function (index) { $('#world_popup_entries_list .world_entry').each(function (index) {
const uid = $(this).data('uid'); const uid = $(this).data('uid');
@ -511,8 +527,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return; return;
} }
item.displayIndex = index; item.displayIndex = minDisplayIndex + index;
setOriginalDataValue(data, uid, 'extensions.display_index', index); setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
}); });
console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex }))); console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
@ -587,7 +603,7 @@ function getWorldEntry(name, data, entry) {
setOriginalDataValue(data, uid, "keys", data.entries[uid].key); setOriginalDataValue(data, uid, "keys", data.entries[uid].key);
saveWorldInfo(name, data); saveWorldInfo(name, data);
}); });
keyInput.val(entry.key.join(",")).trigger("input"); keyInput.val(entry.key.join(", ")).trigger("input");
//initScrollHeight(keyInput); //initScrollHeight(keyInput);
// logic AND/NOT // logic AND/NOT
@ -708,7 +724,7 @@ function getWorldEntry(name, data, entry) {
saveWorldInfo(name, data); saveWorldInfo(name, data);
}); });
keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input"); keySecondaryInput.val(entry.keysecondary.join(", ")).trigger("input");
initScrollHeight(keySecondaryInput); initScrollHeight(keySecondaryInput);
// comment // comment
@ -1582,9 +1598,7 @@ async function checkWorldInfo(chat, maxContext) {
over_max = ( over_max = (
world_info_min_activations_depth_max > 0 && world_info_min_activations_depth_max > 0 &&
minActivationMsgIndex > world_info_min_activations_depth_max minActivationMsgIndex > world_info_min_activations_depth_max
) || ( ) || (minActivationMsgIndex >= chat.length)
minActivationMsgIndex >= chat.length
)
if (!over_max) { if (!over_max) {
needsToScan = true needsToScan = true
textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join("")); textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join(""));
@ -2167,11 +2181,9 @@ jQuery(() => {
updateEditor(navigation_option.previous); updateEditor(navigation_option.previous);
}); });
$('#world_info_sort_order').on('change', function (e) { $('#world_info_sort_order').on('change', function () {
if (e.target instanceof HTMLOptionElement) { const value = String($(this).find(":selected").val());
localStorage.setItem(SORT_ORDER_KEY, e.target.value); localStorage.setItem(SORT_ORDER_KEY, value);
}
updateEditor(navigation_option.none); updateEditor(navigation_option.none);
}) })

View File

@ -63,6 +63,45 @@ function registerEndpoints(app, jsonParser) {
} }
}); });
app.post('/api/openai/generate-voice', jsonParser, async (request, response) => {
try {
const key = readSecret(SECRET_KEYS.OPENAI);
if (!key) {
console.log('No OpenAI key found');
return response.sendStatus(401);
}
const result = await fetch('https://api.openai.com/v1/audio/speech', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${key}`,
},
body: JSON.stringify({
input: request.body.text,
response_format: 'mp3',
voice: request.body.voice ?? 'alloy',
speed: request.body.speed ?? 1,
model: request.body.model ?? 'tts-1',
}),
});
if (!result.ok) {
const text = await result.text();
console.log('OpenAI request failed', result.statusText, text);
return response.status(500).send(text);
}
const buffer = await result.arrayBuffer();
response.setHeader('Content-Type', 'audio/mpeg');
return response.send(Buffer.from(buffer));
} catch (error) {
console.error('OpenAI TTS generation failed', error);
response.status(500).send('Internal server error');
}
});
app.post('/api/openai/generate-image', jsonParser, async (request, response) => { app.post('/api/openai/generate-image', jsonParser, async (request, response) => {
try { try {
const key = readSecret(SECRET_KEYS.OPENAI); const key = readSecret(SECRET_KEYS.OPENAI);