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>

a93fc0443e6ed0e2662b383e3e45f6b54e369d57

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
2 files changed, +101 -22Ignore whitespace
public/global.d.ts+2 -0
@@ -139,6 +139,8 @@ declare global {
139139 interface ImageGenerationAttachmentProps {
140140 generation_type?: number;
141141 negative?: string;
142+ width?: number;
143+ height?: number;
142144 }
143145
144146 interface ImageCaptionAttachmentProps {
public/scripts/extensions/stable-diffusion/index.js+99 -22
@@ -810,13 +810,60 @@ async function onRenameStyleClick() {
810810/**
811811 * Modifies prompt based on user inputs.
812812 * @param {string} prompt Prompt to refine
813813 * @param {booleanobject} isNegative Whether the prompt[args] isAdditional aarguments negativefor onerefinement
814+ * @param {string} [args.negative] Negative prompt to prefill
815+ * @param {string} [args.resolution] Saved resolution to offer as a checkbox option
814816 * @returns {Promise<string>} Refined prompt
815817 */
816818async function refinePrompt(prompt, isNegativeargs = null) {
817819 if (extension_settings.sd.refine_mode) {
818- const text = isNegative ? '<h3>Review and edit the <i>negative</i> prompt:</h3>' : '<h3>Review and edit the prompt:</h3>';
820+ /** @type {import('../../popup.js').CustomPopupInput[]} */
819- const refinedPrompt = await callGenericPopup(text + 'Press "Cancel" to abort the image generation.', POPUP_TYPE.INPUT, prompt.trim(), { rows: 8, okButton: 'Continue' });
821+ const customInputs = [];
822+
823+ if (args?.negative) {
824+ customInputs.push({
825+ id: 'sd_refine_negative',
826+ label: t`Negative prompt (optional)`,
827+ type: 'textarea',
828+ rows: 4,
829+ defaultState: String(args.negative || ''),
830+ });
831+ }
832+
833+ if (args?.resolution) {
834+ customInputs.push({
835+ id: 'sd_use_saved_resolution',
836+ label: t`Use saved resolution (${args.resolution})`,
837+ type: 'checkbox',
838+ defaultState: true,
839+ });
840+ }
841+
842+ const refinedPrompt = await Popup.show.input(
843+ t`Review and edit the prompt:`,
844+ t`Press "Cancel" to abort the image generation.`,
845+ prompt.trim(),
846+ {
847+ rows: 8,
848+ okButton: t`Continue`,
849+ cancelButton: t`Cancel`,
850+ customInputs,
851+ onClose: (popup) => {
852+ if (!popup.result || !args) {
853+ return;
854+ }
855+
856+ const negativeInput = popup.inputResults.get('sd_refine_negative');
857+ const useSavedResolution = popup.inputResults.get('sd_use_saved_resolution');
858+
859+ if (negativeInput) {
860+ args.negative = negativeInput.toString().trim();
861+ }
862+ if (!useSavedResolution) {
863+ args.resolution = null;
864+ }
865+ },
866+ });
820867
821868 if (refinedPrompt) {
822869 return String(refinedPrompt);
@@ -3035,23 +3082,29 @@ async function generatePicture(initiator, args, trigger, message, callback) {
30353082 return imagePath;
30363083}
30373084
3038-function setTypeSpecificDimensions(generationType) {
3085+/**
3086+ * Adjusts image generation dimensions based on the generation type and/or previous media attachment.
3087+ * @param {number} generationType The type of image generation to perform, used to determine dimension adjustments
3088+ * @param {MediaAttachment} [mediaAttachment] Media attachment to base dimension adjustments on
3089+ * @returns {{height: number, width: number}} Previous dimensions before modification
3090+ */
3091+function setTypeSpecificDimensions(generationType, mediaAttachment = null) {
30393092 const prevSDHeight = extension_settings.sd.height;
30403093 const prevSDWidth = extension_settings.sd.width;
30413094 const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;
30423095
3043- // Face images are always portrait (pun intended)
3096+ // 1. If there's a media attachment, match its previous dimensions
3044- if ((generationType === generationMode.FACE || generationType === generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) {
3097+ // 2. Face images are always portrait (pun intended) - increase height if needed
3098+ // 3. Background images are always landscape - increase width if needed
3099+ if (Number.isInteger(mediaAttachment?.width) && Number.isInteger(mediaAttachment?.height)) {
3100+ extension_settings.sd.width = mediaAttachment.width;
3101+ extension_settings.sd.height = mediaAttachment.height;
3102+ } else if ((generationType === generationMode.FACE || generationType === generationMode.FACE_MULTIMODAL) && aspectRatio >= 1) {
30453103 // Round to nearest multiple of 64
30463104 extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64;
3047- }
3105+ } else if (generationType === generationMode.BACKGROUND && aspectRatio <= 1) {
3048-
3106+ // Round to nearest multiple of 64
3049- if (generationType === generationMode.BACKGROUND) {
3107+ extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
3050- // Background images are always landscape
3051- if (aspectRatio <= 1) {
3052- // Round to nearest multiple of 64
3053- extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
3054- }
30553108 }
30563109
30573110 if (extension_settings.sd.snap) {
@@ -3079,6 +3132,10 @@ function setTypeSpecificDimensions(generationType) {
30793132 return { height: prevSDHeight, width: prevSDWidth };
30803133}
30813134
3135+/**
3136+ * Restores the original image generation dimensions after generation is complete.
3137+ * @param {{height: number, width: number}} savedParams The original dimensions to restore
3138+ */
30823139function restoreOriginalDimensions(savedParams) {
30833140 extension_settings.sd.height = savedParams.height;
30843141 extension_settings.sd.width = savedParams.width;
@@ -3118,7 +3175,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
31183175 }
31193176
31203177 if (generationType !== generationMode.FREE) {
31213178 prompt = await refinePrompt(prompt, false);
31223179 }
31233180
31243181 return prompt;
@@ -5150,7 +5207,7 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
51505207 const stopButton = document.getElementById('sd_stop_gen');
51515208 const stopListener = () => abortController.abort('Aborted by user');
51525209 const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE;
5153- const dimensions = setTypeSpecificDimensions(generationType);
5210+ let dimensions = { width: extension_settings.sd.width, height: extension_settings.sd.height };
51545211 extension_settings.sd.original_seed = extension_settings.sd.seed;
51555212 extension_settings.sd.seed = extension_settings.sd.seed >= 0 ? Math.round(Math.random() * (Math.pow(2, 32) - 1)) : -1;
51565213
@@ -5166,9 +5223,13 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
51665223 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
51675224 const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };
51685225 const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';
5169- const prompt = await refinePrompt(savedPrompt, false);
51705226 const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? '';
5171- const negative = savedNegative ? await refinePrompt(savedNegative, true) : '';
5227+ const refineArgs = {
5228+ negative: savedNegative,
5229+ resolution: mediaAttachment?.width && mediaAttachment?.height ? `${mediaAttachment.width}x${mediaAttachment.height}` : null,
5230+ };
5231+ const prompt = await refinePrompt(savedPrompt, refineArgs);
5232+ dimensions = setTypeSpecificDimensions(generationType, refineArgs.resolution ? mediaAttachment : null);
51725233
51735234 const context = getContext();
51745235 const characterName = context.groupId
@@ -5176,10 +5237,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
51765237 : context.characters[context.characterId]?.name;
51775238
51785239 onStart();
51795240 result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal);
51805241 result.generation_type = generationType;
51815242 result.title = prompt;
51825243 result.negative = refineArgs.negative;
51835244 } finally {
51845245 onComplete();
51855246 $(stopButton).hide();
@@ -5359,7 +5420,23 @@ jQuery(async () => {
53595420 const currentSettings = applyCommandArguments(args);
53605421
53615422 try {
53625423 returnconst url = await generatePicture(initiators.command, args, String(trigger));
5424+
5425+ // Save override width/height into a message result
5426+ if (!isTrueBoolean(args?.quiet?.toString()) && Object.hasOwn(args, 'width') && Object.hasOwn(args, 'height')) {
5427+ const context = getContext();
5428+ const message = context.chat.at(-1);
5429+ if (Array.isArray(message?.extra?.media) && message.extra.media.length > 0) {
5430+ const mediaAttachment = message.extra.media.findLast(m => m.url === url);
5431+ if (mediaAttachment) {
5432+ mediaAttachment.width = extension_settings.sd.width;
5433+ mediaAttachment.height = extension_settings.sd.height;
5434+ await context.saveChat();
5435+ }
5436+ }
5437+ }
5438+
5439+ return url;
53635440 } catch (error) {
53645441 console.error('Failed to generate image:', error);
53655442 return '';