Preserve image dimensions when generating swipe images (#5205)
* Initial plan * feat: preserve image width/height when generating swipe images Save width and height in ImageGenerationAttachmentProps when creating MediaAttachment objects, and apply saved dimensions when generating image swipes. Falls back to current extension settings if not specified. Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: validate dimension override with Number.isInteger and skip setTypeSpecificDimensions when override is present Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: call restoreOriginalDimensions when setTypeSpecificDimensions was used Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Contain the logic to setTypeSpecificDimensions * Diff clean-up * feat: enhance prompt refinement with negative input and saved resolution options * fix: Only save image dimensions when running from command * Fix wording in refine prompt dialog --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
Vendored
+2
@@ -139,6 +139,8 @@ declare global {
|
||||
interface ImageGenerationAttachmentProps {
|
||||
generation_type?: number;
|
||||
negative?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
interface ImageCaptionAttachmentProps {
|
||||
|
||||
@@ -810,13 +810,60 @@ async function onRenameStyleClick() {
|
||||
/**
|
||||
* Modifies prompt based on user inputs.
|
||||
* @param {string} prompt Prompt to refine
|
||||
* @param {boolean} isNegative Whether the prompt is a negative one
|
||||
* @param {object} [args] Additional arguments for refinement
|
||||
* @param {string} [args.negative] Negative prompt to prefill
|
||||
* @param {string} [args.resolution] Saved resolution to offer as a checkbox option
|
||||
* @returns {Promise<string>} Refined prompt
|
||||
*/
|
||||
async function refinePrompt(prompt, isNegative) {
|
||||
async function refinePrompt(prompt, args = null) {
|
||||
if (extension_settings.sd.refine_mode) {
|
||||
const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
|
||||
const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 8, okButton: 'Continue' });
|
||||
/** @type {import('../../popup.js').CustomPopupInput[]} */
|
||||
const customInputs = [];
|
||||
|
||||
if (args?.negative) {
|
||||
customInputs.push({
|
||||
id: 'sd_refine_negative',
|
||||
label: t`Negative prompt (optional)`,
|
||||
type: 'textarea',
|
||||
rows: 4,
|
||||
defaultState: String(args.negative || ''),
|
||||
});
|
||||
}
|
||||
|
||||
if (args?.resolution) {
|
||||
customInputs.push({
|
||||
id: 'sd_use_saved_resolution',
|
||||
label: t`Use saved resolution (${args.resolution})`,
|
||||
type: 'checkbox',
|
||||
defaultState: true,
|
||||
});
|
||||
}
|
||||
|
||||
const refinedPrompt = await Popup.show.input(
|
||||
t`Review and edit the prompt:`,
|
||||
t`Press "Cancel" to abort the image generation.`,
|
||||
prompt.trim(),
|
||||
{
|
||||
rows: 8,
|
||||
okButton: t`Continue`,
|
||||
cancelButton: t`Cancel`,
|
||||
customInputs,
|
||||
onClose: (popup) => {
|
||||
if (!popup.result || !args) {
|
||||
return;
|
||||
}
|
||||
|
||||
const negativeInput = popup.inputResults.get('sd_refine_negative');
|
||||
const useSavedResolution = popup.inputResults.get('sd_use_saved_resolution');
|
||||
|
||||
if (negativeInput) {
|
||||
args.negative = negativeInput.toString().trim();
|
||||
}
|
||||
if (!useSavedResolution) {
|
||||
args.resolution = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (refinedPrompt) {
|
||||
return String(refinedPrompt);
|
||||
@@ -3035,23 +3082,29 @@ async function generatePicture(initiator, args, trigger, message, callback) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
function setTypeSpecificDimensions(generationType) {
|
||||
/**
|
||||
* Adjusts image generation dimensions based on the generation type and/or previous media attachment.
|
||||
* @param {number} generationType The type of image generation to perform, used to determine dimension adjustments
|
||||
* @param {MediaAttachment} [mediaAttachment] Media attachment to base dimension adjustments on
|
||||
* @returns {{height: number, width: number}} Previous dimensions before modification
|
||||
*/
|
||||
function setTypeSpecificDimensions(generationType, mediaAttachment = null) {
|
||||
const prevSDHeight = extension_settings.sd.height;
|
||||
const prevSDWidth = extension_settings.sd.width;
|
||||
const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
|
||||
|
||||
// Face images are always portrait (pun intended)
|
||||
if ((generationType === generationMode.FACE || generationType === generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) {
|
||||
// 1. If there's a media attachment, match its previous dimensions
|
||||
// 2. Face images are always portrait (pun intended) - increase height if needed
|
||||
// 3. Background images are always landscape - increase width if needed
|
||||
if (Number.isInteger(mediaAttachment?.width) && Number.isInteger(mediaAttachment?.height)) {
|
||||
extension_settings.sd.width = mediaAttachment.width;
|
||||
extension_settings.sd.height = mediaAttachment.height;
|
||||
} else if ((generationType === generationMode.FACE || generationType === generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) {
|
||||
// Round to nearest multiple of 64
|
||||
extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64;
|
||||
}
|
||||
|
||||
if (generationType === generationMode.BACKGROUND) {
|
||||
// Background images are always landscape
|
||||
if (aspectRatio <= 1) {
|
||||
// Round to nearest multiple of 64
|
||||
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
|
||||
}
|
||||
} else if (generationType === generationMode.BACKGROUND && aspectRatio <= 1) {
|
||||
// Round to nearest multiple of 64
|
||||
extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
|
||||
}
|
||||
|
||||
if (extension_settings.sd.snap) {
|
||||
@@ -3079,6 +3132,10 @@ function setTypeSpecificDimensions(generationType) {
|
||||
return { height: prevSDHeight, width: prevSDWidth };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the original image generation dimensions after generation is complete.
|
||||
* @param {{height: number, width: number}} savedParams The original dimensions to restore
|
||||
*/
|
||||
function restoreOriginalDimensions(savedParams) {
|
||||
extension_settings.sd.height = savedParams.height;
|
||||
extension_settings.sd.width = savedParams.width;
|
||||
@@ -3118,7 +3175,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
|
||||
}
|
||||
|
||||
if (generationType !== generationMode.FREE) {
|
||||
prompt = await refinePrompt(prompt, false);
|
||||
prompt = await refinePrompt(prompt);
|
||||
}
|
||||
|
||||
return prompt;
|
||||
@@ -5150,7 +5207,7 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
const stopButton = document.getElementById('sd_stop_gen');
|
||||
const stopListener = () => abortController.abort('Aborted by user');
|
||||
const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE;
|
||||
const dimensions = setTypeSpecificDimensions(generationType);
|
||||
let dimensions = { width: extension_settings.sd.width, height: extension_settings.sd.height };
|
||||
extension_settings.sd.original_seed = extension_settings.sd.seed;
|
||||
extension_settings.sd.seed = extension_settings.sd.seed >= 0 ? Math.round(Math.random() * (Math.pow(2, 32) - 1)) : -1;
|
||||
|
||||
@@ -5166,9 +5223,13 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
|
||||
const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };
|
||||
const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';
|
||||
const prompt = await refinePrompt(savedPrompt, false);
|
||||
const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? '';
|
||||
const negative = savedNegative ? await refinePrompt(savedNegative, true) : '';
|
||||
const refineArgs = {
|
||||
negative: savedNegative,
|
||||
resolution: mediaAttachment?.width && mediaAttachment?.height ? `${mediaAttachment.width}x${mediaAttachment.height}` : null,
|
||||
};
|
||||
const prompt = await refinePrompt(savedPrompt, refineArgs);
|
||||
dimensions = setTypeSpecificDimensions(generationType, refineArgs.resolution ? mediaAttachment : null);
|
||||
|
||||
const context = getContext();
|
||||
const characterName = context.groupId
|
||||
@@ -5176,10 +5237,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
|
||||
: context.characters[context.characterId]?.name;
|
||||
|
||||
onStart();
|
||||
result.url = await sendGenerationRequest(generationType, prompt, negative, characterName, callback, initiators.swipe, abortController.signal);
|
||||
result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal);
|
||||
result.generation_type = generationType;
|
||||
result.title = prompt;
|
||||
result.negative = negative;
|
||||
result.negative = refineArgs.negative;
|
||||
} finally {
|
||||
onComplete();
|
||||
$(stopButton).hide();
|
||||
@@ -5359,7 +5420,23 @@ jQuery(async () => {
|
||||
const currentSettings = applyCommandArguments(args);
|
||||
|
||||
try {
|
||||
return await generatePicture(initiators.command, args, String(trigger));
|
||||
const url = await generatePicture(initiators.command, args, String(trigger));
|
||||
|
||||
// Save override width/height into a message result
|
||||
if (!isTrueBoolean(args?.quiet?.toString()) && Object.hasOwn(args, 'width') && Object.hasOwn(args, 'height')) {
|
||||
const context = getContext();
|
||||
const message = context.chat.at(-1);
|
||||
if (Array.isArray(message?.extra?.media) && message.extra.media.length > 0) {
|
||||
const mediaAttachment = message.extra.media.findLast(m => m.url === url);
|
||||
if (mediaAttachment) {
|
||||
mediaAttachment.width = extension_settings.sd.width;
|
||||
mediaAttachment.height = extension_settings.sd.height;
|
||||
await context.saveChat();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate image:', error);
|
||||
return '';
|
||||
|
||||
Reference in New Issue
Block a user