Multiple expressions per group in waifu mode

This commit is contained in:
Cohee
2023-06-03 22:17:25 +03:00
parent b0f8e51c42
commit faa097fabd
4 changed files with 280 additions and 22 deletions

View File

@ -425,6 +425,8 @@ export const event_types = {
IMPERSONATE_READY: 'impersonate_ready', IMPERSONATE_READY: 'impersonate_ready',
CHAT_CHANGED: 'chat_id_changed', CHAT_CHANGED: 'chat_id_changed',
GENERATION_STOPPED: 'generation_stopped', GENERATION_STOPPED: 'generation_stopped',
SETTINGS_UPDATED: 'settings_updated',
GROUP_UPDATED: 'group_updated',
} }
export const eventSource = new EventEmitter(); export const eventSource = new EventEmitter();
@ -3955,6 +3957,7 @@ function selectKoboldGuiPreset() {
async function saveSettings(type) { async function saveSettings(type) {
//console.log('Entering settings with name1 = '+name1); //console.log('Entering settings with name1 = '+name1);
eventSource.emit(event_types.SETTINGS_UPDATED);
return jQuery.ajax({ return jQuery.ajax({
type: "POST", type: "POST",
url: "/savesettings", url: "/savesettings",

View File

@ -1,5 +1,8 @@
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
import { deviceInfo } from "../../RossAscends-mods.js";
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js"; import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
import { power_user } from "../../power-user.js";
import { onlyUnique, debounce } from "../../utils.js";
export { MODULE_NAME }; export { MODULE_NAME };
const MODULE_NAME = 'expressions'; const MODULE_NAME = 'expressions';
@ -41,6 +44,201 @@ let lastMessage = null;
let spriteCache = {}; let spriteCache = {};
let inApiCall = false; let inApiCall = false;
function isVisualNovelMode() {
return Boolean(!deviceInfo.isMobile && power_user.waifuMode && getContext().groupId);
}
async function forceUpdateVisualNovelMode() {
if (isVisualNovelMode()) {
await updateVisualNovelMode();
}
}
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
async function updateVisualNovelMode(name, expression) {
const container = $('#visual-novel-wrapper');
await visualNovelRemoveInactive(container);
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
// calculate layer indices based on recent messages
await visualNovelUpdateLayers(container);
await Promise.allSettled(setSpritePromises);
// update again based on new sprites
if (setSpritePromises.length > 0) {
await visualNovelUpdateLayers(container);
}
}
async function visualNovelRemoveInactive(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const members = group.members;
const removeInactiveCharactersPromises = [];
// remove inactive characters after 1 second
container.find('.expression-holder').each((_, current) => {
const promise = new Promise(resolve => {
const element = $(current);
const avatar = element.data('avatar');
if (!members.includes(avatar) || group.disabled_members.includes(avatar)) {
element.fadeOut(250, () => {
element.remove();
resolve();
});
} else {
resolve();
}
});
removeInactiveCharactersPromises.push(promise);
});
await Promise.allSettled(removeInactiveCharactersPromises);
}
async function visualNovelSetCharacterSprites(container, name, expression) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const members = group.members;
const labels = await getExpressionsList();
const createCharacterPromises = [];
const setSpritePromises = [];
for (const avatar of members) {
const isDisabled = group.disabled_members.includes(avatar);
// skip disabled characters
if (isDisabled) {
continue;
}
const character = context.characters.find(x => x.avatar == avatar);
// download images if not downloaded yet
if (spriteCache[character.name] === undefined) {
spriteCache[character.name] = await getSpritesList(character.name, character);
}
const sprites = spriteCache[character.name];
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
const defaultSpritePath = sprites.find(x => x.label === 'joy')?.path;
if (expressionImage.length > 0) {
if (name == character.name) {
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
const path = currentSpritePath || defaultSpritePath || '';
const img = expressionImage.find('img');
setImage(img, path);
}
} else {
const template = $('#expression-holder').clone();
template.attr('data-avatar', avatar);
$('#visual-novel-wrapper').append(template);
setImage(template.find('img'), defaultSpritePath || '');
const fadeInPromise = new Promise(resolve => {
template.fadeIn(250, () => resolve());
});
createCharacterPromises.push(fadeInPromise);
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
setSpritePromises.push(setSpritePromise);
}
}
await Promise.allSettled(createCharacterPromises);
return setSpritePromises;
}
async function visualNovelUpdateLayers(container) {
const context = getContext();
const group = context.groups.find(x => x.id == context.groupId);
const members = group.members;
const recentMessages = context.chat.map(x => x.original_avatar).filter(onlyUnique);
const filteredMembers = members.filter(x => !group.disabled_members.includes(x));
const layerIndices = filteredMembers.slice().sort((a, b) => recentMessages.indexOf(a) - recentMessages.indexOf(b));
const setLayerIndicesPromises = [];
const sortFunction = (a, b) => {
const avatarA = $(a).data('avatar');
const avatarB = $(b).data('avatar');
const indexA = filteredMembers.indexOf(avatarA);
const indexB = filteredMembers.indexOf(avatarB);
return indexA - indexB;
};
const containerWidth = container.width();
const pivotalPoint = containerWidth * 0.5;
let images = $('.expression-holder');
let imagesWidth = [];
images.sort(sortFunction).each(function () {
imagesWidth.push($(this).width());
});
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
let currentPosition = pivotalPoint - (totalWidth / 2);
if (totalWidth > containerWidth) {
let overlap = (totalWidth - containerWidth) / (imagesWidth.length - 1);
imagesWidth = imagesWidth.map((width) => width - overlap);
currentPosition = 0; // Reset the initial position to 0
}
images.sort(sortFunction).each((index, current) => {
const element = $(current);
const avatar = element.data('avatar');
const layerIndex = layerIndices.indexOf(avatar);
element.css('z-index', layerIndex);
element.show();
const promise = new Promise(resolve => {
element.animate({ left: currentPosition + 'px' }, 500, () => {
resolve();
});
});
currentPosition += imagesWidth[index];
setLayerIndicesPromises.push(promise);
});
await Promise.allSettled(setLayerIndicesPromises);
}
async function setLastMessageSprite(img, avatar, labels) {
const context = getContext();
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
if (lastMessage) {
const text = lastMessage.mes || '';
const sprites = spriteCache[lastMessage.name] || [];
const label = await getExpressionLabel(text);
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
if (path) {
setImage(img, path);
}
}
}
function setImage(img, path) {
img.attr('src', path);
img.removeClass('default');
img.off('error');
img.on('error', function () {
$(this).attr('src', '');
});
}
function onExpressionsShowDefaultInput() { function onExpressionsShowDefaultInput() {
const value = $(this).prop('checked'); const value = $(this).prop('checked');
extension_settings.expressions.showDefault = value; extension_settings.expressions.showDefault = value;
@ -73,6 +271,23 @@ async function moduleWorker() {
spriteCache = {}; spriteCache = {};
} }
const vnMode = isVisualNovelMode();
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
if (vnMode) {
$('#expression-wrapper').hide();
$('#visual-novel-wrapper').show();
} else {
$('#expression-wrapper').show();
$('#visual-novel-wrapper').hide();
}
const vnStateChanged = vnMode !== vnWrapperVisible;
if (vnStateChanged) {
lastMessage = null;
}
const currentLastMessage = getLastCharacterMessage(); const currentLastMessage = getLastCharacterMessage();
// character has no expressions or it is not loaded // character has no expressions or it is not loaded
@ -119,29 +334,19 @@ async function moduleWorker() {
try { try {
inApiCall = true; inApiCall = true;
const url = new URL(getApiUrl()); let expression = await getExpressionLabel(currentLastMessage.mes);
url.pathname = '/api/classify';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: currentLastMessage.mes })
});
if (apiResult.ok) {
const name = context.groupId ? currentLastMessage.name : context.name2; const name = context.groupId ? currentLastMessage.name : context.name2;
const force = !!context.groupId; const force = !!context.groupId;
const data = await apiResult.json();
let expression = data.classification[0].label;
// Character won't be angry on you for swiping // Character won't be angry on you for swiping
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) { if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
expression = 'joy'; expression = 'joy';
} }
if (vnMode) {
await updateVisualNovelMode(name, expression);
} else {
setExpression(name, expression, force); setExpression(name, expression, force);
} }
@ -156,6 +361,25 @@ async function moduleWorker() {
} }
} }
async function getExpressionLabel(text) {
const url = new URL(getApiUrl());
url.pathname = '/api/classify';
const apiResult = await doExtrasFetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Bypass-Tunnel-Reminder': 'bypass',
},
body: JSON.stringify({ text: text }),
});
if (apiResult.ok) {
const data = await apiResult.json();
return data.classification[0].label;
}
}
function getLastCharacterMessage() { function getLastCharacterMessage() {
const context = getContext(); const context = getContext();
const reversedChat = context.chat.slice().reverse(); const reversedChat = context.chat.slice().reverse();
@ -450,6 +674,14 @@ async function onClickExpressionDelete(event) {
</div>`; </div>`;
$('body').append(html); $('body').append(html);
} }
function addVisualNovelMode() {
const html = `
<div id="visual-novel-wrapper">
</div>`
const element = $(html);
element.hide();
$('body').append(element);
}
function addSettings() { function addSettings() {
const html = ` const html = `
@ -486,12 +718,17 @@ async function onClickExpressionDelete(event) {
$(document).on('click', '.expression_list_item', onClickExpressionImage); $(document).on('click', '.expression_list_item', onClickExpressionImage);
$(document).on('click', '.expression_list_upload', onClickExpressionUpload); $(document).on('click', '.expression_list_upload', onClickExpressionUpload);
$(document).on('click', '.expression_list_delete', onClickExpressionDelete); $(document).on('click', '.expression_list_delete', onClickExpressionDelete);
$(window).on("resize", updateVisualNovelModeDebounced);
$('.expression_settings').hide(); $('.expression_settings').hide();
} }
addExpressionImage(); addExpressionImage();
addVisualNovelMode();
addSettings(); addSettings();
const wrapper = new ModuleWorkerWrapper(moduleWorker); const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); const updateFunction = wrapper.update.bind(wrapper);
setInterval(updateFunction, UPDATE_INTERVAL);
moduleWorker(); moduleWorker();
eventSource.on(event_types.CHAT_CHANGED, updateFunction);
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
})(); })();

View File

@ -10,6 +10,21 @@
width: 100vw; width: 100vw;
} }
#visual-novel-wrapper {
display: flex;
height: calc(100vh - 40px);
width: 100vw;
position: relative;
}
#visual-novel-wrapper .expression-holder {
width: max-content;
}
#visual-novel-wrapper img.expression {
object-fit: cover;
}
.expression-holder { .expression-holder {
min-width: 100px; min-width: 100px;
min-height: 100px; min-height: 100px;

View File

@ -185,6 +185,8 @@ function getFirstCharacterMessage(character) {
mes["name"] = character.name; mes["name"] = character.name;
mes["is_name"] = true; mes["is_name"] = true;
mes["send_date"] = humanizedDateTime(); mes["send_date"] = humanizedDateTime();
mes["original_avatar"] = character.avatar;
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
mes["mes"] = character.first_mes mes["mes"] = character.first_mes
? substituteParams(character.first_mes.trim(), name1, character.name) ? substituteParams(character.first_mes.trim(), name1, character.name)
: default_ch_mes; : default_ch_mes;
@ -1084,6 +1086,7 @@ function select_group_chats(groupId, skipAnimation) {
} }
sortGroupMembers("#rm_group_add_members .group_member"); sortGroupMembers("#rm_group_add_members .group_member");
await eventSource.emit(event_types.GROUP_UPDATED);
}); });
} }