diff --git a/.gitignore b/.gitignore index 458054342..03b2447a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ public/backgrounds/ public/groups/ public/group chats/ public/worlds/ +public/user/ public/css/bg_load.css public/themes/ public/OpenAI Settings/ diff --git a/public/script.js b/public/script.js index d5dcd3b2b..536bfd2e8 100644 --- a/public/script.js +++ b/public/script.js @@ -5629,7 +5629,14 @@ export async function getChatsFromFiles(data, isGroupChat) { return chat_dict; } - +/** + * Fetches the metadata of all past chats related to a specific character based on its avatar URL. + * The function sends a POST request to the server to retrieve all chats for the character. It then + * processes the received data, sorts it by the file name, and returns the sorted data. + * + * @returns {Array} - An array containing metadata of all past chats of the character, sorted + * in descending order by file name. Returns `undefined` if the fetch request is unsuccessful. + */ async function getPastCharacterChats() { const response = await fetch("/getallchatsofcharacter", { method: 'POST', diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 41accbb12..797936209 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -537,6 +537,38 @@ function processReply(str) { return str; } +async function saveBase64AsFile(base64Data, characterName, ext) { + // Construct the full data URL + const format = ext; // Extract the file extension (jpg, png, webp) + const dataURL = `data:image/${format};base64,${base64Data}`; + + // Prepare the request body + const requestBody = { + image: dataURL, + ch_name: characterName + }; + + // Send the data URL to your backend using fetch + const response = await fetch('/uploadimage', { + method: 'POST', + body: JSON.stringify(requestBody), + headers: { + ...getRequestHeaders(), + 'Content-Type': 'application/json' + }, + }); + + // If the response is successful, get the saved image path from the server's response + if (response.ok) { + const responseData = await response.json(); + return responseData.path; + } else { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to upload the image to the server'); + } +} + + function getRawLastMessage() { const context = getContext(); const lastMessage = context.chat.slice(-1)[0].mes, @@ -565,6 +597,10 @@ async function generatePicture(_, trigger, message, callback) { const quiet_prompt = getQuietPrompt(generationType, trigger); const context = getContext(); + // if context.characterId is not null, then we get context.characters[context.characterId].avatar, else we get groupId and context.groups[groupId].id + // sadly, groups is not an array, but is a dict with keys being index numbers, so we have to filter it + const characterName = context.characterId ? context.characters[context.characterId].name : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]].id.toString(); + const prevSDHeight = extension_settings.sd.height; const prevSDWidth = extension_settings.sd.width; const aspectRatio = extension_settings.sd.width / extension_settings.sd.height; @@ -580,8 +616,10 @@ async function generatePicture(_, trigger, message, callback) { // Round to nearest multiple of 64 extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64; const callbackOriginal = callback; - callback = function (prompt, base64Image) { + callback = async function (prompt, base64Image) { + const imagePath = base64Image; const imgUrl = `url(${base64Image})`; + if ('forceSetBackground' in window) { forceSetBackground(imgUrl); } else { @@ -590,9 +628,9 @@ async function generatePicture(_, trigger, message, callback) { } if (typeof callbackOriginal === 'function') { - callbackOriginal(prompt, base64Image); + callbackOriginal(prompt, imagePath); } else { - sendMessage(prompt, base64Image); + sendMessage(prompt, imagePath); } } } @@ -604,7 +642,7 @@ async function generatePicture(_, trigger, message, callback) { context.deactivateSendButtons(); hideSwipeButtons(); - await sendGenerationRequest(generationType, prompt, callback); + await sendGenerationRequest(generationType, prompt, characterName, callback); } catch (err) { console.trace(err); throw new Error('SD prompt text generation failed.') @@ -644,19 +682,19 @@ async function generatePrompt(quiet_prompt) { return processReply(reply); } -async function sendGenerationRequest(generationType, prompt, callback) { +async function sendGenerationRequest(generationType, prompt, characterName=null, callback) { const prefix = generationType !== generationMode.BACKGROUND ? combinePrefixes(extension_settings.sd.prompt_prefix, getCharacterPrefix()) : extension_settings.sd.prompt_prefix; if (extension_settings.sd.horde) { - await generateHordeImage(prompt, prefix, callback); + await generateHordeImage(prompt, prefix, characterName, callback); } else { - await generateExtrasImage(prompt, prefix, callback); + await generateExtrasImage(prompt, prefix, characterName, callback); } } -async function generateExtrasImage(prompt, prefix, callback) { +async function generateExtrasImage(prompt, prefix, characterName, callback) { console.debug(extension_settings.sd); const url = new URL(getApiUrl()); url.pathname = '/api/image'; @@ -680,14 +718,14 @@ async function generateExtrasImage(prompt, prefix, callback) { if (result.ok) { const data = await result.json(); - const base64Image = `data:image/jpeg;base64,${data.image}`; + const base64Image = await saveBase64AsFile(data.image, characterName, "jpg"); callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image); } else { callPopup('Image generation has failed. Please try again.', 'text'); } } -async function generateHordeImage(prompt, prefix, callback) { +async function generateHordeImage(prompt, prefix, characterName, callback) { const result = await fetch('/horde_generateimage', { method: 'POST', headers: getRequestHeaders(), @@ -709,7 +747,8 @@ async function generateHordeImage(prompt, prefix, callback) { if (result.ok) { const data = await result.text(); - const base64Image = `data:image/webp;base64,${data}`; + + const base64Image = await saveBase64AsFile(data, characterName, "webp"); callback ? callback(prompt, base64Image) : sendMessage(prompt, base64Image); } else { toastr.error('Image generation has failed. Please try again.'); @@ -842,7 +881,7 @@ async function sdMessageButton(e) { message.extra.title = prompt; console.log('Regenerating an image, using existing prompt:', prompt); - await sendGenerationRequest(generationMode.FREE, prompt, saveGeneratedImage); + await sendGenerationRequest(generationMode.FREE, prompt, characterName, saveGeneratedImage); } else { console.log("doing /sd raw last"); diff --git a/server.js b/server.js index 0af4efd4c..788dfd22e 100644 --- a/server.js +++ b/server.js @@ -296,6 +296,8 @@ const baseRequestArgs = { headers: { "Content-Type": "application/json" } }; const directories = { worlds: 'public/worlds/', avatars: 'public/User Avatars', + images: 'public/img/', + userImages: 'public/user/images/', groups: 'public/groups/', groupChats: 'public/group chats', chats: 'public/chats/', @@ -2585,6 +2587,53 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => { } }); + + +function ensureDirectoryExistence(filePath) { + const dirname = path.dirname(filePath); + if (fs.existsSync(dirname)) { + return true; + } + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +} + +app.post('/uploadimage', jsonParser, async (request, response) => { + // Check for image data + if (!request.body || !request.body.image) { + return response.status(400).send({ error: "No image data provided" }); + } + + // Extracting the base64 data and the image format + const match = request.body.image.match(/^data:image\/(png|jpg|webp);base64,(.+)$/); + if (!match) { + return response.status(400).send({ error: "Invalid image format" }); + } + + const [, format, base64Data] = match; + + // Constructing filename and path + const filename = `${Date.now()}.${format}`; + // if character is defined, save to a sub folder for that character + let pathToNewFile = path.join(directories.userImages, filename); + if(request.body.ch_name){ + pathToNewFile = path.join(directories.userImages, request.body.ch_name, filename); + } + + try { + ensureDirectoryExistence(pathToNewFile); + const imageBuffer = Buffer.from(base64Data, 'base64'); + await fs.promises.writeFile(pathToNewFile, imageBuffer); + // send the path to the image, relative to the client folder, which means removing the first folder from the path which is 'public' + pathToNewFile = pathToNewFile.split(path.sep).slice(1).join(path.sep); + response.send({ path:pathToNewFile }); + } catch (error) { + console.log(error); + response.status(500).send({ error: "Failed to save the image" }); + } +}); + + app.post('/getgroups', jsonParser, (_, response) => { const groups = [];