diff --git a/public/script.js b/public/script.js
index 11d9499e5..61f49a397 100644
--- a/public/script.js
+++ b/public/script.js
@@ -239,6 +239,7 @@ let exportPopper = Popper.createPopper(document.getElementById('export_button'),
let dialogueResolve = null;
let chat_metadata = {};
let streamingProcessor = null;
+let crop_data = undefined;
let fav_ch_checked = false;
@@ -2821,60 +2822,67 @@ async function saveChat(chat_name, withMetadata) {
async function read_avatar_load(input) {
if (input.files && input.files[0]) {
- const reader = new FileReader();
if (selected_button == "create") {
create_save_avatar = input.files;
}
- reader.onload = async function (e) {
- /* $('#dialogue_popup').addClass('large_dialogue_popup');
- $('#dialogue_popup').addClass('wide_dialogue_popup');
-
- await callPopup(`
-
Click image to start cropping. Click Ok to finalize
-
-

-
- `, 'avatarToCrop'); */
- $("#avatar_load_preview").attr("src", e.target.result);
+ const e = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = resolve;
+ reader.onerror = reject;
+ reader.readAsDataURL(input.files[0]);
+ })
- if (menu_type != "create") {
- $("#create_button").trigger('click');
+ $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
- const formData = new FormData($("#form_create").get(0));
+ const croppedImage = await callPopup(getCropPopup(e.target.result), 'avatarToCrop');
- $(".mes").each(async function () {
- if ($(this).attr("is_system") == 'true') {
- return;
- }
- if ($(this).attr("is_user") == 'true') {
- return;
- }
- if ($(this).attr("ch_name") == formData.get('ch_name')) {
- const previewSrc = $("#avatar_load_preview").attr("src");
- const avatar = $(this).find(".avatar img");
- avatar.attr('src', default_avatar);
- await delay(1);
- avatar.attr('src', previewSrc);
- }
- });
+ $("#avatar_load_preview").attr("src", croppedImage || e.target.result);
- await delay(durationSaveEdit);
- await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
- method: 'GET',
- headers: {
- 'pragma': 'no-cache',
- 'cache-control': 'no-cache',
- }
- });
- console.log('Avatar refreshed');
+ if (menu_type == "create") {
+ return;
+ }
+
+ $("#create_button").trigger('click');
+
+ const formData = new FormData($("#form_create").get(0));
+
+ $(".mes").each(async function () {
+ if ($(this).attr("is_system") == 'true') {
+ return;
}
- };
+ if ($(this).attr("is_user") == 'true') {
+ return;
+ }
+ if ($(this).attr("ch_name") == formData.get('ch_name')) {
+ const previewSrc = $("#avatar_load_preview").attr("src");
+ const avatar = $(this).find(".avatar img");
+ avatar.attr('src', default_avatar);
+ await delay(1);
+ avatar.attr('src', previewSrc);
+ }
+ });
+
+ await delay(durationSaveEdit);
+ await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), {
+ method: 'GET',
+ headers: {
+ 'pragma': 'no-cache',
+ 'cache-control': 'no-cache',
+ }
+ });
+ console.log('Avatar refreshed');
- reader.readAsDataURL(input.files[0]);
}
}
+function getCropPopup(src) {
+ return `Set the crop position of the avatar image and click Ok to confirm.
+
+

+
`;
+}
+
function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
}
@@ -3736,10 +3744,6 @@ function callPopup(text, type, inputValue = '') {
$("#dialogue_popup_input").val(inputValue);
- if (popup_type == 'avatarToCrop') {
-
- }
-
if (popup_type == 'input') {
$("#dialogue_popup_input").css("display", "block");
$("#dialogue_popup_ok").text("Save");
@@ -3753,6 +3757,17 @@ function callPopup(text, type, inputValue = '') {
if (popup_type == 'input') {
$("#dialogue_popup_input").focus();
}
+ if (popup_type == 'avatarToCrop') {
+ // unset existing data
+ crop_data = undefined;
+
+ $('#avatarToCrop').cropper({
+ aspectRatio: 2 / 3,
+ crop: function (event) {
+ crop_data = event.detail;
+ }
+ });
+ }
$("#shadow_popup").transition({
opacity: 1,
duration: 200,
@@ -4403,7 +4418,7 @@ $(document).ready(function () {
$(document).on("click", "#user_avatar_block .avatar_upload", function () {
$("#avatar_upload_file").click();
});
- $("#avatar_upload_file").on("change", function (e) {
+ $("#avatar_upload_file").on("change", async function (e) {
const file = e.target.files[0];
if (!file) {
@@ -4412,9 +4427,25 @@ $(document).ready(function () {
const formData = new FormData($("#form_upload_avatar").get(0));
+ const dataUrl = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = resolve;
+ reader.onerror = reject;
+ reader.readAsDataURL(file);
+ });
+
+ $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
+ await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop');
+
+ let url = "/uploaduseravatar";
+
+ if (crop_data !== undefined) {
+ url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
+ }
+
jQuery.ajax({
type: "POST",
- url: "/uploaduseravatar",
+ url: url,
data: formData,
beforeSend: () => { },
cache: false,
@@ -4424,6 +4455,7 @@ $(document).ready(function () {
if (data.path) {
appendUserAvatar(data.path);
}
+ crop_data = undefined;
},
error: (jqXHR, exception) => { },
});
@@ -4522,32 +4554,7 @@ $(document).ready(function () {
// $("#shadow_popup").css("opacity:", 0.0);
if (popup_type == 'avatarToCrop') {
-
- //ideally this is where we would grab the crop coordinates
- //and send back to read_avatar_load() to be sent to server for actual file resizing/cropping.
- //the code below does not work 'cropper not defined'.
-
- $("#avatarToCrop").cropper.getCroppedCanvas();
-
- $("#avatarToCrop").cropper.getCroppedCanvas({
- width: 160,
- height: 90,
- minWidth: 400,
- minHeight: 600,
- maxWidth: 4096,
- maxHeight: 4096,
- fillColor: 'transparent',
- imageSmoothingEnabled: false,
- imageSmoothingQuality: 'high',
- });
-
- $("#avatarToCrop").cropper.getCroppedCanvas().toBlob((blob) => {
- currentCroppedAvatar = new FormData();
- currentCroppedAvatar.append('croppedImage', blob/*, 'example.png' */);
- });
-
- return currentCroppedAvatar;
-
+ dialogueResolve($("#avatarToCrop").data('cropper').getCroppedCanvas().toDataURL('image/jpeg'));
};
if (popup_type == "del_bg") {
@@ -4705,10 +4712,15 @@ $(document).ready(function () {
if ($("#form_create").attr("actiontype") == "createcharacter") {
if ($("#character_name_pole").val().length > 0) {
//if the character name text area isn't empty (only posible when creating a new character)
- //console.log('/createcharacter entered');
+ let url = "/createcharacter";
+
+ if (crop_data != undefined) {
+ url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
+ }
+
jQuery.ajax({
type: "POST",
- url: "/createcharacter",
+ url: url,
data: formData,
beforeSend: function () {
$("#create_button").attr("disabled", true);
@@ -4760,6 +4772,7 @@ $(document).ready(function () {
select_rm_info(`Character created
${DOMPurify.sanitize(save_name)}
`, oldSelectedChar);
$("#rm_info_block").transition({ opacity: 1.0, duration: 2000 });
+ crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
@@ -4769,11 +4782,15 @@ $(document).ready(function () {
$("#result_info").html("Name not entered");
}
} else {
- //console.log('/editcharacter -- entered.');
- //console.log('Avatar Button Value:'+$("#add_avatar_button").val());
+ let url = '/editcharacter';
+
+ if (crop_data != undefined) {
+ url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
+ }
+
jQuery.ajax({
type: "POST",
- url: "/editcharacter",
+ url: url,
data: formData,
beforeSend: function () {
//$("#create_button").attr("disabled", true);
@@ -4817,6 +4834,7 @@ $(document).ready(function () {
$("#add_avatar_button").val("").clone(true)
);
$("#create_button").attr("value", "Save");
+ crop_data = undefined;
},
error: function (jqXHR, exception) {
$("#create_button").removeAttr("disabled");
@@ -5878,28 +5896,4 @@ $(document).ready(function () {
$(masterElement).val(myValue).trigger('input');
restoreCaretPosition($(this).get(0), caretPosition);
});
-
- /* $("#dialogue_popup_text").on('click', '#avatarToCrop', function () {
-
- var $image = $('#avatarToCrop');
-
- $image.cropper({
- aspectRatio: 3 / 4,
- crop: function (event) {
- console.log(event.detail.x);
- console.log(event.detail.y);
- console.log(event.detail.width);
- console.log(event.detail.height);
- console.log(event.detail.rotate);
- console.log(event.detail.scaleX);
- console.log(event.detail.scaleY);
- }
- });
-
- // Get the Cropper.js instance after initialized
- var cropper = $image.data('cropper');
-
-
- }); */
-
})
diff --git a/public/style.css b/public/style.css
index 7f3263ab1..6b2aa3eb1 100644
--- a/public/style.css
+++ b/public/style.css
@@ -2617,7 +2617,7 @@ h5 {
}
#avatarCropWrap {
- margin: 10px;
+ margin: 10px auto;
max-height: 90%;
max-width: 90%;
}
diff --git a/server.js b/server.js
index f076ac54f..d6410587d 100644
--- a/server.js
+++ b/server.js
@@ -164,6 +164,8 @@ function humanizedISO8601DateTime() {
var is_colab = process.env.colaburl !== undefined;
var charactersPath = 'public/characters/';
var chatsPath = 'public/chats/';
+const AVATAR_WIDTH = 400;
+const AVATAR_HEIGHT = 600;
const jsonParser = express.json({ limit: '100mb' });
const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' });
const baseRequestArgs = { headers: { "Content-Type": "application/json" } };
@@ -703,8 +705,9 @@ app.post("/createcharacter", urlencodedParser, function (request, response) {
if (!request.file) {
charaWrite(defaultAvatar, char, internalName, response, avatarName);
} else {
+ const crop = tryParse(request.query.crop);
const uploadPath = path.join("./uploads/", request.file.filename);
- charaWrite(uploadPath, char, internalName, response, avatarName);
+ charaWrite(uploadPath, char, internalName, response, avatarName, crop);
}
});
@@ -799,9 +802,10 @@ app.post("/editcharacter", urlencodedParser, async function (request, response)
const avatarPath = path.join(charactersPath, request.body.avatar_url);
await charaWrite(avatarPath, char, target_img, response, 'Character saved');
} else {
+ const crop = tryParse(request.query.crop);
const newAvatarPath = path.join("./uploads/", request.file.filename);
invalidateThumbnail('avatar', request.body.avatar_url);
- await charaWrite(newAvatarPath, char, target_img, response, 'Character saved');
+ await charaWrite(newAvatarPath, char, target_img, response, 'Character saved', crop);
}
}
catch {
@@ -845,11 +849,17 @@ app.post("/deletecharacter", urlencodedParser, function (request, response) {
});
});
-async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok') {
+async function charaWrite(img_url, data, target_img, response = undefined, mes = 'ok', crop = undefined) {
try {
// Read the image, resize, and save it as a PNG into the buffer
- const rawImg = await jimp.read(img_url);
- const image = await rawImg.cover(400, 600).getBufferAsync(jimp.MIME_PNG);
+ let rawImg = await jimp.read(img_url);
+
+ // Apply crop if defined
+ if (typeof crop == 'object') {
+ rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
+ }
+
+ const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG);
// Get the chunks
const chunks = extract(image);
@@ -1525,6 +1535,7 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
let filedata = request.file;
let uploadPath = path.join('./uploads', filedata.filename);
var format = request.body.file_type;
+ const defaultAvatarPath = './public/img/ai4.png';
//console.log(format);
if (filedata) {
if (format == 'json') {
@@ -1539,16 +1550,37 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
jsonData.name = sanitize(jsonData.name);
png_name = getPngName(jsonData.name);
- let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
+ let char = {
+ "name": jsonData.name,
+ "description": jsonData.description ?? '',
+ "personality": jsonData.personality ?? '',
+ "first_mes": jsonData.first_mes ?? '',
+ "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(),
+ "mes_example": jsonData.mes_example ?? '',
+ "scenario": jsonData.scenario ?? '',
+ "create_date": humanizedISO8601DateTime(),
+ "talkativeness": jsonData.talkativeness ?? 0.5
+ };
char = JSON.stringify(char);
- charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name });
+ charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else if (jsonData.char_name !== undefined) {//json Pygmalion notepad
jsonData.char_name = sanitize(jsonData.char_name);
png_name = getPngName(jsonData.char_name);
- let char = { "name": jsonData.char_name, "description": jsonData.char_persona ?? '', "personality": '', "first_mes": jsonData.char_greeting ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.example_dialogue ?? '', "scenario": jsonData.world_scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
+ let char = {
+ "name": jsonData.char_name,
+ "description": jsonData.char_persona ?? '',
+ "personality": '',
+ "first_mes": jsonData.char_greeting ?? '',
+ "avatar": 'none',
+ "chat": jsonData.name + " - " + humanizedISO8601DateTime(),
+ "mes_example": jsonData.example_dialogue ?? '',
+ "scenario": jsonData.world_scenario ?? '',
+ "create_date": humanizedISO8601DateTime(),
+ "talkativeness": jsonData.talkativeness ?? 0.5
+ };
char = JSON.stringify(char);
- charaWrite('./public/img/ai4.png', char, png_name, response, { file_name: png_name });
+ charaWrite(defaultAvatarPath, char, png_name, response, { file_name: png_name });
} else {
console.log('Incorrect character format .json');
response.send({ error: true });
@@ -1561,15 +1593,32 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
jsonData.name = sanitize(jsonData.name);
if (format == 'webp') {
- let convertedPath = path.join('./uploads', path.basename(uploadPath, ".webp") + ".png")
- await webp.dwebp(uploadPath, convertedPath, "-o");
- uploadPath = convertedPath;
+ try {
+ let convertedPath = path.join('./uploads', path.basename(uploadPath, ".webp") + ".png")
+ await webp.dwebp(uploadPath, convertedPath, "-o");
+ uploadPath = convertedPath;
+ }
+ catch {
+ console.error('WEBP image conversion failed. Using the default character image.');
+ uploadPath = defaultAvatarPath;
+ }
}
png_name = getPngName(jsonData.name);
if (jsonData.name !== undefined) {
- let char = { "name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": jsonData.name + " - " + humanizedISO8601DateTime(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": humanizedISO8601DateTime(), "talkativeness": jsonData.talkativeness ?? 0.5 };
+ let char = {
+ "name": jsonData.name,
+ "description": jsonData.description ?? '',
+ "personality": jsonData.personality ?? '',
+ "first_mes": jsonData.first_mes ?? '',
+ "avatar": 'none',
+ "chat": jsonData.name + " - " + humanizedISO8601DateTime(),
+ "mes_example": jsonData.mes_example ?? '',
+ "scenario": jsonData.scenario ?? '',
+ "create_date": humanizedISO8601DateTime(),
+ "talkativeness": jsonData.talkativeness ?? 0.5
+ };
char = JSON.stringify(char);
await charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
}
@@ -1804,8 +1853,14 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
try {
const pathToUpload = path.join('./uploads/' + request.file.filename);
- const rawImg = await jimp.read(pathToUpload);
- const image = await rawImg.cover(400, 400).getBufferAsync(jimp.MIME_PNG);
+ const crop = tryParse(request.query.crop);
+ let rawImg = await jimp.read(pathToUpload);
+
+ if (typeof crop == 'object') {
+ rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
+ }
+
+ const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG);
const filename = `${Date.now()}.png`;
const pathToNewFile = path.join(directories.avatars, filename);