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>
Signed| @@ -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 | } |
| 143 | 145 | ||
| 144 | interface ImageCaptionAttachmentProps { | 146 | interface ImageCaptionAttachmentProps { |
| @@ -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 refine | 812 | * @param {string} prompt Prompt to refine |
| 813 | * @param {boolean} isNegative Whether the prompt is a negative one | 813 | * @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 prompt | 816 | * @returns {Promise<string>} Refined prompt |
| 815 | */ | 817 | */ |
| 816 | async function refinePrompt(prompt, isNegative) { | 818 | async 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 | }); | ||
| 820 | 867 | ||
| 821 | if (refinedPrompt) { | 868 | if (refinedPrompt) { |
| 822 | return String(refinedPrompt); | 869 | return String(refinedPrompt); |
| @@ -3035,23 +3082,29 @@ async function generatePicture(initiator, args, trigger, message, callback) { | |||
| 3035 | return imagePath; | 3082 | return imagePath; |
| 3036 | } | 3083 | } |
| 3037 | 3084 | ||
| 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) { | ||
| 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; |
| 3042 | 3095 | ||
| 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 64 | 3103 | // 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 | 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 | } | ||
| 3055 | } | 3108 | } |
| 3056 | 3109 | ||
| 3057 | if (extension_settings.sd.snap) { | 3110 | if (extension_settings.sd.snap) { |
| @@ -3079,6 +3132,10 @@ function setTypeSpecificDimensions(generationType) { | |||
| 3079 | return { height: prevSDHeight, width: prevSDWidth }; | 3132 | return { height: prevSDHeight, width: prevSDWidth }; |
| 3080 | } | 3133 | } |
| 3081 | 3134 | ||
| 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 | */ | ||
| 3082 | function restoreOriginalDimensions(savedParams) { | 3139 | function 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 | } |
| 3119 | 3176 | ||
| 3120 | if (generationType !== generationMode.FREE) { | 3177 | if (generationType !== generationMode.FREE) { |
| 3121 | prompt = await refinePrompt(prompt, false); | 3178 | prompt = await refinePrompt(prompt); |
| 3122 | } | 3179 | } |
| 3123 | 3180 | ||
| 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; |
| 5156 | 5213 | ||
| @@ -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); | ||
| 5172 | 5233 | ||
| 5173 | const context = getContext(); | 5234 | const context = getContext(); |
| 5174 | const characterName = context.groupId | 5235 | 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; |
| 5177 | 5238 | ||
| 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); |
| 5360 | 5421 | ||
| 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 ''; |