Add shallow character load mode

This commit is contained in:
Cohee 2025-03-04 10:00:12 +00:00
parent 70f762c006
commit 383806325a
6 changed files with 125 additions and 10 deletions

View File

@ -4424,6 +4424,13 @@
<option data-i18n="tag_import_existing" value="4">Existing</option>
</select>
</div>
<label class="checkbox_label" for="shallow_characters" data-i18n="[title]Experimental feature. May behave unstable and have compatibility issues." title="Experimental feature. May behave unstable and have compatibility issues.">
<input id="shallow_characters" type="checkbox" />
<small data-i18n="Lazy Load Characters">
Lazy Load Characters
</small>
<i class="fa-solid fa-flask"></i>
</label>
<label class="checkbox_label" for="fuzzy_search_checkbox" title="Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring." data-i18n="[title]Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring">
<input id="fuzzy_search_checkbox" type="checkbox" />
<small data-i18n="Advanced Character Search">Advanced Character Search</small>

View File

@ -1781,7 +1781,7 @@ export async function getCharacters() {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
'': '',
shallow: power_user.shallow_characters,
}),
});
if (response.ok === true) {
@ -3658,6 +3658,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
setGenerationProgress(0);
generation_started = new Date();
// Prevent generation from shallow characters
await unshallowCharacter(this_chid);
// Occurs every time, even if the generation is aborted due to slash commands execution
await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
@ -6704,9 +6707,42 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t
}
}
/**
* Loads all the data of a shallow character.
* @param {string|undefined} characterId Array index
* @returns {Promise<void>} Promise that resolves when the character is unshallowed
*/
export async function unshallowCharacter(characterId) {
if (characterId === undefined) {
console.warn('Undefined character cannot be unshallowed');
return;
}
const character = characters[characterId];
if (!character) {
console.warn('Character not found:', characterId);
return;
}
// Character is not shallow
if (!character.shallow) {
return;
}
const avatar = character.avatar;
if (!avatar) {
console.warn('Character has no avatar field:', characterId);
return;
}
await getOneCharacter(avatar);
}
export async function getChat() {
//console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name);
try {
await unshallowCharacter(this_chid);
const response = await $.ajax({
type: 'POST',
url: '/api/chats/get',

View File

@ -72,6 +72,7 @@ import {
animation_duration,
depth_prompt_role_default,
shouldAutoContinue,
unshallowCharacter,
} from '../script.js';
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
@ -216,6 +217,7 @@ export async function getGroupChat(groupId, reload = false) {
// Run validation before any loading
validateGroup(group);
await unshallowGroupMembers(groupId);
const chat_id = group.chat_id;
const data = await loadGroupChat(chat_id);
@ -824,6 +826,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
}
try {
await unshallowGroupMembers(selected_group);
throwIfAborted();
hideSwipeButtons();
is_group_generating = true;
@ -1137,6 +1141,29 @@ export async function editGroup(id, immediately, reload = true) {
saveGroupDebounced(group, reload);
}
/**
* Unshallows all definitions of group members.
* @param {string} groupId Id of the group
* @returns {Promise<void>} Promise that resolves when all group members are unshallowed
*/
export async function unshallowGroupMembers(groupId) {
const group = groups.find(x => x.id == groupId);
if (!group) {
return;
}
const members = group.members;
if (!Array.isArray(members)) {
return;
}
for (const member of members) {
const index = characters.findIndex(x => x.avatar === member);
if (index === -1) {
continue;
}
await unshallowCharacter(String(index));
}
}
let groupAutoModeAbortController = null;
async function groupChatAutoModeWorker() {
@ -1158,9 +1185,9 @@ async function groupChatAutoModeWorker() {
await generateGroupWrapper(true, 'auto', { signal: groupAutoModeAbortController.signal });
}
async function modifyGroupMember(chat_id, groupMember, isDelete) {
async function modifyGroupMember(groupId, groupMember, isDelete) {
const id = groupMember.data('id');
const thisGroup = groups.find((x) => x.id == chat_id);
const thisGroup = groups.find((x) => x.id == groupId);
const membersArray = thisGroup?.members ?? newGroupMembers;
if (isDelete) {
@ -1173,6 +1200,7 @@ async function modifyGroupMember(chat_id, groupMember, isDelete) {
}
if (openGroupId) {
await unshallowGroupMembers(openGroupId);
await editGroup(openGroupId, false, false);
updateGroupAvatar(thisGroup);
}
@ -1638,7 +1666,7 @@ async function onGroupActionClick(event) {
}
if (action === 'view') {
openCharacterDefinition(member);
await openCharacterDefinition(member);
}
if (action === 'speak') {
@ -1690,7 +1718,7 @@ export async function openGroupById(groupId) {
return false;
}
function openCharacterDefinition(characterSelect) {
async function openCharacterDefinition(characterSelect) {
if (is_group_generating) {
toastr.warning(t`Can't peek a character while group reply is being generated`);
console.warn('Can\'t peek a character def while group reply is being generated');
@ -1703,6 +1731,7 @@ function openCharacterDefinition(characterSelect) {
return;
}
await unshallowCharacter(chid);
setCharacterId(chid);
select_selected_character(chid);
// Gentle nudge to recalculate tokens

View File

@ -314,6 +314,7 @@ let power_user = {
forbid_external_media: true,
external_media_allowed_overrides: [],
external_media_forbidden_overrides: [],
shallow_characters: false,
};
let themes = [];
@ -1595,6 +1596,7 @@ async function loadPowerUserSettings(settings, data) {
$('#auto-connect-checkbox').prop('checked', power_user.auto_connect);
$('#auto-load-chat-checkbox').prop('checked', power_user.auto_load_chat);
$('#forbid_external_media').prop('checked', power_user.forbid_external_media);
$('#shallow_characters').prop('checked', power_user.shallow_characters);
for (const theme of themes) {
const option = document.createElement('option');
@ -3888,6 +3890,12 @@ $(document).ready(() => {
await exportTheme();
});
$('#shallow_characters').on('input', function () {
power_user.shallow_characters = !!$(this).prop('checked');
saveSettingsDebounced();
toastr.info('Reload the page for this setting to take effect');
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);

View File

@ -47,6 +47,7 @@ import {
updateMessageBlock,
printMessages,
clearChat,
unshallowCharacter,
} from '../script.js';
import {
extension_settings,
@ -55,7 +56,7 @@ import {
renderExtensionTemplateAsync,
writeExtensionField,
} from './extensions.js';
import { groups, openGroupChat, selected_group } from './group-chats.js';
import { groups, openGroupChat, selected_group, unshallowGroupMembers } from './group-chats.js';
import { addLocaleData, getCurrentLocale, t, translate } from './i18n.js';
import { hideLoader, showLoader } from './loader.js';
import { MacrosParser } from './macros.js';
@ -210,6 +211,8 @@ export function getContext() {
clearChat,
ChatCompletionService,
TextCompletionService,
unshallowCharacter,
unshallowGroupMembers,
};
}

View File

@ -200,14 +200,45 @@ const calculateDataSize = (data) => {
return typeof data === 'object' ? Object.values(data).reduce((acc, val) => acc + String(val).length, 0) : 0;
};
/**
* Only get fields that are used to display the character list.
* @param {object} character Character object
* @returns {{shallow: true, [key: string]: any}} Shallow character
*/
const toShallow = (character) => {
return {
name: character.name,
avatar: character.avatar,
chat: character.chat,
fav: character.fav,
date_added: character.date_added,
create_date: character.create_date,
date_last_chat: character.date_last_chat,
chat_size: character.chat_size,
data_size: character.data_size,
data: {
name: _.get(character, 'data.name', ''),
character_version: _.get(character, 'data.character_version', ''),
creator: _.get(character, 'data.creator', ''),
creator_notes: _.get(character, 'data.creator_notes', ''),
extensions: {
fav: _.get(character, 'data.extensions.fav', false),
},
},
shallow: true,
};
};
/**
* processCharacter - Process a given character, read its data and calculate its statistics.
*
* @param {string} item The name of the character.
* @param {import('../users.js').UserDirectoryList} directories User directories
* @param {object} options Options for the character processing
* @param {boolean} options.shallow If true, only return the core character's metadata
* @return {Promise<object>} A Promise that resolves when the character processing is done.
*/
const processCharacter = async (item, directories) => {
const processCharacter = async (item, directories, { shallow }) => {
try {
const imgFile = path.join(directories.characters, item);
const imgData = await readCharacterData(imgFile);
@ -226,7 +257,7 @@ const processCharacter = async (item, directories) => {
character['chat_size'] = chatSize;
character['date_last_chat'] = dateLastChat;
character['data_size'] = calculateDataSize(jsonObject?.data);
return character;
return shallow ? toShallow(character) : character;
}
catch (err) {
console.error(`Could not process character: ${item}`);
@ -991,9 +1022,10 @@ router.post('/delete', jsonParser, validateAvatarUrlMiddleware, async function (
*/
router.post('/all', jsonParser, async function (request, response) {
try {
const shallow = !!request.body.shallow;
const files = fs.readdirSync(request.user.directories.characters);
const pngFiles = files.filter(file => file.endsWith('.png'));
const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories));
const processingPromises = pngFiles.map(file => processCharacter(file, request.user.directories, { shallow }));
const data = (await Promise.all(processingPromises)).filter(c => c.name);
return response.send(data);
} catch (err) {
@ -1012,7 +1044,7 @@ router.post('/get', jsonParser, validateAvatarUrlMiddleware, async function (req
return response.sendStatus(404);
}
const data = await processCharacter(item, request.user.directories);
const data = await processCharacter(item, request.user.directories, { shallow: false });
return response.send(data);
} catch (err) {