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, +99 -20Showing whitespace changes
public/global.d.ts+2 -0
@@ -139,6 +139,8 @@ declare global {
139 interface ImageGenerationAttachmentProps {139 interface ImageGenerationAttachmentProps {
140 generation_type?: number;140 generation_type?: number;
141 negative?: string;141 negative?: string;
142 width?: number;
143 height?: number;
142 }144 }
143145
144 interface ImageCaptionAttachmentProps {146 interface ImageCaptionAttachmentProps {
public/scripts/extensions/stable-diffusion/index.js+97 -20
@@ -810,13 +810,60 @@ async function onRenameStyleClick() {
810/**810/**
811 * Modifies prompt based on user inputs.811 * Modifies prompt based on user inputs.
812 * @param {string} prompt Prompt to refine812 * @param {string} prompt Prompt to refine
813 * @param {boolean} isNegative Whether the prompt is a negative one813 * @param {object} [args] Additional arguments for refinement
814 * @param {string} [args.negative] Negative prompt to prefill
815 * @param {string} [args.resolution] Saved resolution to offer as a checkbox option
814 * @returns {Promise<string>} Refined prompt816 * @returns {Promise<string>} Refined prompt
815 */817 */
816async function refinePrompt(prompt, isNegative) {818async function refinePrompt(prompt, args = null) {
817 if (extension_settings.sd.refine_mode) {819 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
821 if (refinedPrompt) {868 if (refinedPrompt) {
822 return String(refinedPrompt);869 return String(refinedPrompt);
@@ -3035,24 +3082,30 @@ async function generatePicture(initiator, args, trigger, message, callback) {
3035 return imagePath;3082 return imagePath;
3036}3083}
30373084
3038function 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 */
3091function setTypeSpecificDimensions(generationType, mediaAttachment = null) {
3039 const prevSDHeight = extension_settings.sd.height;3092 const prevSDHeight = extension_settings.sd.height;
3040 const prevSDWidth = extension_settings.sd.width;3093 const prevSDWidth = extension_settings.sd.width;
3041 const aspectRatio = extension_settings.sd.width / extension_settings.sd.height;3094 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) {
3045 // Round to nearest multiple of 643103 // Round to nearest multiple of 64
3046 extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64;3104 extension_settings.sd.height = Math.round(extension_settings.sd.width * 1.5 / 64) * 64;
3047 }3105 } else if (generationType === generationMode.BACKGROUND && aspectRatio <= 1) {
3048
3049 if (generationType === generationMode.BACKGROUND) {
3050 // Background images are always landscape
3051 if (aspectRatio <= 1) {
3052 // Round to nearest multiple of 643106 // Round to nearest multiple of 64
3053 extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;3107 extension_settings.sd.width = Math.round(extension_settings.sd.height * 1.8 / 64) * 64;
3054 }3108 }
3055 }
30563109
3057 if (extension_settings.sd.snap) {3110 if (extension_settings.sd.snap) {
3058 // Force to use roughly the same pixel count as before rescaling3111 // Force to use roughly the same pixel count as before rescaling
@@ -3079,6 +3132,10 @@ function setTypeSpecificDimensions(generationType) {
3079 return { height: prevSDHeight, width: prevSDWidth };3132 return { height: prevSDHeight, width: prevSDWidth };
3080}3133}
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 */
3082function restoreOriginalDimensions(savedParams) {3139function restoreOriginalDimensions(savedParams) {
3083 extension_settings.sd.height = savedParams.height;3140 extension_settings.sd.height = savedParams.height;
3084 extension_settings.sd.width = savedParams.width;3141 extension_settings.sd.width = savedParams.width;
@@ -3118,7 +3175,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt, combineN
3118 }3175 }
31193176
3120 if (generationType !== generationMode.FREE) {3177 if (generationType !== generationMode.FREE) {
3121 prompt = await refinePrompt(prompt, false);3178 prompt = await refinePrompt(prompt);
3122 }3179 }
31233180
3124 return prompt;3181 return prompt;
@@ -5150,7 +5207,7 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
5150 const stopButton = document.getElementById('sd_stop_gen');5207 const stopButton = document.getElementById('sd_stop_gen');
5151 const stopListener = () => abortController.abort('Aborted by user');5208 const stopListener = () => abortController.abort('Aborted by user');
5152 const generationType = mediaAttachment.generation_type ?? message?.extra?.generationType ?? generationMode.FREE;5209 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 };
5154 extension_settings.sd.original_seed = extension_settings.sd.seed;5211 extension_settings.sd.original_seed = extension_settings.sd.seed;
5155 extension_settings.sd.seed = extension_settings.sd.seed >= 0 ? Math.round(Math.random() * (Math.pow(2, 32) - 1)) : -1;5212 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,
5166 eventSource.once(CUSTOM_STOP_EVENT, stopListener);5223 eventSource.once(CUSTOM_STOP_EVENT, stopListener);
5167 const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };5224 const callback = (_a, _b, _c, _d, _e, _f, format) => { result.type = isVideo(format) ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.IMAGE; };
5168 const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';5225 const savedPrompt = mediaAttachment.title ?? message.extra.title ?? '';
5169 const prompt = await refinePrompt(savedPrompt, false);
5170 const savedNegative = mediaAttachment.negative ?? message.extra.negative ?? '';5226 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
5173 const context = getContext();5234 const context = getContext();
5174 const characterName = context.groupId5235 const characterName = context.groupId
@@ -5176,10 +5237,10 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete,
5176 : context.characters[context.characterId]?.name;5237 : context.characters[context.characterId]?.name;
51775238
5178 onStart();5239 onStart();
5179 result.url = await sendGenerationRequest(generationType, prompt, negative, characterName, callback, initiators.swipe, abortController.signal);5240 result.url = await sendGenerationRequest(generationType, prompt, refineArgs.negative, characterName, callback, initiators.swipe, abortController.signal);
5180 result.generation_type = generationType;5241 result.generation_type = generationType;
5181 result.title = prompt;5242 result.title = prompt;
5182 result.negative = negative;5243 result.negative = refineArgs.negative;
5183 } finally {5244 } finally {
5184 onComplete();5245 onComplete();
5185 $(stopButton).hide();5246 $(stopButton).hide();
@@ -5359,7 +5420,23 @@ jQuery(async () => {
5359 const currentSettings = applyCommandArguments(args);5420 const currentSettings = applyCommandArguments(args);
53605421
5361 try {5422 try {
5362 return await generatePicture(initiators.command, args, String(trigger));5423 const 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;
5363 } catch (error) {5440 } catch (error) {
5364 console.error('Failed to generate image:', error);5441 console.error('Failed to generate image:', error);
5365 return '';5442 return '';