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",
"version": "1.12.0",
"version": "1.12.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sillytavern",
"version": "1.12.0",
"version": "1.12.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {

View File

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

View File

@ -592,3 +592,23 @@ textarea:disabled {
text-align: center;
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-input" type="number" min="-1" max="8192" step="1" data-for="rep_pen_range_textgenerationwebui" id="rep_pen_range_counter_textgenerationwebui">
</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">
<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">
@ -2764,6 +2769,7 @@
<option value="mistral-small-latest">mistral-small-latest</option>
<option value="mistral-medium-latest">mistral-medium-latest</option>
<option value="mistral-large-latest">mistral-large-latest</option>
<option value="codestral-latest">codestral-latest</option>
</optgroup>
<optgroup label="Sub-versions">
<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-medium-2312">mistral-medium-2312</option>
<option value="mistral-large-2402">mistral-large-2402</option>
<option value="codestral-2405">codestral-2405</option>
</optgroup>
</select>
</div>
@ -2817,10 +2824,8 @@
<optgroup label="Open-Source Models">
<option value="llama-3-8b-instruct">llama-3-8b-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="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
<option value="mixtral-8x22b-instruct">mixtral-8x22b-instruct</option>
</optgroup>
</select>
</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:": "警告:",
"This action is irreversible.": "此操作不可逆。",
"Type the user's handle below to confirm:": "在下面输入用户的名称以确认:",
"Import Characters": "导入角色",
"Enter the URL of the content to import": "输入要导入的内容的URL",
"Supported sources:": "支持的来源:",
"char_import_1": "Chub 角色直链或ID",
@ -1394,6 +1395,8 @@
"char_import_6": "被允许的PNG直链请参阅",
"char_import_7": "",
"char_import_8": "RisuRealm 角色(直链)",
"Supports importing multiple characters.": "支持导入多个角色。",
"Write each URL or ID into a new line.": "将每个 URL 或 ID 写入新行。",
"Export for character": "导出角色",
"Export prompts for this character, including their order.": "导出此角色的提示,包括其顺序。",
"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',
GROUP_CHAT_DELETED: 'group_chat_deleted',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts',
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
WORLD_INFO_ACTIVATED: 'world_info_activated',
TEXT_COMPLETION_SETTINGS_READY: 'text_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',
// TODO: Naming convention is inconsistent with other events
CHARACTER_DELETED: 'characterDeleted',
@ -4014,6 +4016,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
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 thisPromptBits = [];
@ -5455,70 +5461,95 @@ export function setSendButtonState(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 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) {
const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue });
const response = await fetch('/api/characters/rename', {
method: 'POST',
headers: getRequestHeaders(),
body,
});
if (!newValue) {
toastr.warning('No character name provided.', 'Rename Character');
return false;
}
if (newValue === characters[this_chid].name) {
toastr.info('Same character name provided, so name did not change.', 'Rename Character');
return false;
}
try {
if (response.ok) {
const data = await response.json();
const newAvatar = data.avatar;
const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue });
const response = await fetch('/api/characters/rename', {
method: 'POST',
headers: getRequestHeaders(),
body,
});
// Replace tags list
renameTagKey(oldAvatar, newAvatar);
try {
if (response.ok) {
const data = await response.json();
const newAvatar = data.avatar;
// Reload characters list
await getCharacters();
// Replace tags list
renameTagKey(oldAvatar, newAvatar);
// Find newly renamed character
const newChId = characters.findIndex(c => c.avatar == data.avatar);
// Reload characters list
await getCharacters();
if (newChId !== -1) {
// Select the character after the renaming
this_chid = -1;
await selectCharacterById(String(newChId));
// Find newly renamed character
const newChId = characters.findIndex(c => c.avatar == data.avatar);
// Async delay to update UI
await delay(1);
if (newChId !== -1) {
// Select the character after the renaming
this_chid = -1;
await selectCharacterById(String(newChId));
if (this_chid === -1) {
throw new Error('New character not selected');
}
// Async delay to update UI
await delay(1);
// Also rename as a group member
await renameGroupMember(oldAvatar, newAvatar, newValue);
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!');
}
if (this_chid === -1) {
throw new Error('New character not selected');
}
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 {
throw new Error('Could not rename the character');
throw new Error('Newly renamed character was lost?');
}
}
catch {
// Reloading to prevent data corruption
await callPopup('Something went wrong. The page will be reloaded.', 'text');
location.reload();
else {
throw new Error('Could not rename the character');
}
}
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) {
@ -7678,6 +7709,8 @@ window['SillyTavern'].getContext = function () {
setExtensionPrompt: setExtensionPrompt,
updateChatMetadata: updateChatMetadata,
saveChat: saveChatConditional,
openCharacterChat: openCharacterChat,
openGroupChat: openGroupChat,
saveMetadata: saveMetadata,
sendSystemMessage: sendSystemMessage,
activateSendButtons,
@ -10568,54 +10601,60 @@ jQuery(async function () {
$(document).on('click', '.external_import_button, #external_import_button', async () => {
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) {
console.debug('Custom content import cancelled');
return;
}
const url = input.trim();
var request;
// break input into one input per line
const inputs = input.split('\n').map(x => x.trim()).filter(x => x.length > 0);
if (isValidUrl(url)) {
console.debug('Custom content import started for URL: ', url);
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 }),
});
}
for (const url of inputs) {
let request;
if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed');
console.error('Custom content import failed', request.status, request.statusText);
return;
}
if (isValidUrl(url)) {
console.debug('Custom content import started for URL: ', url);
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 }),
});
}
const data = await request.blob();
const customContentType = request.headers.get('X-Custom-Content-Type');
const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
const file = new File([data], fileName, { type: data.type });
if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed');
console.error('Custom content import failed', request.status, request.statusText);
return;
}
switch (customContentType) {
case 'character':
await processDroppedFiles([file]);
break;
case 'lorebook':
await importWorldInfo(file);
break;
default:
toastr.warning('Unknown content type');
console.error('Unknown content type', customContentType);
break;
const data = await request.blob();
const customContentType = request.headers.get('X-Custom-Content-Type');
const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
const file = new File([data], fileName, { type: data.type });
switch (customContentType) {
case 'character':
await processDroppedFiles([file]);
break;
case 'lorebook':
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 { 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 { getMessageTimeStamp } from '../../RossAscends-mods.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() {
const sendButton = $(`
<div id="send_picture" class="list-group-item flex-container flexGap5">
@ -399,102 +399,12 @@ jQuery(function () {
saveSettingsDebounced();
});
}
function addSettings() {
const html = `
<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>
`;
async function addSettings() {
const html = await renderExtensionTemplateAsync('caption', 'settings');
$('#extensions_settings2').append(html);
}
addSettings();
await addSettings();
addPictureSendForm();
addSendPictureButton();
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 { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.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 { getSortedEntries } from '../../world-info.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
@ -66,11 +66,13 @@ const settings = {
size_threshold: 10,
chunk_size: 5000,
chunk_count: 2,
overlap_percent: 0,
// For Data Bank
size_threshold_db: 5,
chunk_size_db: 2500,
chunk_count_db: 5,
overlap_percent_db: 0,
file_template_db: 'Related information:\n{{text}}',
file_position_db: extension_prompt_types.IN_PROMPT,
file_depth_db: 4,
@ -369,7 +371,7 @@ async function processFiles(chat) {
// File is already in the collection
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);
@ -409,7 +411,7 @@ async function ingestDataBankAttachments(source) {
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
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;
@ -467,9 +469,10 @@ async function retrieveFileChunks(queryText, collectionId) {
* @param {string} fileName File name
* @param {string} collectionId File collection ID
* @param {number} chunkSize Chunk size
* @param {number} overlapPercent Overlap size (in %)
* @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 {
if (settings.translate_files && typeof window['translate'] === 'function') {
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 chunks = splitRecursive(fileText, chunkSize);
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks);
const overlapSize = Math.round(chunkSize * overlapPercent / 100);
// 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 }));
await insertVectorItems(collectionId, items);
@ -588,6 +594,26 @@ function getPromptText(queriedMessages) {
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;
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>
Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false });
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false },
);
const chat = getContext().chat;
for (const message of chat) {
@ -1010,6 +1037,23 @@ async function onVectorizeAllFilesClick() {
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;
for (const file of allFiles) {
@ -1023,7 +1067,8 @@ async function onVectorizeAllFilesClick() {
}
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) {
allSuccess = false;
@ -1343,6 +1388,18 @@ jQuery(async () => {
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', () => {
settings.file_template_db = String($('#vectors_file_template_db').val());
Object.assign(extension_settings.vectors, settings);

View File

@ -193,19 +193,25 @@
<label for="vectors_size_threshold">
<small>Size threshold (KB)</small>
</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 class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size">
<small>Chunk size (chars)</small>
</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 class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count">
<small>Retrieve chunks</small>
</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 class="flex justifyCenter" title="These settings apply to files stored in the Data Bank.">
@ -216,19 +222,25 @@
<label for="vectors_size_threshold_db">
<small>Size threshold (KB)</small>
</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 class="flex1" title="Chunk size for file splitting.">
<label for="vectors_chunk_size_db">
<small>Chunk size (chars)</small>
</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 class="flex1" title="How many chunks to retrieve when querying.">
<label for="vectors_chunk_count_db">
<small>Retrieve chunks</small>
</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 class="flex-container flexFlowColumn">

View File

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

View File

@ -1284,6 +1284,10 @@ export async function prepareOpenAIMessages({
}
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;
return [chat, promptManager.tokenHandler.counts];

View File

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

View File

@ -23,6 +23,7 @@ import {
name2,
reloadCurrentChat,
removeMacros,
renameCharacter,
saveChatConditional,
sendMessageAsUser,
sendSystemMessage,
@ -323,6 +324,29 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
helpString: 'Opens up a chat with the character or group by its name',
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({
name: 'sysgen',
callback: generateSystemMessage,
@ -336,15 +360,17 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'ask',
callback: askCharacter,
unnamedArgumentList: [
new SlashCommandArgument(
'character name', [ARGUMENT_TYPE.STRING], true,
),
new SlashCommandArgument(
'prompt', [ARGUMENT_TYPE.STRING], true,
namedArgumentList: [
new SlashCommandNamedArgument(
'name', 'character name', [ARGUMENT_TYPE.STRING], true, false, '',
),
],
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({
name: 'delname',
@ -1129,9 +1155,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
helpString: 'Lists all script injections for the current chat.',
}));
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,
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({
name: 'tokens',
@ -1231,7 +1263,7 @@ function injectCallback(args, value) {
}
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');
return '';
}
@ -1251,17 +1283,29 @@ function listInjectsCallback() {
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) {
return '';
}
const idArgument = resolveVariable(value, args._scope);
for (const [id, inject] of Object.entries(chat_metadata.script_injects)) {
if (idArgument && id !== idArgument) {
continue;
}
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
setExtensionPrompt(prefixedId, '', inject.position, inject.depth, inject.scan, inject.role);
delete chat_metadata.script_injects[id];
}
chat_metadata.script_injects = {};
saveMetadataDebounced();
return '';
}
@ -1797,7 +1841,7 @@ async function deleteSwipeCallback(_, arg) {
await reloadCurrentChat();
}
async function askCharacter(_, text) {
async function askCharacter(args, text) {
// Prevent generate recursion
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
@ -1810,17 +1854,25 @@ async function askCharacter(_, text) {
if (!text) {
console.warn('WARN: 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.');
toastr.warning('No text provided for /ask command');
return;
}
// Grabbing the message
const name = parts.shift().trim();
let mesText = parts.join('\n').trim();
let name = '';
let mesText = '';
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;
// Find the character
@ -1831,7 +1883,7 @@ async function askCharacter(_, text) {
}
// Override character and send a user message
setCharacterId(chId);
setCharacterId(String(chId));
// TODO: Maybe look up by filename instead of name
const character = characters[chId];

View File

@ -17,6 +17,11 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* }} 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
*/
@ -28,7 +33,7 @@ export class SlashCommand {
* Creates a SlashCommand from a properties object.
* @param {Object} props
* @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 {boolean} [props.splitUnnamedArgument]
* @param {string[]} [props.aliases]

View File

@ -842,7 +842,7 @@ function newTag(tagName) {
name: tagName,
folder_type: TAG_FOLDER_DEFAULT_TYPE,
filter_state: DEFAULT_FILTER_STATE,
sort_order: tags.length,
sort_order: Math.max(0, ...tags.map(t => t.sort_order)) + 1,
color: '',
color2: '',
create_date: Date.now(),
@ -1204,13 +1204,27 @@ function onGroupCreateClick() {
}
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);
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
}
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);
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>
<span data-i18n="Supported sources:">Supported sources:</span><br>
<ul class="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>
<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_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_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_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_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>
<h3 data-i18n="Import Characters">Import Characters</h3>
<h4 data-i18n="Enter the URL of the content to import">Enter the URL of the content to import</h3>
<div class="sources_list justifyLeft">
<span data-i18n="Supported sources:">Supported sources:</span><br>
<ul class="marginTop5 li-padding-bot5">
<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_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_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_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_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_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_range: 0,
rep_pen_decay: 0,
rep_pen_slope: 1,
no_repeat_ngram_size: 0,
penalty_alpha: 0,
num_beams: 1,
@ -180,6 +181,7 @@ export const setting_names = [
'rep_pen',
'rep_pen_range',
'rep_pen_decay',
'rep_pen_slope',
'no_repeat_ngram_size',
'top_k',
'top_p',
@ -1105,6 +1107,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
'mirostat': settings.mirostat_mode,
'ignore_eos': settings.ban_eos_token,
'n_probs': power_user.request_token_probabilities ? 10 : undefined,
'rep_pen_slope': settings.rep_pen_slope,
};
const vllmParams = {
'n': canMultiSwipe ? settings.n : 1,

View File

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

View File

@ -456,6 +456,21 @@ code {
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 {
background-image: linear-gradient(90deg, var(--transparent), var(--SmartThemeBodyColor), var(--transparent));

View File

@ -1,9 +1,11 @@
const express = require('express');
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 { getOverrideHeaders, setAdditionalHeaders } = require('../../additional-headers');
const { getOverrideHeaders, setAdditionalHeaders, setAdditionalHeadersByType } = require('../../additional-headers');
const { TEXTGEN_TYPES } = require('../../constants');
const router = express.Router();
@ -185,4 +187,55 @@ router.post('/status', jsonParser, async function (request, response) {
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 };

View File

@ -956,6 +956,10 @@ router.post('/chats', jsonParser, async function (request, response) {
return;
}
if (request.body.simple) {
return response.send(jsonFiles.map(file => ({ file_name: file })));
}
const jsonFilesPromise = jsonFiles.map((file) => {
return new Promise(async (res) => {
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 ?? '',
};
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)) {
fs.mkdirSync(request.user.directories.groups);
@ -93,7 +93,7 @@ router.post('/edit', jsonParser, (request, response) => {
}
const id = request.body.id;
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);
return response.send({ ok: true });

View File

@ -65,7 +65,7 @@ router.post('/text-workers', jsonParser, async (request, response) => {
}
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: {
'Client-Agent': agent,
},
@ -88,7 +88,7 @@ router.post('/text-models', jsonParser, async (request, response) => {
}
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: {
'Client-Agent': agent,
},
@ -106,7 +106,7 @@ router.post('/text-models', jsonParser, async (request, response) => {
router.post('/status', jsonParser, async (_, response) => {
try {
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: {
'Client-Agent': agent,
},
@ -123,7 +123,7 @@ router.post('/cancel-task', jsonParser, async (request, response) => {
try {
const taskId = request.body.taskId;
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',
headers: {
'Client-Agent': agent,
@ -143,7 +143,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
try {
const taskId = request.body.taskId;
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: {
'Client-Agent': agent,
},
@ -160,7 +160,7 @@ router.post('/task-status', jsonParser, async (request, response) => {
router.post('/generate-text', jsonParser, async (request, response) => {
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();
console.log(request.body);