diff --git a/CHANGELOG.md b/CHANGELOG.md index 4236b8a84..7629ab3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,24 @@ # Change Log for SD.Next -## Update for 2024-09-21 +## Update for 2024-09-24 -- **flux** +- **reprocess** + - new top-leve button: reprocess your last generated image + - generate using full-quality:off and then reprocess using *full quality decode* + - generate without hires/refine and then *reprocess with hires/refine* + *note*: you can change hires/refine settings and run-reprocess again! + - reprocess using *face-restore* +- **text encoder**: + - allow loading different custom text encoders: *clip-vit-l, clip-vit-g, t5* + will automatically find appropriate encoder in the loaded model and replace it with loaded text encoder + download text encoders into folder set in settings -> system paths -> text encoders + default `models/Text-encoder` folder is used if no custom path is set + finetuned *clip-vit-l* models: [Detailed, Smooth](https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14), [LongCLIP](https://huggingface.co/zer0int/LongCLIP-GmP-ViT-L-14) + reference *clip-vit-l* and *clip-vit-g* models: [OpenCLIP-Laion2b](https://huggingface.co/collections/laion/openclip-laion-2b-64fcade42d20ced4e9389b30) + *note* sd/sdxl contain heavily distilled versions of reference models, so switching to reference model produces vastly different results + - xyz grid support for text encoder + - full prompt parser now correctly works with different prompts in batch +- **flux** - avoid unet load if unchanged - mark specific unet as unavailable if load failed - fix diffusers local model name parsing @@ -22,13 +38,12 @@ - enable working with different resolutions now you can adjust width/height in the grid just as any other param - renamed options to include section name and adjusted cost of each option + - added additional metadata - **interrogate** - add additional blip models: *blip-base, blip-large, blip-t5-xl, blip-t5-xxl, opt-2.7b, opt-6.7b* - - change default params for better memory utilization + - change default params for better memory utilization - add optional advanced params - update logging -- **reprocess** generate images using taesd (full quality off) and reprocess selected ones using full vae - - right click on *generate* button -> *reprocess* - **lora** auto-apply tags to prompt - controlled via *settings -> networks -> lora_apply_tags* *0:disable, -1:all-tags, n:top-n-tags* @@ -36,24 +51,14 @@ - if lora contains no tags, lora name itself will be used as a tag - if prompt contains `_tags_` it will be used as placeholder for replacement, otherwise tags will be appended - used tags are also logged and registered in image metadata - - loras are no longer filtered per detected type vs loaded model type as its unreliable - - loras display in networks now shows possible version in top-left corner + - loras are no longer filtered per detected type vs loaded model type as its unreliable + - loras display in networks now shows possible version in top-left corner - correct using of `extra_networks_default_multiplier` if not scale is specified - always keep lora on gpu -- **text encoder**: - - allow loading different custom text encoders: *clip-vit-l, clip-vit-g, t5* - will automatically find appropriate encoder in the loaded model and replace it with loaded text encoder - download text encoders into folder set in settings -> system paths -> text encoders - default `models/Text-encoder` folder is used if no custom path is set - finetuned *clip-vit-l* models: [Detailed, Smooth](https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14), [LongCLIP](https://huggingface.co/zer0int/LongCLIP-GmP-ViT-L-14) - reference *clip-vit-l* and *clip-vit-g* models: [OpenCLIP-Laion2b](https://huggingface.co/collections/laion/openclip-laion-2b-64fcade42d20ced4e9389b30) - *note* sd/sdxl contain heavily distilled versions of reference models, so switching to reference model produces vastly different results - - xyz grid support for text encoder - - full prompt parser now correctly works with different prompts in batch -- **huggingface**: +- **huggingface**: - force logout/login on token change - - unified handling of cache folder: set via `HF_HUB` or `HF_HUB_CACHE` or via settings -> system paths -- **cogvideox**: + - unified handling of cache folder: set via `HF_HUB` or `HF_HUB_CACHE` or via settings -> system paths +- **cogvideox**: - add support for *image2video* (in addition to previous *text2video* and *video2video*) - *note*: *image2video* requires separate 5b model variant - **backend=original** is now marked as in maintenance-only mode @@ -63,11 +68,17 @@ - **ui** - hide token counter until tokens are known - minor ui optimizations -- **free-u** check if device/dtype are fft compatible -- massive log cleanup -- full lint pass -- **experimental** - - flux t5 load from gguf: requires transformers pr + - fix update infotext on image select + - fix imageviewer exif parser +- **free-u** check if device/dtype are fft compatible and cast as necessary +- **experimental** + - flux t5 load from gguf: requires transformers pr + - rocm triton backend for flash attention +- **refactor** + - modularize main process loop + - massive log cleanup + - full lint pass + ## Update for 2024-09-13 diff --git a/extensions-builtin/sdnext-modernui b/extensions-builtin/sdnext-modernui index 2c95d480d..49f944e5f 160000 --- a/extensions-builtin/sdnext-modernui +++ b/extensions-builtin/sdnext-modernui @@ -1 +1 @@ -Subproject commit 2c95d480d63d46232122ddbd4161b73cba8c258a +Subproject commit 49f944e5fb0df2e2e7d6c7834d758364d528e774 diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index fe07571ee..a0424f266 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -31,7 +31,7 @@ const contextMenuInit = () => { if ((windowHeight - posy) < menuHeight) contextMenu.style.top = `${windowHeight - menuHeight}px`; } - function appendContextMenuOption(targetElementSelector, entryName, entryFunction) { + function appendContextMenuOption(targetElementSelector, entryName, entryFunction, primary = false) { let currentItems = menuSpecs.get(targetElementSelector); if (!currentItems) { currentItems = []; @@ -41,7 +41,8 @@ const contextMenuInit = () => { id: `${targetElementSelector}_${uid()}`, name: entryName, func: entryFunction, - isNew: true, + primary, + // isNew: true, }; currentItems.push(newItem); return newItem.id; @@ -64,13 +65,21 @@ const contextMenuInit = () => { if (!e.isTrusted) return; const oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) oldMenu.remove(); + menuSpecs.forEach((v, k) => { + const items = v.filter((item) => item.primary); + if (items.length > 0 && e.composedPath()[0].matches(k)) { + showContextMenu(e, e.composedPath()[0], items); + e.preventDefault(); + } + }); }); gradioApp().addEventListener('contextmenu', (e) => { const oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) oldMenu.remove(); menuSpecs.forEach((v, k) => { - if (e.composedPath()[0].matches(k)) { - showContextMenu(e, e.composedPath()[0], v); + const items = v.filter((item) => !item.primary); + if (items.length > 0 && e.composedPath()[0].matches(k)) { + showContextMenu(e, e.composedPath()[0], items); e.preventDefault(); } }); @@ -80,10 +89,10 @@ const contextMenuInit = () => { return [appendContextMenuOption, removeContextMenuOption, addContextMenuEventListener]; }; -const initResponse = contextMenuInit(); -const appendContextMenuOption = initResponse[0]; -const removeContextMenuOption = initResponse[1]; -const addContextMenuEventListener = initResponse[2]; +const initContextResponse = contextMenuInit(); +const appendContextMenuOption = initContextResponse[0]; +const removeContextMenuOption = initContextResponse[1]; +const addContextMenuEventListener = initContextResponse[2]; const generateForever = (genbuttonid) => { if (window.generateOnRepeatInterval) { @@ -102,22 +111,25 @@ const generateForever = (genbuttonid) => { } }; -const reprocessLatent = (btnId) => { - const btn = document.getElementById(btnId); +const reprocessClick = (tabId, state) => { + const btn = document.getElementById(`${tabId}_${state}`); + window.submit_state = state; if (btn) btn.click(); }; async function initContextMenu() { + let id = ''; for (const tab of ['txt2img', 'img2img', 'control']) { - for (const el of ['generate', 'interrupt', 'skip', 'pause', 'paste', 'clear_prompt', 'extra_networks_btn']) { - const id = `#${tab}_${el}`; - appendContextMenuOption(id, 'Copy to clipboard', () => navigator.clipboard.writeText(document.querySelector(`#${tab}_prompt > label > textarea`).value)); - appendContextMenuOption(id, 'Generate forever', () => generateForever(`#${tab}_generate`)); - appendContextMenuOption(id, 'Apply selected style', quickApplyStyle); - appendContextMenuOption(id, 'Quick save style', quickSaveStyle); - appendContextMenuOption(id, 'nVidia overlay', initNVML); - appendContextMenuOption(id, 'Reprocess last image', () => reprocessLatent(`${tab}_reprocess`)); - } + id = `#${tab}_generate`; + appendContextMenuOption(id, 'Copy to clipboard', () => navigator.clipboard.writeText(document.querySelector(`#${tab}_prompt > label > textarea`).value)); + appendContextMenuOption(id, 'Generate forever', () => generateForever(`#${tab}_generate`)); + appendContextMenuOption(id, 'Apply selected style', quickApplyStyle); + appendContextMenuOption(id, 'Quick save style', quickSaveStyle); + appendContextMenuOption(id, 'nVidia overlay', initNVML); + id = `#${tab}_reprocess`; + appendContextMenuOption(id, 'Decode full quality', () => reprocessClick(`${tab}`, 'reprocess_decode'), true); + appendContextMenuOption(id, 'Refine & HiRes pass', () => reprocessClick(`${tab}`, 'reprocess_refine'), true); + appendContextMenuOption(id, 'Face restore', () => reprocessClick(`${tab}`, 'reprocess_face'), true); } addContextMenuEventListener(); } diff --git a/javascript/emerald-paradise.css b/javascript/emerald-paradise.css index 5b3bc419f..f951356cc 100644 --- a/javascript/emerald-paradise.css +++ b/javascript/emerald-paradise.css @@ -115,8 +115,6 @@ button.selected {background: var(--button-primary-background-fill);} #txt2img_checkboxes, #img2img_checkboxes { background-color: transparent; } #txt2img_checkboxes, #img2img_checkboxes { margin-bottom: 0.2em; } #txt2img_actions_column, #img2img_actions_column { flex-flow: wrap; justify-content: space-between; } -#txt2img_enqueue_wrapper, #img2img_enqueue_wrapper { min-width: unset; width: 48%; } -#txt2img_generate_box, #img2img_generate_box { min-width: unset; width: 48%; } #extras_upscale { margin-top: 10px } #txt2img_progress_row > div { min-width: var(--left-column); max-width: var(--left-column); } diff --git a/javascript/generationParams.js b/javascript/generationParams.js index c3bdbb6e8..8847e3535 100644 --- a/javascript/generationParams.js +++ b/javascript/generationParams.js @@ -1,20 +1,22 @@ // attaches listeners to the txt2img and img2img galleries to update displayed generation param text when the image changes -function attachGalleryListeners(tab_name) { - const gallery = gradioApp().querySelector(`#${tab_name}_gallery`); +function attachGalleryListeners(tabName) { + const gallery = gradioApp().querySelector(`#${tabName}_gallery`); if (!gallery) return null; - gallery.addEventListener('click', () => setTimeout(() => { - log('galleryItemSelected:', tab_name); - gradioApp().getElementById(`${tab_name}_generation_info_button`)?.click(); - }, 500)); + gallery.addEventListener('click', () => { + // log('galleryItemSelected:', tabName); + const btn = gradioApp().getElementById(`${tabName}_generation_info_button`); + if (btn) btn.click(); + }); gallery?.addEventListener('keydown', (e) => { - if (e.keyCode === 37 || e.keyCode === 39) gradioApp().getElementById(`${tab_name}_generation_info_button`).click(); // left or right arrow + if (e.keyCode === 37 || e.keyCode === 39) gradioApp().getElementById(`${tabName}_generation_info_button`).click(); // left or right arrow }); return gallery; } let txt2img_gallery; let img2img_gallery; +let control_gallery; let modal; async function initiGenerationParams() { @@ -23,14 +25,17 @@ async function initiGenerationParams() { const modalObserver = new MutationObserver((mutations) => { mutations.forEach((mutationRecord) => { - let selectedTab = gradioApp().querySelector('#tabs div button.selected')?.innerText; - if (!selectedTab) selectedTab = gradioApp().querySelector('#tabs div button')?.innerText; - if (mutationRecord.target.style.display === 'none' && (selectedTab === 'txt2img' || selectedTab === 'img2img')) { gradioApp().getElementById(`${selectedTab}_generation_info_button`)?.click(); } + const tabName = getENActiveTab(); + if (mutationRecord.target.style.display === 'none') { + const btn = gradioApp().getElementById(`${tabName}_generation_info_button`); + if (btn) btn.click(); + } }); }); if (!txt2img_gallery) txt2img_gallery = attachGalleryListeners('txt2img'); if (!img2img_gallery) img2img_gallery = attachGalleryListeners('img2img'); + if (!control_gallery) control_gallery = attachGalleryListeners('control'); modalObserver.observe(modal, { attributes: true, attributeFilter: ['style'] }); log('initGenerationParams'); } diff --git a/javascript/imageViewer.js b/javascript/imageViewer.js index 85fa435a7..2c360b881 100644 --- a/javascript/imageViewer.js +++ b/javascript/imageViewer.js @@ -65,8 +65,8 @@ async function getExif(el) { // let html = `Image ${el.src} Size ${el.naturalWidth}x${el.naturalHeight}
`; let html = ''; let params; - if (exif.paramters) { - params = exif.paramters; + if (exif.parameters) { + params = exif.parameters; } else if (exif.userComment) { params = Array.from(exif.userComment) .map((c) => String.fromCharCode(c)) diff --git a/javascript/orchid-dreams.css b/javascript/orchid-dreams.css index c290240c1..4b121c761 100644 --- a/javascript/orchid-dreams.css +++ b/javascript/orchid-dreams.css @@ -115,8 +115,6 @@ button.selected {background: var(--button-primary-background-fill);} #txt2img_checkboxes, #img2img_checkboxes { background-color: transparent; } #txt2img_checkboxes, #img2img_checkboxes { margin-bottom: 0.2em; } #txt2img_actions_column, #img2img_actions_column { flex-flow: wrap; justify-content: space-between; } -#txt2img_enqueue_wrapper, #img2img_enqueue_wrapper { min-width: unset; width: 48%; } -#txt2img_generate_box, #img2img_generate_box { min-width: unset; width: 48%; } #extras_upscale { margin-top: 10px } #txt2img_progress_row > div { min-width: var(--left-column); max-width: var(--left-column); } diff --git a/javascript/progressBar.js b/javascript/progressBar.js index 7fe68d3bd..a9ecb31e9 100644 --- a/javascript/progressBar.js +++ b/javascript/progressBar.js @@ -42,7 +42,8 @@ function checkPaused(state) { function setProgress(res) { const elements = ['txt2img_generate', 'img2img_generate', 'extras_generate', 'control_generate']; const progress = (res?.progress || 0); - const job = res?.job || ''; + let job = res?.job || ''; + job = job.replace('txt2img', 'Generate').replace('img2img', 'Generate'); const perc = res && (progress > 0) ? `${Math.round(100.0 * progress)}%` : ''; let sec = res?.eta || 0; let eta = ''; diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 04bea121b..fa0ab5541 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -90,17 +90,17 @@ button.custom-button { border-radius: var(--button-large-radius); padding: var(- #control-inputs { margin-top: 1em; } #txt2img_prompt_container, #img2img_prompt_container, #control_prompt_container { margin-right: var(--layout-gap) } #txt2img_footer, #img2img_footer, #control_footer { height: fit-content; display: none; } -#txt2img_generate_box, #img2img_generate_box, #control_general_box { gap: 0.5em; flex-wrap: wrap-reverse; height: fit-content; } +#txt2img_generate_box, #img2img_generate_box, #control_generate_box { gap: 0.5em; flex-wrap: unset; min-width: unset; width: 66.6%; } #txt2img_actions_column, #img2img_actions_column, #control_actions_column { gap: 0.3em; height: fit-content; } -#txt2img_generate_box>button, #img2img_generate_box>button, #control_generate_box>button, #txt2img_enqueue, #img2img_enqueue { min-height: 44px !important; max-height: 44px !important; line-height: 1em; } +#txt2img_generate_box>button, #img2img_generate_box>button, #control_generate_box>button, #txt2img_enqueue, #img2img_enqueue, #txt2img_enqueue>button, #img2img_enqueue>button { min-height: 44px !important; max-height: 44px !important; line-height: 1em; white-space: break-spaces; min-width: unset; } +#txt2img_enqueue_wrapper, #img2img_enqueue_wrapper, #control_enqueue_wrapper { min-width: unset !important; width: 31%; } #txt2img_generate_line2, #img2img_generate_line2, #txt2img_tools, #img2img_tools, #control_generate_line2, #control_tools { display: flex; } #txt2img_generate_line2>button, #img2img_generate_line2>button, #extras_generate_box>button, #control_generate_line2>button, #txt2img_tools>button, #img2img_tools>button, #control_tools>button { height: 2em; line-height: 0; font-size: var(--text-md); min-width: unset; display: block !important; } #txt2img_prompt, #txt2img_neg_prompt, #img2img_prompt, #img2img_neg_prompt, #control_prompt, #control_neg_prompt { display: contents; } -#txt2img_generate_box, #img2img_generate_box { min-width: unset; width: 66%; } -#control_generate_box { min-width: unset; width: 100%; } #txt2img_actions_column, #img2img_actions_column, #control_actions { flex-flow: wrap; justify-content: space-between; } -#txt2img_enqueue_wrapper, #img2img_enqueue_wrapper, #control_enqueue_wrapper { min-width: unset !important; width: 32%; } + + .interrogate-clip { position: absolute; right: 6em; top: 8px; max-width: fit-content; background: none !important; z-index: 50; } .interrogate-blip { position: absolute; right: 4em; top: 8px; max-width: fit-content; background: none !important; z-index: 50; } .interrogate-col { min-width: 0 !important; max-width: fit-content; margin-right: var(--spacing-xxl); } diff --git a/javascript/startup.js b/javascript/startup.js index 245a3eae8..81bfa1a61 100644 --- a/javascript/startup.js +++ b/javascript/startup.js @@ -7,7 +7,6 @@ async function initStartup() { // all items here are non-blocking async calls initModels(); getUIDefaults(); - initiGenerationParams(); initPromptChecker(); initLogMonitor(); initContextMenu(); @@ -16,6 +15,7 @@ async function initStartup() { initSettings(); initImageViewer(); initGallery(); + initiGenerationParams(); setupControlUI(); // reconnect server session diff --git a/javascript/timeless-beige.css b/javascript/timeless-beige.css index fbd9ec1a7..4b0f7d9e4 100644 --- a/javascript/timeless-beige.css +++ b/javascript/timeless-beige.css @@ -115,8 +115,6 @@ button.selected {background: var(--button-primary-background-fill);} #txt2img_checkboxes, #img2img_checkboxes { background-color: transparent; } #txt2img_checkboxes, #img2img_checkboxes { margin-bottom: 0.2em; } #txt2img_actions_column, #img2img_actions_column { flex-flow: wrap; justify-content: space-between; } -#txt2img_enqueue_wrapper, #img2img_enqueue_wrapper { min-width: unset; width: 48%; } -#txt2img_generate_box, #img2img_generate_box { min-width: unset; width: 48%; } #extras_upscale { margin-top: 10px } #txt2img_progress_row > div { min-width: var(--left-column); max-width: var(--left-column); } diff --git a/javascript/ui.js b/javascript/ui.js index c7ecd6680..8808f1c8b 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -204,6 +204,8 @@ function submit_txt2img(...args) { requestProgress(id, null, gradioApp().getElementById('txt2img_gallery')); const res = create_submit_args(args); res[0] = id; + res[1] = window.submit_state; + window.submit_state = ''; return res; } @@ -214,7 +216,9 @@ function submit_img2img(...args) { requestProgress(id, null, gradioApp().getElementById('img2img_gallery')); const res = create_submit_args(args); res[0] = id; - res[1] = get_tab_index('mode_img2img'); + res[1] = window.submit_state; + res[2] = get_tab_index('mode_img2img'); + window.submit_state = ''; return res; } @@ -225,7 +229,9 @@ function submit_control(...args) { requestProgress(id, null, gradioApp().getElementById('control_gallery')); const res = create_submit_args(args); res[0] = id; - res[1] = gradioApp().querySelector('#control-tabs > .tab-nav > .selected')?.innerText.toLowerCase() || ''; // selected tab name + res[1] = window.submit_state; + res[2] = gradioApp().querySelector('#control-tabs > .tab-nav > .selected')?.innerText.toLowerCase() || ''; // selected tab name + window.submit_state = ''; return res; } @@ -236,6 +242,7 @@ function submit_postprocessing(...args) { } window.submit = submit_txt2img; +window.submit_state = ''; function modelmerger(...args) { const id = randomId(); diff --git a/modules/control/run.py b/modules/control/run.py index 522857a27..4f8b48cd2 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -53,7 +53,8 @@ def control_set(kwargs): p_extra_args[k] = v -def control_run(units: List[unit.Unit] = [], inputs: List[Image.Image] = [], inits: List[Image.Image] = [], mask: Image.Image = None, unit_type: str = None, is_generator: bool = True, +def control_run(state: str = '', + units: List[unit.Unit] = [], inputs: List[Image.Image] = [], inits: List[Image.Image] = [], mask: Image.Image = None, unit_type: str = None, is_generator: bool = True, input_type: int = 0, prompt: str = '', negative_prompt: str = '', styles: List[str] = [], steps: int = 20, sampler_index: int = None, @@ -148,6 +149,7 @@ def control_run(units: List[unit.Unit] = [], inputs: List[Image.Image] = [], ini outpath_samples=shared.opts.outdir_samples or shared.opts.outdir_control_samples, outpath_grids=shared.opts.outdir_grids or shared.opts.outdir_control_grids, ) + p.state = state # processing.process_init(p) resize_mode_before = resize_mode_before if resize_name_before != 'None' and inputs is not None and len(inputs) > 0 else 0 @@ -732,6 +734,7 @@ def control_run(units: List[unit.Unit] = [], inputs: List[Image.Image] = [], ini output_filename = '' image_txt = f'| Frames {len(output_images)} | Size {output_images[0].width}x{output_images[0].height}' + p.close() restore_pipeline() debug(f'Ready: {image_txt}') diff --git a/modules/images.py b/modules/images.py index 151500770..e85dd3fc6 100644 --- a/modules/images.py +++ b/modules/images.py @@ -12,7 +12,7 @@ import piexif import piexif.helper from PIL import Image, PngImagePlugin, ExifTags from modules import sd_samplers, shared, script_callbacks, errors, paths -from modules.images_grid import image_grid, split_grid, combine_grid, check_grid_size, get_font, draw_grid_annotations, draw_prompt_matrix, GridAnnotation, Grid # pylint: disable=unused-import +from modules.images_grid import image_grid, get_grid_size, split_grid, combine_grid, check_grid_size, get_font, draw_grid_annotations, draw_prompt_matrix, GridAnnotation, Grid # pylint: disable=unused-import from modules.images_resize import resize_image # pylint: disable=unused-import from modules.images_namegen import FilenameGenerator, get_next_sequence_number # pylint: disable=unused-import diff --git a/modules/images_grid.py b/modules/images_grid.py index c50d81612..194b0d1cd 100644 --- a/modules/images_grid.py +++ b/modules/images_grid.py @@ -19,7 +19,7 @@ def check_grid_size(imgs): return ok -def image_grid(imgs, batch_size=1, rows=None): +def get_grid_size(imgs, batch_size=1, rows=None): if rows is None: if shared.opts.n_rows > 0: rows = shared.opts.n_rows @@ -32,6 +32,11 @@ def image_grid(imgs, batch_size=1, rows=None): if rows > len(imgs): rows = len(imgs) cols = math.ceil(len(imgs) / rows) + return rows, cols + + +def image_grid(imgs, batch_size=1, rows=None): + rows, cols = get_grid_size(imgs, batch_size, rows=rows) params = script_callbacks.ImageGridLoopParams(imgs, cols, rows) script_callbacks.image_grid_callback(params) imgs = [i for i in imgs if i is not None] if imgs is not None else [] diff --git a/modules/img2img.py b/modules/img2img.py index 4bf057d4a..6e2986608 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -107,7 +107,7 @@ def process_batch(p, input_files, input_dir, output_dir, inpaint_mask_dir, args) shared.log.debug(f'Processed: images={len(batch_image_files)} memory={memory_stats()} batch') -def img2img(id_task: str, mode: int, +def img2img(id_task: str, state: str, mode: int, prompt, negative_prompt, prompt_styles, init_img, sketch, @@ -251,6 +251,7 @@ def img2img(id_task: str, mode: int, ) p.scripts = modules.scripts.scripts_img2img p.script_args = args + p.state = state if mask: p.extra_generation_params["Mask blur"] = mask_blur p.extra_generation_params["Mask alpha"] = mask_alpha diff --git a/modules/infotext.py b/modules/infotext.py index 43603b4ce..b16c3769e 100644 --- a/modules/infotext.py +++ b/modules/infotext.py @@ -3,7 +3,11 @@ import re import json -debug = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment +if os.environ.get('SD_PASTE_DEBUG', None) is not None: + from modules.errors import log + debug = log.trace +else: + debug = lambda *args, **kwargs: None # pylint: disable=unnecessary-lambda-assignment re_size = re.compile(r"^(\d+)x(\d+)$") # int x int re_param = re.compile(r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)') # multi-word: value diff --git a/modules/processing.py b/modules/processing.py index bf704a83a..fb7bc0752 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -272,9 +272,6 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): p.scripts.process(p) - def infotext(_inxex=0): # dummy function overriden if there are iterations - return '' - ema_scope_context = p.sd_model.ema_scope if not shared.native else nullcontext shared.state.job_count = p.n_iter with devices.inference_context(), ema_scope_context(): @@ -312,55 +309,53 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): p.scripts.process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) - x_samples_ddim = None + samples = None timer.process.record('init') if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): - x_samples_ddim = p.scripts.process_images(p) - if x_samples_ddim is None: + samples = p.scripts.process_images(p) + if samples is None: if not shared.native: from modules.processing_original import process_original - x_samples_ddim = process_original(p) + samples = process_original(p) elif shared.native: from modules.processing_diffusers import process_diffusers - x_samples_ddim = process_diffusers(p) + samples = process_diffusers(p) else: raise ValueError(f"Unknown backend {shared.backend}") timer.process.record('process') if not shared.opts.keep_incomplete and shared.state.interrupted: - x_samples_ddim = [] + samples = [] if not shared.native and (shared.cmd_opts.lowvram or shared.cmd_opts.medvram): lowvram.send_everything_to_cpu() devices.torch_gc() if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): - p.scripts.postprocess_batch(p, x_samples_ddim, batch_number=n) + p.scripts.postprocess_batch(p, samples, batch_number=n) if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): p.prompts = p.all_prompts[n * p.batch_size:(n+1) * p.batch_size] p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n+1) * p.batch_size] - batch_params = scripts.PostprocessBatchListArgs(list(x_samples_ddim)) + batch_params = scripts.PostprocessBatchListArgs(list(samples)) p.scripts.postprocess_batch_list(p, batch_params, batch_number=n) - x_samples_ddim = batch_params.images + samples = batch_params.images - def infotext(index): # pylint: disable=function-redefined - return create_infotext(p, p.prompts, p.seeds, p.subseeds, index=index, all_negative_prompts=p.negative_prompts) - - for i, x_sample in enumerate(x_samples_ddim): - debug(f'Processing result: index={i+1}/{len(x_samples_ddim)} iteration={n+1}/{p.n_iter}') + for i, sample in enumerate(samples): + debug(f'Processing result: index={i+1}/{len(samples)} iteration={n+1}/{p.n_iter}') p.batch_index = i - if type(x_sample) == Image.Image: - image = x_sample - x_sample = np.array(x_sample) + info = create_infotext(p, p.prompts, p.seeds, p.subseeds, index=i, all_negative_prompts=p.negative_prompts) + if type(sample) == Image.Image: + image = sample + sample = np.array(sample) else: - x_sample = validate_sample(x_sample) - image = Image.fromarray(x_sample) + sample = validate_sample(sample) + image = Image.fromarray(sample) if p.restore_faces: if not p.do_not_save_samples and shared.opts.save_images_before_face_restoration: - images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=infotext(i), p=p, suffix="-before-face-restore") + images.save_image(Image.fromarray(sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restore") p.ops.append('face') - x_sample = face_restoration.restore_faces(x_sample, p) - if x_sample is not None: - image = Image.fromarray(x_sample) + sample = face_restoration.restore_faces(sample, p) + if sample is not None: + image = Image.fromarray(sample) if p.scripts is not None and isinstance(p.scripts, scripts.ScriptRunner): pp = scripts.PostprocessImageArgs(image) p.scripts.postprocess_image(p, pp) @@ -370,7 +365,6 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if not p.do_not_save_samples and shared.opts.save_images_before_color_correction: orig = p.color_corrections p.color_corrections = None - info = infotext(i) p.color_corrections = orig image_without_cc = apply_overlay(image, p.paste_to, i, p.overlay_images) images.save_image(image_without_cc, path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-color-correct") @@ -378,12 +372,11 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: image = apply_color_correction(p.color_corrections[i], image) if shared.opts.mask_apply_overlay: image = apply_overlay(image, p.paste_to, i, p.overlay_images) - text = infotext(i) - infotexts.append(text) - image.info["parameters"] = text + infotexts.append(info) + image.info["parameters"] = info output_images.append(image) if shared.opts.samples_save and not p.do_not_save_samples and p.outpath_samples is not None: - images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p) # main save image + images.save_image(image, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p) # main save image if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([shared.opts.save_mask, shared.opts.save_mask_composite, shared.opts.return_mask, shared.opts.return_mask_composite]): image_mask = p.mask_for_overlay.convert('RGB') image1 = image.convert('RGBA').convert('RGBa') @@ -391,15 +384,15 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: mask = images.resize_image(3, p.mask_for_overlay, image.width, image.height).convert('L') image_mask_composite = Image.composite(image1, image2, mask).convert('RGBA') if shared.opts.save_mask: - images.save_image(image_mask, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p, suffix="-mask") + images.save_image(image_mask, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask") if shared.opts.save_mask_composite: - images.save_image(image_mask_composite, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=text, p=p, suffix="-mask-composite") + images.save_image(image_mask_composite, p.outpath_samples, "", p.seeds[i], p.prompts[i], shared.opts.samples_format, info=info, p=p, suffix="-mask-composite") if shared.opts.return_mask: output_images.append(image_mask) if shared.opts.return_mask_composite: output_images.append(image_mask_composite) timer.process.record('post') - del x_samples_ddim + del samples devices.torch_gc() if hasattr(shared.sd_model, 'restore_pipeline') and shared.sd_model.restore_pipeline is not None: @@ -413,15 +406,16 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: index_of_first_image = 0 if (shared.opts.return_grid or shared.opts.grid_save) and not p.do_not_save_grid and len(output_images) > 1: if images.check_grid_size(output_images): + r, c = images.get_grid_size(output_images, p.batch_size) grid = images.image_grid(output_images, p.batch_size) + grid_text = f'{r}x{c}' + grid_info = create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, index=0, grid=grid_text) if shared.opts.return_grid: - text = infotext(-1) - infotexts.insert(0, text) - grid.info["parameters"] = text + infotexts.insert(0, grid_info) output_images.insert(0, grid) index_of_first_image = 1 if shared.opts.grid_save: - images.save_image(grid, p.outpath_grids, "", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=infotext(-1), p=p, grid=True, suffix="-grid") # main save grid + images.save_image(grid, p.outpath_grids, "", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=grid_info, p=p, grid=True, suffix="-grid") # main save grid if shared.native: from modules import ipadapter @@ -445,7 +439,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p, images_list=output_images, seed=p.all_seeds[0], - info=infotext(0), + info=infotexts[0] if len(infotexts) > 0 else '', comments="\n".join(comments), subseed=p.all_subseeds[0], index_of_first_image=index_of_first_image, diff --git a/modules/processing_class.py b/modules/processing_class.py index 43cb3407b..dcf003f11 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -21,6 +21,8 @@ class StableDiffusionProcessing: The first set of paramaters: sd_models -> do_not_reload_embeddings represent the minimum required to create a StableDiffusionProcessing """ def __init__(self, sd_model=None, outpath_samples=None, outpath_grids=None, prompt: str = "", styles: List[str] = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, seed_enable_extras: bool = True, sampler_name: str = None, hr_sampler_name: str = None, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, image_cfg_scale: float = None, clip_skip: int = 1, width: int = 512, height: int = 512, full_quality: bool = True, restore_faces: bool = False, tiling: bool = False, hidiffusion: bool = False, do_not_save_samples: bool = False, do_not_save_grid: bool = False, extra_generation_params: Dict[Any, Any] = None, overlay_images: Any = None, negative_prompt: str = None, eta: float = None, do_not_reload_embeddings: bool = False, denoising_strength: float = 0, diffusers_guidance_rescale: float = 0.7, pag_scale: float = 0.0, pag_adaptive: float = 0.5, cfg_end: float = 1, resize_mode: int = 0, resize_name: str = 'None', resize_context: str = 'None', scale_by: float = 0, selected_scale_tab: int = 0, hdr_mode: int = 0, hdr_brightness: float = 0, hdr_color: float = 0, hdr_sharpen: float = 0, hdr_clamp: bool = False, hdr_boundary: float = 4.0, hdr_threshold: float = 0.95, hdr_maximize: bool = False, hdr_max_center: float = 0.6, hdr_max_boundry: float = 1.0, hdr_color_picker: str = None, hdr_tint_ratio: float = 0, override_settings: Dict[str, Any] = None, override_settings_restore_afterwards: bool = True, sampler_index: int = None, script_args: list = None): # pylint: disable=unused-argument + self.state: str = '' + self.skip = [] self.outpath_samples: str = outpath_samples self.outpath_grids: str = outpath_grids self.prompt: str = prompt @@ -192,6 +194,7 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): def __init__(self, enable_hr: bool = False, denoising_strength: float = 0.75, firstphase_width: int = 0, firstphase_height: int = 0, hr_scale: float = 2.0, hr_force: bool = False, hr_resize_mode: int = 0, hr_resize_context: str = 'None', hr_upscaler: str = None, hr_second_pass_steps: int = 0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_steps: int = 5, refiner_start: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): super().__init__(**kwargs) + self.reprocess = {} self.enable_hr = enable_hr self.denoising_strength = denoising_strength self.hr_scale = hr_scale @@ -220,6 +223,7 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): self.scripts = None self.script_args = [] + def init(self, all_prompts=None, all_seeds=None, all_subseeds=None): if shared.native: shared.sd_model = sd_models.set_diffuser_pipe(self.sd_model, sd_models.DiffusersTaskType.TEXT_2_IMAGE) diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 0c458c9dc..1cb0c936b 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -5,68 +5,52 @@ import numpy as np import torch import torchvision.transforms.functional as TF from modules import shared, devices, processing, sd_models, errors, sd_hijack_hypertile, processing_vae, sd_models_compile, hidiffusion, timer -from modules.processing_helpers import resize_hires, calculate_base_steps, calculate_hires_steps, calculate_refiner_steps, save_intermediate, update_sampler +from modules.processing_helpers import resize_hires, calculate_base_steps, calculate_hires_steps, calculate_refiner_steps, save_intermediate, update_sampler, is_txt2img, is_refiner_enabled from modules.processing_args import set_pipeline_args from modules.onnx_impl import preprocess_pipeline as preprocess_onnx_pipeline, check_parameters_changed as olive_check_parameters_changed debug = shared.log.trace if os.environ.get('SD_DIFFUSERS_DEBUG', None) is not None else lambda *args, **kwargs: None debug('Trace: DIFFUSERS') +last_p = None -def process_diffusers(p: processing.StableDiffusionProcessing): - debug(f'Process diffusers args: {vars(p)}') - orig_pipeline = shared.sd_model - results = [] +def restore_state(p: processing.StableDiffusionProcessing): + if p.state in ['reprocess_refine', 'reprocess_face']: + # validate + if last_p is None: + shared.log.warning(f'Restore state: op={p.state} last state missing') + return p + if p.__class__ != last_p.__class__: + shared.log.warning(f'Restore state: op={p.state} last state is different type') + return p + if processing_vae.last_latent is None: + shared.log.warning(f'Restore state: op={p.state} last latents missing') + return p + state = p.state - def is_txt2img(): - return sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.TEXT_2_IMAGE + # set ops + if state == 'reprocess_refine': + # use new upscale values + hr_scale, hr_upscaler, hr_resize_mode, hr_resize_context, hr_resize_x, hr_resize_y, hr_upscale_to_x, hr_upscale_to_y = p.hr_scale, p.hr_upscaler, p.hr_resize_mode, p.hr_resize_context, p.hr_resize_x, p.hr_resize_y, p.hr_upscale_to_x, p.hr_upscale_to_y # txt2img + height, width, scale_by, resize_mode, resize_name, resize_context = p.height, p.width, p.scale_by, p.resize_mode, p.resize_name, p.resize_context # img2img + p = last_p + p.skip = ['encode', 'base'] + p.state = state + p.enable_hr = True + p.hr_force = True + p.hr_scale, p.hr_upscaler, p.hr_resize_mode, p.hr_resize_context, p.hr_resize_x, p.hr_resize_y, p.hr_upscale_to_x, p.hr_upscale_to_y = hr_scale, hr_upscaler, hr_resize_mode, hr_resize_context, hr_resize_x, hr_resize_y, hr_upscale_to_x, hr_upscale_to_y + p.height, p.width, p.scale_by, p.resize_mode, p.resize_name, p.resize_context = height, width, scale_by, resize_mode, resize_name, resize_context + p.init_images = None + if state == 'reprocess_face': + p.skip = ['encode', 'base', 'hires'] + p.restore_faces = True + shared.log.info(f'Restore state: op={p.state} skip={p.skip}') + return p - def is_refiner_enabled(): - return p.enable_hr and p.refiner_steps > 0 and p.refiner_start > 0 and p.refiner_start < 1 and shared.sd_refiner is not None - def update_pipeline(sd_model, p: processing.StableDiffusionProcessing): - if sd_models.get_diffusers_task(sd_model) == sd_models.DiffusersTaskType.INPAINTING and getattr(p, 'image_mask', None) is None and p.task_args.get('image_mask', None) is None and getattr(p, 'mask', None) is None: - shared.log.warning('Processing: mode=inpaint mask=None') - sd_model = sd_models.set_diffuser_pipe(sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) - if shared.opts.cuda_compile_backend == "olive-ai": - sd_model = olive_check_parameters_changed(p, is_refiner_enabled()) - if sd_model.__class__.__name__ == "OnnxRawPipeline": - sd_model = preprocess_onnx_pipeline(p) - nonlocal orig_pipeline - orig_pipeline = sd_model # processed ONNX pipeline should not be replaced with original pipeline. - if getattr(sd_model, "current_attn_name", None) != shared.opts.cross_attention_optimization: - shared.log.info(f"Setting attention optimization: {shared.opts.cross_attention_optimization}") - sd_models.set_diffusers_attention(sd_model) - return sd_model - - # sanitize init_images - if hasattr(p, 'init_images') and getattr(p, 'init_images', None) is None: - del p.init_images - if hasattr(p, 'init_images') and not isinstance(getattr(p, 'init_images', []), list): - p.init_images = [p.init_images] - if len(getattr(p, 'init_images', [])) > 0: - while len(p.init_images) < len(p.prompts): - p.init_images.append(p.init_images[-1]) - - if shared.state.interrupted or shared.state.skipped: - shared.sd_model = orig_pipeline - return results - - # pipeline type is set earlier in processing, but check for sanity - is_control = getattr(p, 'is_control', False) is True - has_images = len(getattr(p, 'init_images' ,[])) > 0 - if sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.TEXT_2_IMAGE and not has_images and not is_control: - shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.TEXT_2_IMAGE) # reset pipeline - if hasattr(shared.sd_model, 'unet') and hasattr(shared.sd_model.unet, 'config') and hasattr(shared.sd_model.unet.config, 'in_channels') and shared.sd_model.unet.config.in_channels == 9 and not is_control: - shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.INPAINTING) # force pipeline - if len(getattr(p, 'init_images', [])) == 0: - p.init_images = [TF.to_pil_image(torch.rand((3, getattr(p, 'height', 512), getattr(p, 'width', 512))))] - - sd_models.move_model(shared.sd_model, devices.device) - sd_models_compile.openvino_recompile_model(p, hires=False, refiner=False) # recompile if a parameter changes - - use_refiner_start = is_txt2img() and is_refiner_enabled() and not p.is_hr_pass and p.refiner_start > 0 and p.refiner_start < 1 +def process_base(p: processing.StableDiffusionProcessing): + use_refiner_start = is_txt2img() and is_refiner_enabled(p) and not p.is_hr_pass and p.refiner_start > 0 and p.refiner_start < 1 use_denoise_start = not is_txt2img() and p.refiner_start > 0 and p.refiner_start < 1 shared.sd_model = update_pipeline(shared.sd_model, p) @@ -141,14 +125,22 @@ def process_diffusers(p: processing.StableDiffusionProcessing): p.extra_generation_params['Embeddings'] = ', '.join(shared.sd_model.embedding_db.embeddings_used) shared.state.nextjob() - if shared.state.interrupted or shared.state.skipped: - shared.sd_model = orig_pipeline - return results + return output + +def process_hires(p: processing.StableDiffusionProcessing, output): # optional second pass if p.enable_hr: p.is_hr_pass = True - p.init_hr(p.hr_scale, p.hr_upscaler, force=p.hr_force) + if hasattr(p, 'init_hr'): + p.init_hr(p.hr_scale, p.hr_upscaler, force=p.hr_force) + else: # fake hires for img2img + p.hr_scale = p.scale_by + p.hr_upscaler = p.resize_name + p.hr_resize_mode = p.resize_mode + p.hr_resize_context = p.resize_context + p.hr_upscale_to_x = p.width + p.hr_upscale_to_y = p.height prev_job = shared.state.job # hires runs on original pipeline @@ -156,7 +148,7 @@ def process_diffusers(p: processing.StableDiffusionProcessing): shared.sd_model.restore_pipeline() # upscale - if hasattr(p, 'height') and hasattr(p, 'width') and p.hr_resize_mode >0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5): + if hasattr(p, 'height') and hasattr(p, 'width') and p.hr_resize_mode > 0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5): shared.log.info(f'Upscale: mode={p.hr_resize_mode} upscaler="{p.hr_upscaler}" context="{p.hr_resize_context}" resize={p.hr_resize_x}x{p.hr_resize_y} upscale={p.hr_upscale_to_x}x{p.hr_upscale_to_y}') p.ops.append('upscale') if shared.opts.samples_save and not p.do_not_save_samples and shared.opts.save_images_before_highres_fix and hasattr(shared.sd_model, 'vae'): @@ -225,9 +217,12 @@ def process_diffusers(p: processing.StableDiffusionProcessing): shared.state.nextjob() p.is_hr_pass = False timer.process.record('hires') + return output + +def process_refine(p: processing.StableDiffusionProcessing, output): # optional refiner pass or decode - if is_refiner_enabled(): + if is_refiner_enabled(p): prev_job = shared.state.job shared.state.job = 'Refine' shared.state.job_count +=1 @@ -238,7 +233,7 @@ def process_diffusers(p: processing.StableDiffusionProcessing): sd_models.move_model(shared.sd_model, devices.cpu) if shared.state.interrupted or shared.state.skipped: shared.sd_model = orig_pipeline - return results + return output if shared.opts.diffusers_offload_mode == "balanced": shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model) if shared.opts.diffusers_move_refiner: @@ -282,17 +277,19 @@ def process_diffusers(p: processing.StableDiffusionProcessing): try: if 'requires_aesthetics_score' in shared.sd_refiner.config: # sdxl-model needs false and sdxl-refiner needs true shared.sd_refiner.register_to_config(requires_aesthetics_score = getattr(shared.sd_refiner, 'tokenizer', None) is None) - refiner_output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable - if isinstance(refiner_output, dict): - refiner_output = SimpleNamespace(**refiner_output) + output = shared.sd_refiner(**refiner_args) # pylint: disable=not-callable + if isinstance(output, dict): + output = SimpleNamespace(**output) sd_models_compile.openvino_post_compile(op="refiner") except AssertionError as e: shared.log.info(e) + """ # TODO decode using refiner if not shared.state.interrupted and not shared.state.skipped: refiner_images = processing_vae.vae_decode(latents=refiner_output.images, model=shared.sd_refiner, full_quality=True, width=max(p.width, p.hr_upscale_to_x), height=max(p.height, p.hr_upscale_to_y)) for refiner_image in refiner_images: results.append(refiner_image) + """ if shared.opts.diffusers_offload_mode == "balanced": shared.sd_refiner = sd_models.apply_balanced_offload(shared.sd_refiner) @@ -303,30 +300,113 @@ def process_diffusers(p: processing.StableDiffusionProcessing): shared.state.nextjob() p.is_refiner_pass = False timer.process.record('refine') + return output - # final decode since there is no refiner - if not is_refiner_enabled(): - if output is not None: - if not hasattr(output, 'images') and hasattr(output, 'frames'): - shared.log.debug(f'Generated: frames={len(output.frames[0])}') - output.images = output.frames[0] - if hasattr(shared.sd_model, "vae") and output.images is not None and len(output.images) > 0: - if p.hr_resize_mode > 0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5): - width = max(getattr(p, 'width', 0), getattr(p, 'hr_upscale_to_x', 0)) - height = max(getattr(p, 'height', 0), getattr(p, 'hr_upscale_to_y', 0)) - else: - width = getattr(p, 'width', 0) - height = getattr(p, 'height', 0) - results = processing_vae.vae_decode(latents=output.images, model=shared.sd_model, full_quality=p.full_quality, width=width, height=height) - elif hasattr(output, 'images'): - results = output.images + +def process_decode(p: processing.StableDiffusionProcessing, output): + if output is not None: + if not hasattr(output, 'images') and hasattr(output, 'frames'): + shared.log.debug(f'Generated: frames={len(output.frames[0])}') + output.images = output.frames[0] + if hasattr(shared.sd_model, "vae") and output.images is not None and len(output.images) > 0: + if p.hr_resize_mode > 0 and (p.hr_upscaler != 'None' or p.hr_resize_mode == 5): + width = max(getattr(p, 'width', 0), getattr(p, 'hr_upscale_to_x', 0)) + height = max(getattr(p, 'height', 0), getattr(p, 'hr_upscale_to_y', 0)) else: - shared.log.warning('Processing returned no results') - results = [] + width = getattr(p, 'width', 0) + height = getattr(p, 'height', 0) + results = processing_vae.vae_decode( + latents = output.images, + model = shared.sd_model if not is_refiner_enabled(p) else shared.sd_refiner, + full_quality = p.full_quality, + width = width, + height = height, + save = p.state == '', + ) + elif hasattr(output, 'images'): + results = output.images else: shared.log.warning('Processing returned no results') results = [] + else: + shared.log.warning('Processing returned no results') + results = [] + return results + + +orig_pipeline = shared.sd_model +def update_pipeline(sd_model, p: processing.StableDiffusionProcessing): + if sd_models.get_diffusers_task(sd_model) == sd_models.DiffusersTaskType.INPAINTING and getattr(p, 'image_mask', None) is None and p.task_args.get('image_mask', None) is None and getattr(p, 'mask', None) is None: + shared.log.warning('Processing: mode=inpaint mask=None') + sd_model = sd_models.set_diffuser_pipe(sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) + if shared.opts.cuda_compile_backend == "olive-ai": + sd_model = olive_check_parameters_changed(p, is_refiner_enabled(p)) + if sd_model.__class__.__name__ == "OnnxRawPipeline": + sd_model = preprocess_onnx_pipeline(p) + global orig_pipeline # pylint: disable=global-statement + orig_pipeline = sd_model # processed ONNX pipeline should not be replaced with original pipeline. + if getattr(sd_model, "current_attn_name", None) != shared.opts.cross_attention_optimization: + shared.log.info(f"Setting attention optimization: {shared.opts.cross_attention_optimization}") + sd_models.set_diffusers_attention(sd_model) + return sd_model + + +def process_diffusers(p: processing.StableDiffusionProcessing): + debug(f'Process diffusers args: {vars(p)}') + results = [] + p = restore_state(p) + + if shared.state.interrupted or shared.state.skipped: + shared.sd_model = orig_pipeline + return results + + # sanitize init_images + if hasattr(p, 'init_images') and getattr(p, 'init_images', None) is None: + del p.init_images + if hasattr(p, 'init_images') and not isinstance(getattr(p, 'init_images', []), list): + p.init_images = [p.init_images] + if len(getattr(p, 'init_images', [])) > 0: + while len(p.init_images) < len(p.prompts): + p.init_images.append(p.init_images[-1]) + # pipeline type is set earlier in processing, but check for sanity + is_control = getattr(p, 'is_control', False) is True + has_images = len(getattr(p, 'init_images' ,[])) > 0 + if sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.TEXT_2_IMAGE and not has_images and not is_control: + shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.TEXT_2_IMAGE) # reset pipeline + if hasattr(shared.sd_model, 'unet') and hasattr(shared.sd_model.unet, 'config') and hasattr(shared.sd_model.unet.config, 'in_channels') and shared.sd_model.unet.config.in_channels == 9 and not is_control: + shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.INPAINTING) # force pipeline + if len(getattr(p, 'init_images', [])) == 0: + p.init_images = [TF.to_pil_image(torch.rand((3, getattr(p, 'height', 512), getattr(p, 'width', 512))))] + + sd_models.move_model(shared.sd_model, devices.device) + sd_models_compile.openvino_recompile_model(p, hires=False, refiner=False) # recompile if a parameter changes + + if 'base' not in p.skip: + output = process_base(p) + else: + output = SimpleNamespace(images=processing_vae.last_latent) + + if shared.state.interrupted or shared.state.skipped: + shared.sd_model = orig_pipeline + return results + + if 'hires' not in p.skip: + output = process_hires(p, output) + if shared.state.interrupted or shared.state.skipped: + shared.sd_model = orig_pipeline + return results + + if 'refine' not in p.skip: + output = process_refine(p, output) + if shared.state.interrupted or shared.state.skipped: + shared.sd_model = orig_pipeline + return results + + results = process_decode(p, output) timer.process.record('decode') shared.sd_model = orig_pipeline + if p.state == '': + global last_p # pylint: disable=global-statement + last_p = p return results diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index 56b4f65eb..2e0d748e3 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -17,6 +17,14 @@ debug_steps = shared.log.trace if os.environ.get('SD_STEPS_DEBUG', None) is not debug_steps('Trace: STEPS') +def is_txt2img(): + return sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.TEXT_2_IMAGE + + +def is_refiner_enabled(p): + return p.enable_hr and p.refiner_steps > 0 and p.refiner_start > 0 and p.refiner_start < 1 and shared.sd_refiner is not None + + def setup_color_correction(image): debug("Calibrating color correction") correction_target = cv2.cvtColor(np.asarray(image.copy()), cv2.COLOR_RGB2LAB) @@ -435,8 +443,7 @@ def fix_prompts(prompts, negative_prompts, prompts_2, negative_prompts_2): def calculate_base_steps(p, use_denoise_start, use_refiner_start): if len(getattr(p, 'timesteps', [])) > 0: return None - is_txt2img = sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.TEXT_2_IMAGE - if not is_txt2img: + if not is_txt2img(): if use_denoise_start and shared.sd_model_type == 'sdxl': steps = p.steps // (1 - p.refiner_start) elif p.denoising_strength > 0: diff --git a/modules/processing_info.py b/modules/processing_info.py index ac05aa58d..bd146daa6 100644 --- a/modules/processing_info.py +++ b/modules/processing_info.py @@ -10,7 +10,7 @@ else: sd_hijack = None -def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=None, all_subseeds=None, comments=None, iteration=0, position_in_batch=0, index=None, all_negative_prompts=None): +def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=None, all_subseeds=None, comments=None, iteration=0, position_in_batch=0, index=None, all_negative_prompts=None, grid=None): if p is None: shared.log.warning('Processing info: no data') return '' @@ -45,7 +45,6 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No "CFG scale": p.cfg_scale, "Size": f"{p.width}x{p.height}" if hasattr(p, 'width') and hasattr(p, 'height') else None, "Batch": f'{p.n_iter}x{p.batch_size}' if p.n_iter > 1 or p.batch_size > 1 else None, - "Index": f'{p.iteration + 1}x{index + 1}' if (p.n_iter > 1 or p.batch_size > 1) and index >= 0 else None, "Parser": shared.opts.prompt_attention, "Model": None if (not shared.opts.add_model_name_to_info) or (not shared.sd_model.sd_checkpoint_info.model_name) else shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', ''), "Model hash": getattr(p, 'sd_model_hash', None if (not shared.opts.add_model_hash_to_info) or (not shared.sd_model.sd_model_hash) else shared.sd_model.sd_model_hash), @@ -64,6 +63,10 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No "Operations": '; '.join(ops).replace('"', '') if len(p.ops) > 0 else 'none', } # native + if grid is None and (p.n_iter > 1 or p.batch_size > 1) and index >= 0: + args['Index'] = f'{p.iteration + 1}x{index + 1}' + if grid is not None: + args['Grid'] = grid if shared.native: args['Pipeline'] = shared.sd_model.__class__.__name__ args['T5'] = None if (not shared.opts.add_model_name_to_info or shared.opts.sd_text_encoder is None or shared.opts.sd_text_encoder == 'None') else shared.opts.sd_text_encoder diff --git a/modules/processing_vae.py b/modules/processing_vae.py index 2edff668f..68057700d 100644 --- a/modules/processing_vae.py +++ b/modules/processing_vae.py @@ -140,7 +140,7 @@ def taesd_vae_encode(image): return encoded -def vae_decode(latents, model, output_type='np', full_quality=True, width=None, height=None): +def vae_decode(latents, model, output_type='np', full_quality=True, width=None, height=None, save=True): global last_latent # pylint: disable=global-statement t0 = time.time() if latents is None or not torch.is_tensor(latents): # already decoded @@ -163,7 +163,8 @@ def vae_decode(latents, model, output_type='np', full_quality=True, width=None, latents = latents.unsqueeze(0) if latents.shape[0] == 4 and latents.shape[1] != 4: # likely animatediff latent latents = latents.permute(1, 0, 2, 3) - last_latent = latents.clone().detach() + if save: + last_latent = latents.clone().detach() if latents.shape[-1] <= 4: # not a latent, likely an image decoded = latents.float().cpu().numpy() diff --git a/modules/txt2img.py b/modules/txt2img.py index e438c2e47..e23b3efd7 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -8,7 +8,7 @@ debug = shared.log.trace if os.environ.get('SD_PROCESS_DEBUG', None) is not None debug('Trace: PROCESS') -def txt2img(id_task, +def txt2img(id_task, state, prompt, negative_prompt, prompt_styles, steps, sampler_index, hr_sampler_index, full_quality, restore_faces, tiling, hidiffusion, @@ -88,6 +88,7 @@ def txt2img(id_task, ) p.scripts = scripts.scripts_txt2img p.script_args = args + p.state = state processed = scripts.scripts_txt2img.run(p, *args) if processed is None: processed = processing.process_images(p) diff --git a/modules/ui_common.py b/modules/ui_common.py index 32109b571..5e873355b 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -42,7 +42,7 @@ def infotext_to_html(text): negative = res.get('Negative prompt', '') res.pop('Prompt', None) res.pop('Negative prompt', None) - params = [f'{k}: {v}' for k, v in res.items() if v is not None and 'size-' not in k.lower()] + params = [f'{k}: {v}' for k, v in res.items() if v is not None and not k.endswith('-1') and not k.endswith('-2')] params = '| '.join(params) if len(params) > 0 else '' code = '' if len(prompt) > 0: diff --git a/modules/ui_control.py b/modules/ui_control.py index 5bad230bf..a65a6ef8f 100644 --- a/modules/ui_control.py +++ b/modules/ui_control.py @@ -59,7 +59,7 @@ def get_units(*values): break -def generate_click(job_id: str, active_tab: str, *args): +def generate_click(job_id: str, state: str, active_tab: str, *args): while helpers.busy: time.sleep(0.01) from modules.control.run import control_run @@ -71,7 +71,7 @@ def generate_click(job_id: str, active_tab: str, *args): shared.mem_mon.reset() progress.start_task(job_id) try: - for results in control_run(units, helpers.input_source, helpers.input_init, helpers.input_mask, active_tab, True, *args): + for results in control_run(state, units, helpers.input_source, helpers.input_init, helpers.input_mask, active_tab, True, *args): progress.record_results(job_id, results) yield return_controls(results) except Exception as e: @@ -103,6 +103,7 @@ def create_ui(_blocks: gr.Blocks=None): with gr.Row(elem_id='control_settings'): full_quality, restore_faces, tiling, hidiffusion = ui_sections.create_options('control') + state = gr.Textbox(value='', visible=False) with gr.Accordion(open=False, label="Input", elem_id="control_input", elem_classes=["small-accordion"]): with gr.Row(): @@ -503,7 +504,6 @@ def create_ui(_blocks: gr.Blocks=None): btn_negative_counter.click(fn=call_queue.wrap_queued_call(ui_common.update_token_counter), inputs=[negative, steps], outputs=[negative_counter]) btn_interrogate_clip.click(fn=helpers.interrogate_clip, inputs=[], outputs=[prompt]) btn_interrogate_booru.click(fn=helpers.interrogate_booru, inputs=[], outputs=[prompt]) - btn_reprocess.click(fn=processing_vae.reprocess, inputs=[output_gallery], outputs=[output_gallery]) select_fields = [input_mode, input_image, init_image, input_type, input_resize, input_inpaint, input_video, input_batch, input_folder] select_output = [output_tabs, preview_process, result_txt] @@ -517,6 +517,7 @@ def create_ui(_blocks: gr.Blocks=None): ) prompt.submit(**select_dict) + negative.submit(**select_dict) btn_generate.click(**select_dict) for ctrl in [input_image, input_resize, input_video, input_batch, input_folder, init_image, init_video, init_batch, init_folder, tab_image, tab_video, tab_batch, tab_folder, tab_image_init, tab_video_init, tab_batch_init, tab_folder_init]: if hasattr(ctrl, 'change'): @@ -527,7 +528,7 @@ def create_ui(_blocks: gr.Blocks=None): if hasattr(ctrl, 'upload'): ctrl.upload(**select_dict) - tabs_state = gr.Text(value='none', visible=False) + tabs_state = gr.Textbox(value='none', visible=False) input_fields = [ input_type, prompt, negative, styles, @@ -553,13 +554,18 @@ def create_ui(_blocks: gr.Blocks=None): control_dict = dict( fn=generate_click, _js="submit_control", - inputs=[tabs_state, tabs_state] + input_fields + input_script_args, + inputs=[tabs_state, state, tabs_state] + input_fields + input_script_args, outputs=output_fields, show_progress=True, ) prompt.submit(**control_dict) + negative.submit(**control_dict) btn_generate.click(**control_dict) + btn_reprocess[1].click(fn=processing_vae.reprocess, inputs=[output_gallery], outputs=[output_gallery]) # full-decode + btn_reprocess[2].click(**control_dict) # hires-refine + btn_reprocess[3].click(**control_dict) # face-restore + paste_fields = [ # prompt (prompt, "Prompt"), diff --git a/modules/ui_img2img.py b/modules/ui_img2img.py index b8d1a1b10..1cbf30345 100644 --- a/modules/ui_img2img.py +++ b/modules/ui_img2img.py @@ -66,6 +66,7 @@ def create_ui(): with gr.Tabs(elem_id="mode_img2img"): img2img_selected_tab = gr.State(0) # pylint: disable=abstract-class-instantiated + state = gr.Textbox(value='', visible=False) with gr.TabItem('Image', id='img2img', elem_id="img2img_img2img_tab") as tab_img2img: init_img = gr.Image(label="Image for img2img", elem_id="img2img_image", show_label=False, source="upload", interactive=True, type="pil", tool="editor", image_mode="RGBA", height=512) interrogate_clip, interrogate_booru = ui_sections.create_interrogate_buttons('img2img') @@ -159,13 +160,11 @@ def create_ui(): ui_common.connect_reuse_seed(seed, reuse_seed, img2img_generation_info, is_subseed=False) ui_common.connect_reuse_seed(subseed, reuse_subseed, img2img_generation_info, is_subseed=True, subseed_strength=subseed_strength) - img2img_reprocess.click(fn=processing_vae.reprocess, inputs=[img2img_gallery], outputs=[img2img_gallery]) - img2img_prompt_img.change(fn=modules.images.image_data, inputs=[img2img_prompt_img], outputs=[img2img_prompt, img2img_prompt_img]) dummy_component1 = gr.Textbox(visible=False, value='dummy') dummy_component2 = gr.Number(visible=False, value=0) img2img_args = [ - dummy_component1, dummy_component2, + dummy_component1, state, dummy_component2, img2img_prompt, img2img_negative_prompt, img2img_prompt_styles, init_img, sketch, @@ -210,8 +209,13 @@ def create_ui(): img2img_prompt.submit(**img2img_dict) img2img_negative_prompt.submit(**img2img_dict) img2img_submit.click(**img2img_dict) + dummy_component = gr.Textbox(visible=False, value='dummy') + img2img_reprocess[1].click(fn=processing_vae.reprocess, inputs=[img2img_gallery], outputs=[img2img_gallery]) # full-decode + img2img_reprocess[2].click(**img2img_dict) # hires-refine + img2img_reprocess[3].click(**img2img_dict) # face-restore + interrogate_args = dict( _js="get_img2img_tab_index", inputs=[ diff --git a/modules/ui_sections.py b/modules/ui_sections.py index e74b80586..b7da7d5fd 100644 --- a/modules/ui_sections.py +++ b/modules/ui_sections.py @@ -16,7 +16,7 @@ def create_toprow(is_img2img: bool = False, id_part: str = None): if id_part is None: id_part = "img2img" if is_img2img else "txt2img" with gr.Row(elem_id=f"{id_part}_toprow", variant="compact"): - with gr.Column(elem_id=f"{id_part}_prompt_container", scale=5): + with gr.Column(elem_id=f"{id_part}_prompt_container", scale=4): with gr.Row(): with gr.Column(scale=80): with gr.Row(): @@ -27,8 +27,12 @@ def create_toprow(is_img2img: bool = False, id_part: str = None): negative_prompt = gr.Textbox(elem_id=f"{id_part}_neg_prompt", label="Negative prompt", show_label=False, lines=3, placeholder="Negative prompt", elem_classes=["prompt"]) with gr.Column(scale=1, elem_id=f"{id_part}_actions_column"): with gr.Row(elem_id=f"{id_part}_generate_box"): + reprocess = [] submit = gr.Button('Generate', elem_id=f"{id_part}_generate", variant='primary') - reprocess = gr.Button('Reprocess', elem_id=f"{id_part}_reprocess", variant='secondary', visible=False) + reprocess.append(gr.Button('Reprocess', elem_id=f"{id_part}_reprocess", variant='primary', visible=True)) + reprocess.append(gr.Button('Reprocess decode', elem_id=f"{id_part}_reprocess_decode", variant='primary', visible=False)) + reprocess.append(gr.Button('Reprocess refine', elem_id=f"{id_part}_reprocess_refine", variant='primary', visible=False)) + reprocess.append(gr.Button('Reprocess face', elem_id=f"{id_part}_reprocess_face", variant='primary', visible=False)) with gr.Row(elem_id=f"{id_part}_generate_line2"): interrupt = gr.Button('Stop', elem_id=f"{id_part}_interrupt") interrupt.click(fn=lambda: shared.state.interrupt(), _js="requestInterrupt", inputs=[], outputs=[]) diff --git a/modules/ui_txt2img.py b/modules/ui_txt2img.py index 3a5538cde..f4ecf3e49 100644 --- a/modules/ui_txt2img.py +++ b/modules/ui_txt2img.py @@ -1,11 +1,10 @@ import gradio as gr from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call -from modules import timer, shared, ui_common, ui_sections, generation_parameters_copypaste, processing_vae +from modules import timer, shared, ui_common, ui_sections, generation_parameters_copypaste, processing, processing_vae, devices from modules.ui_components import ToolButton # pylint: disable=unused-import def calc_resolution_hires(width, height, hr_scale, hr_resize_x, hr_resize_y, hr_upscaler): - from modules import processing, devices if hr_upscaler == "None": return "Hires resize: None" p = processing.StableDiffusionProcessingTxt2Img(width=width, height=height, enable_hr=True, hr_scale=hr_scale, hr_resize_x=hr_resize_x, hr_resize_y=hr_resize_y) @@ -50,6 +49,7 @@ def create_ui(): hdr_mode, hdr_brightness, hdr_color, hdr_sharpen, hdr_clamp, hdr_boundary, hdr_threshold, hdr_maximize, hdr_max_center, hdr_max_boundry, hdr_color_picker, hdr_tint_ratio = ui_sections.create_correction_inputs('txt2img') enable_hr, hr_sampler_index, denoising_strength, hr_resize_mode, hr_resize_context, hr_upscaler, hr_force, hr_second_pass_steps, hr_scale, hr_resize_x, hr_resize_y, refiner_steps, refiner_start, refiner_prompt, refiner_negative = ui_sections.create_hires_inputs('txt2img') override_settings = ui_common.create_override_inputs('txt2img') + state = gr.Textbox(value='', visible=False) with gr.Group(elem_id="txt2img_script_container"): txt2img_script_inputs = modules.scripts.scripts_txt2img.setup_ui(parent='txt2img', accordion=True) @@ -58,11 +58,10 @@ def create_ui(): ui_common.connect_reuse_seed(seed, reuse_seed, txt2img_generation_info, is_subseed=False) ui_common.connect_reuse_seed(subseed, reuse_subseed, txt2img_generation_info, is_subseed=True, subseed_strength=subseed_strength) - txt2img_reprocess.click(fn=processing_vae.reprocess, inputs=[txt2img_gallery], outputs=[txt2img_gallery]) - dummy_component = gr.Textbox(visible=False, value='dummy') + txt2img_args = [ - dummy_component, + dummy_component, state, txt2img_prompt, txt2img_negative_prompt, txt2img_prompt_styles, steps, sampler_index, hr_sampler_index, full_quality, restore_faces, tiling, hidiffusion, @@ -89,9 +88,15 @@ def create_ui(): ], show_progress=False, ) + txt2img_prompt.submit(**txt2img_dict) txt2img_negative_prompt.submit(**txt2img_dict) txt2img_submit.click(**txt2img_dict) + + txt2img_reprocess[1].click(fn=processing_vae.reprocess, inputs=[txt2img_gallery], outputs=[txt2img_gallery]) # full-decode + txt2img_reprocess[2].click(**txt2img_dict) # hires-refine + txt2img_reprocess[3].click(**txt2img_dict) # face-restore + txt2img_paste_fields = [ # prompt (txt2img_prompt, "Prompt"), diff --git a/scripts/face_details.py b/scripts/face_details.py index d59f9adc2..0d803ccb3 100644 --- a/scripts/face_details.py +++ b/scripts/face_details.py @@ -164,6 +164,7 @@ class FaceRestorerYolo(FaceRestoration): shared.log.debug(f'Face HiRes: faces={report} args={faces[0].args} denoise={p.denoising_strength} blur={p.mask_blur} width={p.width} height={p.height} padding={p.inpaint_full_res_padding}') mask_all = [] + p.state = '' for face in faces: if face.mask is None: continue @@ -187,6 +188,7 @@ class FaceRestorerYolo(FaceRestoration): p = processing_class.switch_class(p, orig_cls, orig_p) p.init_images = getattr(orig_p, 'init_images', None) p.image_mask = getattr(orig_p, 'image_mask', None) + p.state = getattr(orig_p, 'state', None) shared.opts.data['mask_apply_overlay'] = orig_apply_overlay np_image = np.array(image) diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 787fbaa87..aa99c21cf 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -284,7 +284,7 @@ class Script(scripts.Script): pc.extra_generation_params["Y Values"] = y_values if y_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) - grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(x_values)}x{len(y_values)}') if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: # Sets main grid infotext pc.extra_generation_params = copy(pc.extra_generation_params) if z_opt.label != 'Nothing': @@ -292,7 +292,7 @@ class Script(scripts.Script): pc.extra_generation_params["Z Values"] = z_values if z_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) - grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(z_values)}x{len(x_values)}x{len(y_values)}') return res with SharedSettingsStackHelper(): diff --git a/scripts/xyz_grid_on.py b/scripts/xyz_grid_on.py index b1aa47c13..d20dff945 100644 --- a/scripts/xyz_grid_on.py +++ b/scripts/xyz_grid_on.py @@ -298,7 +298,7 @@ class Script(scripts.Script): pc.extra_generation_params["Y Values"] = y_values if y_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) - grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=f'{len(x_values)}x{len(y_values)}') if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: # Sets main grid infotext pc.extra_generation_params = copy(pc.extra_generation_params) if z_opt.label != 'Nothing': @@ -306,7 +306,8 @@ class Script(scripts.Script): pc.extra_generation_params["Z Values"] = z_values if z_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) - grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + grid_text = f'{len(z_values)}x{len(x_values)}x{len(y_values)}' if len(z_values) > 0 else f'{len(x_values)}x{len(y_values)}' + grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds, grid=grid_text) return res with SharedSettingsStackHelper():