diff --git a/default/settings.json b/default/settings.json index fd4edfda7..f8c4b54bf 100644 --- a/default/settings.json +++ b/default/settings.json @@ -163,6 +163,8 @@ "custom_stopping_strings_macro": true, "fuzzy_search": true, "encode_tags": false, + "enableLabMode": false, + "enableZenSliders": false, "ui_mode": 1 }, "extension_settings": { diff --git a/package-lock.json b/package-lock.json index 64f3f1cae..666982fbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.10.8", + "version": "1.10.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.10.8", + "version": "1.10.9", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 39f731ae6..ab117a4a7 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.10.8", + "version": "1.10.9", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index 701d03890..d86d53f49 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -229,6 +229,10 @@ display: flex; } +.flexBasis100p { + flex-basis: 100%; +} + .flexBasis50p { flex-basis: 50% } @@ -263,6 +267,10 @@ flex-shrink: 1 } +.flexWrap { + flex-wrap: wrap; +} + .flexnowrap { flex-wrap: nowrap; } diff --git a/public/css/tags.css b/public/css/tags.css index 788e7dc05..3ad18c468 100644 --- a/public/css/tags.css +++ b/public/css/tags.css @@ -13,7 +13,7 @@ .tag_view_item { display: flex; flex-direction: row; - align-items: baseline; + align-items: center; gap: 10px; margin-bottom: 5px; } diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 4bf01922e..692ebd233 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -358,3 +358,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtons { body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { display: none !important; } + +#openai_image_inlining:not(:checked) ~ #image_inlining_hint { + display: none; +} + +#openai_image_inlining:checked ~ #image_inlining_hint { + display: block; +} diff --git a/public/index.html b/public/index.html index 0e3bf0dfd..24f21dedf 100644 --- a/public/index.html +++ b/public/index.html @@ -715,8 +715,8 @@ Temperature
- - + +
@@ -1723,6 +1723,7 @@ + @@ -1745,6 +1746,17 @@ Show "External" models (provided by API) +
@@ -2502,6 +2514,7 @@ + + + +
@@ -3752,7 +3772,7 @@
- +
@@ -3854,7 +3874,7 @@
- +
@@ -4049,6 +4069,7 @@
+
@@ -4167,7 +4188,7 @@
- +
@@ -4180,6 +4201,18 @@
+
+
+
+ +
+
+
+ Go back +
+
+
+
diff --git a/public/script.js b/public/script.js index 327ae6438..3912d1577 100644 --- a/public/script.js +++ b/public/script.js @@ -989,8 +989,15 @@ export async function selectCharacterById(id) { } } -function getTagBlock(item) { - const count = Object.values(tag_map).flat().filter(x => x == item.id).length; +function getTagBlock(item, entities) { + let count = 0; + + for (const entity of entities) { + if (entitiesFilter.isElementTagged(entity, item.id)) { + count++; + } + } + const template = $('#bogus_folder_template .bogus_folder_select').clone(); template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` }); template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 }); @@ -999,6 +1006,11 @@ function getTagBlock(item) { return template; } +function getBackBlock() { + const template = $('#bogus_folder_back_template .bogus_folder_select').clone(); + return template; +} + function getEmptyBlock() { 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']; @@ -1038,9 +1050,10 @@ function getCharacterBlock(item, id) { template.find('.ch_description').hide(); } - const version = item.data?.character_version || ''; - if (version) { - template.find('.character_version').text(version); + const auxFieldName = power_user.aux_field || 'character_version'; + const auxFieldValue = (item.data && item.data[auxFieldName]) || ''; + if (auxFieldValue) { + template.find('.character_version').text(auxFieldValue); } else { template.find('.character_version').hide(); @@ -1060,10 +1073,9 @@ async function printCharacters(fullRefresh = false) { saveCharactersPage = 0; printTagFilters(tag_filter_types.character); printTagFilters(tag_filter_types.group_member); - const isBogusFolderOpen = !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus; // Return to main list - if (isBogusFolderOpen) { + if (isBogusFolderOpen()) { entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] }); } @@ -1072,8 +1084,11 @@ async function printCharacters(fullRefresh = false) { } const storageKey = 'Characters_PerPage'; + const listId = '#rm_print_characters_block'; + const entities = getEntitiesList({ doFilter: true }); + $("#rm_print_characters_pagination").pagination({ - dataSource: getEntitiesList({ doFilter: true }), + dataSource: entities, pageSize: Number(localStorage.getItem(storageKey)) || per_page_default, sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000], pageRange: 1, @@ -1086,20 +1101,25 @@ async function printCharacters(fullRefresh = false) { formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, callback: function (data) { - $("#rm_print_characters_block").empty(); - for (const i of data) { - if (i.type === 'character') { - $("#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)); - } + $(listId).empty(); + if (isBogusFolderOpen()) { + $(listId).append(getBackBlock()); } 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); }, @@ -1110,26 +1130,60 @@ async function printCharacters(fullRefresh = false) { saveCharactersPage = e; }, afterRender: function () { - $('#rm_print_characters_block').scrollTop(0); + $(listId).scrollTop(0); }, }); favsToHotswap(); } -export function getEntitiesList({ doFilter } = {}) { - let entities = []; - entities.push(...characters.map((item, index) => ({ item, id: index, type: 'character' }))); - entities.push(...groups.map((item) => ({ item, id: item.id, type: 'group' }))); +/** + * Indicates whether a user is currently in a bogus folder. + * @returns {boolean} If currently viewing a folder + */ +function isBogusFolderOpen() { + return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus; +} - if (power_user.bogus_folders) { - entities.push(...tags.map((item) => ({ item, id: item.id, type: 'tag' }))); +export function getEntitiesList({ doFilter } = {}) { + 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) { 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); return entities; } @@ -3355,7 +3409,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues); } else if (main_api == 'openai') { - let [prompt, counts] = prepareOpenAIMessages({ + let [prompt, counts] = await prepareOpenAIMessages({ name2: name2, charDescription: description, charPersonality: personality, @@ -7523,8 +7577,25 @@ jQuery(async function () { $(document).on("click", ".bogus_folder_select", function () { const tagId = $(this).attr('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 () { scroll_holder = $("#chat").scrollTop(); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index b40e5f85a..35f7b1c2c 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -371,7 +371,7 @@ function RA_checkOnlineStatus() { connection_made = false; } else { 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"); $("#API-status-top").removeClass("fa-plug-circle-exclamation redOverlayGlow"); $("#API-status-top").addClass("fa-plug"); diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 05f70dff4..4d83d1419 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -1,8 +1,9 @@ import { getBase64Async, saveBase64AsFile } from "../../utils.js"; import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js"; -import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js"; +import { appendImageToMessage, callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js"; import { getMessageTimeStamp } from "../../RossAscends-mods.js"; import { SECRET_KEYS, secret_state } from "../../secrets.js"; +import { isImageInliningSupported } from "../../openai.js"; export { MODULE_NAME }; const MODULE_NAME = 'caption'; @@ -223,6 +224,83 @@ function onRefineModeInput() { 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('

Enter a comment or question (optional)

', '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 () { function addSendPictureButton() { const sendButton = $(` @@ -234,6 +312,12 @@ jQuery(function () { $('#extensionsMenu').prepend(sendButton); $(sendButton).hide(); $(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 = (modules.includes('caption') && extension_settings.caption.source === 'extras') || (extension_settings.caption.source === 'openai' && secret_state[SECRET_KEYS.OPENAI]) || @@ -249,10 +333,12 @@ jQuery(function () { }); } function addPictureSendForm() { - const inputHtml = ``; + const inputHtml = ``; + const embedInputHtml = ``; const imgForm = document.createElement('form'); imgForm.id = 'img_form'; $(imgForm).append(inputHtml); + $(imgForm).append(embedInputHtml); $(imgForm).hide(); $('#form_sheld').append(imgForm); $('#img_file').on('change', onSelectImage); @@ -312,5 +398,6 @@ jQuery(function () { extension_settings.caption.template = String($('#caption_template').val()); saveSettingsDebounced(); }); + $(document).on('click', '.mes_embed', onImageEmbedClicked); setInterval(moduleWorker, UPDATE_INTERVAL); }); diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index dd964b55c..c859fc250 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -71,7 +71,7 @@ const triggerWords = { } 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: { [generationMode.CHARACTER]: ['you', 'yourself'], [generationMode.USER]: ['me', 'myself'], @@ -251,12 +251,12 @@ function processTriggers(chat, _, abort) { 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) { if (subject === trigger) { subject = triggerWords[specialMode][0]; console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`); - break; + break outer; } } } diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 3e0c90fe2..4cb7813fe 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -45,6 +45,8 @@ class ElevenLabsTtsProvider { this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.model = $('#elevenlabs_tts_model').find(':selected').val() + $('#elevenlabs_tts_stability_output').text(this.settings.stability); + $('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost); saveTtsProviderSettings() } @@ -79,6 +81,8 @@ class ElevenLabsTtsProvider { $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this)) + $('#elevenlabs_tts_stability_output').text(this.settings.stability); + $('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost); try { await this.checkReady() diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 0b22753d1..2ff01190a 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js' import { NovelTtsProvider } from './novel.js' import { power_user } from '../../power-user.js' import { registerSlashCommand } from '../../slash-commands.js' +import { OpenAITtsProvider } from './openai.js' export { talkingAnimation }; const UPDATE_INTERVAL = 1000 @@ -73,6 +74,7 @@ let ttsProviders = { Coqui: CoquiTtsProvider, Edge: EdgeTtsProvider, Novel: NovelTtsProvider, + OpenAI: OpenAITtsProvider, } let ttsProvider let ttsProviderName diff --git a/public/scripts/extensions/tts/openai.js b/public/scripts/extensions/tts/openai.js new file mode 100644 index 000000000..393a82940 --- /dev/null +++ b/public/scripts/extensions/tts/openai.js @@ -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 = ` +
Use OpenAI's TTS engine.
+ Hint: Save an API key in the OpenAI API settings to use it here. +
+ + +
+
+ + +
`; + 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; + } +} diff --git a/public/scripts/filters.js b/public/scripts/filters.js index f382845cc..ca2e5a103 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -69,6 +69,20 @@ export class FilterHelper { 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. * @param {any[]} data The data to filter. @@ -82,19 +96,12 @@ export class FilterHelper { return data; } - function 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; - } - - function getIsTagged(entity) { - const tagFlags = selected.map(tagId => isElementTagged(entity, tagId)); + const getIsTagged = (entity) => { + const tagFlags = selected.map(tagId => this.isElementTagged(entity, tagId)); const trueFlags = tagFlags.filter(x => x); 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); if (isExcluded) { diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index f2bc7ef73..8cab041fd 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -31,6 +31,11 @@ export const kai_settings = { 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.} kai_flags + */ export const kai_flags = { can_use_tokenization: false, can_use_stop_sequence: false, @@ -38,6 +43,7 @@ export const kai_flags = { can_use_default_badwordsids: false, can_use_mirostat: false, can_use_grammar: false, + can_use_min_p: false, }; 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_MIROSTAT_KCPPVERSION = '1.35'; const MIN_GRAMMAR_KCPPVERSION = '1.44'; +const MIN_MIN_P_KCPPVERSION = '1.48'; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; export function formatKoboldUrl(value) { @@ -114,7 +121,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon top_a: kai_settings.top_a, top_k: kai_settings.top_k, 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, s1: sampler_order[0], 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, streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet', can_abort: kai_flags.can_use_streaming, - mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined, - mirostat_tau: kai_flags.can_use_mirostat ? kai_settings.mirostat_tau : undefined, - mirostat_eta: kai_flags.can_use_mirostat ? kai_settings.mirostat_eta : undefined, - use_default_badwordsids: kai_flags.can_use_default_badwordsids ? kai_settings.use_default_badwordsids : undefined, - grammar: kai_flags.can_use_grammar ? substituteParams(kai_settings.grammar) : undefined, + mirostat: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat : undefined, + mirostat_tau: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_tau : undefined, + mirostat_eta: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_eta : undefined, + use_default_badwordsids: (kai_flags.can_use_default_badwordsids || isHorde) ? kai_settings.use_default_badwordsids : undefined, + grammar: (kai_flags.can_use_grammar || isHorde) ? substituteParams(kai_settings.grammar) : undefined, sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined, }; return generate_data; @@ -302,6 +309,7 @@ export function setKoboldFlags(version, koboldVersion) { kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version); kai_flags.can_use_mirostat = canUseMirostat(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; } +/** + * 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. * @param {any[]} orderArray Sampler order array. diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ec5e712bf..7242e2cd3 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -54,7 +54,9 @@ import { import { delay, download, + getBase64Async, getFileText, getSortableDelay, + isDataURL, parseJsonFile, resetScrollHeight, stringFormat, @@ -70,7 +72,6 @@ export { setOpenAIMessages, setOpenAIMessageExamples, setupChatCompletionPromptManager, - prepareOpenAIMessages, sendOpenAIRequest, getChatCompletionModel, TokenHandler, @@ -221,6 +222,7 @@ const default_settings = { exclude_assistant: false, use_alt_scale: false, squash_system_messages: false, + image_inlining: false, }; const oai_settings = { @@ -267,6 +269,7 @@ const oai_settings = { exclude_assistant: false, use_alt_scale: false, squash_system_messages: false, + image_inlining: false, }; let openai_setting_names; @@ -409,7 +412,8 @@ function setOpenAIMessages(chat) { // Apply the "wrap in quotes" option if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; 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++; } } @@ -592,7 +596,7 @@ export function isOpenRouterWithInstruct() { * @param type * @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')); 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'); } + const imageInlining = isImageInliningSupported(); + // 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 const prompt = new Prompt(chatPrompt); prompt.identifier = `chatHistory-${openai_msgs.length - index}`; @@ -641,10 +650,16 @@ function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = chatMessage.setName(messageName); } - if (chatCompletion.canAfford(chatMessage)) chatCompletion.insertAtStart(chatMessage, 'chatHistory'); - else return false; - return true; - }); + if (imageInlining && chatPrompt.image) { + await chatMessage.addImage(chatPrompt.image); + } + + if (chatCompletion.canAfford(chatMessage)) { + chatCompletion.insertAtStart(chatMessage, 'chatHistory'); + } else { + break; + } + } // Insert and free new chat chatCompletion.freeBudget(newChatMessage); @@ -724,7 +739,7 @@ function getPromptPosition(position) { * @param {string} options.quietPrompt - Instruction prompt for extras * @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 const addToChatCompletion = (source, target = null) => { // 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 if (power_user.pin_examples) { populateDialogueExamples(prompts, chatCompletion); - populateChatHistory(prompts, chatCompletion, type, cyclePrompt); + await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); } else { - populateChatHistory(prompts, chatCompletion, type, cyclePrompt); + await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); populateDialogueExamples(prompts, chatCompletion); } @@ -969,7 +984,7 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor * @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. */ -function prepareOpenAIMessages({ +export async function prepareOpenAIMessages({ name2, charDescription, charPersonality, @@ -1012,7 +1027,7 @@ function prepareOpenAIMessages({ }); // 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) { if (error instanceof TokenBudgetExceededError) { 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), }; + // 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 if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) { validateReverseProxy(); @@ -1640,7 +1665,18 @@ class InvalidCharacterNameError extends Error { * Used for creating, managing, and interacting with a specific message object. */ 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 @@ -1665,6 +1701,30 @@ class Message { 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. * @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.proxy_password = settings.proxy_password ?? default_settings.proxy_password; 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.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); $('#openai_proxy_password').val(oai_settings.proxy_password); $('#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 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, use_alt_scale: settings.use_alt_scale, squash_system_messages: settings.squash_system_messages, + image_inlining: settings.image_inlining, }; const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, { @@ -2741,6 +2804,7 @@ function onSettingsPresetChange() { exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true], squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true], + image_inlining: ['#openai_image_inlining', 'image_inlining', true], }; const presetName = $('#settings_preset_openai').find(":selected").text(); @@ -2785,6 +2849,9 @@ function getMaxContextOpenAI(value) { else if (value.includes('gpt-4-1106')) { return max_128k; } + else if (value.includes('gpt-4-vision')) { + return max_128k; + } else if (value.includes('gpt-3.5-turbo-1106')) { return max_16k; } @@ -2831,6 +2898,9 @@ function getMaxContextWindowAI(value) { else if (value.includes('gpt-4-1106')) { return max_128k; } + else if (value.includes('gpt-4-vision')) { + return max_128k; + } else if (value.includes('gpt-4-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 () { $('#test_api_button').on('click', testApiConnection); @@ -3463,6 +3558,11 @@ $(document).ready(async function () { saveSettingsDebounced(); }); + $('#openai_image_inlining').on('input', function () { + oai_settings.image_inlining = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + $(document).on('input', '#openai_settings .autoSetHeight', function () { resetScrollHeight($(this)); }); diff --git a/public/scripts/personas.js b/public/scripts/personas.js index fc91e8008..8a3f0341d 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -39,7 +39,23 @@ async function uploadUserAvatar(url, name) { } async function createDummyPersona() { - await uploadUserAvatar(default_avatar); + const personaName = await callPopup('

Enter a name for this persona:

', '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) { diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 61266b61f..d2ec1c0c2 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -220,6 +220,7 @@ let power_user = { encode_tags: false, servers: [], bogus_folders: false, + aux_field: 'character_version', }; let themes = []; @@ -1257,6 +1258,7 @@ function loadPowerUserSettings(settings, data) { $(`#chat_display option[value=${power_user.chat_display}]`).attr("selected", true).trigger('change'); $('#chat_width_slider').val(power_user.chat_width); $("#token_padding").val(power_user.token_padding); + $("#aux_field").val(power_user.aux_field); $("#font_scale").val(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale); @@ -1357,7 +1359,7 @@ function loadMaxContextUnlocked() { } 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 minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin; const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep; @@ -1366,7 +1368,7 @@ function switchMaxContextSize() { element.attr('max', maxValue); element.attr('step', steps); - if (element.attr('id') == 'max_context') { + if (element.attr('id').indexOf('max_context') !== -1) { element.attr('min', minValue); } const value = Number(element.val()); @@ -1651,7 +1653,17 @@ function sortEntitiesList(entities) { 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() { @@ -2827,6 +2839,13 @@ $(document).ready(() => { 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 () { const functionId = $(this).data('debug-function'); const functionRecord = debug_functions.find(f => f.functionId === functionId); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index bbc782c70..b1af99469 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -26,7 +26,7 @@ import { setCharacterName, } from "../script.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 { chat_styles, power_user } from "./power-user.js"; import { autoSelectPersona } from "./personas.js"; @@ -270,7 +270,12 @@ async function unhideMessageCallback(_, arg) { async function triggerGroupMessageCallback(_, arg) { 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; } diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 02425f644..a746bb7b7 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -38,14 +38,13 @@ export const tag_filter_types = { }; 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' }, 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' }, } 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 = [ @@ -321,9 +320,9 @@ function appendTagToList(listElement, tag, { removable, selectable, action, isGe tagElement.on('click', () => action.bind(tagElement)(filter)); tagElement.addClass('actionable'); } - if (action && tag.id === 2) { + /*if (action && tag.id === 2) { tagElement.addClass('innerActionable hidden'); - } + }*/ $(listElement).append(tagElement); } diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index bd58ae1fe..dc2844439 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -1,26 +1,26 @@ System-wide Replacement Macros (in order of evaluation):
    -
  • {{original}} - global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
  • -
  • {{input}} - the user input
  • -
  • {{description}} - the Character's Description
  • -
  • {{personality}} - the Character's Personality
  • -
  • {{scenario}} - the Character's Scenario
  • -
  • {{persona}} - your current Persona Description
  • -
  • {{mesExamples}} - the Character's Dialogue Examples
  • -
  • {{user}} - your current Persona username
  • -
  • {{char}} - the Character's name
  • -
  • {{lastMessageId}} - index # of the latest chat message. Useful for slash command batching.
  • -
  • {{// (note)}} - you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.
  • -
  • {{time}} - the current time
  • -
  • {{date}} - the current date
  • -
  • {{weekday}} - the current weekday
  • -
  • {{isotime}} - the current ISO date (YYYY-MM-DD)
  • -
  • {{isodate}} - the current ISO time (24-hour clock)
  • -
  • {{datetimeformat …}} - the current date/time in the specified format, e. g. for German date/time: {{datetimeformat DD.MM.YYYY HH:mm}}
  • -
  • {{time_UTC±#}} - the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2
  • -
  • {{idle_duration}} - the time since the last user message was sent
  • -
  • {{bias "text here"}} - sets a behavioral bias for the AI until the next user input. Quotes around the text are important.
  • -
  • {{random:(args)}} - returns a random item from the list. (ex: {{random:1,2,3,4}} will return 1 of the 4 numbers at random. Works with text lists too.
  • -
  • {{roll:(formula)}} - rolls a dice. (ex: {{roll:1d6}} will roll a 6-sided dice and return a number between 1 and 6)
  • -
  • {{banned "text here"}} - 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.
  • +
  • {{original}} – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
  • +
  • {{input}} – the user input
  • +
  • {{description}} – the Character's Description
  • +
  • {{personality}} – the Character's Personality
  • +
  • {{scenario}} – the Character's Scenario
  • +
  • {{persona}} – your current Persona Description
  • +
  • {{mesExamples}} – the Character's Dialogue Examples
  • +
  • {{user}} – your current Persona username
  • +
  • {{char}} – the Character's name
  • +
  • {{lastMessageId}} – index # of the latest chat message. Useful for slash command batching.
  • +
  • {{// (note)}} – you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.
  • +
  • {{time}} – the current time
  • +
  • {{date}} – the current date
  • +
  • {{weekday}} – the current weekday
  • +
  • {{isotime}} – the current ISO date (YYYY-MM-DD)
  • +
  • {{isodate}} – the current ISO time (24-hour clock)
  • +
  • {{datetimeformat …}} – the current date/time in the specified format, e. g. for German date/time: {{datetimeformat DD.MM.YYYY HH:mm}}
  • +
  • {{time_UTC±#}} – the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2
  • +
  • {{idle_duration}} – the time since the last user message was sent
  • +
  • {{bias "text here"}} – sets a behavioral bias for the AI until the next user input. Quotes around the text are important.
  • +
  • {{random:(args)}} – returns a random item from the list. (ex: {{random:1,2,3,4}} will return 1 of the 4 numbers at random. Works with text lists too.
  • +
  • {{roll:(formula)}} – rolls a dice. (ex: {{roll:1d6}} will roll a 6- sided dice and return a number between 1 and 6)
  • +
  • {{banned "text here"}} – 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.
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 7eee1fc8c..f5f3c60f5 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -269,7 +269,15 @@ function sortEntries(data) { const sortRule = option.data('rule'); 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 data.sort((a, b) => { const aValue = a.constant ? 0 : a.disable ? 2 : 1; @@ -375,7 +383,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { nextText: '>', formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, - callback: function (page) { + callback: function (/** @type {object[]} */ page) { $("#world_popup_entries_list").empty(); const keywordHeaders = `
@@ -399,6 +407,12 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
` 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(blocks); }, @@ -500,6 +514,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { delay: getSortableDelay(), handle: ".drag-handle", 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) { const uid = $(this).data('uid'); @@ -511,8 +527,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { return; } - item.displayIndex = index; - setOriginalDataValue(data, uid, 'extensions.display_index', index); + item.displayIndex = minDisplayIndex + 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 }))); @@ -587,7 +603,7 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, "keys", data.entries[uid].key); saveWorldInfo(name, data); }); - keyInput.val(entry.key.join(",")).trigger("input"); + keyInput.val(entry.key.join(", ")).trigger("input"); //initScrollHeight(keyInput); // logic AND/NOT @@ -708,7 +724,7 @@ function getWorldEntry(name, data, entry) { saveWorldInfo(name, data); }); - keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input"); + keySecondaryInput.val(entry.keysecondary.join(", ")).trigger("input"); initScrollHeight(keySecondaryInput); // comment @@ -1582,9 +1598,7 @@ async function checkWorldInfo(chat, maxContext) { over_max = ( world_info_min_activations_depth_max > 0 && minActivationMsgIndex > world_info_min_activations_depth_max - ) || ( - minActivationMsgIndex >= chat.length - ) + ) || (minActivationMsgIndex >= chat.length) if (!over_max) { needsToScan = true textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join("")); @@ -2167,11 +2181,9 @@ jQuery(() => { updateEditor(navigation_option.previous); }); - $('#world_info_sort_order').on('change', function (e) { - if (e.target instanceof HTMLOptionElement) { - localStorage.setItem(SORT_ORDER_KEY, e.target.value); - } - + $('#world_info_sort_order').on('change', function () { + const value = String($(this).find(":selected").val()); + localStorage.setItem(SORT_ORDER_KEY, value); updateEditor(navigation_option.none); }) diff --git a/src/openai.js b/src/openai.js index aa2445ada..857c0198f 100644 --- a/src/openai.js +++ b/src/openai.js @@ -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) => { try { const key = readSecret(SECRET_KEYS.OPENAI);