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:
Copilot
2026-02-23 20:51:25 +02:00
committed by GitHub
parent 0ba0418fac
commit a93fc0443e
2 changed files with 101 additions and 22 deletions
+2
View File
@@ -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 '';