Merge branch 'staging' into pm-i18n

This commit is contained in:
Cohee 2024-05-24 21:56:34 +03:00
commit 8bcb1ef2db
33 changed files with 837 additions and 167 deletions

4
.github/readme.md vendored
View File

@ -229,7 +229,9 @@ You will need two mandatory directory mappings and a port mapping to allow Silly
#### Install command #### Install command
1. Open your Command Line 1. Open your Command Line
2. Run the following command `docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]' ` 2. Run the following command
`docker create --name='sillytavern' --net='[DockerNet]' -e TZ="[TimeZone]" -p '8000:8000/tcp' -v '[plugins]':'/home/node/app/plugins':'rw' -v '[config]':'/home/node/app/config':'rw' -v '[data]':'/home/node/app/data':'rw' 'ghcr.io/sillytavern/sillytavern:[version]'`
> Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config. > Note that 8000 is a default listening port. Don't forget to use an appropriate port if you change it in the config.

View File

@ -1224,6 +1224,11 @@
<input class="neo-range-slider" type="range" id="rep_pen_range_textgenerationwebui" name="volume" min="-1" max="8192" step="1"> <input class="neo-range-slider" type="range" id="rep_pen_range_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
<input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui"> <input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui">
</div> </div>
<div data-tg-type="tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="rep.pen decay">Rep Pen Decay</small>
<input class="neo-range-slider" type="range" id="rep_pen_decay_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
<input class="neo-range-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_decay_textgenerationwebui" id="rep_pen_decay_counter_textgenerationwebui">
</div>
<div data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0"> <div data-tg-type="ooba" data-newbie-hidden class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Encoder Rep. Pen.">Encoder Penalty</small> <small data-i18n="Encoder Rep. Pen.">Encoder Penalty</small>
<input class="neo-range-slider" type="range" id="encoder_rep_pen_textgenerationwebui" name="volume" min="0.8" max="1.5" step="0.01" /> <input class="neo-range-slider" type="range" id="encoder_rep_pen_textgenerationwebui" name="volume" min="0.8" max="1.5" step="0.01" />
@ -1244,6 +1249,11 @@
<input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1"> <input class="neo-range-slider" type="range" id="no_repeat_ngram_size_textgenerationwebui" name="volume" min="0" max="20" step="1">
<input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui"> <input class="neo-range-input" type="number" min="0" max="20" step="1" data-for="no_repeat_ngram_size_textgenerationwebui" id="no_repeat_ngram_size_counter_textgenerationwebui">
</div> </div>
<div data-newbie-hidden data-tg-type="tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Skew">Skew</small>
<input class="neo-range-slider" type="range" id="skew_textgenerationwebui" name="volume" min="-5" max="5" step="0.01" />
<input class="neo-range-input" type="number" min="-5" max="5" step="0.01" data-for="skew_textgenerationwebui" id="skew_counter_textgenerationwebui">
</div>
<div data-newbie-hidden data-tg-type="mancer, ooba, tabby, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0"> <div data-newbie-hidden data-tg-type="mancer, ooba, tabby, dreamgen" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Min Length">Min Length</small> <small data-i18n="Min Length">Min Length</small>
<input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" /> <input class="neo-range-slider" type="range" id="min_length_textgenerationwebui" name="volume" min="0" max="2000" step="1" />
@ -1272,6 +1282,45 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Enable for llama.cpp when the PR is merged: https://github.com/ggerganov/llama.cpp/pull/6839 -->
<div data-newbie-hidden data-tg-type="ooba" id="dryBlock" class="wide100p">
<h4 class="wide100p textAlignCenter" title="DRY penalizes tokens that would extend the end of the input into a sequence that has previously occurred in the input. Set multiplier to 0 to disable.">
<label data-i18n="DRY Repetition Penalty">DRY Repetition Penalty</label>
<a href="https://github.com/oobabooga/text-generation-webui/pull/5677" target="_blank">
<div class=" fa-solid fa-circle-info opacity50p"></div>
</a>
</h4>
<div class="flex-container flexFlowRow gap10px flexShrink">
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" title="Set to value > 0 to enable DRY. Controls the magnitude of the penalty for the shortest penalized sequences.">
<small data-i18n="Multiplier">Multiplier</small>
<input class="neo-range-slider" type="range" id="dry_multiplier_textgenerationwebui" min="0" max="5" step="0.01" />
<input class="neo-range-input" type="number" min="0" max="5" step="0.01" data-for="dry_multiplier_textgenerationwebui" id="dry_multiplier_counter_textgenerationwebui">
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" title="Controls how fast the penalty grows with increasing sequence length.">
<small data-i18n="Base">Base</small>
<input class="neo-range-slider" type="range" id="dry_base_textgenerationwebui" min="1" max="4" step="0.01" />
<input class="neo-range-input" type="number" min="1" max="4" step="0.01" data-for="dry_base_textgenerationwebui" id="dry_base_counter_textgenerationwebui">
</div>
<div class="alignitemscenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" title="Longest sequence that can be repeated without being penalized.">
<small data-i18n="Allowed Length">Allowed Length</small>
<input class="neo-range-slider" type="range" id="dry_allowed_length_textgenerationwebui" min="1" max="20" step="1" />
<input class="neo-range-input" type="number" min="1" max="20" step="1" data-for="dry_allowed_length_textgenerationwebui" id="dry_allowed_length_counter_textgenerationwebui">
</div>
<div class="alignItemsCenter flex-container flexFlowColumn flexBasis48p flexGrow flexShrink gap0" data-tg-type="llamacpp">
<small data-i18n="Penalty Range">Penalty Range</small>
<input class="neo-range-slider" type="range" id="dry_penalty_last_n_textgenerationwebui" min="0" max="8192" step="1" />
<input class="neo-range-input" type="number" min="0" max="8192" step="1" data-for="dry_penalty_last_n_textgenerationwebui" id="dry_penalty_last_n_counter_textgenerationwebui">
</div>
</div>
<div class="range-block marginTop5" title="Tokens across which sequence matching is not continued. Specified as a comma-separated list of quoted strings.">
<div class="range-block-title textAlignCenter">
<small data-i18n="Sequence Breakers">Sequence Breakers</small>
</div>
<div class="wide100p">
<textarea id="dry_sequence_breakers_textgenerationwebui" class="text_pole textarea_compact" name="sequence_breakers" rows="3" data-i18n="[placeholder]JSON-serialized array of strings." placeholder="JSON-serialized array of strings."></textarea>
</div>
</div>
</div>
<div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p"> <div data-newbie-hidden data-tg-type="ooba, mancer, koboldcpp, tabby, llamacpp, aphrodite" id="dynatemp_block_ooba" class="wide100p">
<h4 class="wide100p textAlignCenter"> <h4 class="wide100p textAlignCenter">
<div class="flex-container alignitemscenter justifyCenter"> <div class="flex-container alignitemscenter justifyCenter">
@ -1405,6 +1454,13 @@
<div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Use the temperature sampler last" title="Use the temperature sampler last. This is almost always the sensible thing to do.&#13;When enabled: sample the set of plausible tokens first, then apply temperature to adjust their relative probabilities (technically, logits).&#13;When disabled: apply temperature to adjust the relative probabilities of ALL tokens first, then sample plausible tokens from that.&#13;Disabling Temperature Last boosts the probabilities in the tail of the distribution, which tends to amplify the chances of getting an incoherent response."></div> <div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Use the temperature sampler last" title="Use the temperature sampler last. This is almost always the sensible thing to do.&#13;When enabled: sample the set of plausible tokens first, then apply temperature to adjust their relative probabilities (technically, logits).&#13;When disabled: apply temperature to adjust the relative probabilities of ALL tokens first, then sample plausible tokens from that.&#13;Disabling Temperature Last boosts the probabilities in the tail of the distribution, which tends to amplify the chances of getting an incoherent response."></div>
</label> </label>
</label> </label>
<label data-tg-type="tabby" class="checkbox_label flexGrow flexShrink" for="speculative_ngram_textgenerationwebui">
<input type="checkbox" id="speculative_ngram_textgenerationwebui" />
<label>
<small data-i18n="Speculative Ngram">Speculative Ngram</small>
<div class="fa-solid fa-circle-info opacity50p " data-i18n="[title]Use a different speculative decoding method without a draft model" title="Use a different speculative decoding method without a draft model.&#13;Using a draft model is preferred. Speculative ngram is not as effective."></div>
</label>
</label>
<label data-tg-type="vllm, aphrodite" class="checkbox_label" for="spaces_between_special_tokens_textgenerationwebui"> <label data-tg-type="vllm, aphrodite" class="checkbox_label" for="spaces_between_special_tokens_textgenerationwebui">
<input type="checkbox" id="spaces_between_special_tokens_textgenerationwebui" /> <input type="checkbox" id="spaces_between_special_tokens_textgenerationwebui" />
@ -2297,7 +2353,7 @@
<option value="windowai">Window AI</option> <option value="windowai">Window AI</option>
</optgroup> </optgroup>
</select> </select>
<div data-newbie-hidden class="inline-drawer wide100p" data-source="openai,claude,mistralai"> <div data-newbie-hidden class="inline-drawer wide100p" data-source="openai,claude,mistralai,makersuite">
<div class="inline-drawer-toggle inline-drawer-header"> <div class="inline-drawer-toggle inline-drawer-header">
<b data-i18n="Reverse Proxy">Reverse Proxy</b> <b data-i18n="Reverse Proxy">Reverse Proxy</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div> <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
@ -2765,6 +2821,7 @@
<h4 data-i18n="Cohere Model">Cohere Model</h4> <h4 data-i18n="Cohere Model">Cohere Model</h4>
<select id="model_cohere_select"> <select id="model_cohere_select">
<optgroup label="Stable"> <optgroup label="Stable">
<option value="c4ai-aya-23">c4ai-aya-23</option>
<option value="command-light">command-light</option> <option value="command-light">command-light</option>
<option value="command">command</option> <option value="command">command</option>
<option value="command-r">command-r</option> <option value="command-r">command-r</option>

View File

@ -154,6 +154,7 @@ import {
isValidUrl, isValidUrl,
ensureImageFormatSupported, ensureImageFormatSupported,
flashHighlight, flashHighlight,
checkOverwriteExistingData,
} from './scripts/utils.js'; } from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js'; import { debounce_timeout } from './scripts/constants.js';
@ -684,6 +685,7 @@ export function reloadMarkdownProcessor(render_formulas = false) {
parseImgDimensions: true, parseImgDimensions: true,
simpleLineBreaks: true, simpleLineBreaks: true,
strikethrough: true, strikethrough: true,
disableForced4SpacesIndentedSublists: true,
extensions: [ extensions: [
showdownKatex( showdownKatex(
{ {
@ -704,6 +706,7 @@ export function reloadMarkdownProcessor(render_formulas = false) {
underline: true, underline: true,
simpleLineBreaks: true, simpleLineBreaks: true,
strikethrough: true, strikethrough: true,
disableForced4SpacesIndentedSublists: true,
extensions: [markdownUnderscoreExt()], extensions: [markdownUnderscoreExt()],
}); });
} }
@ -6463,7 +6466,8 @@ export async function getChatsFromFiles(data, isGroupChat) {
* @param {null|number} [characterId=null] - When set, the function will use this character id instead of this_chid. * @param {null|number} [characterId=null] - When set, the function will use this character id instead of this_chid.
* *
* @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted * @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted
* in descending order by file name. Returns `undefined` if the fetch request is unsuccessful. * in descending order by file name. Returns an empty array if the fetch request is unsuccessful or the
* response is an object with an `error` property set to `true`.
*/ */
export async function getPastCharacterChats(characterId = null) { export async function getPastCharacterChats(characterId = null) {
characterId = characterId ?? this_chid; characterId = characterId ?? this_chid;
@ -6479,10 +6483,13 @@ export async function getPastCharacterChats(characterId = null) {
return []; return [];
} }
let data = await response.json(); const data = await response.json();
data = Object.values(data); if (typeof data === 'object' && data.error === true) {
data = data.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse(); return [];
return data; }
const chats = Object.values(data);
return chats.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse();
} }
/** /**
@ -8013,12 +8020,14 @@ const swipe_right = () => {
const CONNECT_API_MAP = { const CONNECT_API_MAP = {
'kobold': { 'kobold': {
selected: 'kobold',
button: '#api_button', button: '#api_button',
}, },
'horde': { 'horde': {
selected: 'koboldhorde', selected: 'koboldhorde',
}, },
'novel': { 'novel': {
selected: 'novel',
button: '#api_button_novel', button: '#api_button_novel',
}, },
'ooba': { 'ooba': {
@ -8056,6 +8065,11 @@ const CONNECT_API_MAP = {
button: '#api_button_textgenerationwebui', button: '#api_button_textgenerationwebui',
type: textgen_types.APHRODITE, type: textgen_types.APHRODITE,
}, },
'koboldcpp': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
type: textgen_types.KOBOLDCPP,
},
'kcpp': { 'kcpp': {
selected: 'textgenerationwebui', selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui', button: '#api_button_textgenerationwebui',
@ -8066,6 +8080,11 @@ const CONNECT_API_MAP = {
button: '#api_button_textgenerationwebui', button: '#api_button_textgenerationwebui',
type: textgen_types.TOGETHERAI, type: textgen_types.TOGETHERAI,
}, },
'openai': {
selected: 'openai',
button: '#api_button_openai',
source: chat_completion_sources.OPENAI,
},
'oai': { 'oai': {
selected: 'openai', selected: 'openai',
button: '#api_button_openai', button: '#api_button_openai',
@ -8193,7 +8212,29 @@ async function disableInstructCallback() {
* @param {string} text API name * @param {string} text API name
*/ */
async function connectAPISlash(_, text) { async function connectAPISlash(_, text) {
if (!text) return; if (!text.trim()) {
for (const [key, config] of Object.entries(CONNECT_API_MAP)) {
if (config.selected !== main_api) continue;
if (config.source) {
if (oai_settings.chat_completion_source === config.source) {
return key;
} else {
continue;
}
}
if (config.type) {
if (textgen_settings.type === config.type) {
return key;
} else {
continue;
}
}
return key;
}
}
const apiConfig = CONNECT_API_MAP[text.toLowerCase()]; const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
if (!apiConfig) { if (!apiConfig) {
@ -8453,11 +8494,28 @@ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats)
return; return;
} }
const avatar = characters[this_chid].avatar; await deleteCharacter(characters[this_chid].avatar, { deleteChats: delete_chats });
const name = characters[this_chid].name; }
const pastChats = await getPastCharacterChats();
const msg = { avatar_url: avatar, delete_chats: delete_chats }; /**
* Deletes a character completely, including associated chats if specified
*
* @param {string} characterKey - The key (avatar) of the character to be deleted
* @param {Object} [options] - Optional parameters for the deletion
* @param {boolean} [options.deleteChats=true] - Whether to delete associated chats or not
* @return {Promise<void>} - A promise that resolves when the character is successfully deleted
*/
export async function deleteCharacter(characterKey, { deleteChats = true } = {}) {
const character = characters.find(x => x.avatar == characterKey);
if (!character) {
toastr.warning(`Character ${characterKey} not found. Cannot be deleted.`);
return;
}
const chid = characters.indexOf(character);
const pastChats = await getPastCharacterChats(chid);
const msg = { avatar_url: character.avatar, delete_chats: deleteChats };
const response = await fetch('/api/characters/delete', { const response = await fetch('/api/characters/delete', {
method: 'POST', method: 'POST',
@ -8466,18 +8524,18 @@ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats)
cache: 'no-cache', cache: 'no-cache',
}); });
if (response.ok) { if (!response.ok) {
await deleteCharacter(name, avatar); throw new Error(`Failed to delete character: ${response.status} ${response.statusText}`);
}
if (delete_chats) { await removeCharacterFromUI(character.name, character.avatar);
if (deleteChats) {
for (const chat of pastChats) { for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', ''); const name = chat.file_name.replace('.jsonl', '');
await eventSource.emit(event_types.CHAT_DELETED, name); await eventSource.emit(event_types.CHAT_DELETED, name);
} }
} }
} else {
console.error('Failed to delete character: ', response.status, response.statusText);
}
} }
/** /**
@ -8493,7 +8551,7 @@ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats)
* @param {string} avatar - The avatar URL of the character to be deleted. * @param {string} avatar - The avatar URL of the character to be deleted.
* @param {boolean} reloadCharacters - Whether the character list should be refreshed after deletion. * @param {boolean} reloadCharacters - Whether the character list should be refreshed after deletion.
*/ */
export async function deleteCharacter(name, avatar, reloadCharacters = true) { async function removeCharacterFromUI(name, avatar, reloadCharacters = true) {
await clearChat(); await clearChat();
$('#character_cross').click(); $('#character_cross').click();
this_chid = undefined; this_chid = undefined;
@ -8622,7 +8680,7 @@ jQuery(async function () {
], ],
helpString: ` helpString: `
<div> <div>
Connect to an API. Connect to an API. If no argument is provided, it will return the currently connected API.
</div> </div>
<div> <div>
<strong>Available APIs:</strong> <strong>Available APIs:</strong>
@ -9971,6 +10029,7 @@ jQuery(async function () {
a.setAttribute('download', filename); a.setAttribute('download', filename);
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
URL.revokeObjectURL(a.href);
document.body.removeChild(a); document.body.removeChild(a);
} }

View File

@ -4,7 +4,6 @@ import {
characterGroupOverlay, characterGroupOverlay,
callPopup, callPopup,
characters, characters,
deleteCharacter,
event_types, event_types,
eventSource, eventSource,
getCharacters, getCharacters,
@ -13,6 +12,7 @@ import {
buildAvatarList, buildAvatarList,
characterToEntity, characterToEntity,
printCharactersDebounced, printCharactersDebounced,
deleteCharacter,
} from '../script.js'; } from '../script.js';
import { favsToHotswap } from './RossAscends-mods.js'; import { favsToHotswap } from './RossAscends-mods.js';
@ -115,24 +115,7 @@ class CharacterContextMenu {
static delete = async (characterId, deleteChats = false) => { static delete = async (characterId, deleteChats = false) => {
const character = CharacterContextMenu.#getCharacter(characterId); const character = CharacterContextMenu.#getCharacter(characterId);
return fetch('/api/characters/delete', { await deleteCharacter(character.avatar, { deleteChats: deleteChats });
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: character.avatar, delete_chats: deleteChats }),
cache: 'no-cache',
}).then(response => {
if (response.ok) {
eventSource.emit(event_types.CHARACTER_DELETED, { id: characterId, character: character });
return deleteCharacter(character.name, character.avatar, false).then(() => {
if (deleteChats) getPastCharacterChats(characterId).then(pastChats => {
for (const chat of pastChats) {
const name = chat.file_name.replace('.jsonl', '');
eventSource.emit(event_types.CHAT_DELETED, name);
}
});
});
}
});
}; };
static #getCharacter = (characterId) => characters[characterId] ?? null; static #getCharacter = (characterId) => characters[characterId] ?? null;

View File

@ -348,8 +348,8 @@ jQuery(function () {
(modules.includes('caption') && extension_settings.caption.source === 'extras') || (modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && secret_state[SECRET_KEYS.CLAUDE]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) || (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
@ -465,7 +465,7 @@ jQuery(function () {
<option data-type="custom" value="custom_current">[Currently selected]</option> <option data-type="custom" value="custom_current">[Currently selected]</option>
</select> </select>
</div> </div>
<label data-type="openai,anthropic" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid."> <label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox"> <input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
Allow reverse proxy Allow reverse proxy
</label> </label>

View File

@ -357,6 +357,7 @@ export class SettingsUi {
a.download = `${this.currentQrSet.name}.json`; a.download = `${this.currentQrSet.name}.json`;
a.click(); a.click();
} }
URL.revokeObjectURL(url);
} }
selectQrSet(qrs) { selectQrSet(qrs) {

View File

@ -12,7 +12,13 @@ import { createThumbnail, isValidUrl } from '../utils.js';
* @returns {Promise<string>} Generated caption * @returns {Promise<string>} Generated caption
*/ */
export async function getMultimodalCaption(base64Img, prompt) { export async function getMultimodalCaption(base64Img, prompt) {
throwIfInvalidModel(); const useReverseProxy =
(['openai', 'anthropic', 'google'].includes(extension_settings.caption.multimodal_api))
&& extension_settings.caption.allow_reverse_proxy
&& oai_settings.reverse_proxy
&& isValidUrl(oai_settings.reverse_proxy);
throwIfInvalidModel(useReverseProxy);
const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api); const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
@ -39,27 +45,18 @@ export async function getMultimodalCaption(base64Img, prompt) {
} }
} }
const useReverseProxy =
(extension_settings.caption.multimodal_api === 'openai' || extension_settings.caption.multimodal_api === 'anthropic')
&& extension_settings.caption.allow_reverse_proxy
&& oai_settings.reverse_proxy
&& isValidUrl(oai_settings.reverse_proxy);
const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : ''; const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
const proxyPassword = useReverseProxy ? oai_settings.proxy_password : ''; const proxyPassword = useReverseProxy ? oai_settings.proxy_password : '';
const requestBody = { const requestBody = {
image: base64Img, image: base64Img,
prompt: prompt, prompt: prompt,
reverse_proxy: proxyUrl,
proxy_password: proxyPassword,
api: extension_settings.caption.multimodal_api || 'openai',
model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',
}; };
if (!isGoogle) {
requestBody.api = extension_settings.caption.multimodal_api || 'openai';
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-turbo';
requestBody.reverse_proxy = proxyUrl;
requestBody.proxy_password = proxyPassword;
}
if (isOllama) { if (isOllama) {
if (extension_settings.caption.multimodal_model === 'ollama_current') { if (extension_settings.caption.multimodal_model === 'ollama_current') {
requestBody.model = textgenerationwebui_settings.ollama_model; requestBody.model = textgenerationwebui_settings.ollama_model;
@ -117,8 +114,8 @@ export async function getMultimodalCaption(base64Img, prompt) {
return String(caption).trim(); return String(caption).trim();
} }
function throwIfInvalidModel() { function throwIfInvalidModel(useReverseProxy) {
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) { if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI] && !useReverseProxy) {
throw new Error('OpenAI API key is not set.'); throw new Error('OpenAI API key is not set.');
} }
@ -126,7 +123,11 @@ function throwIfInvalidModel() {
throw new Error('OpenRouter API key is not set.'); throw new Error('OpenRouter API key is not set.');
} }
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE]) { if (extension_settings.caption.multimodal_api === 'anthropic' && !secret_state[SECRET_KEYS.CLAUDE] && !useReverseProxy) {
throw new Error('Anthropic (Claude) API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
throw new Error('MakerSuite API key is not set.'); throw new Error('MakerSuite API key is not set.');
} }

View File

@ -3160,7 +3160,9 @@ jQuery(async () => {
} }
eventSource.on(event_types.EXTRAS_CONNECTED, async () => { eventSource.on(event_types.EXTRAS_CONNECTED, async () => {
if (extension_settings.sd.source === sources.extras) {
await loadSettingOptions(); await loadSettingOptions();
}
}); });
eventSource.on(event_types.CHAT_CHANGED, onChatChanged); eventSource.on(event_types.CHAT_CHANGED, onChatChanged);

View File

@ -0,0 +1,207 @@
import { callPopup, getRequestHeaders } from '../../../script.js';
import { SECRET_KEYS, findSecret, secret_state, writeSecret } from '../../secrets.js';
import { getPreviewString, saveTtsProviderSettings } from './index.js';
export { AzureTtsProvider };
class AzureTtsProvider {
//########//
// Config //
//########//
settings;
voices = [];
separator = ' . ';
audioElement = document.createElement('audio');
defaultSettings = {
region: '',
voiceMap: {},
};
get settingsHtml() {
let html = `
<div class="azure_tts_settings">
<div class="flex-container alignItemsBaseline">
<h4 for="azure_tts_key" class="flex1 margin0">
<a href="https://portal.azure.com/" target="_blank">Azure TTS Key</a>
</h4>
<div id="azure_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i>
<span>Click to set</span>
</div>
</div>
<label for="azure_tts_region">Region:</label>
<input id="azure_tts_region" type="text" class="text_pole" placeholder="e.g. westus" />
<hr>
</div>
`;
return html;
}
onSettingsChange() {
// Update dynamically
this.settings.region = String($('#azure_tts_region').val());
// Reset voices
this.voices = [];
saveTtsProviderSettings();
}
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}`;
}
}
$('#azure_tts_region').val(this.settings.region).on('input', () => this.onSettingsChange());
$('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
$('#azure_tts_key').on('click', async () => {
const popupText = 'Azure TTS API Key';
const savedKey = secret_state[SECRET_KEYS.AZURE_TTS] ? await findSecret(SECRET_KEYS.AZURE_TTS) : '';
const key = await callPopup(popupText, 'input', savedKey);
if (key == false || key == '') {
return;
}
await writeSecret(SECRET_KEYS.AZURE_TTS, key);
toastr.success('API Key saved');
$('#azure_tts_key').addClass('success');
await this.onRefreshClick();
});
try {
await this.checkReady();
console.debug('Azure: Settings loaded');
} catch {
console.debug('Azure: Settings loaded, but not ready');
}
}
// Perform a simple readiness check by trying to fetch voiceIds
async checkReady() {
if (secret_state[SECRET_KEYS.AZURE_TTS]) {
await this.fetchTtsVoiceObjects();
} else {
this.voices = [];
}
}
async onRefreshClick() {
await this.checkReady();
}
//#################//
// TTS Interfaces //
//#################//
async getVoice(voiceName) {
if (this.voices.length == 0) {
this.voices = await this.fetchTtsVoiceObjects();
}
const match = this.voices.filter(
voice => voice.name == voiceName,
)[0];
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
}
return match;
}
async generateTts(text, voiceId) {
const response = await this.fetchTtsGeneration(text, voiceId);
return response;
}
//###########//
// API CALLS //
//###########//
async fetchTtsVoiceObjects() {
if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
console.warn('Azure TTS API Key not set');
return [];
}
if (!this.settings.region) {
console.warn('Azure TTS region not set');
return [];
}
const response = await fetch('/api/azure/list', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
region: this.settings.region,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
let responseJson = await response.json();
responseJson = responseJson
.sort((a, b) => a.Locale.localeCompare(b.Locale) || a.ShortName.localeCompare(b.ShortName))
.map(x => ({ name: x.ShortName, voice_id: x.ShortName, preview_url: false, lang: x.Locale }));
return responseJson;
}
/**
* Preview TTS for a given voice ID.
* @param {string} id Voice ID
*/
async previewTtsVoice(id) {
this.audioElement.pause();
this.audioElement.currentTime = 0;
const voice = await this.getVoice(id);
const text = getPreviewString(voice.lang);
const response = await this.fetchTtsGeneration(text, id);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const audio = await response.blob();
const url = URL.createObjectURL(audio);
this.audioElement.src = url;
this.audioElement.play();
URL.revokeObjectURL(url);
}
async fetchTtsGeneration(text, voiceId) {
if (!secret_state[SECRET_KEYS.AZURE_TTS]) {
throw new Error('Azure TTS API Key not set');
}
if (!this.settings.region) {
throw new Error('Azure TTS region not set');
}
const response = await fetch('/api/azure/generate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
text: text,
voice: voiceId,
region: this.settings.region,
}),
});
if (!response.ok) {
toastr.error(response.statusText, 'TTS Generation Failed');
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
return response;
}
}

View File

@ -155,6 +155,7 @@ class EdgeTtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url);
} }
/** /**

View File

@ -13,6 +13,7 @@ import { XTTSTtsProvider } from './xtts.js';
import { GSVITtsProvider } from './gsvi.js'; import { GSVITtsProvider } from './gsvi.js';
import { AllTalkTtsProvider } from './alltalk.js'; import { AllTalkTtsProvider } from './alltalk.js';
import { SpeechT5TtsProvider } from './speecht5.js'; import { SpeechT5TtsProvider } from './speecht5.js';
import { AzureTtsProvider } from './azure.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
@ -83,6 +84,7 @@ const ttsProviders = {
OpenAI: OpenAITtsProvider, OpenAI: OpenAITtsProvider,
AllTalk: AllTalkTtsProvider, AllTalk: AllTalkTtsProvider,
SpeechT5: SpeechT5TtsProvider, SpeechT5: SpeechT5TtsProvider,
Azure: AzureTtsProvider,
}; };
let ttsProvider; let ttsProvider;
let ttsProviderName; let ttsProviderName;

View File

@ -180,6 +180,7 @@ class NovelTtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url);
} }
async* fetchTtsGeneration(inputText, voiceId) { async* fetchTtsGeneration(inputText, voiceId) {

View File

@ -60,6 +60,7 @@ class SpeechT5TtsProvider {
const url = URL.createObjectURL(audio); const url = URL.createObjectURL(audio);
this.audioElement.src = url; this.audioElement.src = url;
this.audioElement.play(); this.audioElement.play();
URL.revokeObjectURL(url);
} }
async loadSettings(settings) { async loadSettings(settings) {

View File

@ -52,6 +52,7 @@ const settings = {
insert: 3, insert: 3,
query: 2, query: 2,
message_chunk_size: 400, message_chunk_size: 400,
score_threshold: 0.25,
// For files // For files
enabled_files: false, enabled_files: false,
@ -760,6 +761,7 @@ async function queryCollection(collectionId, searchText, topK) {
searchText: searchText, searchText: searchText,
topK: topK, topK: topK,
source: settings.source, source: settings.source,
threshold: settings.score_threshold,
}), }),
}); });
@ -788,6 +790,7 @@ async function queryMultipleCollections(collectionIds, searchText, topK) {
searchText: searchText, searchText: searchText,
topK: topK, topK: topK,
source: settings.source, source: settings.source,
threshold: settings.score_threshold,
}), }),
}); });
@ -1310,6 +1313,12 @@ jQuery(async () => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_score_threshold').val(settings.score_threshold).on('input', () => {
settings.score_threshold = Number($('#vectors_score_threshold').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI]; const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key'; const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$('#api_key_nomicai').attr('placeholder', placeholder); $('#api_key_nomicai').attr('placeholder', placeholder);

View File

@ -81,12 +81,20 @@
</div> </div>
</div> </div>
<div class="flex-container flexFlowColumn" title="How many last messages will be matched for relevance."> <div class="flex-container marginTopBot5">
<div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">
<label for="vectors_query"> <label for="vectors_query">
<span>Query messages</span> <span>Query messages</span>
</label> </label>
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" /> <input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
</div> </div>
<div class="flex-container flex1 flexFlowColumn" title="Cut-off score for relevance. Helps to filter out irrelevant data.">
<label for="vectors_query">
<span>Score threshold</span>
</label>
<input type="number" id="vectors_score_threshold" class="text_pole widthUnset" min="0" max="1" step="0.05" />
</div>
</div>
<div class="flex-container"> <div class="flex-container">
<label class="checkbox_label expander" for="vectors_include_wi" title="Query results can activate World Info entries."> <label class="checkbox_label expander" for="vectors_include_wi" title="Query results can activate World Info entries.">

View File

@ -1743,8 +1743,8 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.stop; delete generate_data.stop;
} }
// Proxy is only supported for Claude, OpenAI and Mistral // Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI].includes(oai_settings.chat_completion_source)) { if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE].includes(oai_settings.chat_completion_source)) {
validateReverseProxy(); validateReverseProxy();
generate_data['reverse_proxy'] = oai_settings.reverse_proxy; generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
generate_data['proxy_password'] = oai_settings.proxy_password; generate_data['proxy_password'] = oai_settings.proxy_password;
@ -3857,6 +3857,9 @@ async function onModelChange() {
else if (['command-r', 'command-r-plus'].includes(oai_settings.cohere_model)) { else if (['command-r', 'command-r-plus'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_128k); $('#openai_max_context').attr('max', max_128k);
} }
else if(['c4ai-aya-23'].includes(oai_settings.cohere_model)) {
$('#openai_max_context').attr('max', max_8k);
}
else { else {
$('#openai_max_context').attr('max', max_4k); $('#openai_max_context').attr('max', max_4k);
} }
@ -4035,7 +4038,7 @@ async function onConnectButtonClick(e) {
await writeSecret(SECRET_KEYS.MAKERSUITE, api_key_makersuite); await writeSecret(SECRET_KEYS.MAKERSUITE, api_key_makersuite);
} }
if (!secret_state[SECRET_KEYS.MAKERSUITE]) { if (!secret_state[SECRET_KEYS.MAKERSUITE] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for MakerSuite'); console.log('No secret key saved for MakerSuite');
return; return;
} }
@ -4087,7 +4090,7 @@ async function onConnectButtonClick(e) {
await writeSecret(SECRET_KEYS.MISTRALAI, api_key_mistralai); await writeSecret(SECRET_KEYS.MISTRALAI, api_key_mistralai);
} }
if (!secret_state[SECRET_KEYS.MISTRALAI]) { if (!secret_state[SECRET_KEYS.MISTRALAI] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for MistralAI'); console.log('No secret key saved for MistralAI');
return; return;
} }

View File

@ -678,6 +678,9 @@ async function CreateZenSliders(elmnt) {
sliderID == 'top_k' || sliderID == 'top_k' ||
sliderID == 'mirostat_mode_kobold' || sliderID == 'mirostat_mode_kobold' ||
sliderID == 'rep_pen_range' || sliderID == 'rep_pen_range' ||
sliderID == 'dry_allowed_length_textgenerationwebui' ||
sliderID == 'rep_pen_decay_textgenerationwebui' ||
sliderID == 'dry_penalty_last_n_textgenerationwebui' ||
sliderID == 'max_tokens_second_textgenerationwebui') { sliderID == 'max_tokens_second_textgenerationwebui') {
decimals = 0; decimals = 0;
} }
@ -685,7 +688,9 @@ async function CreateZenSliders(elmnt) {
sliderID == 'max_temp_textgenerationwebui' || sliderID == 'max_temp_textgenerationwebui' ||
sliderID == 'dynatemp_exponent_textgenerationwebui' || sliderID == 'dynatemp_exponent_textgenerationwebui' ||
sliderID == 'smoothing_curve_textgenerationwebui' || sliderID == 'smoothing_curve_textgenerationwebui' ||
sliderID == 'smoothing_factor_textgenerationwebui') { sliderID == 'smoothing_factor_textgenerationwebui' ||
sliderID == 'dry_multiplier_textgenerationwebui' ||
sliderID == 'dry_base_textgenerationwebui') {
decimals = 2; decimals = 2;
} }
if (sliderID == 'eta_cutoff_textgenerationwebui' || if (sliderID == 'eta_cutoff_textgenerationwebui' ||
@ -746,6 +751,8 @@ async function CreateZenSliders(elmnt) {
sliderID == 'rep_pen_slope' || sliderID == 'rep_pen_slope' ||
sliderID == 'smoothing_factor_textgenerationwebui' || sliderID == 'smoothing_factor_textgenerationwebui' ||
sliderID == 'smoothing_curve_textgenerationwebui' || sliderID == 'smoothing_curve_textgenerationwebui' ||
sliderID == 'skew_textgenerationwebui' ||
sliderID == 'dry_multiplier_textgenerationwebui' ||
sliderID == 'min_length_textgenerationwebui') { sliderID == 'min_length_textgenerationwebui') {
offVal = 0; offVal = 0;
} }
@ -1754,11 +1761,24 @@ function loadMaxContextUnlocked() {
} }
function switchMaxContextSize() { function switchMaxContextSize() {
const elements = [$('#max_context'), $('#max_context_counter'), $('#rep_pen_range'), $('#rep_pen_range_counter'), $('#rep_pen_range_textgenerationwebui'), $('#rep_pen_range_counter_textgenerationwebui')]; const elements = [
$('#max_context'),
$('#max_context_counter'),
$('#rep_pen_range'),
$('#rep_pen_range_counter'),
$('#rep_pen_range_textgenerationwebui'),
$('#rep_pen_range_counter_textgenerationwebui'),
$('#dry_penalty_last_n_textgenerationwebui'),
$('#dry_penalty_last_n_counter_textgenerationwebui'),
$('#rep_pen_decay_textgenerationwebui'),
$('#rep_pen_decay_counter_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;
$('#rep_pen_range_textgenerationwebui_zenslider').remove(); //unsure why, but this is necessary. $('#rep_pen_range_textgenerationwebui_zenslider').remove(); //unsure why, but this is necessary.
$('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
$('#rep_pen_decay_textgenerationwebui_zenslider').remove();
for (const element of elements) { for (const element of elements) {
const id = element.attr('id'); const id = element.attr('id');
element.attr('max', maxValue); element.attr('max', maxValue);
@ -1787,6 +1807,10 @@ function switchMaxContextSize() {
CreateZenSliders($('#max_context')); CreateZenSliders($('#max_context'));
$('#rep_pen_range_textgenerationwebui_zenslider').remove(); $('#rep_pen_range_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#rep_pen_range_textgenerationwebui')); CreateZenSliders($('#rep_pen_range_textgenerationwebui'));
$('#dry_penalty_last_n_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#dry_penalty_last_n_textgenerationwebui'));
$('#rep_pen_decay_textgenerationwebui_zenslider').remove();
CreateZenSliders($('#rep_pen_decay_textgenerationwebui'));
} }
} }
@ -3009,7 +3033,7 @@ $(document).ready(() => {
var coreTruthWinHeight = window.innerHeight; var coreTruthWinHeight = window.innerHeight;
$(window).on('resize', async () => { $(window).on('resize', async () => {
console.log(`Window resize: ${coreTruthWinWidth}x${coreTruthWinHeight} -> ${window.innerWidth}x${window.innerHeight}`) console.log(`Window resize: ${coreTruthWinWidth}x${coreTruthWinHeight} -> ${window.innerWidth}x${window.innerHeight}`);
adjustAutocompleteDebounced(); adjustAutocompleteDebounced();
setHotswapsDebounced(); setHotswapsDebounced();
@ -3062,7 +3086,7 @@ $(document).ready(() => {
} }
} }
} else { } else {
console.log('aborting MUI reset', Object.keys(power_user.movingUIState).length) console.log('aborting MUI reset', Object.keys(power_user.movingUIState).length);
} }
saveSettingsDebounced(); saveSettingsDebounced();
coreTruthWinWidth = window.innerWidth; coreTruthWinWidth = window.innerWidth;

View File

@ -27,6 +27,7 @@ export const SECRET_KEYS = {
COHERE: 'api_key_cohere', COHERE: 'api_key_cohere',
PERPLEXITY: 'api_key_perplexity', PERPLEXITY: 'api_key_perplexity',
GROQ: 'api_key_groq', GROQ: 'api_key_groq',
AZURE_TTS: 'api_key_azure_tts',
}; };
const INPUT_MAP = { const INPUT_MAP = {

View File

@ -1112,6 +1112,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'role', 'role for in-chat injections', [ARGUMENT_TYPE.STRING], false, false, 'system', ['system', 'user', 'assistant'], 'role', 'role for in-chat injections', [ARGUMENT_TYPE.STRING], false, false, 'system', ['system', 'user', 'assistant'],
), ),
new SlashCommandNamedArgument(
'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'],
),
], ],
unnamedArgumentList: [ unnamedArgumentList: [
new SlashCommandArgument( new SlashCommandArgument(
@ -1173,6 +1176,7 @@ function injectCallback(args, value) {
}; };
const id = resolveVariable(args?.id); const id = resolveVariable(args?.id);
const ephemeral = isTrueBoolean(args?.ephemeral);
if (!id) { if (!id) {
console.warn('WARN: No ID provided for /inject command'); console.warn('WARN: No ID provided for /inject command');
@ -1206,6 +1210,23 @@ function injectCallback(args, value) {
setExtensionPrompt(prefixedId, value, position, depth, scan, role); setExtensionPrompt(prefixedId, value, position, depth, scan, role);
saveMetadataDebounced(); saveMetadataDebounced();
if (ephemeral) {
let deleted = false;
const unsetInject = () => {
if (deleted) {
return;
}
console.log('Removing ephemeral script injection', id);
delete chat_metadata.script_injects[id];
setExtensionPrompt(prefixedId, '', position, depth, scan, role);
saveMetadataDebounced();
deleted = true;
};
eventSource.once(event_types.GENERATION_ENDED, unsetInject);
eventSource.once(event_types.GENERATION_STOPPED, unsetInject);
}
return ''; return '';
} }

View File

@ -100,6 +100,7 @@ const settings = {
min_p: 0, min_p: 0,
rep_pen: 1.2, rep_pen: 1.2,
rep_pen_range: 0, rep_pen_range: 0,
rep_pen_decay: 0,
no_repeat_ngram_size: 0, no_repeat_ngram_size: 0,
penalty_alpha: 0, penalty_alpha: 0,
num_beams: 1, num_beams: 1,
@ -108,6 +109,7 @@ const settings = {
encoder_rep_pen: 1, encoder_rep_pen: 1,
freq_pen: 0, freq_pen: 0,
presence_pen: 0, presence_pen: 0,
skew: 0,
do_sample: true, do_sample: true,
early_stopping: false, early_stopping: false,
dynatemp: false, dynatemp: false,
@ -116,6 +118,11 @@ const settings = {
dynatemp_exponent: 1.0, dynatemp_exponent: 1.0,
smoothing_factor: 0.0, smoothing_factor: 0.0,
smoothing_curve: 1.0, smoothing_curve: 1.0,
dry_allowed_length: 2,
dry_multiplier: 0.0,
dry_base: 1.75,
dry_sequence_breakers: '["\\n", ":", "\\"", "*"]',
dry_penalty_last_n: 0,
max_tokens_second: 0, max_tokens_second: 0,
seed: -1, seed: -1,
preset: 'Default', preset: 'Default',
@ -139,6 +146,7 @@ const settings = {
//best_of_aphrodite: 1, //best_of_aphrodite: 1,
ignore_eos_token: false, ignore_eos_token: false,
spaces_between_special_tokens: true, spaces_between_special_tokens: true,
speculative_ngram: false,
//logits_processors_aphrodite: [], //logits_processors_aphrodite: [],
//log_probs_aphrodite: 0, //log_probs_aphrodite: 0,
//prompt_log_probs_aphrodite: 0, //prompt_log_probs_aphrodite: 0,
@ -171,6 +179,7 @@ export const setting_names = [
'temperature_last', 'temperature_last',
'rep_pen', 'rep_pen',
'rep_pen_range', 'rep_pen_range',
'rep_pen_decay',
'no_repeat_ngram_size', 'no_repeat_ngram_size',
'top_k', 'top_k',
'top_p', 'top_p',
@ -190,10 +199,16 @@ export const setting_names = [
'dynatemp_exponent', 'dynatemp_exponent',
'smoothing_factor', 'smoothing_factor',
'smoothing_curve', 'smoothing_curve',
'dry_allowed_length',
'dry_multiplier',
'dry_base',
'dry_sequence_breakers',
'dry_penalty_last_n',
'max_tokens_second', 'max_tokens_second',
'encoder_rep_pen', 'encoder_rep_pen',
'freq_pen', 'freq_pen',
'presence_pen', 'presence_pen',
'skew',
'do_sample', 'do_sample',
'early_stopping', 'early_stopping',
'seed', 'seed',
@ -214,6 +229,7 @@ export const setting_names = [
//'best_of_aphrodite', //'best_of_aphrodite',
'ignore_eos_token', 'ignore_eos_token',
'spaces_between_special_tokens', 'spaces_between_special_tokens',
'speculative_ngram',
//'logits_processors_aphrodite', //'logits_processors_aphrodite',
//'log_probs_aphrodite', //'log_probs_aphrodite',
//'prompt_log_probs_aphrodite' //'prompt_log_probs_aphrodite'
@ -638,6 +654,7 @@ jQuery(function () {
'min_p_textgenerationwebui': 0, 'min_p_textgenerationwebui': 0,
'rep_pen_textgenerationwebui': 1, 'rep_pen_textgenerationwebui': 1,
'rep_pen_range_textgenerationwebui': 0, 'rep_pen_range_textgenerationwebui': 0,
'rep_pen_decay_textgenerationwebui': 0,
'dynatemp_textgenerationwebui': false, 'dynatemp_textgenerationwebui': false,
'seed_textgenerationwebui': -1, 'seed_textgenerationwebui': -1,
'ban_eos_token_textgenerationwebui': false, 'ban_eos_token_textgenerationwebui': false,
@ -656,7 +673,9 @@ jQuery(function () {
'encoder_rep_pen_textgenerationwebui': 1, 'encoder_rep_pen_textgenerationwebui': 1,
'freq_pen_textgenerationwebui': 0, 'freq_pen_textgenerationwebui': 0,
'presence_pen_textgenerationwebui': 0, 'presence_pen_textgenerationwebui': 0,
'skew_textgenerationwebui': 0,
'no_repeat_ngram_size_textgenerationwebui': 0, 'no_repeat_ngram_size_textgenerationwebui': 0,
'speculative_ngram_textgenerationwebui': false,
'min_length_textgenerationwebui': 0, 'min_length_textgenerationwebui': 0,
'num_beams_textgenerationwebui': 1, 'num_beams_textgenerationwebui': 1,
'length_penalty_textgenerationwebui': 1, 'length_penalty_textgenerationwebui': 1,
@ -665,6 +684,10 @@ jQuery(function () {
'guidance_scale_textgenerationwebui': 1, 'guidance_scale_textgenerationwebui': 1,
'smoothing_factor_textgenerationwebui': 0, 'smoothing_factor_textgenerationwebui': 0,
'smoothing_curve_textgenerationwebui': 1, 'smoothing_curve_textgenerationwebui': 1,
'dry_allowed_length_textgenerationwebui': 2,
'dry_multiplier_textgenerationwebui': 0,
'dry_base_textgenerationwebui': 1.75,
'dry_penalty_last_n_textgenerationwebui': 0,
}; };
for (const [id, value] of Object.entries(inputs)) { for (const [id, value] of Object.entries(inputs)) {
@ -1014,6 +1037,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'frequency_penalty': settings.freq_pen, 'frequency_penalty': settings.freq_pen,
'presence_penalty': settings.presence_pen, 'presence_penalty': settings.presence_pen,
'top_k': settings.top_k, 'top_k': settings.top_k,
'skew': settings.skew,
'min_length': settings.type === OOBA ? settings.min_length : undefined, 'min_length': settings.type === OOBA ? settings.min_length : undefined,
'minimum_message_content_tokens': settings.type === DREAMGEN ? settings.min_length : undefined, 'minimum_message_content_tokens': settings.type === DREAMGEN ? settings.min_length : undefined,
'min_tokens': settings.min_length, 'min_tokens': settings.min_length,
@ -1028,6 +1052,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined, 'dynatemp_exponent': settings.dynatemp ? settings.dynatemp_exponent : undefined,
'smoothing_factor': settings.smoothing_factor, 'smoothing_factor': settings.smoothing_factor,
'smoothing_curve': settings.smoothing_curve, 'smoothing_curve': settings.smoothing_curve,
'dry_allowed_length': settings.dry_allowed_length,
'dry_multiplier': settings.dry_multiplier,
'dry_base': settings.dry_base,
'dry_sequence_breakers': settings.dry_sequence_breakers,
'dry_penalty_last_n': settings.dry_penalty_last_n,
'max_tokens_second': settings.max_tokens_second, 'max_tokens_second': settings.max_tokens_second,
'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined, 'sampler_priority': settings.type === OOBA ? settings.sampler_priority : undefined,
'samplers': settings.type === LLAMACPP ? settings.samplers : undefined, 'samplers': settings.type === LLAMACPP ? settings.samplers : undefined,
@ -1055,11 +1084,13 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
const nonAphroditeParams = { const nonAphroditeParams = {
'rep_pen': settings.rep_pen, 'rep_pen': settings.rep_pen,
'rep_pen_range': settings.rep_pen_range, 'rep_pen_range': settings.rep_pen_range,
'repetition_decay': settings.type === TABBY ? settings.rep_pen_decay : undefined,
'repetition_penalty_range': settings.rep_pen_range, 'repetition_penalty_range': settings.rep_pen_range,
'encoder_repetition_penalty': settings.type === OOBA ? settings.encoder_rep_pen : undefined, 'encoder_repetition_penalty': settings.type === OOBA ? settings.encoder_rep_pen : undefined,
'no_repeat_ngram_size': settings.type === OOBA ? settings.no_repeat_ngram_size : undefined, 'no_repeat_ngram_size': settings.type === OOBA ? settings.no_repeat_ngram_size : undefined,
'penalty_alpha': settings.type === OOBA ? settings.penalty_alpha : undefined, 'penalty_alpha': settings.type === OOBA ? settings.penalty_alpha : undefined,
'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined, 'temperature_last': (settings.type === OOBA || settings.type === APHRODITE || settings.type == TABBY) ? settings.temperature_last : undefined,
'speculative_ngram': settings.type === TABBY ? settings.speculative_ngram : undefined,
'do_sample': settings.type === OOBA ? settings.do_sample : undefined, 'do_sample': settings.type === OOBA ? settings.do_sample : undefined,
'seed': settings.seed, 'seed': settings.seed,
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1, 'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,

View File

@ -560,7 +560,7 @@ export function countTokensOpenAI(messages, full = false) {
if (shouldTokenizeAI21) { if (shouldTokenizeAI21) {
tokenizerEndpoint = '/api/tokenizers/ai21/count'; tokenizerEndpoint = '/api/tokenizers/ai21/count';
} else if (shouldTokenizeGoogle) { } else if (shouldTokenizeGoogle) {
tokenizerEndpoint = `/api/tokenizers/google/count?model=${getTokenizerModel()}`; tokenizerEndpoint = `/api/tokenizers/google/count?model=${getTokenizerModel()}&reverse_proxy=${oai_settings.reverse_proxy}&proxy_password=${oai_settings.proxy_password}`;
} else { } else {
tokenizerEndpoint = `/api/tokenizers/openai/count?model=${getTokenizerModel()}`; tokenizerEndpoint = `/api/tokenizers/openai/count?model=${getTokenizerModel()}`;
} }

View File

@ -1,5 +1,5 @@
import { getContext } from './extensions.js'; import { getContext } from './extensions.js';
import { getRequestHeaders } from '../script.js'; import { callPopup, getRequestHeaders } from '../script.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js'; import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js'; import { debounce_timeout } from './constants.js';
@ -139,6 +139,7 @@ export function download(content, fileName, contentType) {
a.href = URL.createObjectURL(file); a.href = URL.createObjectURL(file);
a.download = fileName; a.download = fileName;
a.click(); a.click();
URL.revokeObjectURL(a.href);
} }
/** /**
@ -1020,6 +1021,36 @@ export function extractDataFromPng(data, identifier = 'chara') {
} }
} }
/**
* Sends a request to the server to sanitize a given filename
*
* @param {string} fileName - The name of the file to sanitize
* @returns {Promise<string>} A Promise that resolves to the sanitized filename if successful, or rejects with an error message if unsuccessful
*/
export async function getSanitizedFilename(fileName) {
try {
const result = await fetch('/api/files/sanitize-filename', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
fileName: fileName,
}),
});
if (!result.ok) {
const error = await result.text();
throw new Error(error);
}
const responseData = await result.json();
return responseData.fileName;
} catch (error) {
toastr.error(String(error), 'Could not sanitize fileName');
console.error('Could not sanitize fileName', error);
throw error;
}
}
/** /**
* Sends a base64 encoded image to the backend to be saved as a file. * Sends a base64 encoded image to the backend to be saved as a file.
* *
@ -1468,23 +1499,47 @@ export function flashHighlight(element, timespan = 2000) {
setTimeout(() => element.removeClass('flash animated'), timespan); setTimeout(() => element.removeClass('flash animated'), timespan);
} }
/**
* A common base function for case-insensitive and accent-insensitive string comparisons.
*
* @param {string} a - The first string to compare.
* @param {string} b - The second string to compare.
* @param {(a:string,b:string)=>boolean} comparisonFunction - The function to use for the comparison.
* @returns {*} - The result of the comparison.
*/
export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) {
if (!a || !b) return comparisonFunction(a, b); // Return the comparison result if either string is empty
// Normalize and remove diacritics, then convert to lower case
const normalizedA = a.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
const normalizedB = b.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
// Check if the normalized strings are equal
return comparisonFunction(normalizedA, normalizedB);
}
/** /**
* Performs a case-insensitive and accent-insensitive substring search. * Performs a case-insensitive and accent-insensitive substring search.
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents. * This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
* *
* @param {string} text - The text in which to search for the substring. * @param {string} text - The text in which to search for the substring
* @param {string} searchTerm - The substring to search for in the text. * @param {string} searchTerm - The substring to search for in the text
* @returns {boolean} - Returns true if the searchTerm is found within the text, otherwise returns false. * @returns {boolean} true if the searchTerm is found within the text, otherwise returns false
*/ */
export function includesIgnoreCaseAndAccents(text, searchTerm) { export function includesIgnoreCaseAndAccents(text, searchTerm) {
if (!text || !searchTerm) return false; // Return false if either string is empty return compareIgnoreCaseAndAccents(text, searchTerm, (a, b) => a?.includes(b) === true);
}
// Normalize and remove diacritics, then convert to lower case /**
const normalizedText = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); * Performs a case-insensitive and accent-insensitive equality check.
const normalizedSearchTerm = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); * This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents.
*
// Check if the normalized text includes the normalized search term * @param {string} a - The first string to compare
return normalizedText.includes(normalizedSearchTerm); * @param {string} b - The second string to compare
* @returns {boolean} true if the strings are equal, otherwise returns false
*/
export function equalsIgnoreCaseAndAccents(a, b) {
return compareIgnoreCaseAndAccents(a, b, (a, b) => a === b);
} }
/** /**
@ -1665,3 +1720,38 @@ export function highlightRegex(regexStr) {
return `<span class="regex-highlight">${regexStr}</span>`; return `<span class="regex-highlight">${regexStr}</span>`;
} }
/**
* Confirms if the user wants to overwrite an existing data object (like character, world info, etc) if one exists.
* If no data with the name exists, this simply returns true.
*
* @param {string} type - The type of the check ("World Info", "Character", etc)
* @param {string[]} existingNames - The list of existing names to check against
* @param {string} name - The new name
* @param {object} options - Optional parameters
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when needing to overwrite an existing data object
* @param {string} [options.actionName='overwrite'] - The action name to display in the confirmation dialog
* @param {(existingName:string)=>void} [options.deleteAction=null] - Optional action to execute wen deleting an existing data object on overwrite
* @returns {Promise<boolean>} True if the user confirmed the overwrite or there is no overwrite needed, false otherwise
*/
export async function checkOverwriteExistingData(type, existingNames, name, { interactive = false, actionName = 'Overwrite', deleteAction = null } = {}) {
const existing = existingNames.find(x => equalsIgnoreCaseAndAccents(x, name));
if (!existing) {
return true;
}
const overwrite = interactive ? await callPopup(`<h3>${type} ${actionName}</h3><p>A ${type.toLowerCase()} with the same name already exists:<br />${existing}</p>Do you want to overwrite it?`, 'confirm') : false;
if (!overwrite) {
toastr.warning(`${type} ${actionName.toLowerCase()} cancelled. A ${type.toLowerCase()} with the same name already exists:<br />${existing}`, `${type} ${actionName}`, { escapeHtml: false });
return false;
}
toastr.info(`Overwriting Existing ${type}:<br />${existing}`, `${type} ${actionName}`, { escapeHtml: false });
// If there is an action to delete the existing data, do it, as the name might be slightly different so file name would not be the same
if (deleteAction) {
deleteAction(existing);
}
return true;
}

View File

@ -795,27 +795,26 @@ function letCallback(args, value) {
/** /**
* Set or retrieve a variable in the current scope or nearest ancestor scope. * Set or retrieve a variable in the current scope or nearest ancestor scope.
* @param {{_scope:SlashCommandScope, key?:string, index?:String|Number}} args Named arguments. * @param {{_scope:SlashCommandScope, key?:string, index?:string|number}} args Named arguments.
* @param {String|[String, SlashCommandClosure]} value Name and optional value for the variable. * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]} value Name and optional value for the variable.
* @returns The variable's value * @returns The variable's value
*/ */
function varCallback(args, value) { function varCallback(args, value) {
if (Array.isArray(value)) { if (!Array.isArray(value)) value = [value];
args._scope.setVariable(value[0], typeof value[1] == 'string' ? value.slice(1).join(' ') : value[1], args.index);
return value[1];
}
if (args.key !== undefined) { if (args.key !== undefined) {
const key = args.key; const key = args.key;
const val = value; const val = value.join(' ');
args._scope.setVariable(key, val, args.index);
return val;
} else if (value.includes(' ')) {
const key = value.split(' ')[0];
const val = value.split(' ').slice(1).join(' ');
args._scope.setVariable(key, val, args.index); args._scope.setVariable(key, val, args.index);
return val; return val;
} }
return args._scope.getVariable(args.key ?? value, args.index); const key = value.shift();
if (value.length > 0) {
const val = value.join(' ');
args._scope.setVariable(key, val, args.index);
return val;
} else {
return args._scope.getVariable(key, args.index);
}
} }
export function registerVariableCommands() { export function registerVariableCommands() {
@ -1733,7 +1732,7 @@ export function registerVariableCommands() {
returns: 'the variable value', returns: 'the variable value',
namedArgumentList: [ namedArgumentList: [
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'key', 'variable name', [ARGUMENT_TYPE.VARIABLE_NAME], false, 'key', 'variable name; forces setting the variable, even if no value is provided', [ARGUMENT_TYPE.VARIABLE_NAME], false,
), ),
new SlashCommandNamedArgument( new SlashCommandNamedArgument(
'index', 'index',
@ -1769,7 +1768,7 @@ export function registerVariableCommands() {
<pre><code class="language-stscript">/let x foo | /var x foo bar | /var x | /echo</code></pre> <pre><code class="language-stscript">/let x foo | /var x foo bar | /var x | /echo</code></pre>
</li> </li>
<li> <li>
<pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var key=x | /echo</code></pre> <pre><code class="language-stscript">/let x foo | /var key=x foo bar | /var x | /echo</code></pre>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,5 +1,5 @@
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js'; import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } from './utils.js'; import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, equalsIgnoreCaseAndAccents, getSanitizedFilename, checkOverwriteExistingData } from './utils.js';
import { extension_settings, getContext } from './extensions.js'; import { extension_settings, getContext } from './extensions.js';
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js'; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
import { isMobile } from './RossAscends-mods.js'; import { isMobile } from './RossAscends-mods.js';
@ -50,6 +50,7 @@ const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry');
let world_info = {}; let world_info = {};
let selected_world_info = []; let selected_world_info = [];
/** @type {string[]} */
let world_names; let world_names;
let world_info_depth = 2; let world_info_depth = 2;
let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
@ -1057,6 +1058,34 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
return; return;
} }
// Regardless of whether success is displayed or not. Make sure the delete button is available.
// Do not put this code behind.
$('#world_popup_delete').off('click').on('click', async () => {
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
if (!confirmation) {
return;
}
if (world_info.charLore) {
world_info.charLore.forEach((charLore, index) => {
if (charLore.extraBooks?.includes(name)) {
const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
if (tempCharLore.length === 0) {
world_info.charLore.splice(index, 1);
} else {
charLore.extraBooks = tempCharLore;
}
}
});
saveSettingsDebounced();
}
// Selected world_info automatically refreshes
await deleteWorldInfo(name);
});
// Before printing the WI, we check if we should enable/disable search sorting // Before printing the WI, we check if we should enable/disable search sorting
verifyWorldInfoSearchSortRule(); verifyWorldInfoSearchSortRule();
@ -1225,32 +1254,6 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
} }
}); });
$('#world_popup_delete').off('click').on('click', async () => {
const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, 'confirm');
if (!confirmation) {
return;
}
if (world_info.charLore) {
world_info.charLore.forEach((charLore, index) => {
if (charLore.extraBooks?.includes(name)) {
const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
if (tempCharLore.length === 0) {
world_info.charLore.splice(index, 1);
} else {
charLore.extraBooks = tempCharLore;
}
}
});
saveSettingsDebounced();
}
// Selected world_info automatically refreshes
await deleteWorldInfo(name);
});
// Check if a sortable instance exists // Check if a sortable instance exists
if (worldEntriesList.sortable('instance') !== undefined) { if (worldEntriesList.sortable('instance') !== undefined) {
// Destroy the instance // Destroy the instance
@ -2542,9 +2545,15 @@ async function renameWorldInfo(name, data) {
} }
} }
/**
* Deletes a world info with the given name
*
* @param {string} worldInfoName - The name of the world info to delete
* @returns {Promise<boolean>} A promise that resolves to true if the world info was successfully deleted, false otherwise
*/
async function deleteWorldInfo(worldInfoName) { async function deleteWorldInfo(worldInfoName) {
if (!world_names.includes(worldInfoName)) { if (!world_names.includes(worldInfoName)) {
return; return false;
} }
const response = await fetch('/api/worldinfo/delete', { const response = await fetch('/api/worldinfo/delete', {
@ -2553,7 +2562,10 @@ async function deleteWorldInfo(worldInfoName) {
body: JSON.stringify({ name: worldInfoName }), body: JSON.stringify({ name: worldInfoName }),
}); });
if (response.ok) { if (!response.ok) {
return false;
}
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName); const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) { if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1); selected_world_info.splice(existingWorldIndex, 1);
@ -2570,7 +2582,8 @@ async function deleteWorldInfo(worldInfoName) {
saveCharacterDebounced(); saveCharacterDebounced();
} }
} }
}
return true;
} }
function getFreeWorldEntryUid(data) { function getFreeWorldEntryUid(data) {
@ -2602,22 +2615,40 @@ function getFreeWorldName() {
return undefined; return undefined;
} }
async function createNewWorldInfo(worldInfoName) { /**
* Creates a new world info/lorebook with the given name.
* Checks if a world with the same name already exists, providing a warning or optionally a user confirmation dialog.
*
* @param {string} worldName - The name of the new world info
* @param {Object} options - Optional parameters
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
* @returns {Promise<boolean>} - True if the world info was successfully created, false otherwise
*/
async function createNewWorldInfo(worldName, { interactive = false } = {}) {
const worldInfoTemplate = { entries: {} }; const worldInfoTemplate = { entries: {} };
if (!worldInfoName) { if (!worldName) {
return; return false;
} }
await saveWorldInfo(worldInfoName, worldInfoTemplate, true); const sanitizedWorldName = await getSanitizedFilename(worldName);
const allowed = await checkOverwriteExistingData('World Info', world_names, sanitizedWorldName, { interactive: interactive, actionName: 'Create', deleteAction: (existingName) => deleteWorldInfo(existingName) });
if (!allowed) {
return false;
}
await saveWorldInfo(worldName, worldInfoTemplate, true);
await updateWorldInfoList(); await updateWorldInfoList();
const selectedIndex = world_names.indexOf(worldInfoName); const selectedIndex = world_names.indexOf(worldName);
if (selectedIndex !== -1) { if (selectedIndex !== -1) {
$('#world_editor_select').val(selectedIndex).trigger('change'); $('#world_editor_select').val(selectedIndex).trigger('change');
} else { } else {
hideWorldEditor(); hideWorldEditor();
} }
return true;
} }
async function getCharacterLore() { async function getCharacterLore() {
@ -3550,6 +3581,13 @@ export async function importWorldInfo(file) {
return; return;
} }
const worldName = file.name.substr(0, file.name.lastIndexOf("."));
const sanitizedWorldName = await getSanitizedFilename(worldName);
const allowed = await checkOverwriteExistingData('World Info', world_names, sanitizedWorldName, { interactive: true, actionName: 'Import', deleteAction: (existingName) => deleteWorldInfo(existingName) });
if (!allowed) {
return false;
}
jQuery.ajax({ jQuery.ajax({
type: 'POST', type: 'POST',
url: '/api/worldinfo/import', url: '/api/worldinfo/import',
@ -3567,7 +3605,7 @@ export async function importWorldInfo(file) {
$('#world_editor_select').val(newIndex).trigger('change'); $('#world_editor_select').val(newIndex).trigger('change');
} }
toastr.info(`World Info "${data.name}" imported successfully!`); toastr.success(`World Info "${data.name}" imported successfully!`);
} }
}, },
error: (_jqXHR, _exception) => { }, error: (_jqXHR, _exception) => { },
@ -3642,7 +3680,7 @@ jQuery(() => {
const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName); const finalName = await callPopup('<h3>Create a new World Info?</h3>Enter a name for the new file:', 'input', tempName);
if (finalName) { if (finalName) {
await createNewWorldInfo(finalName); await createNewWorldInfo(finalName, { interactive: true });
} }
}); });

View File

@ -519,6 +519,9 @@ app.use('/api/backends/scale-alt', require('./src/endpoints/backends/scale-alt')
// Speech (text-to-speech and speech-to-text) // Speech (text-to-speech and speech-to-text)
app.use('/api/speech', require('./src/endpoints/speech').router); app.use('/api/speech', require('./src/endpoints/speech').router);
// Azure TTS
app.use('/api/azure', require('./src/endpoints/azure').router);
const tavernUrl = new URL( const tavernUrl = new URL(
(cliArguments.ssl ? 'https://' : 'http://') + (cliArguments.ssl ? 'https://' : 'http://') +
(listen ? '0.0.0.0' : '127.0.0.1') + (listen ? '0.0.0.0' : '127.0.0.1') +

92
src/endpoints/azure.js Normal file
View File

@ -0,0 +1,92 @@
const { readSecret, SECRET_KEYS } = require('./secrets');
const fetch = require('node-fetch').default;
const express = require('express');
const { jsonParser } = require('../express-common');
const router = express.Router();
router.post('/list', jsonParser, async (req, res) => {
try {
const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
if (!key) {
console.error('Azure TTS API Key not set');
return res.sendStatus(403);
}
const region = req.body.region;
if (!region) {
console.error('Azure TTS region not set');
return res.sendStatus(400);
}
const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/voices/list`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': key,
},
});
if (!response.ok) {
console.error('Azure Request failed', response.status, response.statusText);
return res.sendStatus(500);
}
const voices = await response.json();
return res.json(voices);
} catch (error) {
console.error('Azure Request failed', error);
return res.sendStatus(500);
}
});
router.post('/generate', jsonParser, async (req, res) => {
try {
const key = readSecret(req.user.directories, SECRET_KEYS.AZURE_TTS);
if (!key) {
console.error('Azure TTS API Key not set');
return res.sendStatus(403);
}
const { text, voice, region } = req.body;
if (!text || !voice || !region) {
console.error('Missing required parameters');
return res.sendStatus(400);
}
const url = `https://${region}.tts.speech.microsoft.com/cognitiveservices/v1`;
const lang = String(voice).split('-').slice(0, 2).join('-');
const escapedText = String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const ssml = `<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='${lang}'><voice xml:lang='${lang}' name='${voice}'>${escapedText}</voice></speak>`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Ocp-Apim-Subscription-Key': key,
'Content-Type': 'application/ssml+xml',
'X-Microsoft-OutputFormat': 'ogg-48khz-16bit-mono-opus',
},
body: ssml,
});
if (!response.ok) {
console.error('Azure Request failed', response.status, response.statusText);
return res.sendStatus(500);
}
const audio = await response.buffer();
res.set('Content-Type', 'audio/ogg');
return res.send(audio);
} catch (error) {
console.error('Azure Request failed', error);
return res.sendStatus(500);
}
});
module.exports = {
router,
};

View File

@ -16,6 +16,7 @@ const API_MISTRAL = 'https://api.mistral.ai/v1';
const API_COHERE = 'https://api.cohere.ai/v1'; const API_COHERE = 'https://api.cohere.ai/v1';
const API_PERPLEXITY = 'https://api.perplexity.ai'; const API_PERPLEXITY = 'https://api.perplexity.ai';
const API_GROQ = 'https://api.groq.com/openai/v1'; const API_GROQ = 'https://api.groq.com/openai/v1';
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
/** /**
* Applies a post-processing step to the generated messages. * Applies a post-processing step to the generated messages.
@ -232,9 +233,10 @@ async function sendScaleRequest(request, response) {
* @param {express.Response} response Express response * @param {express.Response} response Express response
*/ */
async function sendMakerSuiteRequest(request, response) { async function sendMakerSuiteRequest(request, response) {
const apiKey = readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
if (!apiKey) { if (!request.body.reverse_proxy && !apiKey) {
console.log('MakerSuite API key is missing.'); console.log('MakerSuite API key is missing.');
return response.status(400).send({ error: true }); return response.status(400).send({ error: true });
} }
@ -316,7 +318,7 @@ async function sendMakerSuiteRequest(request, response) {
? (stream ? 'streamGenerateContent' : 'generateContent') ? (stream ? 'streamGenerateContent' : 'generateContent')
: (isText ? 'generateText' : 'generateMessage'); : (isText ? 'generateText' : 'generateMessage');
const generateResponse = await fetch(`https://generativelanguage.googleapis.com/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, { const generateResponse = await fetch(`${apiUrl.origin}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
body: JSON.stringify(body), body: JSON.stringify(body),
method: 'POST', method: 'POST',
headers: { headers: {

View File

@ -2,11 +2,27 @@ const path = require('path');
const fs = require('fs'); const fs = require('fs');
const writeFileSyncAtomic = require('write-file-atomic').sync; const writeFileSyncAtomic = require('write-file-atomic').sync;
const express = require('express'); const express = require('express');
const sanitize = require('sanitize-filename');
const router = express.Router(); const router = express.Router();
const { validateAssetFileName } = require('./assets'); const { validateAssetFileName } = require('./assets');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const { clientRelativePath } = require('../util'); const { clientRelativePath } = require('../util');
router.post('/sanitize-filename', jsonParser, async (request, response) => {
try {
const fileName = String(request.body.fileName);
if (!fileName) {
return response.status(400).send('No fileName specified');
}
const sanitizedFilename = sanitize(fileName);
return response.send({ fileName: sanitizedFilename });
} catch (error) {
console.log(error);
return response.sendStatus(500);
}
});
router.post('/upload', jsonParser, async (request, response) => { router.post('/upload', jsonParser, async (request, response) => {
try { try {
if (!request.body.name) { if (!request.body.name) {

View File

@ -4,14 +4,18 @@ const express = require('express');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const { GEMINI_SAFETY } = require('../constants'); const { GEMINI_SAFETY } = require('../constants');
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
const router = express.Router(); const router = express.Router();
router.post('/caption-image', jsonParser, async (request, response) => { router.post('/caption-image', jsonParser, async (request, response) => {
try { try {
const mimeType = request.body.image.split(';')[0].split(':')[1]; const mimeType = request.body.image.split(';')[0].split(':')[1];
const base64Data = request.body.image.split(',')[1]; const base64Data = request.body.image.split(',')[1];
const key = readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${key}`; const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
const model = request.body.model || 'gemini-pro-vision';
const url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;
const body = { const body = {
contents: [{ contents: [{
parts: [ parts: [
@ -27,7 +31,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
generationConfig: { maxOutputTokens: 1000 }, generationConfig: { maxOutputTokens: 1000 },
}; };
console.log('Multimodal captioning request', body); console.log('Multimodal captioning request', model, body);
const result = await fetch(url, { const result = await fetch(url, {
body: JSON.stringify(body), body: JSON.stringify(body),

View File

@ -39,6 +39,7 @@ const SECRET_KEYS = {
COHERE: 'api_key_cohere', COHERE: 'api_key_cohere',
PERPLEXITY: 'api_key_perplexity', PERPLEXITY: 'api_key_perplexity',
GROQ: 'api_key_groq', GROQ: 'api_key_groq',
AZURE_TTS: 'api_key_azure_tts',
}; };
// These are the keys that are safe to expose, even if allowKeysExposure is false // These are the keys that are safe to expose, even if allowKeysExposure is false

View File

@ -10,6 +10,8 @@ const { TEXTGEN_TYPES } = require('../constants');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const { setAdditionalHeaders } = require('../additional-headers'); const { setAdditionalHeaders } = require('../additional-headers');
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
/** /**
* @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler * @typedef { (req: import('express').Request, res: import('express').Response) => Promise<any> } TokenizationHandler
*/ */
@ -555,8 +557,11 @@ router.post('/google/count', jsonParser, async function (req, res) {
body: JSON.stringify({ contents: convertGooglePrompt(req.body, String(req.query.model)).contents }), body: JSON.stringify({ contents: convertGooglePrompt(req.body, String(req.query.model)).contents }),
}; };
try { try {
const key = readSecret(req.user.directories, SECRET_KEYS.MAKERSUITE); const reverseProxy = req.query.reverse_proxy?.toString() || '';
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${req.query.model}:countTokens?key=${key}`, options); const proxyPassword = req.query.proxy_password?.toString() || '';
const apiKey = reverseProxy ? proxyPassword : readSecret(req.user.directories, SECRET_KEYS.MAKERSUITE);
const apiUrl = new URL(reverseProxy || API_MAKERSUITE);
const response = await fetch(`${apiUrl.origin}/v1beta/models/${req.query.model}:countTokens?key=${apiKey}`, options);
const data = await response.json(); const data = await response.json();
return res.send({ 'token_count': data?.totalTokens || 0 }); return res.send({ 'token_count': data?.totalTokens || 0 });
} catch (err) { } catch (err) {

View File

@ -168,14 +168,15 @@ async function deleteVectorItems(directories, collectionId, source, hashes) {
* @param {Object} sourceSettings - Settings for the source, if it needs any * @param {Object} sourceSettings - Settings for the source, if it needs any
* @param {string} searchText - The text to search for * @param {string} searchText - The text to search for
* @param {number} topK - The number of results to return * @param {number} topK - The number of results to return
* @param {number} threshold - The threshold for the search
* @returns {Promise<{hashes: number[], metadata: object[]}>} - The metadata of the items that match the search text * @returns {Promise<{hashes: number[], metadata: object[]}>} - The metadata of the items that match the search text
*/ */
async function queryCollection(directories, collectionId, source, sourceSettings, searchText, topK) { async function queryCollection(directories, collectionId, source, sourceSettings, searchText, topK, threshold) {
const store = await getIndex(directories, collectionId, source); const store = await getIndex(directories, collectionId, source);
const vector = await getVector(source, sourceSettings, searchText, true, directories); const vector = await getVector(source, sourceSettings, searchText, true, directories);
const result = await store.queryItems(vector, topK); const result = await store.queryItems(vector, topK);
const metadata = result.map(x => x.item.metadata); const metadata = result.filter(x => x.score >= threshold).map(x => x.item.metadata);
const hashes = result.map(x => Number(x.item.metadata.hash)); const hashes = result.map(x => Number(x.item.metadata.hash));
return { metadata, hashes }; return { metadata, hashes };
} }
@ -188,9 +189,11 @@ async function queryCollection(directories, collectionId, source, sourceSettings
* @param {Object} sourceSettings - Settings for the source, if it needs any * @param {Object} sourceSettings - Settings for the source, if it needs any
* @param {string} searchText - The text to search for * @param {string} searchText - The text to search for
* @param {number} topK - The number of results to return * @param {number} topK - The number of results to return
* @param {number} threshold - The threshold for the search
*
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - The top K results from each collection * @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - The top K results from each collection
*/ */
async function multiQueryCollection(directories, collectionIds, source, sourceSettings, searchText, topK) { async function multiQueryCollection(directories, collectionIds, source, sourceSettings, searchText, topK, threshold) {
const vector = await getVector(source, sourceSettings, searchText, true, directories); const vector = await getVector(source, sourceSettings, searchText, true, directories);
const results = []; const results = [];
@ -200,9 +203,10 @@ async function multiQueryCollection(directories, collectionIds, source, sourceSe
results.push(...result.map(result => ({ collectionId, result }))); results.push(...result.map(result => ({ collectionId, result })));
} }
// Sort results by descending similarity // Sort results by descending similarity, apply threshold, and take top K
const sortedResults = results const sortedResults = results
.sort((a, b) => b.result.score - a.result.score) .sort((a, b) => b.result.score - a.result.score)
.filter(x => x.result.score >= threshold)
.slice(0, topK); .slice(0, topK);
/** /**
@ -274,10 +278,11 @@ router.post('/query', jsonParser, async (req, res) => {
const collectionId = String(req.body.collectionId); const collectionId = String(req.body.collectionId);
const searchText = String(req.body.searchText); const searchText = String(req.body.searchText);
const topK = Number(req.body.topK) || 10; const topK = Number(req.body.topK) || 10;
const threshold = Number(req.body.threshold) || 0.0;
const source = String(req.body.source) || 'transformers'; const source = String(req.body.source) || 'transformers';
const sourceSettings = getSourceSettings(source, req); const sourceSettings = getSourceSettings(source, req);
const results = await queryCollection(req.user.directories, collectionId, source, sourceSettings, searchText, topK); const results = await queryCollection(req.user.directories, collectionId, source, sourceSettings, searchText, topK, threshold);
return res.json(results); return res.json(results);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -294,10 +299,11 @@ router.post('/query-multi', jsonParser, async (req, res) => {
const collectionIds = req.body.collectionIds.map(x => String(x)); const collectionIds = req.body.collectionIds.map(x => String(x));
const searchText = String(req.body.searchText); const searchText = String(req.body.searchText);
const topK = Number(req.body.topK) || 10; const topK = Number(req.body.topK) || 10;
const threshold = Number(req.body.threshold) || 0.0;
const source = String(req.body.source) || 'transformers'; const source = String(req.body.source) || 'transformers';
const sourceSettings = getSourceSettings(source, req); const sourceSettings = getSourceSettings(source, req);
const results = await multiQueryCollection(req.user.directories, collectionIds, source, sourceSettings, searchText, topK); const results = await multiQueryCollection(req.user.directories, collectionIds, source, sourceSettings, searchText, topK, threshold);
return res.json(results); return res.json(results);
} catch (error) { } catch (error) {
console.error(error); console.error(error);