Merge branch 'staging' into smol-tag-improvements

This commit is contained in:
Wolfsblvt 2024-06-08 21:13:11 +02:00
commit d98d811cc1
39 changed files with 18308 additions and 11333 deletions

33
.github/workflows/update-i18n.yaml vendored Normal file
View File

@ -0,0 +1,33 @@
name: Update i18n data
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions: # Job-level permissions configuration starts here
contents: write # 'write' access to repository contents
steps:
- name: disable auto crlf
uses: steve02081504/disable-autocrlf@v1
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository.
- name: Create local changes
run: |
aria2c https://raw.githubusercontent.com/SillyTavern/SillyTavern-i18n/main/generate.py
aria2c https://raw.githubusercontent.com/SillyTavern/SillyTavern-i18n/main/requirements.txt
pip install -r ./requirements.txt
python ./generate.py "" --sort-keys
rm -f ./generate.py ./requirements.txt
- name: add all
run: git add -A
- name: push
uses: actions-go/push@master
with:
author-email: 41898282+github-actions[bot]@users.noreply.github.com
author-name: github-actions[bot]
commit-message: 'i18n changes'
remote: origin

4
package-lock.json generated
View File

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

View File

@ -70,7 +70,7 @@
"type": "git", "type": "git",
"url": "https://github.com/SillyTavern/SillyTavern.git" "url": "https://github.com/SillyTavern/SillyTavern.git"
}, },
"version": "1.12.0", "version": "1.12.1",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"start:no-csrf": "node server.js --disableCsrf", "start:no-csrf": "node server.js --disableCsrf",

View File

@ -592,3 +592,23 @@ textarea:disabled {
text-align: center; text-align: center;
padding: 5px; padding: 5px;
} }
ul.li-padding-b-1 li {
padding-bottom: 1em;
}
ul.li-padding-b-2 li {
padding-bottom: 2em;
}
ul.li-padding-b-5 li {
padding-bottom: 5em;
}
ul.li-padding-bot5 li {
padding-bottom: 5px;
}
ul.li-padding-bot10 li {
padding-bottom: 10px;
}

View File

@ -1230,6 +1230,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="koboldcpp" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="Rep. Pen. Slope">Rep Pen Slope</small>
<input class="neo-range-slider" type="range" id="rep_pen_slope_textgenerationwebui" name="volume" min="0" max="10" step="0.01">
<input class="neo-range-input" type="number" min="0" max="10" step="0.01" data-for="rep_pen_slope_textgenerationwebui" id="rep_pen_slope_counter_textgenerationwebui">
</div>
<div data-tg-type="tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0"> <div data-tg-type="tabby" class="alignitemscenter flex-container flexFlowColumn flexBasis30p flexGrow flexShrink gap0">
<small data-i18n="rep.pen decay">Rep Pen Decay</small> <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-slider" type="range" id="rep_pen_decay_textgenerationwebui" name="volume" min="-1" max="8192" step="1">
@ -2764,6 +2769,7 @@
<option value="mistral-small-latest">mistral-small-latest</option> <option value="mistral-small-latest">mistral-small-latest</option>
<option value="mistral-medium-latest">mistral-medium-latest</option> <option value="mistral-medium-latest">mistral-medium-latest</option>
<option value="mistral-large-latest">mistral-large-latest</option> <option value="mistral-large-latest">mistral-large-latest</option>
<option value="codestral-latest">codestral-latest</option>
</optgroup> </optgroup>
<optgroup label="Sub-versions"> <optgroup label="Sub-versions">
<option value="open-mixtral-8x22b-2404">open-mixtral-8x22b-2404</option> <option value="open-mixtral-8x22b-2404">open-mixtral-8x22b-2404</option>
@ -2772,6 +2778,7 @@
<option value="mistral-small-2402">mistral-small-2402</option> <option value="mistral-small-2402">mistral-small-2402</option>
<option value="mistral-medium-2312">mistral-medium-2312</option> <option value="mistral-medium-2312">mistral-medium-2312</option>
<option value="mistral-large-2402">mistral-large-2402</option> <option value="mistral-large-2402">mistral-large-2402</option>
<option value="codestral-2405">codestral-2405</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>
@ -2817,10 +2824,8 @@
<optgroup label="Open-Source Models"> <optgroup label="Open-Source Models">
<option value="llama-3-8b-instruct">llama-3-8b-instruct</option> <option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
<option value="llama-3-70b-instruct">llama-3-70b-instruct</option> <option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
<option value="codellama-70b-instruct">codellama-70b-instruct</option>
<option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option> <option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
<option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option> <option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
<option value="mixtral-8x22b-instruct">mixtral-8x22b-instruct</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1383,6 +1383,7 @@
"Warning:": "警告:", "Warning:": "警告:",
"This action is irreversible.": "此操作不可逆。", "This action is irreversible.": "此操作不可逆。",
"Type the user's handle below to confirm:": "在下面输入用户的名称以确认:", "Type the user's handle below to confirm:": "在下面输入用户的名称以确认:",
"Import Characters": "导入角色",
"Enter the URL of the content to import": "输入要导入的内容的URL", "Enter the URL of the content to import": "输入要导入的内容的URL",
"Supported sources:": "支持的来源:", "Supported sources:": "支持的来源:",
"char_import_1": "Chub 角色直链或ID", "char_import_1": "Chub 角色直链或ID",
@ -1394,6 +1395,8 @@
"char_import_6": "被允许的PNG直链请参阅", "char_import_6": "被允许的PNG直链请参阅",
"char_import_7": "", "char_import_7": "",
"char_import_8": "RisuRealm 角色(直链)", "char_import_8": "RisuRealm 角色(直链)",
"Supports importing multiple characters.": "支持导入多个角色。",
"Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
"Export for character": "导出角色", "Export for character": "导出角色",
"Export prompts for this character, including their order.": "导出此角色的提示,包括其顺序。", "Export prompts for this character, including their order.": "导出此角色的提示,包括其顺序。",
"Export all": "全部导出", "Export all": "全部导出",

File diff suppressed because it is too large Load Diff

View File

@ -433,10 +433,12 @@ export const event_types = {
CHAT_DELETED: 'chat_deleted', CHAT_DELETED: 'chat_deleted',
GROUP_CHAT_DELETED: 'group_chat_deleted', GROUP_CHAT_DELETED: 'group_chat_deleted',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts', GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
GROUP_MEMBER_DRAFTED: 'group_member_drafted', GROUP_MEMBER_DRAFTED: 'group_member_drafted',
WORLD_INFO_ACTIVATED: 'world_info_activated', WORLD_INFO_ACTIVATED: 'world_info_activated',
TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready', TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready',
CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready', CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready',
CHAT_COMPLETION_PROMPT_READY: 'chat_completion_prompt_ready',
CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected', CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected',
// TODO: Naming convention is inconsistent with other events // TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted', CHARACTER_DELETED: 'characterDeleted',
@ -4014,6 +4016,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
let finalPrompt = getCombinedPrompt(false); let finalPrompt = getCombinedPrompt(false);
const eventData = { prompt: finalPrompt, dryRun: dryRun };
await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData);
finalPrompt = eventData.prompt;
let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate
let thisPromptBits = []; let thisPromptBits = [];
@ -5455,70 +5461,95 @@ export function setSendButtonState(value) {
is_send_press = value; is_send_press = value;
} }
async function renameCharacter() { export async function renameCharacter(name = null, { silent = false, renameChats = null } = {}) {
if (!name && silent) {
toastr.warning('No character name provided.', 'Rename Character');
return false;
}
if (this_chid === undefined) {
toastr.warning('No character selected.', 'Rename Character');
return false;
}
const oldAvatar = characters[this_chid].avatar; const oldAvatar = characters[this_chid].avatar;
const newValue = await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name); const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name);
if (newValue && newValue !== characters[this_chid].name) { if (!newValue) {
const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue }); toastr.warning('No character name provided.', 'Rename Character');
const response = await fetch('/api/characters/rename', { return false;
method: 'POST', }
headers: getRequestHeaders(), if (newValue === characters[this_chid].name) {
body, toastr.info('Same character name provided, so name did not change.', 'Rename Character');
}); return false;
}
try { const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue });
if (response.ok) { const response = await fetch('/api/characters/rename', {
const data = await response.json(); method: 'POST',
const newAvatar = data.avatar; headers: getRequestHeaders(),
body,
});
// Replace tags list try {
renameTagKey(oldAvatar, newAvatar); if (response.ok) {
const data = await response.json();
const newAvatar = data.avatar;
// Reload characters list // Replace tags list
await getCharacters(); renameTagKey(oldAvatar, newAvatar);
// Find newly renamed character // Reload characters list
const newChId = characters.findIndex(c => c.avatar == data.avatar); await getCharacters();
if (newChId !== -1) { // Find newly renamed character
// Select the character after the renaming const newChId = characters.findIndex(c => c.avatar == data.avatar);
this_chid = -1;
await selectCharacterById(String(newChId));
// Async delay to update UI if (newChId !== -1) {
await delay(1); // Select the character after the renaming
this_chid = -1;
await selectCharacterById(String(newChId));
if (this_chid === -1) { // Async delay to update UI
throw new Error('New character not selected'); await delay(1);
}
// Also rename as a group member if (this_chid === -1) {
await renameGroupMember(oldAvatar, newAvatar, newValue); throw new Error('New character not selected');
const renamePastChatsConfirm = await callPopup(`<h3>Character renamed!</h3>
<p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p>
<i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm');
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
toastr.success('Character renamed and past chats updated!');
}
} }
else {
throw new Error('Newly renamed character was lost?'); // Also rename as a group member
await renameGroupMember(oldAvatar, newAvatar, newValue);
const renamePastChatsConfirm = renameChats !== null ? renameChats
: silent ? false : await callPopup(`<h3>Character renamed!</h3>
<p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p>
<i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm');
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
toastr.success('Character renamed and past chats updated!', 'Rename Character');
} else {
toastr.success('Character renamed!', 'Rename Character');
} }
} }
else { else {
throw new Error('Could not rename the character'); throw new Error('Newly renamed character was lost?');
} }
} }
catch { else {
// Reloading to prevent data corruption throw new Error('Could not rename the character');
await callPopup('Something went wrong. The page will be reloaded.', 'text');
location.reload();
} }
} }
catch (error) {
// Reloading to prevent data corruption
if (!silent) await callPopup('Something went wrong. The page will be reloaded.', 'text');
else toastr.error('Something went wrong. The page will be reloaded.', 'Rename Character');
console.log('Renaming character error:', error);
location.reload();
return false;
}
return true;
} }
async function renamePastChats(newAvatar, newValue) { async function renamePastChats(newAvatar, newValue) {
@ -7678,6 +7709,8 @@ window['SillyTavern'].getContext = function () {
setExtensionPrompt: setExtensionPrompt, setExtensionPrompt: setExtensionPrompt,
updateChatMetadata: updateChatMetadata, updateChatMetadata: updateChatMetadata,
saveChat: saveChatConditional, saveChat: saveChatConditional,
openCharacterChat: openCharacterChat,
openGroupChat: openGroupChat,
saveMetadata: saveMetadata, saveMetadata: saveMetadata,
sendSystemMessage: sendSystemMessage, sendSystemMessage: sendSystemMessage,
activateSendButtons, activateSendButtons,
@ -10568,54 +10601,60 @@ jQuery(async function () {
$(document).on('click', '.external_import_button, #external_import_button', async () => { $(document).on('click', '.external_import_button, #external_import_button', async () => {
const html = await renderTemplateAsync('importCharacters'); const html = await renderTemplateAsync('importCharacters');
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { okButton: $('#popup_template').attr('popup_text_import'), rows: 4 });
/** @type {string?} */
const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#popup_template').attr('popup_text_import'), rows: 4 });
if (!input) { if (!input) {
console.debug('Custom content import cancelled'); console.debug('Custom content import cancelled');
return; return;
} }
const url = input.trim(); // break input into one input per line
var request; const inputs = input.split('\n').map(x => x.trim()).filter(x => x.length > 0);
if (isValidUrl(url)) { for (const url of inputs) {
console.debug('Custom content import started for URL: ', url); let request;
request = await fetch('/api/content/importURL', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
} else {
console.debug('Custom content import started for Char UUID: ', url);
request = await fetch('/api/content/importUUID', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
}
if (!request.ok) { if (isValidUrl(url)) {
toastr.info(request.statusText, 'Custom content import failed'); console.debug('Custom content import started for URL: ', url);
console.error('Custom content import failed', request.status, request.statusText); request = await fetch('/api/content/importURL', {
return; method: 'POST',
} headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
} else {
console.debug('Custom content import started for Char UUID: ', url);
request = await fetch('/api/content/importUUID', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
}
const data = await request.blob(); if (!request.ok) {
const customContentType = request.headers.get('X-Custom-Content-Type'); toastr.info(request.statusText, 'Custom content import failed');
const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, ''); console.error('Custom content import failed', request.status, request.statusText);
const file = new File([data], fileName, { type: data.type }); return;
}
switch (customContentType) { const data = await request.blob();
case 'character': const customContentType = request.headers.get('X-Custom-Content-Type');
await processDroppedFiles([file]); const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
break; const file = new File([data], fileName, { type: data.type });
case 'lorebook':
await importWorldInfo(file); switch (customContentType) {
break; case 'character':
default: await processDroppedFiles([file]);
toastr.warning('Unknown content type'); break;
console.error('Unknown content type', customContentType); case 'lorebook':
break; await importWorldInfo(file);
break;
default:
toastr.warning('Unknown content type');
console.error('Unknown content type', customContentType);
break;
}
} }
}); });

View File

@ -1,5 +1,5 @@
import { getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js'; import { getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from '../../extensions.js'; import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js'; import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { getMessageTimeStamp } from '../../RossAscends-mods.js'; import { getMessageTimeStamp } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js'; import { SECRET_KEYS, secret_state } from '../../secrets.js';
@ -334,7 +334,7 @@ async function captionCommandCallback(args, prompt) {
}); });
} }
jQuery(function () { jQuery(async function () {
function addSendPictureButton() { function addSendPictureButton() {
const sendButton = $(` const sendButton = $(`
<div id="send_picture" class="list-group-item flex-container flexGap5"> <div id="send_picture" class="list-group-item flex-container flexGap5">
@ -399,102 +399,12 @@ jQuery(function () {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
} }
function addSettings() { async function addSettings() {
const html = ` const html = await renderExtensionTemplateAsync('caption', 'settings');
<div class="caption_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Image Captioning</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="caption_source">Source</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
</select>
<div id="caption_multimodal_block" class="flex-container wide100p">
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_api">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="anthropic">Anthropic</option>
<option value="custom">Custom (OpenAI-compatible)</option>
<option value="google">Google MakerSuite</option>
<option value="koboldcpp">KoboldCpp</option>
<option value="llamacpp">llama.cpp</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
<option value="ooba">Text Generation WebUI (oobabooga)</option>
</select>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
<option data-type="ollama" value="llava:latest">llava:latest</option>
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
<option data-type="koboldcpp" value="koboldcpp_current">[Currently loaded]</option>
<option data-type="custom" value="custom_current">[Currently selected]</option>
</select>
</div>
<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">
Allow reverse proxy
</label>
<div class="flexBasis100p m-b-1">
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
</div>
</div>
<div id="caption_prompt_block">
<label for="caption_prompt">Caption Prompt</label>
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
Ask every time
</label>
</div>
<label for="caption_template">Message Template <small>(use <code>{{caption}}</code> macro)</small></label>
<textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
<input id="caption_refine_mode" type="checkbox" class="checkbox">
Edit captions before saving
</label>
</div>
</div>
</div>
`;
$('#extensions_settings2').append(html); $('#extensions_settings2').append(html);
} }
addSettings(); await addSettings();
addPictureSendForm(); addPictureSendForm();
addSendPictureButton(); addSendPictureButton();
setImageIcon(); setImageIcon();

View File

@ -0,0 +1,90 @@
<div class="caption_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Image Captioning</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<label for="caption_source">Source</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="multimodal">Multimodal (OpenAI / Anthropic / llama / Google)</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
</select>
<div id="caption_multimodal_block" class="flex-container wide100p">
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_api">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="anthropic">Anthropic</option>
<option value="custom">Custom (OpenAI-compatible)</option>
<option value="google">Google MakerSuite</option>
<option value="koboldcpp">KoboldCpp</option>
<option value="llamacpp">llama.cpp</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
<option value="ooba">Text Generation WebUI (oobabooga)</option>
</select>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
<option data-type="openrouter" value="anthropic/claude-3-haiku:beta">anthropic/claude-3-haiku:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
<option data-type="ollama" value="llava:latest">llava:latest</option>
<option data-type="llamacpp" value="llamacpp_current">[Currently loaded]</option>
<option data-type="ooba" value="ooba_current">[Currently loaded]</option>
<option data-type="koboldcpp" value="koboldcpp_current">[Currently loaded]</option>
<option data-type="custom" value="custom_current">[Currently selected]</option>
</select>
</div>
<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">
Allow reverse proxy
</label>
<div class="flexBasis100p m-b-1">
<small><b>Hint:</b> Set your API keys and endpoints in the 'API Connections' tab first.</small>
</div>
</div>
<div id="caption_prompt_block">
<label for="caption_prompt">Caption Prompt</label>
<textarea id="caption_prompt" class="text_pole" rows="1" placeholder="&lt; Use default &gt;">${PROMPT_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_prompt_ask" title="Ask for a custom prompt every time an image is captioned.">
<input id="caption_prompt_ask" type="checkbox" class="checkbox">
Ask every time
</label>
</div>
<label for="caption_template">Message Template <small>(use <code>&lcub;&lcub;caption&rcub;&rcub;</code> macro)</small></label>
<textarea id="caption_template" class="text_pole" rows="2" placeholder="&lt; Use default &gt;">${TEMPLATE_DEFAULT}</textarea>
<label class="checkbox_label margin-bot-10px" for="caption_refine_mode">
<input id="caption_refine_mode" type="checkbox" class="checkbox">
Edit captions before saving
</label>
</div>
</div>
</div>

View File

@ -22,7 +22,7 @@ import {
import { collapseNewlines } from '../../power-user.js'; import { collapseNewlines } from '../../power-user.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js'; import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js'; import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js'; import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js';
import { debounce_timeout } from '../../constants.js'; import { debounce_timeout } from '../../constants.js';
import { getSortedEntries } from '../../world-info.js'; import { getSortedEntries } from '../../world-info.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js'; import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
@ -66,11 +66,13 @@ const settings = {
size_threshold: 10, size_threshold: 10,
chunk_size: 5000, chunk_size: 5000,
chunk_count: 2, chunk_count: 2,
overlap_percent: 0,
// For Data Bank // For Data Bank
size_threshold_db: 5, size_threshold_db: 5,
chunk_size_db: 2500, chunk_size_db: 2500,
chunk_count_db: 5, chunk_count_db: 5,
overlap_percent_db: 0,
file_template_db: 'Related information:\n{{text}}', file_template_db: 'Related information:\n{{text}}',
file_position_db: extension_prompt_types.IN_PROMPT, file_position_db: extension_prompt_types.IN_PROMPT,
file_depth_db: 4, file_depth_db: 4,
@ -369,7 +371,7 @@ async function processFiles(chat) {
// File is already in the collection // File is already in the collection
if (!hashesInCollection.length) { if (!hashesInCollection.length) {
await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size); await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size, settings.overlap_percent);
} }
const queryText = await getQueryText(chat); const queryText = await getQueryText(chat);
@ -409,7 +411,7 @@ async function ingestDataBankAttachments(source) {
const thresholdLength = settings.size_threshold_db * 1024; const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold // Use chunk size from settings if file is larger than threshold
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1; const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
await vectorizeFile(file.text, file.name, collectionId, chunkSize); await vectorizeFile(file.text, file.name, collectionId, chunkSize, settings.overlap_percent_db);
} }
return dataBankCollectionIds; return dataBankCollectionIds;
@ -467,9 +469,10 @@ async function retrieveFileChunks(queryText, collectionId) {
* @param {string} fileName File name * @param {string} fileName File name
* @param {string} collectionId File collection ID * @param {string} collectionId File collection ID
* @param {number} chunkSize Chunk size * @param {number} chunkSize Chunk size
* @param {number} overlapPercent Overlap size (in %)
* @returns {Promise<boolean>} True if successful, false if not * @returns {Promise<boolean>} True if successful, false if not
*/ */
async function vectorizeFile(fileText, fileName, collectionId, chunkSize) { async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
try { try {
if (settings.translate_files && typeof window['translate'] === 'function') { if (settings.translate_files && typeof window['translate'] === 'function') {
console.log(`Vectors: Translating file ${fileName} to English...`); console.log(`Vectors: Translating file ${fileName} to English...`);
@ -478,8 +481,11 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize) {
} }
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`); const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
const chunks = splitRecursive(fileText, chunkSize); const overlapSize = Math.round(chunkSize * overlapPercent / 100);
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks); // Overlap should not be included in chunk size. It will be later compensated by overlapChunks
chunkSize = overlapSize > 0 ? (chunkSize - overlapSize) : chunkSize;
const chunks = splitRecursive(fileText, chunkSize).map((x, y, z) => overlapSize > 0 ? overlapChunks(x, y, z, overlapSize) : x);
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks with ${overlapPercent}% overlap`, chunks);
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index })); const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
await insertVectorItems(collectionId, items); await insertVectorItems(collectionId, items);
@ -588,6 +594,26 @@ function getPromptText(queriedMessages) {
return substituteParams(settings.template.replace(/{{text}}/i, queriedText)); return substituteParams(settings.template.replace(/{{text}}/i, queriedText));
} }
/**
* Modifies text chunks to include overlap with adjacent chunks.
* @param {string} chunk Current item
* @param {number} index Current index
* @param {string[]} chunks List of chunks
* @param {number} overlapSize Size of the overlap
* @returns {string} Overlapped chunks, with overlap trimmed to sentence boundaries
*/
function overlapChunks(chunk, index, chunks, overlapSize) {
const halfOverlap = Math.floor(overlapSize / 2);
const nextChunk = chunks[index + 1];
const prevChunk = chunks[index - 1];
const nextOverlap = trimToEndSentence(nextChunk?.substring(0, halfOverlap)) || '';
const prevOverlap = trimToStartSentence(prevChunk?.substring(prevChunk.length - halfOverlap)) || '';
const overlappedChunk = [prevOverlap, chunk, nextOverlap].filter(x => x).join(' ');
return overlappedChunk;
}
window['vectors_rearrangeChat'] = rearrangeChat; window['vectors_rearrangeChat'] = rearrangeChat;
const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_timeout.relaxed); const onChatEvent = debounce(async () => await moduleWorker.update(), debounce_timeout.relaxed);
@ -969,8 +995,9 @@ async function onViewStatsClick() {
toastr.info(`Total hashes: <b>${totalHashes}</b><br> toastr.info(`Total hashes: <b>${totalHashes}</b><br>
Unique hashes: <b>${uniqueHashes}</b><br><br> Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`, I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`, `Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false }); { timeOut: 10000, escapeHtml: false },
);
const chat = getContext().chat; const chat = getContext().chat;
for (const message of chat) { for (const message of chat) {
@ -1010,6 +1037,23 @@ async function onVectorizeAllFilesClick() {
return -1; return -1;
} }
/**
* Gets the overlap percent for a file attachment.
* @param file {import('../../chats.js').FileAttachment} File attachment
* @returns {number} Overlap percent for the file
*/
function getOverlapPercent(file) {
if (chatAttachments.includes(file)) {
return settings.overlap_percent;
}
if (dataBank.includes(file)) {
return settings.overlap_percent_db;
}
return 0;
}
let allSuccess = true; let allSuccess = true;
for (const file of allFiles) { for (const file of allFiles) {
@ -1023,7 +1067,8 @@ async function onVectorizeAllFilesClick() {
} }
const chunkSize = getChunkSize(file); const chunkSize = getChunkSize(file);
const result = await vectorizeFile(text, file.name, collectionId, chunkSize); const overlapPercent = getOverlapPercent(file);
const result = await vectorizeFile(text, file.name, collectionId, chunkSize, overlapPercent);
if (!result) { if (!result) {
allSuccess = false; allSuccess = false;
@ -1343,6 +1388,18 @@ jQuery(async () => {
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#vectors_overlap_percent').val(settings.overlap_percent).on('input', () => {
settings.overlap_percent = Number($('#vectors_overlap_percent').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_overlap_percent_db').val(settings.overlap_percent_db).on('input', () => {
settings.overlap_percent_db = Number($('#vectors_overlap_percent_db').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_file_template_db').val(settings.file_template_db).on('input', () => { $('#vectors_file_template_db').val(settings.file_template_db).on('input', () => {
settings.file_template_db = String($('#vectors_file_template_db').val()); settings.file_template_db = String($('#vectors_file_template_db').val());
Object.assign(extension_settings.vectors, settings); Object.assign(extension_settings.vectors, settings);

View File

@ -193,19 +193,25 @@
<label for="vectors_size_threshold"> <label for="vectors_size_threshold">
<small>Size threshold (KB)</small> <small>Size threshold (KB)</small>
</label> </label>
<input id="vectors_size_threshold" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_size_threshold" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="Chunk size for file splitting."> <div class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size"> <label for="vectors_chunk_size">
<small>Chunk size (chars)</small> <small>Chunk size (chars)</small>
</label> </label>
<input id="vectors_chunk_size" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_chunk_size" type="number" class="text_pole" min="1" max="99999" />
</div>
<div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled.">
<label for="vectors_overlap_percent">
<small>Chunk overlap (%)</small>
</label>
<input id="vectors_overlap_percent" type="number" class="text_pole" min="0" max="99" step="1" />
</div> </div>
<div class="flex1" title="How many chunks to retrieve when querying."> <div class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count"> <label for="vectors_chunk_count">
<small>Retrieve chunks</small> <small>Retrieve chunks</small>
</label> </label>
<input id="vectors_chunk_count" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_chunk_count" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
</div> </div>
<div class="flex justifyCenter" title="These settings apply to files stored in the Data Bank."> <div class="flex justifyCenter" title="These settings apply to files stored in the Data Bank.">
@ -216,19 +222,25 @@
<label for="vectors_size_threshold_db"> <label for="vectors_size_threshold_db">
<small>Size threshold (KB)</small> <small>Size threshold (KB)</small>
</label> </label>
<input id="vectors_size_threshold_db" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_size_threshold_db" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
<div class="flex1" title="Chunk size for file splitting."> <div class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size_db"> <label for="vectors_chunk_size_db">
<small>Chunk size (chars)</small> <small>Chunk size (chars)</small>
</label> </label>
<input id="vectors_chunk_size_db" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_chunk_size_db" type="number" class="text_pole" min="1" max="99999" />
</div>
<div class="flex1" title="The overlap between adjacent chunks in % from chunk size. The overlap text is trimmed to sentence boundaries. 0 = disabled.">
<label for="vectors_overlap_percent_db">
<small>Chunk overlap (%)</small>
</label>
<input id="vectors_overlap_percent_db" type="number" class="text_pole" min="0" max="99" step="1" />
</div> </div>
<div class="flex1" title="How many chunks to retrieve when querying."> <div class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count_db"> <label for="vectors_chunk_count_db">
<small>Retrieve chunks</small> <small>Retrieve chunks</small>
</label> </label>
<input id="vectors_chunk_count_db" type="number" class="text_pole widthUnset" min="1" max="99999" /> <input id="vectors_chunk_count_db" type="number" class="text_pole" min="1" max="99999" />
</div> </div>
</div> </div>
<div class="flex-container flexFlowColumn"> <div class="flex-container flexFlowColumn">

View File

@ -1501,15 +1501,17 @@ async function onGroupActionClick(event) {
const index = _thisGroup.disabled_members.indexOf(member.data('id')); const index = _thisGroup.disabled_members.indexOf(member.data('id'));
if (index !== -1) { if (index !== -1) {
_thisGroup.disabled_members.splice(index, 1); _thisGroup.disabled_members.splice(index, 1);
await editGroup(openGroupId, false, false);
} }
await editGroup(openGroupId, false, false);
} }
if (action === 'disable') { if (action === 'disable') {
member.addClass('disabled'); member.addClass('disabled');
const _thisGroup = groups.find(x => x.id === openGroupId); const _thisGroup = groups.find(x => x.id === openGroupId);
_thisGroup.disabled_members.push(member.data('id')); if (!_thisGroup.disabled_members.includes(member.data('id'))) {
await editGroup(openGroupId, false, false); _thisGroup.disabled_members.push(member.data('id'));
await editGroup(openGroupId, false, false);
}
} }
if (action === 'up' || action === 'down') { if (action === 'up' || action === 'down') {

View File

@ -1284,6 +1284,10 @@ export async function prepareOpenAIMessages({
} }
const chat = chatCompletion.getChat(); const chat = chatCompletion.getChat();
const eventData = { chat, dryRun };
await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
openai_messages_count = chat.filter(x => x?.role === 'user' || x?.role === 'assistant')?.length || 0; openai_messages_count = chat.filter(x => x?.role === 'user' || x?.role === 'assistant')?.length || 0;
return [chat, promptManager.tokenHandler.counts]; return [chat, promptManager.tokenHandler.counts];

View File

@ -771,6 +771,7 @@ async function CreateZenSliders(elmnt) {
sliderID == 'max_temp_textgenerationwebui' || sliderID == 'max_temp_textgenerationwebui' ||
sliderID == 'dynatemp_exponent_textgenerationwebui' || sliderID == 'dynatemp_exponent_textgenerationwebui' ||
sliderID == 'guidance_scale_textgenerationwebui' || sliderID == 'guidance_scale_textgenerationwebui' ||
sliderID == 'rep_pen_slope_textgenerationwebui' ||
sliderID == 'guidance_scale') { sliderID == 'guidance_scale') {
offVal = 1; offVal = 1;
} }

View File

@ -23,6 +23,7 @@ import {
name2, name2,
reloadCurrentChat, reloadCurrentChat,
removeMacros, removeMacros,
renameCharacter,
saveChatConditional, saveChatConditional,
sendMessageAsUser, sendMessageAsUser,
sendSystemMessage, sendSystemMessage,
@ -323,6 +324,29 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
helpString: 'Opens up a chat with the character or group by its name', helpString: 'Opens up a chat with the character or group by its name',
aliases: ['char'], aliases: ['char'],
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'rename-char',
/** @param {{silent: string, chats: string}} options @param {string} name */
callback: async ({ silent = 'true', chats = null }, name) => {
const renamed = await renameCharacter(name, { silent: isTrueBoolean(silent), renameChats: chats !== null ? isTrueBoolean(chats) : null });
return String(renamed);
},
returns: 'true/false - Whether the rename was successful',
namedArgumentList: [
new SlashCommandNamedArgument(
'silent', 'Hide any blocking popups. (if false, the name is optional. If not supplied, a popup asking for it will appear)', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
),
new SlashCommandNamedArgument(
'chats', 'Rename char in all previous chats', [ARGUMENT_TYPE.BOOLEAN], false, false, '<null>',
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'new char name', [ARGUMENT_TYPE.STRING], true,
),
],
helpString: 'Renames the current character.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sysgen', name: 'sysgen',
callback: generateSystemMessage, callback: generateSystemMessage,
@ -336,15 +360,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'ask', name: 'ask',
callback: askCharacter, callback: askCharacter,
unnamedArgumentList: [ namedArgumentList: [
new SlashCommandArgument( new SlashCommandNamedArgument(
'character name', [ARGUMENT_TYPE.STRING], true, 'name', 'character name', [ARGUMENT_TYPE.STRING], true, false, '',
),
new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], true,
), ),
], ],
helpString: 'Asks a specified character card a prompt. Character name and prompt have to be separated by a new line.', unnamedArgumentList: [
new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: 'Asks a specified character card a prompt. Character name must be provided in a named argument.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'delname', name: 'delname',
@ -1129,9 +1155,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
helpString: 'Lists all script injections for the current chat.', helpString: 'Lists all script injections for the current chat.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'flushinjects', name: 'flushinject',
aliases: ['flushinjects'],
unnamedArgumentList: [
new SlashCommandArgument(
'injection ID or a variable name pointing to ID', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, '',
),
],
callback: flushInjectsCallback, callback: flushInjectsCallback,
helpString: 'Removes all script injections for the current chat.', helpString: 'Removes a script injection for the current chat. If no ID is provided, removes all script injections.',
})); }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'tokens', name: 'tokens',
@ -1231,7 +1263,7 @@ function injectCallback(args, value) {
} }
function listInjectsCallback() { function listInjectsCallback() {
if (!chat_metadata.script_injects) { if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
toastr.info('No script injections for the current chat'); toastr.info('No script injections for the current chat');
return ''; return '';
} }
@ -1251,17 +1283,29 @@ function listInjectsCallback() {
sendSystemMessage(system_message_types.GENERIC, htmlMessage); sendSystemMessage(system_message_types.GENERIC, htmlMessage);
} }
function flushInjectsCallback() { /**
* Flushes script injections for the current chat.
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
* @param {string} value Unnamed argument
* @returns {string} Empty string
*/
function flushInjectsCallback(args, value) {
if (!chat_metadata.script_injects) { if (!chat_metadata.script_injects) {
return ''; return '';
} }
const idArgument = resolveVariable(value, args._scope);
for (const [id, inject] of Object.entries(chat_metadata.script_injects)) { for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
if (idArgument && id !== idArgument) {
continue;
}
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`; const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role); setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
delete chat_metadata.script_injects[id];
} }
chat_metadata.script_injects = {};
saveMetadataDebounced(); saveMetadataDebounced();
return ''; return '';
} }
@ -1797,7 +1841,7 @@ async function deleteSwipeCallback(_, arg) {
await reloadCurrentChat(); await reloadCurrentChat();
} }
async function askCharacter(_, text) { async function askCharacter(args, text) {
// Prevent generate recursion // Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
@ -1810,17 +1854,25 @@ async function askCharacter(_, text) {
if (!text) { if (!text) {
console.warn('WARN: No text provided for /ask command'); console.warn('WARN: No text provided for /ask command');
} toastr.warning('No text provided for /ask command');
const parts = text.split('\n');
if (parts.length <= 1) {
toastr.warning('Both character name and message are required. Separate them with a new line.');
return; return;
} }
// Grabbing the message let name = '';
const name = parts.shift().trim(); let mesText = '';
let mesText = parts.join('\n').trim();
if (args?.name) {
name = args.name.trim();
mesText = text.trim();
if (!name && !mesText) {
toastr.warning('You must specify a name and text to ask.');
return;
}
}
mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND);
const prevChId = this_chid; const prevChId = this_chid;
// Find the character // Find the character
@ -1831,7 +1883,7 @@ async function askCharacter(_, text) {
} }
// Override character and send a user message // Override character and send a user message
setCharacterId(chId); setCharacterId(String(chId));
// TODO: Maybe look up by filename instead of name // TODO: Maybe look up by filename instead of name
const character = characters[chId]; const character = characters[chId];

View File

@ -17,6 +17,11 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* }} NamedArguments * }} NamedArguments
*/ */
/**
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
* @typedef {{[id:string]:string|SlashCommandClosure}} NamedArgumentsCapture
*/
/** /**
* @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments * @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments
*/ */
@ -28,7 +33,7 @@ export class SlashCommand {
* Creates a SlashCommand from a properties object. * Creates a SlashCommand from a properties object.
* @param {Object} props * @param {Object} props
* @param {string} [props.name] * @param {string} [props.name]
* @param {(namedArguments:NamedArguments, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback] * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
* @param {string} [props.helpString] * @param {string} [props.helpString]
* @param {boolean} [props.splitUnnamedArgument] * @param {boolean} [props.splitUnnamedArgument]
* @param {string[]} [props.aliases] * @param {string[]} [props.aliases]

View File

@ -842,7 +842,7 @@ function newTag(tagName) {
name: tagName, name: tagName,
folder_type: TAG_FOLDER_DEFAULT_TYPE, folder_type: TAG_FOLDER_DEFAULT_TYPE,
filter_state: DEFAULT_FILTER_STATE, filter_state: DEFAULT_FILTER_STATE,
sort_order: tags.length, sort_order: Math.max(0, ...tags.map(t => t.sort_order)) + 1,
color: '', color: '',
color2: '', color2: '',
create_date: Date.now(), create_date: Date.now(),
@ -1204,13 +1204,27 @@ function onGroupCreateClick() {
} }
export function applyTagsOnCharacterSelect(chid = null) { export function applyTagsOnCharacterSelect(chid = null) {
//clearTagsFilter(); // If we are in create window, we cannot simply redraw, as there are no real persisted tags. Grab them, and pass them in
if (menu_type === 'create') {
const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get();
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
printTagList($('#tagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
return;
}
chid = chid ?? Number(this_chid); chid = chid ?? Number(this_chid);
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } }); printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
} }
export function applyTagsOnGroupSelect(groupId = null) { export function applyTagsOnGroupSelect(groupId = null) {
//clearTagsFilter(); // If we are in create window, we explicitly have to tell the system to print for the new group, not the one selected in the background
if (menu_type === 'group_create') {
const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get();
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
printTagList($('#groupTagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
return;
}
groupId = groupId ?? Number(selected_group); groupId = groupId ?? Number(selected_group);
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } }); printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
} }

View File

@ -1,11 +1,22 @@
<h3 data-i18n="Enter the URL of the content to import">Enter the URL of the content to import</h3> <h3 data-i18n="Import Characters">Import Characters</h3>
<span data-i18n="Supported sources:">Supported sources:</span><br> <h4 data-i18n="Enter the URL of the content to import">Enter the URL of the content to import</h3>
<ul class="justifyLeft"> <div class="sources_list justifyLeft">
<li><span data-i18n="char_import_1">Chub Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>Anonymous/example-character</tt></li> <span data-i18n="Supported sources:">Supported sources:</span><br>
<li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li> <ul class="marginTop5 li-padding-bot5">
<li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li> <li><span data-i18n="char_import_1">Chub Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>Anonymous/example-character</tt></li>
<li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li> <li><span data-i18n="char_import_2">Chub Lorebook (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li><span data-i18n="char_import_5">AICharacterCard.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li> <li><span data-i18n="char_import_3">JanitorAI Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>ddd1498a-a370-4136-b138-a8cd9461fdfe_character-aqua-the-useless-goddess</tt></li>
<li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li> <li><span data-i18n="char_import_4">Pygmalion.chat Character (Direct Link or UUID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>a7ca95a1-0c88-4e23-91b3-149db1e78ab9</tt></li>
<li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li> <li><span data-i18n="char_import_5">AICharacterCard.com Character (Direct Link or ID)</span><br><span data-i18n="char_import_example">Example:</span> <tt>AICC/aicharcards/the-game-master</tt></li>
<ul> <li><span data-i18n="char_import_6">Direct PNG Link (refer to</span> <code>config.yaml</code><span data-i18n="char_import_7"> for allowed hosts)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://files.catbox.moe/notarealfile.png</tt></li>
<li><span data-i18n="char_import_8">RisuRealm Character (Direct Link)</span><br><span data-i18n="char_import_example">Example:</span> <tt>https://realm.risuai.net/character/3ca54c71-6efe-46a2-b9d0-4f62df23d712</tt></li>
</ul>
</div>
<small>
<span data-i18n="Supports importing multiple characters.">
Supports importing multiple characters.
</span>
<span data-i18n="Write each URL or ID into a new line.">
Write each URL or ID into a new line.
</span>
</small>

View File

@ -101,6 +101,7 @@ const settings = {
rep_pen: 1.2, rep_pen: 1.2,
rep_pen_range: 0, rep_pen_range: 0,
rep_pen_decay: 0, rep_pen_decay: 0,
rep_pen_slope: 1,
no_repeat_ngram_size: 0, no_repeat_ngram_size: 0,
penalty_alpha: 0, penalty_alpha: 0,
num_beams: 1, num_beams: 1,
@ -180,6 +181,7 @@ export const setting_names = [
'rep_pen', 'rep_pen',
'rep_pen_range', 'rep_pen_range',
'rep_pen_decay', 'rep_pen_decay',
'rep_pen_slope',
'no_repeat_ngram_size', 'no_repeat_ngram_size',
'top_k', 'top_k',
'top_p', 'top_p',
@ -1105,6 +1107,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'mirostat': settings.mirostat_mode, 'mirostat': settings.mirostat_mode,
'ignore_eos': settings.ban_eos_token, 'ignore_eos': settings.ban_eos_token,
'n_probs': power_user.request_token_probabilities ? 10 : undefined, 'n_probs': power_user.request_token_probabilities ? 10 : undefined,
'rep_pen_slope': settings.rep_pen_slope,
}; };
const vllmParams = { const vllmParams = {
'n': canMultiSwipe ? settings.n : 1, 'n': canMultiSwipe ? settings.n : 1,

View File

@ -491,6 +491,10 @@ export function sortByCssOrder(a, b) {
* trimToEndSentence('Hello, world! I am from'); // 'Hello, world!' * trimToEndSentence('Hello, world! I am from'); // 'Hello, world!'
*/ */
export function trimToEndSentence(input, include_newline = false) { export function trimToEndSentence(input, include_newline = false) {
if (!input) {
return '';
}
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '', '', '”', '', '】', '', '」', '_']); // extend this as you see fit const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '', '', '”', '', '】', '', '」', '_']); // extend this as you see fit
let last = -1; let last = -1;
@ -520,6 +524,10 @@ export function trimToEndSentence(input, include_newline = false) {
} }
export function trimToStartSentence(input) { export function trimToStartSentence(input) {
if (!input) {
return '';
}
let p1 = input.indexOf('.'); let p1 = input.indexOf('.');
let p2 = input.indexOf('!'); let p2 = input.indexOf('!');
let p3 = input.indexOf('?'); let p3 = input.indexOf('?');

View File

@ -456,6 +456,21 @@ code {
color: var(--white70a); color: var(--white70a);
} }
kbd {
display: inline-block;
padding: 2px 4px;
font-family: Consolas, monospace;
white-space: nowrap;
/* background-color: #eeeeee; */
background-color: rgba(255, 255, 255, 0.9);
color: #333;
border: 1px solid #b4b4b4;
border-radius: 3px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
font-size: 90%;
line-height: 1;
}
hr { hr {
background-image: linear-gradient(90deg, var(--transparent), var(--SmartThemeBodyColor), var(--transparent)); background-image: linear-gradient(90deg, var(--transparent), var(--SmartThemeBodyColor), var(--transparent));

View File

@ -1,9 +1,11 @@
const express = require('express'); const express = require('express');
const fetch = require('node-fetch').default; const fetch = require('node-fetch').default;
const fs = require('fs');
const { jsonParser } = require('../../express-common'); const { jsonParser, urlencodedParser } = require('../../express-common');
const { forwardFetchResponse, delay } = require('../../util'); const { forwardFetchResponse, delay } = require('../../util');
const { getOverrideHeaders, setAdditionalHeaders } = require('../../additional-headers'); const { getOverrideHeaders, setAdditionalHeaders, setAdditionalHeadersByType } = require('../../additional-headers');
const { TEXTGEN_TYPES } = require('../../constants');
const router = express.Router(); const router = express.Router();
@ -185,4 +187,55 @@ router.post('/status', jsonParser, async function (request, response) {
response.send(result); response.send(result);
}); });
router.post('/transcribe-audio', urlencodedParser, async function (request, response) {
try {
const server = request.body.server;
if (!server) {
console.log('Server is not set');
return response.sendStatus(400);
}
if (!request.file) {
console.log('No audio file found');
return response.sendStatus(400);
}
console.log('Transcribing audio with KoboldCpp', server);
const fileBase64 = fs.readFileSync(request.file.path).toString('base64');
fs.rmSync(request.file.path);
const headers = {};
setAdditionalHeadersByType(headers, TEXTGEN_TYPES.KOBOLDCPP, server, request.user.directories);
const url = new URL(server);
url.pathname = '/api/extra/transcribe';
const result = await fetch(url, {
method: 'POST',
headers: {
...headers,
},
body: JSON.stringify({
prompt: '',
audio_data: fileBase64,
}),
});
if (!result.ok) {
const text = await result.text();
console.log('KoboldCpp request failed', result.statusText, text);
return response.status(500).send(text);
}
const data = await result.json();
console.log('KoboldCpp transcription response', data);
return response.json(data);
} catch (error) {
console.error('KoboldCpp transcription failed', error);
response.status(500).send('Internal server error');
}
});
module.exports = { router }; module.exports = { router };

View File

@ -956,6 +956,10 @@ router.post('/chats', jsonParser, async function (request, response) {
return; return;
} }
if (request.body.simple) {
return response.send(jsonFiles.map(file => ({ file_name: file })));
}
const jsonFilesPromise = jsonFiles.map((file) => { const jsonFilesPromise = jsonFiles.map((file) => {
return new Promise(async (res) => { return new Promise(async (res) => {
const pathToFile = path.join(request.user.directories.chats, characterDirectory, file); const pathToFile = path.join(request.user.directories.chats, characterDirectory, file);

View File

@ -77,7 +77,7 @@ router.post('/create', jsonParser, (request, response) => {
generation_mode_join_suffix: request.body.generation_mode_join_suffix ?? '', generation_mode_join_suffix: request.body.generation_mode_join_suffix ?? '',
}; };
const pathToFile = path.join(request.user.directories.groups, `${id}.json`); const pathToFile = path.join(request.user.directories.groups, `${id}.json`);
const fileData = JSON.stringify(groupMetadata); const fileData = JSON.stringify(groupMetadata, null, 4);
if (!fs.existsSync(request.user.directories.groups)) { if (!fs.existsSync(request.user.directories.groups)) {
fs.mkdirSync(request.user.directories.groups); fs.mkdirSync(request.user.directories.groups);
@ -93,7 +93,7 @@ router.post('/edit', jsonParser, (request, response) => {
} }
const id = request.body.id; const id = request.body.id;
const pathToFile = path.join(request.user.directories.groups, `${id}.json`); const pathToFile = path.join(request.user.directories.groups, `${id}.json`);
const fileData = JSON.stringify(request.body); const fileData = JSON.stringify(request.body, null, 4);
writeFileAtomicSync(pathToFile, fileData); writeFileAtomicSync(pathToFile, fileData);
return response.send({ ok: true }); return response.send({ ok: true });

View File

@ -65,7 +65,7 @@ router.post('/text-workers', jsonParser, async (request, response) => {
} }
const agent = await getClientAgent(); const agent = await getClientAgent();
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', { const fetchResult = await fetch('https://aihorde.net/api/v2/workers?type=text', {
headers: { headers: {
'Client-Agent': agent, 'Client-Agent': agent,
}, },
@ -88,7 +88,7 @@ router.post('/text-models', jsonParser, async (request, response) => {
} }
const agent = await getClientAgent(); const agent = await getClientAgent();
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/status/models?type=text', { const fetchResult = await fetch('https://aihorde.net/api/v2/status/models?type=text', {
headers: { headers: {
'Client-Agent': agent, 'Client-Agent': agent,
}, },
@ -106,7 +106,7 @@ router.post('/text-models', jsonParser, async (request, response) => {
router.post('/status', jsonParser, async (_, response) => { router.post('/status', jsonParser, async (_, response) => {
try { try {
const agent = await getClientAgent(); const agent = await getClientAgent();
const fetchResult = await fetch('https://horde.koboldai.net/api/v2/status/heartbeat', { const fetchResult = await fetch('https://aihorde.net/api/v2/status/heartbeat', {
headers: { headers: {
'Client-Agent': agent, 'Client-Agent': agent,
}, },
@ -123,7 +123,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
try { try {
const taskId = request.body.taskId; const taskId = request.body.taskId;
const agent = await getClientAgent(); const agent = await getClientAgent();
const fetchResult = await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${taskId}`, { const fetchResult = await fetch(`https://aihorde.net/api/v2/generate/text/status/${taskId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Client-Agent': agent, 'Client-Agent': agent,
@ -143,7 +143,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
try { try {
const taskId = request.body.taskId; const taskId = request.body.taskId;
const agent = await getClientAgent(); const agent = await getClientAgent();
const fetchResult = await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${taskId}`, { const fetchResult = await fetch(`https://aihorde.net/api/v2/generate/text/status/${taskId}`, {
headers: { headers: {
'Client-Agent': agent, 'Client-Agent': agent,
}, },
@ -160,7 +160,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
router.post('/generate-text', jsonParser, async (request, response) => { router.post('/generate-text', jsonParser, async (request, response) => {
const apiKey = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY; const apiKey = readSecret(request.user.directories, SECRET_KEYS.HORDE) || ANONYMOUS_KEY;
const url = 'https://horde.koboldai.net/api/v2/generate/text/async'; const url = 'https://aihorde.net/api/v2/generate/text/async';
const agent = await getClientAgent(); const agent = await getClientAgent();
console.log(request.body); console.log(request.body);