diff --git a/.gitignore b/.gitignore index 13e8eab0c..06c8f11c5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,11 @@ venv *.zip *.rar *.pyc +/*.bat +/*.sh +/*.txt +!webui.bat +!webui.sh # all dynamic stuff /repositories/**/* diff --git a/TODO.md b/TODO.md index 2a6b118d3..167a823ec 100644 --- a/TODO.md +++ b/TODO.md @@ -4,11 +4,11 @@ Stuff to be fixed... -- ClipSkip not updated on read gen info - Run VAE with hires at 1280 - Transformers version - Move Restart Server from WebUI to Launch and reload modules -- follow-up on `p.script_args` +- Follow-up on `p.script_args` +- Mdularize `cli` scripts ## Features @@ -19,8 +19,6 @@ Stuff to be added... - Create new GitHub hooks/actions for CI/CD - Redo Extensions tab: see - Stream-load models as option for slow storage -- AMD optimizations -- Apple optimizations ## Investigate diff --git a/cli/requirements.txt b/cli/requirements.txt index 77e0c427a..b8f807b41 100644 --- a/cli/requirements.txt +++ b/cli/requirements.txt @@ -1,3 +1,4 @@ mediapipe colormap invisible-watermark +filetype diff --git a/html/extra-networks-card.html b/html/extra-networks-card.html index 258bdf0a1..3cf6e2836 100644 --- a/html/extra-networks-card.html +++ b/html/extra-networks-card.html @@ -4,7 +4,9 @@
@@ -12,4 +14,3 @@ {description}
- diff --git a/javascript/black-orange.css b/javascript/black-orange.css index 1bcf4c7fa..a055e2e51 100644 --- a/javascript/black-orange.css +++ b/javascript/black-orange.css @@ -81,7 +81,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } #quicksettings > div, #quicksettings > fieldset { min-width: 26em; max-width: 26em; line-height: 2em; } #refresh_sd_model_checkpoint { height: 40px; margin-left: -14px; background: #333333; box-shadow: none; } -#refresh_txt2img_styles, #refresh_img2img_styles, #open_folder_txt2img, #open_folder_img2img, #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #save_zip_txt2img, #save_zip_img2img, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_res_switch_btn, #img2img_res_switch_btn, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h, #txt2img_tiling { display: none; } +#refresh_txt2img_styles, #refresh_img2img_styles, #open_folder_txt2img, #open_folder_img2img, #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_res_switch_btn, #img2img_res_switch_btn, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h, #txt2img_tiling { display: none; } #save-animation { border-radius: 0 !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } #settings > div.flex-wrap { width: 15em; } @@ -103,6 +103,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } #txt2img_tools, #img2img_tools { margin-top: 54px; scale: 120%; margin-left: 26px; } #txtimg_hr_finalres { max-width: 200px; } #pnginfo_html2_info { margin-top: -18px; background-color: var(--input-background-fill); padding: var(--input-padding) } +#txt2img_extra_refresh, #txt2img_extra_close { height: 1.7em } /* custom elements overrides */ #steps-animation, #controlnet { border-width: 0; } @@ -317,4 +318,6 @@ svg.feather.feather-image, .feather .feather-image { display: none } --button-small-text-size: var(--text-md); --button-small-text-weight: 400; --button-transition: none; + --size-9: 64px; + --size-14: 64px; } diff --git a/javascript/dragdrop.js b/javascript/dragdrop.js index e4cd41ec4..9015e4bd5 100644 --- a/javascript/dragdrop.js +++ b/javascript/dragdrop.js @@ -5,17 +5,13 @@ function isValidImageList( files ) { } function dropReplaceImage( imgWrap, files ) { - if ( ! isValidImageList( files ) ) { - return; - } - + if (!isValidImageList(files)) return; const tmpFile = files[0]; - imgWrap.querySelector('.modify-upload button + button, .touch-none + div button + button')?.click(); const callback = () => { const fileInput = imgWrap.querySelector('input[type="file"]'); - if ( fileInput ) { - if ( files.length === 0 ) { + if (fileInput) { + if (files.length === 0) { files = new DataTransfer(); files.items.add(tmpFile); fileInput.files = files.files; @@ -26,7 +22,7 @@ function dropReplaceImage( imgWrap, files ) { } }; - if ( imgWrap.closest('#pnginfo_image') ) { + if (imgWrap.closest('#pnginfo_image')) { // special treatment for PNG Info tab, wait for fetch request to finish const oldFetch = window.fetch; window.fetch = async (input, options) => { @@ -44,16 +40,14 @@ function dropReplaceImage( imgWrap, files ) { return response; }; } else { - window.requestAnimationFrame( () => callback() ); + window.requestAnimationFrame(() => callback()); } } window.document.addEventListener('dragover', e => { const target = e.composedPath()[0]; const imgWrap = target.closest('[data-testid="image"]'); - if ( !imgWrap && target.placeholder && target.placeholder.indexOf("Prompt") == -1) { - return; - } + if ( !imgWrap && target.placeholder && target.placeholder.indexOf("Prompt") == -1) return; e.stopPropagation(); e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; @@ -62,37 +56,20 @@ window.document.addEventListener('dragover', e => { window.document.addEventListener('drop', e => { const target = e.composedPath()[0]; if (!target.placeholder) return; - if (target.placeholder.indexOf("Prompt") == -1) { - return; - } + if (target.placeholder.indexOf("Prompt") == -1) return; const imgWrap = target.closest('[data-testid="image"]'); - if ( !imgWrap ) { - return; - } + if (!imgWrap) return; e.stopPropagation(); e.preventDefault(); const files = e.dataTransfer.files; - dropReplaceImage( imgWrap, files ); + dropReplaceImage(imgWrap, files); }); window.addEventListener('paste', e => { const files = e.clipboardData.files; - if ( ! isValidImageList( files ) ) { - return; - } - - const visibleImageFields = [...gradioApp().querySelectorAll('[data-testid="image"]')] - .filter(el => uiElementIsVisible(el)); - if ( ! visibleImageFields.length ) { - return; - } - - const firstFreeImageField = visibleImageFields - .filter(el => el.querySelector('input[type=file]'))?.[0]; - - dropReplaceImage( - firstFreeImageField ? - firstFreeImageField : - visibleImageFields[visibleImageFields.length - 1] - , files ); + if ( ! isValidImageList( files ) ) return; + const visibleImageFields = [...gradioApp().querySelectorAll('[data-testid="image"]')].filter(el => uiElementIsVisible(el)); + if ( ! visibleImageFields.length ) return; + const firstFreeImageField = visibleImageFields.filter(el => el.querySelector('input[type=file]'))?.[0]; + dropReplaceImage(firstFreeImageField ? firstFreeImageField : visibleImageFields[visibleImageFields.length - 1], files); }); diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 2f0a7de9d..19b69ec8b 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -5,12 +5,14 @@ function setupExtraNetworksForTab(tabname){ var tabs = gradioApp().querySelector('#'+tabname+'_extra_tabs > div') var search = gradioApp().querySelector('#'+tabname+'_extra_search textarea') var refresh = gradioApp().getElementById(tabname+'_extra_refresh') + var descriptInput = gradioApp().getElementById(tabname+ '_description_input') var close = gradioApp().getElementById(tabname+'_extra_close') search.classList.add('search') tabs.appendChild(search) tabs.appendChild(refresh) tabs.appendChild(close) + tabs.appendChild(descriptInput) search.addEventListener("input", function(evt){ searchTerm = search.value.toLowerCase() @@ -97,6 +99,37 @@ function saveCardPreview(event, tabname, filename){ event.preventDefault() } +function saveCardDescription(event, tabname, filename, descript){ + var textarea = gradioApp().querySelector("#" + tabname + '_description_filename > label > textarea') + var button = gradioApp().getElementById(tabname + '_save_description') + var description = gradioApp().getElementById(tabname+ '_description_input') + + textarea.value = filename + description.value=descript + updateInput(textarea) + + button.click() + + event.stopPropagation() + event.preventDefault() +} + +function readCardDescription(event, tabname, filename, descript){ + var textarea = gradioApp().querySelector("#" + tabname + '_description_filename > label > textarea') + var description_textarea = gradioApp().querySelector("#" + tabname+ '_description_input > label > textarea') + var button = gradioApp().getElementById(tabname + '_read_description') + + textarea.value = filename + description_textarea.value = descript + + updateInput(textarea) + updateInput(description_textarea) + button.click() + + event.stopPropagation() + event.preventDefault() +} + function extraNetworksSearchButton(tabs_id, event){ searchTextarea = gradioApp().querySelector("#" + tabs_id + ' > div > textarea') button = event.target diff --git a/javascript/notification.js b/javascript/notification.js index 3b68ffe53..9f7c2e439 100644 --- a/javascript/notification.js +++ b/javascript/notification.js @@ -1,52 +1,32 @@ // Monitors the gallery and sends a browser notification when the leading image is new. let lastHeadImg = null; - let notificationButton = null; - const regExpTempImage = /(?<=\/|\\)tmp[\w\d]{8}\.png$/gm; onUiUpdate(function(){ if(notificationButton == null){ notificationButton = gradioApp().getElementById('request_notifications') - - if(notificationButton != null){ - notificationButton.addEventListener('click', function (evt) { - Notification.requestPermission(); - },true); - } + if (notificationButton != null) notificationButton.addEventListener('click', (evt) => Notification.requestPermission(), true); } - const galleryPreviews = gradioApp().querySelectorAll('div[id^="tab_"][style*="display: block"] div[id$="_results"] .thumbnail-item > img'); - if (galleryPreviews == null) return; - const headImg = galleryPreviews[0]?.src; - if (headImg == null || headImg == lastHeadImg) return; - if (headImg.search(regExpTempImage) != -1) return; - lastHeadImg = headImg; - // play notification sound if available gradioApp().querySelector('#audio_notification audio')?.play(); - if (document.hasFocus()) return; - // Multiple copies of the images are in the DOM when one is selected. Dedup with a Set to get the real number generated. const imgs = new Set(Array.from(galleryPreviews).map(img => img.src)); - const notification = new Notification( - 'Stable Diffusion', - { + 'Stable Diffusion', { body: `Generated ${imgs.size > 1 ? imgs.size - opts.return_grid : 1} image${imgs.size > 1 ? 's' : ''}`, icon: headImg, - image: headImg, - } + image: headImg } ); - - notification.onclick = function(_){ + notification.onclick = function(_) { parent.focus(); this.close(); }; diff --git a/javascript/ui.js b/javascript/ui.js index 774422b93..52a9341ce 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -338,6 +338,28 @@ function reconnect_ui() { const atEnd = () => showSubmitButtons('txt2img', true) requestProgress(task_id, el1, el2, atEnd, null, true) } + + sd_model = gradioApp().getElementById("setting_sd_model_checkpoint") + let loadingStarted = 0; + let loadingMonitor = 0; + const sd_model_callback = () => { + loading = sd_model.querySelector(".eta-bar") + if (!loading) { + loadingStarted = 0 + clearInterval(loadingMonitor) + } else { + if (loadingStarted === 0) { + loadingStarted = Date.now(); + loadingMonitor = setInterval(() => { + elapsed = Date.now() - loadingStarted; + console.log('Loading', elapsed) + if (elapsed > 3000 && loading) loading.style.display = 'none'; + }, 5000); + } + } + }; + const sd_model_observer = new MutationObserver(sd_model_callback); + sd_model_observer.observe(sd_model, { attributes: true, childList: true, subtree: true }); } -var start_check = setInterval(reconnect_ui, 50) +var start_check = setInterval(reconnect_ui, 50); diff --git a/launch.py b/launch.py index ec9fd514b..bf34ee507 100644 --- a/launch.py +++ b/launch.py @@ -14,6 +14,7 @@ from rich import print # pylint: disable=redefined-builtin,wrong-import-order commandline_args = os.environ.get('COMMANDLINE_ARGS', "") sys.argv += shlex.split(commandline_args) +setup.add_args() setup.extensions_preload(force=False) setup.parse_args() args, _ = modules.cmd_args.parser.parse_known_args() diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 87b47d6c7..692f6451f 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -78,7 +78,7 @@ def compatibility_args(opts, args): opts.use_old_emphasis_implementation = False opts.use_old_karras_scheduler_sigmas = False opts.no_dpmpp_sde_batch_determinism = False - opts.use_old_hires_fix_width_height = False + opts.lora_apply_to_outputs = False parser.add_argument("--lora-dir", help=argparse.SUPPRESS, default=opts.lora_dir) args = parser.parse_args() diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index bc58011f2..964432d72 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -11,7 +11,7 @@ from modules import shared, ui_tempdir, script_callbacks re_param_code = r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)' re_param = re.compile(re_param_code) re_imagesize = re.compile(r"^(\d+)x(\d+)$") -re_hypernet_hash = re.compile("\(([0-9a-f]+)\)$") +re_hypernet_hash = re.compile("\(([0-9a-f]+)\)$") # pylint: disable=anomalous-backslash-in-string type_of_gr_update = type(gr.update()) paste_fields = {} @@ -102,7 +102,6 @@ def bind_buttons(buttons, send_image, send_generate_info): for tabname, button in buttons.items(): source_text_component = send_generate_info if isinstance(send_generate_info, gr.components.Component) else None source_tabname = send_generate_info if isinstance(send_generate_info, str) else None - register_paste_params_button(ParamBinding(paste_button=button, tabname=tabname, source_text_component=source_text_component, source_image_component=send_image, source_tabname=source_tabname)) @@ -116,7 +115,6 @@ def connect_paste_params_buttons(): destination_image_component = paste_fields[binding.tabname]["init_img"] fields = paste_fields[binding.tabname]["fields"] override_settings_component = binding.override_settings_component or paste_fields[binding.tabname]["override_settings_component"] - destination_width_component = next(iter([field for field, name in fields if name == "Size-1"] if fields else []), None) destination_height_component = next(iter([field for field, name in fields if name == "Size-2"] if fields else []), None) @@ -127,17 +125,14 @@ def connect_paste_params_buttons(): else: func = send_image_and_dimensions if destination_width_component else lambda x: x jsfunc = None - binding.paste_button.click( fn=func, _js=jsfunc, inputs=[binding.source_image_component], outputs=[destination_image_component, destination_width_component, destination_height_component] if destination_width_component else [destination_image_component], ) - if binding.source_text_component is not None and fields is not None: connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings_component, binding.tabname) - if binding.source_tabname is not None and fields is not None: paste_field_names = ['Prompt', 'Negative prompt', 'Steps', 'Face restoration'] + (["Seed"] if shared.opts.send_seed else []) + binding.paste_field_names binding.paste_button.click( @@ -145,7 +140,6 @@ def connect_paste_params_buttons(): inputs=[field for field, name in paste_fields[binding.source_tabname]["fields"] if name in paste_field_names], outputs=[field for field, name in fields if name in paste_field_names], ) - binding.paste_button.click( fn=None, _js=f"switch_to_{binding.tabname}", @@ -159,14 +153,12 @@ def send_image_and_dimensions(x): img = x else: img = image_from_url_text(x) - if shared.opts.send_size and isinstance(img, Image.Image): w = img.width h = img.height else: w = gr.update() h = gr.update() - return img, w, h @@ -238,33 +230,25 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model returns a dict with field values """ - res = {} - prompt = "" negative_prompt = "" - done_with_prompt = False - *lines, lastline = x.strip().split("\n") if len(re_param.findall(lastline)) < 3: lines.append(lastline) lastline = '' - for _i, line in enumerate(lines): line = line.strip() if line.startswith("Negative prompt:"): done_with_prompt = True line = line[16:].strip() - if done_with_prompt: negative_prompt += ("" if negative_prompt == "" else "\n") + line else: prompt += ("" if prompt == "" else "\n") + line - res["Prompt"] = prompt res["Negative prompt"] = negative_prompt - for k, v in re_param.findall(lastline): v = v[1:-1] if v[0] == '"' and v[-1] == '"' else v m = re_imagesize.match(v) @@ -273,31 +257,24 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model res[k+"-2"] = m.group(2) else: res[k] = v - # Missing CLIP skip means it was set to 1 (the default) if "Clip skip" not in res: res["Clip skip"] = "1" - hypernet = res.get("Hypernet", None) if hypernet is not None: res["Prompt"] += f"""""" - if "Hires resize-1" not in res: res["Hires resize-1"] = 0 res["Hires resize-2"] = 0 - # Infer additional override settings for token merging token_merging_ratio = res.get("Token merging ratio", None) token_merging_ratio_hr = res.get("Token merging ratio hr", None) - if token_merging_ratio is not None or token_merging_ratio_hr is not None: res["Token merging"] = 'True' - if token_merging_ratio is None: res["Token merging hr only"] = 'True' else: res["Token merging hr only"] = 'False' - if res.get("Token merging random", None) is None: res["Token merging random"] = 'False' if res.get("Token merging merge attention", None) is None: @@ -312,14 +289,12 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model res["Token merging stride y"] = '2' restore_old_hires_fix_params(res) - return res settings_map = {} - infotext_to_setting_name_mapping = [ ('Clip skip', 'CLIP_stop_at_last_layers', ), ('Conditional mask weight', 'inpainting_mask_weight'), @@ -349,34 +324,29 @@ infotext_to_setting_name_mapping = [ def create_override_settings_dict(text_pairs): """creates processing's override_settings parameters from gradio's multiselect - Example input: ['Clip skip: 2', 'Model hash: e6e99610c4', 'ENSD: 31337'] Example output: {'CLIP_stop_at_last_layers': 2, 'sd_model_checkpoint': 'e6e99610c4', 'eta_noise_seed_delta': 31337} """ - res = {} params = {} for pair in text_pairs: k, v = pair.split(":", maxsplit=1) - params[k] = v.strip() - for param_name, setting_name in infotext_to_setting_name_mapping: value = params.get(param_name, None) - if value is None: continue - res[setting_name] = shared.opts.cast_value(setting_name, value) - return res -def connect_paste(button, paste_fields, input_comp, override_settings_component, tabname): +def connect_paste(button, paste_fields, input_comp, override_settings_component, tabname): # pylint: disable=redefined-outer-name def paste_func(prompt): + if 'Negative prompt' not in prompt and 'Steps' not in prompt: + prompt = None if not prompt and not shared.cmd_opts.hide_ui_dir_config: filename = os.path.join(data_path, "params.txt") if os.path.exists(filename): @@ -384,17 +354,14 @@ def connect_paste(button, paste_fields, input_comp, override_settings_component, prompt = file.read() else: prompt = '' - params = parse_generation_parameters(prompt) script_callbacks.infotext_pasted_callback(prompt, params) res = [] - for output, key in paste_fields: if callable(key): v = key(params) else: v = params.get(key, None) - if v is None: res.append(gr.update()) elif isinstance(v, type_of_gr_update): @@ -402,42 +369,31 @@ def connect_paste(button, paste_fields, input_comp, override_settings_component, else: try: valtype = type(output.value) - if valtype == bool and v == "False": val = False else: val = valtype(v) - res.append(gr.update(value=val)) except Exception: res.append(gr.update()) - return res if override_settings_component is not None: def paste_settings(params): vals = {} - for param_name, setting_name in infotext_to_setting_name_mapping: v = params.get(param_name, None) if v is None: continue - if setting_name == "sd_model_checkpoint" and shared.opts.disable_weights_auto_swap: continue - v = shared.opts.cast_value(setting_name, v) current_value = getattr(shared.opts, setting_name, None) - if v == current_value: continue - vals[param_name] = v - vals_pairs = [f"{k}: {v}" for k, v in vals.items()] - return gr.Dropdown.update(value=vals_pairs, choices=vals_pairs, visible=len(vals_pairs) > 0) - paste_fields = paste_fields + [(override_settings_component, paste_settings)] button.click( diff --git a/modules/models/diffusion/uni_pc/sampler.py b/modules/models/diffusion/uni_pc/sampler.py index a241c8a7c..3100522ab 100644 --- a/modules/models/diffusion/uni_pc/sampler.py +++ b/modules/models/diffusion/uni_pc/sampler.py @@ -1,9 +1,11 @@ """SAMPLING ONLY.""" +import numpy as np import torch from .uni_pc import NoiseScheduleVP, model_wrapper, UniPC from modules import shared, devices +from ldm.modules.diffusionmodules.util import extract_into_tensor class UniPCSampler(object): @@ -15,6 +17,103 @@ class UniPCSampler(object): self.after_sample = None self.register_buffer('alphas_cumprod', to_torch(model.alphas_cumprod)) + def make_schedule(self, ddim_num_steps, ddim_discretize="uniform", ddim_eta=0., verbose=True): + # persist steps so we can eventually find denoising strength + self.inflated_steps = ddim_num_steps + + @torch.no_grad() + def stochastic_encode(self, x0, t, use_original_steps=False, noise=None): + if noise is None: + noise = torch.randn_like(x0) + + # first time we have all the info to get the real parameters from the ui + # value from the hires steps slider: + num_inference_steps = t[0] + 1 + # (num_inference_steps // denoising_strength): + inflated_steps = self.inflated_steps + # not exact: + self.denoising_strength = num_inference_steps/inflated_steps + + # values used for timesteps that generate noise in diffusers repo + init_timestep = min( + int(num_inference_steps * self.denoising_strength), + num_inference_steps, + ) + t_start = max(num_inference_steps - init_timestep, 0) + + # actual number of steps we'll run + self.steps = max( + num_inference_steps - init_timestep, + shared.opts.uni_pc_order+1, + ) + + scheduler_timesteps = np.linspace( + 0, + self.model.num_timesteps-1, + num_inference_steps + 1, + ).round()[::-1][:-1].copy().astype(np.int64) + _, unique_indices = np.unique(scheduler_timesteps, return_index=True) + scheduler_timesteps = scheduler_timesteps[np.sort(unique_indices)] + scheduler_timesteps = torch.from_numpy(scheduler_timesteps).to(t.device) + + sample_timesteps = scheduler_timesteps[t_start:] + latent_timestep = sample_timesteps[:1].repeat(x0.shape[0]) + + alphas_cumprod = self.alphas_cumprod + sqrt_alpha_prod = alphas_cumprod[latent_timestep] ** 0.5 + sqrt_alpha_prod = sqrt_alpha_prod.flatten() + while len(sqrt_alpha_prod.shape) < len(x0.shape): + sqrt_alpha_prod = sqrt_alpha_prod.unsqueeze(-1) + + sqrt_one_minus_alpha_prod = (1 - alphas_cumprod[latent_timestep]) ** 0.5 + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.flatten() + while len(sqrt_one_minus_alpha_prod.shape) < len(x0.shape): + sqrt_one_minus_alpha_prod = sqrt_one_minus_alpha_prod.unsqueeze(-1) + + return (sqrt_alpha_prod * x0 + sqrt_one_minus_alpha_prod * noise) + + def decode(self, x_latent, conditioning, t_start, unconditional_guidance_scale=1.0, unconditional_conditioning=None, + use_original_steps=False, callback=None): + #print(f'steps {self.steps} denoising {self.denoising_strength}') + + noise_schedule = NoiseScheduleVP("discrete", alphas_cumprod=self.alphas_cumprod) + + # same as in .sample(), i guess + model_type = "v" if self.model.parameterization == "v" else "noise" + + model_fn = model_wrapper( + lambda x, t, c: self.model.apply_model(x, t, c), + noise_schedule, + model_type=model_type, + guidance_type="classifier-free", + #condition=conditioning, + #unconditional_condition=unconditional_conditioning, + guidance_scale=unconditional_guidance_scale, + ) + + self.uni_pc = UniPC( + model_fn, + noise_schedule, + predict_x0=True, + thresholding=False, + variant=shared.opts.uni_pc_variant, + condition=conditioning, + unconditional_condition=unconditional_conditioning, + before_sample=self.before_sample, + after_sample=self.after_sample, + after_update=self.after_update, + ) + + return self.uni_pc.sample( + x_latent, + steps=self.steps, + skip_type=shared.opts.uni_pc_skip_type, + method="multistep", + order=shared.opts.uni_pc_order, + lower_order_final=shared.opts.uni_pc_lower_order_final, + t_start=self.denoising_strength, + ) + def register_buffer(self, name, attr): if type(attr) == torch.Tensor: if attr.device != devices.device: diff --git a/modules/paths_internal.py b/modules/paths_internal.py index b5d81df51..2601e4709 100644 --- a/modules/paths_internal.py +++ b/modules/paths_internal.py @@ -15,6 +15,6 @@ parser_pre.add_argument("--data-dir", type=str, default=os.path.dirname(os.path. parser_pre.add_argument("--models-dir", type=str, default="models", help="base path where all models are stored",) cmd_opts_pre = parser_pre.parse_known_args()[0] data_path = cmd_opts_pre.data_dir -models_path = os.path.join(data_path, cmd_opts_pre.models_dir) +models_path = cmd_opts_pre.models_dir if os.path.isabs(cmd_opts_pre.models_dir) else os.path.join(data_path, cmd_opts_pre.models_dir) extensions_dir = os.path.join(data_path, "extensions") extensions_builtin_dir = os.path.join(script_path, "extensions-builtin") diff --git a/modules/processing.py b/modules/processing.py index 0ce6d4c91..1c6c36d32 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -111,7 +111,7 @@ 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, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: 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, ddim_discretize: str = None, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.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 + 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, batch_size: int = 1, n_iter: int = 1, steps: int = 20, cfg_scale: float = 6.0, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: 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, ddim_discretize: str = None, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.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 if sampler_index is not None: print("sampler_index argument for StableDiffusionProcessing does not do anything; use sampler_name", file=sys.stderr) @@ -165,6 +165,7 @@ class StableDiffusionProcessing: self.all_negative_prompts = None self.all_seeds = None self.all_subseeds = None + self.clip_skip = opts.CLIP_stop_at_last_layers self.iteration = 0 @property @@ -302,8 +303,7 @@ class Processed: self.index_of_first_image = index_of_first_image self.styles = p.styles self.job_timestamp = state.job_timestamp - self.clip_skip = opts.CLIP_stop_at_last_layers - + self.clip_skip = p.clip_skip self.eta = p.eta self.ddim_discretize = p.ddim_discretize self.s_churn = p.s_churn @@ -457,11 +457,9 @@ def fix_seed(p): p.subseed = get_fixed_seed(p.subseed) -def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iteration=0, position_in_batch=0): # pylint: disable=unused-argument +def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_subseeds, comments=None, iteration=0, position_in_batch=0): # pylint: disable=unused-argument index = position_in_batch + iteration * p.batch_size - clip_skip = getattr(p, 'clip_skip', opts.CLIP_stop_at_last_layers) - generation_params = { "Steps": p.steps, "Sampler": p.sampler_name, @@ -478,7 +476,7 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter "Seed resize from": (None if p.seed_resize_from_w == 0 or p.seed_resize_from_h == 0 else f"{p.seed_resize_from_w}x{p.seed_resize_from_h}"), "Denoising strength": getattr(p, 'denoising_strength', None), "Conditional mask weight": getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None, - "Clip skip": None if clip_skip <= 1 else clip_skip, + "Clip skip": p.clip_skip, "ENSD": None if opts.eta_noise_seed_delta == 0 else opts.eta_noise_seed_delta, "Token merging ratio": None if not (opts.token_merging or cmd_opts.token_merging) or opts.token_merging_hr_only else opts.token_merging_ratio, "Token merging ratio hr": None if not (opts.token_merging or cmd_opts.token_merging) else opts.token_merging_ratio_hr, @@ -972,8 +970,9 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): shared.state.nextjob() img2img_sampler_name = self.sampler_name - if self.sampler_name in ['PLMS', 'UniPC']: # PLMS/UniPC do not support img2img so we just silently switch to DDIM - img2img_sampler_name = 'DDIM' + force_latent_upscaler = shared.opts.data.get('xyz_fallback_sampler') + if self.sampler_name in ['PLMS'] or (force_latent_upscaler is not None and force_latent_upscaler != 'None'): + img2img_sampler_name = force_latent_upscaler or shared.opts.fallback_sampler # PLMS does not support img2img, use fallback instead self.sampler = sd_samplers.create_sampler(img2img_sampler_name, self.sd_model) samples = samples[:, :, self.truncate_y//2:samples.shape[2]-(self.truncate_y+1)//2, self.truncate_x//2:samples.shape[3]-(self.truncate_x+1)//2] @@ -1026,27 +1025,24 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): self.image_conditioning = None def init(self, all_prompts, all_seeds, all_subseeds): + force_latent_upscaler = shared.opts.data.get('xyz_fallback_sampler') + if self.sampler_name in ['PLMS'] or (force_latent_upscaler is not None and force_latent_upscaler != 'None'): + self.sampler_name = force_latent_upscaler or shared.opts.fallback_sampler # PLMS does not support img2img, use fallback instead self.sampler = sd_samplers.create_sampler(self.sampler_name, self.sd_model) crop_region = None - image_mask = self.image_mask - if image_mask is not None: image_mask = image_mask.convert('L') - if self.inpainting_mask_invert: image_mask = ImageOps.invert(image_mask) - if self.mask_blur > 0: image_mask = image_mask.filter(ImageFilter.GaussianBlur(self.mask_blur)) - if self.inpaint_full_res: self.mask_for_overlay = image_mask mask = image_mask.convert('L') crop_region = masking.get_crop_region(np.array(mask), self.inpaint_full_res_padding) crop_region = masking.expand_crop_region(crop_region, self.width, self.height, mask.width, mask.height) x1, y1, x2, y2 = crop_region - mask = mask.crop(crop_region) image_mask = images.resize_image(2, mask, self.width, self.height) self.paste_to = (x1, y1, x2-x1, y2-y1) @@ -1055,42 +1051,31 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): np_mask = np.array(image_mask) np_mask = np.clip((np_mask.astype(np.float32)) * 2, 0, 255).astype(np.uint8) self.mask_for_overlay = Image.fromarray(np_mask) - self.overlay_images = [] - latent_mask = self.latent_mask if self.latent_mask is not None else image_mask - add_color_corrections = opts.img2img_color_correction and self.color_corrections is None if add_color_corrections: self.color_corrections = [] imgs = [] for img in self.init_images: image = images.flatten(img, opts.img2img_background_color) - if crop_region is None and self.resize_mode != 3: image = images.resize_image(self.resize_mode, image, self.width, self.height) - if image_mask is not None: image_masked = Image.new('RGBa', (image.width, image.height)) image_masked.paste(image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(self.mask_for_overlay.convert('L'))) - self.overlay_images.append(image_masked.convert('RGBA')) - # crop_region is not None if we are doing inpaint full res if crop_region is not None: image = image.crop(crop_region) image = images.resize_image(2, image, self.width, self.height) - if image_mask is not None: if self.inpainting_fill != 1: image = masking.fill(image, latent_mask) - if add_color_corrections: self.color_corrections.append(setup_color_correction(image)) - image = np.array(image).astype(np.float32) / 255.0 image = np.moveaxis(image, 2, 0) - imgs.append(image) if len(imgs) == 1: diff --git a/modules/realesrgan_model.py b/modules/realesrgan_model.py index 398f2526b..c9b6cddf0 100644 --- a/modules/realesrgan_model.py +++ b/modules/realesrgan_model.py @@ -13,7 +13,7 @@ import modules.errors as errors class UpscalerRealESRGAN(Upscaler): def __init__(self, path): self.name = "RealESRGAN" - self.user_path = path + self.model_path = path super().__init__() try: from basicsr.archs.rrdbnet_arch import RRDBNet @@ -31,7 +31,7 @@ class UpscalerRealESRGAN(Upscaler): self.enable = False self.scalers = [] - def do_upscale(self, img, path): + def do_upscale(self, img, selected_model): if not self.enable: return img @@ -41,9 +41,9 @@ class UpscalerRealESRGAN(Upscaler): print("Error importing Real-ESRGAN:", file=sys.stderr) return img - info = self.load_model(path) + info = self.load_model(selected_model) if not os.path.exists(info.local_data_path): - print("Unable to load RealESRGAN model: %s" % info.name) + print(f"Unable to load RealESRGAN model: {info.name}") return img upsampler = RealESRGANer( @@ -68,7 +68,6 @@ class UpscalerRealESRGAN(Upscaler): if info is None: print(f"Unable to find model info: {path}") return None - info.local_data_path = load_file_from_url(url=info.data_path, model_dir=self.model_path, progress=True) return info except Exception as e: @@ -128,6 +127,6 @@ def get_realesrgan_models(scaler): ), ] return models - except Exception as e: + except Exception: print("Error creating Real-ESRGAN models list", file=sys.stderr) return [] diff --git a/modules/scripts.py b/modules/scripts.py index 2cc744568..55418dc5b 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -2,7 +2,6 @@ import os import re import sys from collections import namedtuple -from rich import print # pylint: disable=redefined-builtin import gradio as gr from modules import shared, paths, script_callbacks, extensions, script_loading, scripts_postprocessing, errors @@ -19,7 +18,6 @@ class Script: args_from = None args_to = None alwayson = False - is_txt2img = False is_img2img = False @@ -38,7 +36,6 @@ class Script: def title(self): """this function should return the title of the script. This is what will be displayed in the dropdown menu.""" - raise NotImplementedError() def ui(self, is_img2img): @@ -46,19 +43,16 @@ class Script: The return value should be an array of all components that are used in processing. Values of those returned components will be passed to run() and process() functions. """ + pass # pylint: disable=unnecessary-pass - pass - - def show(self, is_img2img): + def show(self, is_img2img): # pylint: disable=unused-argument """ is_img2img is True if this function is called for the img2img interface, and Fasle otherwise - This function should return: - False if the script should not be shown in UI at all - True if the script should be shown in UI if it's selected in the scripts dropdown - script.AlwaysVisible if the script should be shown in UI at all times """ - return True def run(self, p, *args): @@ -66,13 +60,10 @@ class Script: This function is called if the script has been selected in the script dropdown. It must do all processing and return the Processed object with results, same as one returned by processing.process_images. - Usually the processing is done by calling the processing.process_images function. - args contains all values returned by components from ui() """ - - pass + pass # pylint: disable=unnecessary-pass def process(self, p, *args): """ @@ -80,61 +71,52 @@ class Script: You can modify the processing object (p) here, inject hooks, etc. args contains all values returned by components from ui() """ - - pass + pass # pylint: disable=unnecessary-pass def before_process_batch(self, p, *args, **kwargs): """ Called before extra networks are parsed from the prompt, so you can add new extra network keywords to the prompt with this callback. - **kwargs will have those items: - batch_number - index of current batch, from 0 to number of batches-1 - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things - seeds - list of seeds for current batch - subseeds - list of subseeds for current batch """ - - pass + pass # pylint: disable=unnecessary-pass def process_batch(self, p, *args, **kwargs): """ Same as process(), but called for every batch. - **kwargs will have those items: - batch_number - index of current batch, from 0 to number of batches-1 - prompts - list of prompts for current batch; you can change contents of this list but changing the number of entries will likely break things - seeds - list of seeds for current batch - subseeds - list of subseeds for current batch """ - - pass + pass # pylint: disable=unnecessary-pass def postprocess_batch(self, p, *args, **kwargs): """ Same as process_batch(), but called for every batch after it has been generated. - **kwargs will have same items as process_batch, and also: - batch_number - index of current batch, from 0 to number of batches-1 - images - torch tensor with all generated images, with values ranging from 0 to 1; """ - - pass + pass # pylint: disable=unnecessary-pass def postprocess_image(self, p, pp: PostprocessImageArgs, *args): """ Called for every image after it has been generated. """ - - pass + pass # pylint: disable=unnecessary-pass def postprocess(self, p, processed, *args): """ This function is called after processing ends for AlwaysVisible scripts. args contains all values returned by components from ui() """ - - pass + pass # pylint: disable=unnecessary-pass def before_component(self, component, **kwargs): """ @@ -143,15 +125,13 @@ class Script: This can be useful to inject your own components somewhere in the middle of vanilla UI. You can return created components in the ui() function to add them to the list of arguments for your processing functions """ - - pass + pass # pylint: disable=unnecessary-pass def after_component(self, component, **kwargs): """ Called after a component is created. Same as above. """ - - pass + pass # pylint: disable=unnecessary-pass def describe(self): """unused""" @@ -159,11 +139,9 @@ class Script: def elem_id(self, item_id): """helper function to generate id for a HTML element, constructs final id out of script name, tab and user-supplied item_id""" - need_tabname = self.show(True) == self.show(False) tabname = ('img2img' if self.is_img2img else 'txt2txt') + "_" if need_tabname else "" title = re.sub(r'[^a-z_0-9]', '', re.sub(r'\s', '_', self.title().lower())) - return f'script_{tabname}{title}_{item_id}' @@ -179,7 +157,6 @@ def basedir(): ScriptFile = namedtuple("ScriptFile", ["basedir", "filename", "path", "priority"]) - scripts_data = [] postprocessing_scripts_data = [] ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedir", "module"]) @@ -187,16 +164,13 @@ ScriptClassData = namedtuple("ScriptClassData", ["script_class", "path", "basedi def list_scripts(scriptdirname, extension): tmp_list = [] - base = os.path.join(paths.script_path, scriptdirname) if os.path.exists(base): for filename in sorted(os.listdir(base)): tmp_list.append(ScriptFile(paths.script_path, filename, os.path.join(base, filename), '50')) - for ext in extensions.active(): tmp_list += ext.list_files(scriptdirname, extension) - - scripts_list = [] + priority_list = [] for script in tmp_list: if os.path.splitext(script.path)[1].lower() == extension and os.path.isfile(script.path): if script.basedir == paths.script_path: @@ -214,25 +188,20 @@ def list_scripts(scriptdirname, extension): priority = priority + str(f.read().strip()) else: priority = priority + script.priority - scripts_list.append(ScriptFile(script.basedir, script.filename, script.path, priority)) - - priority_sort = sorted(scripts_list, key=lambda item: item.priority + item.path.lower(), reverse=False) + priority_list.append(ScriptFile(script.basedir, script.filename, script.path, priority)) + priority_sort = sorted(priority_list, key=lambda item: item.priority + item.path.lower(), reverse=False) return priority_sort def list_files_with_name(filename): res = [] - dirs = [paths.script_path] + [ext.path for ext in extensions.active()] - for dirpath in dirs: if not os.path.isdir(dirpath): continue - path = os.path.join(dirpath, filename) if os.path.isfile(path): res.append(path) - return res @@ -241,16 +210,13 @@ def load_scripts(): scripts_data.clear() postprocessing_scripts_data.clear() script_callbacks.clear_callbacks() - scripts_list = list_scripts("scripts", ".py") - syspath = sys.path def register_scripts_from_module(module): for _key, script_class in module.__dict__.items(): if type(script_class) != type: continue - if issubclass(script_class, Script): scripts_data.append(ScriptClassData(script_class, scriptfile.path, scriptfile.basedir, module)) elif issubclass(script_class, scripts_postprocessing.ScriptPostprocessing): @@ -276,7 +242,6 @@ def wrap_call(func, filename, funcname, *args, default=None, **kwargs): return res except Exception as e: errors.display(e, f'Calling script: {filename}/{funcname}') - return default @@ -288,6 +253,7 @@ class ScriptRunner: self.titles = [] self.infotext_fields = [] self.paste_field_names = [] + self.script_load_ctr = 0 def initialize_scripts(self, is_img2img): from modules import scripts_auto_postprocessing @@ -295,50 +261,39 @@ class ScriptRunner: self.scripts.clear() self.alwayson_scripts.clear() self.selectable_scripts.clear() - auto_processing_scripts = scripts_auto_postprocessing.create_auto_preprocessing_script_data() - for script_class, path, basedir, script_module in auto_processing_scripts + scripts_data: + for script_class, path, _basedir, _script_module in auto_processing_scripts + scripts_data: script = script_class() script.filename = path script.is_txt2img = not is_img2img script.is_img2img = is_img2img - visibility = script.show(script.is_img2img) - if visibility == AlwaysVisible: self.scripts.append(script) self.alwayson_scripts.append(script) script.alwayson = True - elif visibility: self.scripts.append(script) self.selectable_scripts.append(script) def setup_ui(self): self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts] - inputs = [None] inputs_alwayson = [True] def create_script_ui(script, inputs, inputs_alwayson): script.args_from = len(inputs) script.args_to = len(inputs) - controls = wrap_call(script.ui, script.filename, "ui", script.is_img2img) - if controls is None: return - for control in controls: control.custom_script_source = os.path.basename(script.filename) - if script.infotext_fields is not None: self.infotext_fields += script.infotext_fields - if script.paste_field_names is not None: self.paste_field_names += script.paste_field_names - inputs += controls inputs_alwayson += [script.alwayson for _ in controls] script.args_to = len(inputs) @@ -348,39 +303,27 @@ class ScriptRunner: create_script_ui(script, inputs, inputs_alwayson) script.group = group - dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index") inputs[0] = dropdown - for script in self.selectable_scripts: with gr.Group(visible=False) as group: create_script_ui(script, inputs, inputs_alwayson) - script.group = group def select_script(script_index): selected_script = self.selectable_scripts[script_index - 1] if script_index>0 else None - return [gr.update(visible=selected_script == s) for s in self.selectable_scripts] def init_field(title): """called when an initial value is set from ui-config.json to show script's UI components""" - if title == 'None': return - script_index = self.titles.index(title) self.selectable_scripts[script_index].group.visible = True dropdown.init_field = init_field + dropdown.change(fn=select_script, inputs=[dropdown], outputs=[script.group for script in self.selectable_scripts]) - dropdown.change( - fn=select_script, - inputs=[dropdown], - outputs=[script.group for script in self.selectable_scripts] - ) - - self.script_load_ctr = 0 def onload_script_visibility(params): title = params.get('Script', None) if title: @@ -393,34 +336,25 @@ class ScriptRunner: self.infotext_fields.append( (dropdown, lambda x: gr.update(value=x.get('Script', 'None'))) ) self.infotext_fields.extend( [(script.group, onload_script_visibility) for script in self.selectable_scripts] ) - return inputs def run(self, p, *args): script_index = args[0] - if script_index == 0: return None - script = self.selectable_scripts[script_index-1] - if script is None: return None - script_args = args[script.args_from:script.args_to] processed = script.run(p, *script_args) - shared.total_tqdm.clear() - return processed - def process(self, p): + def process(self, p, **kwargs): for script in self.alwayson_scripts: try: - if p.script_args[0] == 'enabled': - return script_args = p.script_args[script.args_from:script.args_to] - script.process(p, *script_args) + script.process(p, *script_args, **kwargs) except Exception as e: errors.display(e, f'Running script process: {script.filename}') @@ -435,8 +369,6 @@ class ScriptRunner: def process_batch(self, p, **kwargs): for script in self.alwayson_scripts: try: - if p.script_args[0] == 'enabled': - return script_args = p.script_args[script.args_from:script.args_to] script.process_batch(p, *script_args, **kwargs) except Exception as e: @@ -445,8 +377,6 @@ class ScriptRunner: def postprocess(self, p, processed): for script in self.alwayson_scripts: try: - if p.script_args[0] == 'enabled': - return script_args = p.script_args[script.args_from:script.args_to] script.postprocess(p, processed, *script_args) except Exception as e: @@ -455,8 +385,6 @@ class ScriptRunner: def postprocess_batch(self, p, images, **kwargs): for script in self.alwayson_scripts: try: - if p.script_args[0] == 'enabled': - return script_args = p.script_args[script.args_from:script.args_to] script.postprocess_batch(p, *script_args, images=images, **kwargs) except Exception as e: @@ -489,13 +417,11 @@ class ScriptRunner: args_from = script.args_from args_to = script.args_to filename = script.filename - module = cache.get(filename, None) if module is None: module = script_loading.load_module(script.filename) cache[filename] = module - - for key, script_class in module.__dict__.items(): + for _key, script_class in module.__dict__.items(): if type(script_class) == type and issubclass(script_class, Script): self.scripts[si] = script_class() self.scripts[si].filename = filename @@ -516,10 +442,8 @@ def reload_script_body_only(): def reload_scripts(): - global scripts_txt2img, scripts_img2img, scripts_postproc - + global scripts_txt2img, scripts_img2img, scripts_postproc # pylint: disable=global-statement load_scripts() - scripts_txt2img = ScriptRunner() scripts_img2img = ScriptRunner() scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner() @@ -535,27 +459,19 @@ def add_classes_to_gradio_component(comp): if elem_classes is None: elem_classes = [] comp.elem_classes = ["gradio-" + comp.get_block_name(), *(elem_classes)] - if getattr(comp, 'multiselect', False): comp.elem_classes.append('multiselect') - def IOComponent_init(self, *args, **kwargs): if scripts_current is not None: scripts_current.before_component(self, **kwargs) - script_callbacks.before_component_callback(self, **kwargs) - - res = original_IOComponent_init(self, *args, **kwargs) - + res = original_IOComponent_init(self, *args, **kwargs) # pylint: disable=assignment-from-no-return add_classes_to_gradio_component(self) - script_callbacks.after_component_callback(self, **kwargs) - if scripts_current is not None: scripts_current.after_component(self, **kwargs) - return res @@ -564,10 +480,8 @@ gr.components.IOComponent.__init__ = IOComponent_init def BlockContext_init(self, *args, **kwargs): - res = original_BlockContext_init(self, *args, **kwargs) - + res = original_BlockContext_init(self, *args, **kwargs) # pylint: disable=assignment-from-no-return add_classes_to_gradio_component(self) - return res diff --git a/modules/scripts_auto_postprocessing.py b/modules/scripts_auto_postprocessing.py index 30d6d6586..e37c977fc 100644 --- a/modules/scripts_auto_postprocessing.py +++ b/modules/scripts_auto_postprocessing.py @@ -27,15 +27,11 @@ class ScriptPostprocessingForMainUI(scripts.Script): def create_auto_preprocessing_script_data(): - from modules import scripts - res = [] - for name in shared.opts.postprocessing_enable_in_main_ui: script = next(iter([x for x in scripts.postprocessing_scripts_data if x.script_class.name == name]), None) if script is None: continue - constructor = lambda s=script: ScriptPostprocessingForMainUI(s.script_class()) res.append(scripts.ScriptClassData(script_class=constructor, path=script.path, basedir=script.basedir, module=script.module)) diff --git a/modules/sd_hijack_clip.py b/modules/sd_hijack_clip.py index c994a0b67..945f7732d 100644 --- a/modules/sd_hijack_clip.py +++ b/modules/sd_hijack_clip.py @@ -205,7 +205,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): is when you do prompt editing: "a picture of a [cat:dog:0.4] eating ice cream" """ - batch_chunks, token_count = self.process_texts(texts) + batch_chunks, _token_count = self.process_texts(texts) used_embeddings = {} chunk_count = max([len(x) for x in batch_chunks]) @@ -219,7 +219,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): self.hijack.fixes = [x.fixes for x in batch_chunk] for fixes in self.hijack.fixes: - for position, embedding in fixes: + for _position, embedding in fixes: used_embeddings[embedding.name] = embedding z = self.process_tokens(tokens, multipliers) @@ -295,6 +295,8 @@ class FrozenCLIPEmbedderWithCustomWords(FrozenCLIPEmbedderWithCustomWordsBase): return tokenized def encode_with_transformers(self, tokens): + if opts.CLIP_stop_at_last_layers is None: + opts.CLIP_stop_at_last_layers = 1 outputs = self.wrapped.transformer(input_ids=tokens, output_hidden_states=-opts.CLIP_stop_at_last_layers) if opts.CLIP_stop_at_last_layers > 1: diff --git a/modules/sd_vae.py b/modules/sd_vae.py index 237dafa4f..e5c544487 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -100,7 +100,7 @@ def resolve_vae(checkpoint_file): vae_near_checkpoint = find_vae_near_checkpoint(checkpoint_file) if vae_near_checkpoint is not None and (shared.opts.sd_vae_as_default): return vae_near_checkpoint, 'near checkpoint' - + if is_automatic: for named_vae_location in [os.path.join(vae_path, os.path.splitext(os.path.basename(checkpoint_file))[0] + ".vae.pt"), os.path.join(vae_path, os.path.splitext(os.path.basename(checkpoint_file))[0] + ".vae.ckpt"), os.path.join(vae_path, os.path.splitext(os.path.basename(checkpoint_file))[0] + ".vae.safetensors")]: if os.path.isfile(named_vae_location): diff --git a/modules/shared.py b/modules/shared.py index 08ff22cfa..d8a3f9d64 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -1,12 +1,10 @@ -import datetime -import json import os import sys import time - +import json +import datetime import gradio as gr import tqdm - import modules.interrogate import modules.memmon import modules.styles @@ -20,7 +18,7 @@ errors.install(gr) demo: gr.Blocks = None log = setup_log parser = cmd_args.parser - +url = 'https://github.com/vladmandic/automatic' if os.environ.get('IGNORE_CMD_ARGS_ERRORS', None) is None: cmd_opts = parser.parse_args() else: @@ -52,10 +50,7 @@ ui_reorder_categories = [ ] cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or cmd_opts.server_name) and not cmd_opts.enable_insecure - -devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = \ - (devices.cpu if any(y in cmd_opts.use_cpu for y in [x, 'all']) else devices.get_optimal_device() for x in ['sd', 'interrogate', 'gfpgan', 'esrgan', 'codeformer']) - +devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = (devices.cpu if any(y in cmd_opts.use_cpu for y in [x, 'all']) else devices.get_optimal_device() for x in ['sd', 'interrogate', 'gfpgan', 'esrgan', 'codeformer']) device = devices.device is_device_dml = False sd_upscalers = [] @@ -102,7 +97,6 @@ class State: def nextjob(self): if opts.live_previews_enable and opts.show_progress_every_n_steps == -1: self.do_set_current_image() - self.job_no += 1 self.sampling_step = 0 self.current_image_sampling_step = 0 @@ -134,13 +128,11 @@ class State: self.interrupted = False self.textinfo = None self.time_start = time.time() - devices.torch_gc() def end(self): self.job = "" self.job_count = 0 - devices.torch_gc() def set_current_image(self): @@ -167,9 +159,7 @@ class State: state = State() state.server_start = time.time() - interrogator = modules.interrogate.InterrogateModels("interrogate") - face_restorers = [] class OptionInfo: @@ -186,7 +176,6 @@ class OptionInfo: def options_section(section_identifier, options_dict): for _k, v in options_dict.items(): v.section = section_identifier - return options_dict @@ -232,26 +221,25 @@ def refresh_themes(): hide_dirs = {"visible": not cmd_opts.hide_ui_dir_config} tab_names = [] - options_templates = {} default_checkpoint = list_checkpoint_tiles()[0] if len(list_checkpoint_tiles()) > 0 else "model.ckpt" options_templates.update(options_section(('sd', "Stable Diffusion"), { "sd_model_checkpoint": OptionInfo(default_checkpoint, "Stable Diffusion checkpoint", gr.Dropdown, lambda: {"choices": list_checkpoint_tiles()}, refresh=refresh_checkpoints), - "sd_checkpoint_cache": OptionInfo(0, "Checkpoints to cache in RAM", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), - "sd_vae_checkpoint_cache": OptionInfo(0, "VAE Checkpoints to cache in RAM", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), - "sd_vae": OptionInfo("Automatic", "SD VAE", gr.Dropdown, lambda: {"choices": shared_items.sd_vae_items()}, refresh=shared_items.refresh_vae_list), - "sd_vae_as_default": OptionInfo(True, "Ignore selected VAE for stable diffusion checkpoints that have their own .vae.pt next to them"), + "sd_checkpoint_cache": OptionInfo(0, "Model checkpoints to cache in RAM", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), + "sd_vae_checkpoint_cache": OptionInfo(0, "VAE checkpoints to cache in RAM", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), + "sd_vae": OptionInfo("Automatic", "Select VAE", gr.Dropdown, lambda: {"choices": shared_items.sd_vae_items()}, refresh=shared_items.refresh_vae_list), + "sd_vae_as_default": OptionInfo(True, "Ignore selected VAE for stable diffusion checkpoints that have their own .vae.pt next to them", gr.Checkbox, {"visible": False}), "inpainting_mask_weight": OptionInfo(1.0, "Inpainting conditioning mask strength", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), "initial_noise_multiplier": OptionInfo(1.0, "Noise multiplier for img2img", gr.Slider, {"minimum": 0.5, "maximum": 1.5, "step": 0.01}), "img2img_color_correction": OptionInfo(False, "Apply color correction to img2img results to match original colors."), - "img2img_fix_steps": OptionInfo(False, "With img2img, do exactly the amount of steps the slider specifies (normally you'd do less with less denoising)."), + "img2img_fix_steps": OptionInfo(False, "For image processing do exactly the amount of steps as specified."), "img2img_background_color": OptionInfo("#ffffff", "With img2img, fill image's transparent parts with this color.", ui_components.FormColorPicker, {}), "enable_quantization": OptionInfo(True, "Enable quantization in K samplers for sharper and cleaner results. This may change existing seeds."), - "enable_emphasis": OptionInfo(True, "Emphasis: use (text) to make model pay more attention to text and [text] to make it pay less attention"), - "enable_batch_seeds": OptionInfo(True, "Make K-diffusion samplers produce same images in a batch as when making a single image"), + "enable_emphasis": OptionInfo(True, "Emphasis: use (text) to make model pay more attention to text and [text] to make it pay less attention", gr.Checkbox, {"visible": False}), + "enable_batch_seeds": OptionInfo(True, "Make K-diffusion samplers produce same images in a batch as when making a single image", gr.Checkbox, {"visible": False}), "comma_padding_backtrack": OptionInfo(20, "Increase coherency by padding from the last comma within n tokens when using more than 75 tokens", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1 }), - "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}), + "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1, "visible": False}), "upcast_attn": OptionInfo(False, "Upcast cross attention layer to float32"), "cross_attention_optimization": OptionInfo("Sub-quadratic" if is_device_dml else "Scaled-Dot-Product", "Cross-attention optimization method", gr.Radio, lambda: {"choices": shared_items.list_crossattention() }), "cross_attention_options": OptionInfo([], "Cross-attention advanced options", gr.CheckboxGroup, lambda: {"choices": ['xFormers enable flash Attention', 'SDP disable memory attention']}), @@ -259,6 +247,9 @@ options_templates.update(options_section(('sd', "Stable Diffusion"), { "sub_quad_kv_chunk_size": OptionInfo(512, "Sub-quadratic cross-attentionkv chunk size for the sub-quadratic cross-attention layer optimization to use", gr.Slider, {"minimum": 0, "maximum": 8192, "step": 8}), "sub_quad_chunk_threshold": OptionInfo(80, "Sub-quadratic cross-attention percentage of VRAM chunking threshold", gr.Slider, {"minimum": 0, "maximum": 100, "step": 1}), "always_batch_cond_uncond": OptionInfo(False, "Disables cond/uncond batching that is enabled to save memory with --medvram or --lowvram"), + "multiple_tqdm": OptionInfo(False, "Add a second progress bar to the console that shows progress for an entire job.", gr.Checkbox, {"visible": False}), + "print_hypernet_extra": OptionInfo(False, "Print extra hypernetwork information to console.", gr.Checkbox, {"visible": False}), + "dimensions_and_batch_together": OptionInfo(True, "", gr.Checkbox, {"visible": False}), })) options_templates.update(options_section(('system-paths', "System Paths"), { @@ -268,6 +259,7 @@ options_templates.update(options_section(('system-paths', "System Paths"), { "vae_dir": OptionInfo(os.path.join(paths.models_path, 'VAE'), "Path to directory with VAE files"), "embeddings_dir": OptionInfo(os.path.join(paths.models_path, 'embeddings'), "Embeddings directory for textual inversion"), "embeddings_templates_dir": OptionInfo(os.path.join(paths.script_path, 'train/templates'), "Embeddings train templates directory"), + "embeddings_train_log": OptionInfo(os.path.join(paths.script_path, 'train.csv'), "Embeddings train log file"), "hypernetwork_dir": OptionInfo(os.path.join(paths.models_path, 'hypernetworks'), "Hypernetwork directory"), "codeformer_models_path": OptionInfo(os.path.join(paths.models_path, 'Codeformer'), "Path to directory with codeformer model file(s)."), "gfpgan_models_path": OptionInfo(os.path.join(paths.models_path, 'GFPGAN'), "Path to directory with GFPGAN model file(s)"), @@ -327,11 +319,12 @@ options_templates.update(options_section(('saving-paths', "Image Paths"), { })) options_templates.update(options_section(('cuda', "CUDA Settings"), { + "memmon_poll_rate": OptionInfo(2, "VRAM usage polls per second during generation. Set to 0 to disable.", gr.Slider, {"minimum": 0, "maximum": 40, "step": 1}), "precision": OptionInfo("Autocast", "Precision type", gr.Radio, lambda: {"choices": ["Autocast", "Full"]}), - "cuda_dtype": OptionInfo("FP16", "Device precision type", gr.Radio, lambda: {"choices": ["FP32", "FP16", "BF16"]}), + "cuda_dtype": OptionInfo("FP32" if sys.platform == "darwin" else "FP16", "Device precision type", gr.Radio, lambda: {"choices": ["FP32", "FP16", "BF16"]}), "no_half": OptionInfo(True if is_device_dml else False, "Use full precision for model (--no-half)"), "no_half_vae": OptionInfo(True if is_device_dml else False, "Use full precision for VAE (--no-half-vae)"), - "upcast_sampling": OptionInfo(False, "Enable upcast sampling. Usually produces similar results to --no-half with better performance while using less memory"), + "upcast_sampling": OptionInfo(True if sys.platform == "darwin" else False, "Enable upcast sampling. Usually produces similar results to --no-half with better performance while using less memory"), "disable_nan_check": OptionInfo(True, "Do not check if produced images/latent spaces have NaN values"), "rollback_vae": OptionInfo(False, "Attempt to roll back VAE when produced NaN values, requires NaN check (experimental)"), "opt_channelslast": OptionInfo(False, "Use channels last as torch memory format "), @@ -343,11 +336,11 @@ options_templates.update(options_section(('cuda', "CUDA Settings"), { })) options_templates.update(options_section(('upscaling', "Upscaling"), { - "ESRGAN_tile": OptionInfo(192, "Tile size for ESRGAN upscalers. 0 = no tiling.", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}), - "ESRGAN_tile_overlap": OptionInfo(8, "Tile overlap, in pixels for ESRGAN upscalers. Low values = visible seam.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}), - "realesrgan_enabled_models": OptionInfo(["R-ESRGAN 4x+", "R-ESRGAN 4x+ Anime6B"], "Select which Real-ESRGAN models to show in the web UI.", gr.CheckboxGroup, lambda: {"choices": shared_items.realesrgan_models_names()}), - "upscaler_for_img2img": OptionInfo("SwinIR_4x", "Upscaler for img2img", gr.Dropdown, lambda: {"choices": [x.name for x in sd_upscalers]}), - "use_old_hires_fix_width_height": OptionInfo(False, "For hires fix, use width/height sliders to set final resolution rather than first pass (disables Upscale by, Resize width/height to)."), + "ESRGAN_tile": OptionInfo(192, "Tile size for ESRGAN upscalers (0 = no tiling)", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}), + "ESRGAN_tile_overlap": OptionInfo(8, "Tile overlap in pixels for ESRGAN upscalers", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}), + "realesrgan_enabled_models": OptionInfo(["R-ESRGAN 4x+", "R-ESRGAN 4x+ Anime6B"], "Real-ESRGAN available models", gr.CheckboxGroup, lambda: {"choices": shared_items.realesrgan_models_names()}), + "upscaler_for_img2img": OptionInfo("None", "Default upscaler for image resize operations", gr.Dropdown, lambda: {"choices": [x.name for x in sd_upscalers]}), + "use_old_hires_fix_width_height": OptionInfo(False, "Hires fix uses width & height to set final resolution rather than first pass"), "dont_fix_second_order_samplers_schedule": OptionInfo(False, "Do not fix prompt schedule for second order samplers."), })) @@ -357,12 +350,6 @@ options_templates.update(options_section(('face-restoration', "Face restoration" "face_restoration_unload": OptionInfo(False, "Move face restoration model from VRAM into RAM after processing"), })) -options_templates.update(options_section(('system', "System"), { - "memmon_poll_rate": OptionInfo(2, "VRAM usage polls per second during generation. Set to 0 to disable.", gr.Slider, {"minimum": 0, "maximum": 40, "step": 1}), - "multiple_tqdm": OptionInfo(False, "Add a second progress bar to the console that shows progress for an entire job."), - "print_hypernet_extra": OptionInfo(False, "Print extra hypernetwork information to console."), -})) - options_templates.update(options_section(('training', "Training"), { "unload_models_when_training": OptionInfo(False, "Move VAE and CLIP to RAM when training if possible. Saves VRAM."), "pin_memory": OptionInfo(True, "Turn on pin_memory for DataLoader. Makes training slightly faster but can increase memory usage."), @@ -409,13 +396,13 @@ options_templates.update(options_section(('ui', "User interface"), { "do_not_show_images": OptionInfo(False, "Do not show any images in results for web"), "add_model_hash_to_info": OptionInfo(True, "Add model hash to generation information"), "add_model_name_to_info": OptionInfo(True, "Add model name to generation information"), - "disable_weights_auto_swap": OptionInfo(True, "When reading generation parameters from text into UI (from PNG info or pasted text), do not change the selected model/checkpoint."), + "disable_weights_auto_swap": OptionInfo(True, "Do not change the selected model when reading generation parameters."), "send_seed": OptionInfo(True, "Send seed when sending prompt or image to other interface"), "send_size": OptionInfo(True, "Send size when sending prompt or image to another interface"), "font": OptionInfo("", "Font for image grids that have text"), - "js_modal_lightbox": OptionInfo(True, "Enable full page image viewer"), - "js_modal_lightbox_initially_zoomed": OptionInfo(True, "Show images zoomed in by default in full page image viewer"), - "show_progress_in_title": OptionInfo(False, "Show generation progress in window title."), + "js_modal_lightbox": OptionInfo(True, "Enable full page image viewer", gr.Checkbox, {"visible": False}), + "js_modal_lightbox_initially_zoomed": OptionInfo(True, "Show images zoomed in by default in full page image viewer", gr.Checkbox, {"visible": False}), + "show_progress_in_title": OptionInfo(False, "Show generation progress in window title.", gr.Checkbox, {"visible": False}), "keyedit_precision_attention": OptionInfo(0.1, "Ctrl+up/down precision when editing (attention:1.1)", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}), "keyedit_precision_extra": OptionInfo(0.05, "Ctrl+up/down precision when editing ", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}), "quicksettings": OptionInfo("sd_model_checkpoint", "Quicksettings list"), @@ -436,6 +423,7 @@ options_templates.update(options_section(('ui', "Live previews"), { options_templates.update(options_section(('sampler-params', "Sampler parameters"), { "show_samplers": OptionInfo(["Euler a", "UniPC", "DDIM", "DPM++ SDE", "DPM++ SDE", "DPM2 Karras", "DPM++ 2M Karras"], "Show samplers in user interface", gr.CheckboxGroup, lambda: {"choices": [x.name for x in list_samplers()]}), + "fallback_sampler": OptionInfo("Euler a", "Fallback sampler if primary sampler is not compatible", gr.Dropdown, lambda: {"choices": ["None"] + [x.name for x in list_samplers()]}), "eta_ancestral": OptionInfo(1.0, "Noise multiplier for ancestral samplers (eta)", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), "eta_ddim": OptionInfo(0.0, "Noise multiplier for DDIM (eta)", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}), "ddim_discretize": OptionInfo('uniform', "DDIM discretize img2img", gr.Radio, {"choices": ['uniform', 'quad']}), @@ -490,17 +478,14 @@ class Options: def __setattr__(self, key, value): if self.data is not None: if key in self.data or key in self.data_labels: - assert not cmd_opts.freeze_settings, "changing settings is disabled" - - info = opts.data_labels.get(key, None) - comp_args = info.component_args if info else None - if isinstance(comp_args, dict) and comp_args.get('visible', True) is False: - raise RuntimeError(f"not possible to set {key} because it is restricted") - + if cmd_opts.freeze_settings: + print(f'Settings are frozen: {key}') + return if cmd_opts.hide_ui_dir_config and key in restricted_opts: - raise RuntimeError(f"not possible to set {key} because it is restricted") - - self.data[key] = value + print(f'Settings key is restricted: {key}') + return + else: + self.data[key] = value return return super(Options, self).__setattr__(key, value) @@ -509,24 +494,19 @@ class Options: if self.data is not None: if item in self.data: return self.data[item] - if item in self.data_labels: return self.data_labels[item].default - return super(Options, self).__getattribute__(item) def set(self, key, value): """sets an option and calls its onchange callback, returning True if the option changed and False otherwise""" - oldval = self.data.get(key, None) if oldval == value: return False - try: setattr(self, key, value) except RuntimeError: return False - if self.data_labels[key].onchange is not None: try: self.data_labels[key].onchange() @@ -534,37 +514,30 @@ class Options: errors.display(e, f"changing setting {key} to {value}") setattr(self, key, oldval) return False - return True def get_default(self, key): """returns the default value for the key""" - data_label = self.data_labels.get(key) if data_label is None: return None - return data_label.default def save(self, filename): assert not cmd_opts.freeze_settings, "saving settings is disabled" - with open(filename, "w", encoding="utf8") as file: json.dump(self.data, file, indent=4) def same_type(self, x, y): if x is None or y is None: return True - type_x = self.typemap.get(type(x), type(x)) type_y = self.typemap.get(type(y), type(y)) - return type_x == type_y def load(self, filename): with open(filename, "r", encoding="utf8") as file: self.data = json.load(file) - bad_settings = 0 for k, v in self.data.items(): info = self.data_labels.get(k, None) @@ -578,7 +551,6 @@ class Options: def onchange(self, key, func, call=True): item = self.data_labels.get(key) item.onchange = func - if call: func() @@ -591,13 +563,11 @@ class Options: def reorder(self): """reorder settings so that all items related to section always go together""" - section_ids = {} settings_items = self.data_labels.items() for k, item in settings_items: if item.section not in section_ids: section_ids[item.section] = len(section_ids) - self.data_labels = {k: v for k, v in sorted(settings_items, key=lambda x: section_ids[x[1].section])} def cast_value(self, key, value): @@ -623,27 +593,20 @@ class Options: return value - opts = Options() - batch_cond_uncond = opts.always_batch_cond_uncond or not (cmd_opts.lowvram or cmd_opts.medvram) parallel_processing_allowed = not cmd_opts.lowvram and not cmd_opts.medvram xformers_available = False config_filename = cmd_opts.ui_settings_file - os.makedirs(opts.hypernetwork_dir, exist_ok=True) hypernetworks = {} loaded_hypernetworks = [] - if os.path.exists(config_filename): opts.load(config_filename) - cmd_opts = cmd_args.compatibility_args(opts, cmd_opts) prompt_styles = modules.styles.StyleDatabase(opts.styles_dir) - settings_components = None """assinged from ui.py, a mapping on setting names to gradio components repsponsible for those settings""" - latent_upscale_default_mode = "Latent" latent_upscale_modes = { "Latent": {"mode": "bilinear", "antialias": False}, @@ -653,11 +616,10 @@ latent_upscale_modes = { "Latent (nearest)": {"mode": "nearest", "antialias": False}, "Latent (nearest-exact)": {"mode": "nearest-exact", "antialias": False}, } - progress_print_out = sys.stdout - gradio_theme = gr.themes.Base() + def reload_gradio_theme(theme_name=None): global gradio_theme # pylint: disable=global-statement if not theme_name: @@ -718,7 +680,6 @@ class TotalTQDM: total_tqdm = TotalTQDM() - mem_mon = modules.memmon.MemUsageMonitor("MemMon", device, opts) mem_mon.start() diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index 24ccf743f..75bd1cd88 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -526,7 +526,7 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st save_embedding(embedding, optimizer, checkpoint, embedding_name_every, last_saved_file, remove_cached_checksum=True) embedding_yet_to_be_embedded = True - write_loss(log_directory, "train.csv", embedding.step, steps_per_epoch, { + write_loss(log_directory, shared.ops.embeddings_train_log, embedding.step, steps_per_epoch, { "loss": f"{loss_step:.7f}", "learn_rate": scheduler.learn_rate }) diff --git a/modules/txt2img.py b/modules/txt2img.py index 70204293b..2fcb4c49d 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -7,8 +7,8 @@ import modules.shared as shared from modules.ui import plaintext_to_html -def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, override_settings_texts, *args): - override_settings = create_override_settings_dict(override_settings_texts) # pylint: disable=unused-argument +def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, override_settings_texts, *args): # pylint: disable=unused-argument + override_settings = create_override_settings_dict(override_settings_texts) p = StableDiffusionProcessingTxt2Img( sd_model=shared.sd_model, outpath_samples=opts.outdir_samples or opts.outdir_txt2img_samples, diff --git a/modules/ui.py b/modules/ui.py index 83249d39c..c811cd3eb 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -81,34 +81,25 @@ def visit(x, func, path=""): def add_style(name: str, prompt: str, negative_prompt: str): if name is None: return [gr_show() for x in range(4)] - style = modules.styles.PromptStyle(name, prompt, negative_prompt) shared.prompt_styles.styles[style.name] = style - # Save all loaded prompt styles: this allows us to update the storage format in the future more easily, because we - # reserialize all styles every time we save them shared.prompt_styles.save_styles(shared.opts.styles_dir) - return [gr.Dropdown.update(visible=True, choices=list(shared.prompt_styles.styles)) for _ in range(2)] def calc_resolution_hires(enable, width, height, hr_scale, hr_resize_x, hr_resize_y): from modules import processing, devices - if not enable: return "" - 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) - with devices.autocast(): p.init([""], [0], [0]) - return f"resize: from {p.width}x{p.height} to {p.hr_resize_x or p.hr_upscale_to_x}x{p.hr_resize_y or p.hr_upscale_to_y}" def apply_styles(prompt, prompt_neg, styles): prompt = shared.prompt_styles.apply_styles_to_prompt(prompt, styles) prompt_neg = shared.prompt_styles.apply_negative_styles_to_prompt(prompt_neg, styles) - return [gr.Textbox.update(value=prompt), gr.Textbox.update(value=prompt_neg), gr.Dropdown.update(value=[])] @@ -125,7 +116,6 @@ def process_interrogate(interrogation_function, mode, ii_input_dir, ii_output_di os.makedirs(ii_output_dir, exist_ok=True) else: ii_output_dir = ii_input_dir - for image in images: img = Image.open(image) filename = os.path.basename(image) @@ -144,44 +134,35 @@ def interrogate_deepbooru(image): prompt = deepbooru.model.tag(image) return gr.update() if prompt is None else prompt + def change_clip_skip(val): shared.opts.CLIP_stop_at_last_layers = val + def create_seed_inputs(target_interface): with FormRow(elem_id=target_interface + '_seed_row', variant="compact"): seed = gr.Number(label='Seed', value=-1, elem_id=target_interface + '_seed') seed.style(container=False) random_seed = ToolButton(random_symbol, elem_id=target_interface + '_random_seed') reuse_seed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_seed') - seed_checkbox = gr.Checkbox(label='Extra', elem_id=target_interface + '_subseed_show', value=False, visible=False) # Ghost checkbox, so it still gets sent. For compatibility with extensions that call txt2img or img2img manually - with FormRow(visible=True, elem_id=target_interface + '_subseed_row'): subseed = gr.Number(label='Variation seed', value=-1, elem_id=target_interface + '_subseed') subseed.style(container=False) random_subseed = ToolButton(random_symbol, elem_id=target_interface + '_random_subseed') reuse_subseed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_subseed') subseed_strength = gr.Slider(label='Strength', value=0.0, minimum=0, maximum=1, step=0.01, elem_id=target_interface + '_subseed_strength') - with FormRow(visible=False): seed_resize_from_w = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from width", value=0, elem_id=target_interface + '_seed_resize_from_w') seed_resize_from_h = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize seed from height", value=0, elem_id=target_interface + '_seed_resize_from_h') - random_seed.click(fn=lambda: [-1, -1], show_progress=False, inputs=[], outputs=[seed, subseed]) random_subseed.click(fn=lambda: -1, show_progress=False, inputs=[], outputs=[subseed]) - return seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox - def connect_clear_prompt(button): """Given clear button, prompt, and token_counter objects, setup clear prompt button click event""" - button.click( - _js="clear_prompt", - fn=None, - inputs=[], - outputs=[], - ) + button.click(_js="clear_prompt", fn=None, inputs=[], outputs=[]) def connect_reuse_seed(seed: gr.Number, reuse_seed: gr.Button, generation_info: gr.Textbox, dummy_component, is_subseed): @@ -190,7 +171,6 @@ def connect_reuse_seed(seed: gr.Number, reuse_seed: gr.Button, generation_info: was 0, i.e. no variation seed was used, it copies the normal seed value instead.""" def copy_seed(gen_info_string: str, index): res = -1 - try: gen_info = json.loads(gen_info_string) index -= gen_info.get('index_of_first_image', 0) @@ -201,35 +181,24 @@ def connect_reuse_seed(seed: gr.Number, reuse_seed: gr.Button, generation_info: else: all_seeds = gen_info.get('all_seeds', [-1]) res = all_seeds[index if 0 <= index < len(all_seeds) else 0] - except json.decoder.JSONDecodeError: if gen_info_string != '': print("Error parsing JSON generation info:", file=sys.stderr) print(gen_info_string, file=sys.stderr) - return [res, gr_show(False)] - reuse_seed.click( - fn=copy_seed, - _js="(x, y) => [x, selected_gallery_index()]", - show_progress=False, - inputs=[generation_info, dummy_component], - outputs=[seed, dummy_component] - ) + reuse_seed.click(fn=copy_seed, _js="(x, y) => [x, selected_gallery_index()]", show_progress=False, inputs=[generation_info, dummy_component], outputs=[seed, dummy_component]) def update_token_counter(text, steps): try: text, _ = extra_networks.parse_prompt(text) - _, prompt_flat_list, _ = prompt_parser.get_multicond_prompt_list([text]) prompt_schedules = prompt_parser.get_learned_conditioning_prompt_schedules(prompt_flat_list, steps) - except Exception: # a parsing error can happen here during typing, and we don't want to bother the user with # messages related to it in console prompt_schedules = [[[steps, text]]] - flat_prompts = reduce(lambda list1, list2: list1+list2, prompt_schedules) prompts = [prompt_text for step, prompt_text in flat_prompts] token_count, max_length = max([model_hijack.get_prompt_lengths(prompt) for prompt in prompts], key=lambda args: args[0]) @@ -238,103 +207,72 @@ def update_token_counter(text, steps): def create_toprow(is_img2img): 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=6): with gr.Row(): with gr.Column(scale=80): with gr.Row(): prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)") - with gr.Row(): with gr.Column(scale=80): with gr.Row(): negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)") - button_interrogate = None button_deepbooru = None if is_img2img: with gr.Column(scale=1, elem_classes="interrogate-col"): button_interrogate = gr.Button('Interrogate\nCLIP', elem_id="interrogate") button_deepbooru = gr.Button('Interrogate\nDeepBooru', elem_id="deepbooru") - with gr.Column(scale=1, elem_id=f"{id_part}_actions_column"): with gr.Row(elem_id=f"{id_part}_generate_box", elem_classes="generate-box"): interrupt = gr.Button('Stop', elem_id=f"{id_part}_interrupt", elem_classes="generate-box-interrupt") skip = gr.Button('Skip', elem_id=f"{id_part}_skip", elem_classes="generate-box-skip") submit = gr.Button('Generate', elem_id=f"{id_part}_generate", variant='primary') - - skip.click( - fn=lambda: shared.state.skip(), - inputs=[], - outputs=[], - ) - - interrupt.click( - fn=lambda: shared.state.interrupt(), - inputs=[], - outputs=[], - ) - + skip.click(fn=lambda: shared.state.skip(), inputs=[], outputs=[]) + interrupt.click(fn=lambda: shared.state.interrupt(), inputs=[], outputs=[]) with gr.Row(elem_id=f"{id_part}_tools"): paste = ToolButton(value=paste_symbol, elem_id="paste") clear_prompt_button = ToolButton(value=clear_prompt_symbol, elem_id=f"{id_part}_clear_prompt") extra_networks_button = ToolButton(value=extra_networks_symbol, elem_id=f"{id_part}_extra_networks") prompt_style_apply = ToolButton(value=apply_style_symbol, elem_id=f"{id_part}_style_apply") save_style = ToolButton(value=save_style_symbol, elem_id=f"{id_part}_style_create") - token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_token_counter", elem_classes=["token-counter"]) token_button = gr.Button(visible=False, elem_id=f"{id_part}_token_button") negative_token_counter = gr.HTML(value="0/75", elem_id=f"{id_part}_negative_token_counter", elem_classes=["token-counter"]) negative_token_button = gr.Button(visible=False, elem_id=f"{id_part}_negative_token_button") - - clear_prompt_button.click( - fn=lambda *x: x, - _js="confirm_clear_prompt", - inputs=[prompt, negative_prompt], - outputs=[prompt, negative_prompt], - ) - + clear_prompt_button.click(fn=lambda *x: x, _js="confirm_clear_prompt", inputs=[prompt, negative_prompt], outputs=[prompt, negative_prompt]) with gr.Row(elem_id=f"{id_part}_styles_row"): prompt_styles = gr.Dropdown(label="Styles", elem_id=f"{id_part}_styles", choices=[k for k, v in shared.prompt_styles.styles.items()], value=[], multiselect=True) create_refresh_button(prompt_styles, shared.prompt_styles.reload, lambda: {"choices": [k for k, v in shared.prompt_styles.styles.items()]}, f"refresh_{id_part}_styles") - return prompt, prompt_styles, negative_prompt, submit, button_interrogate, button_deepbooru, prompt_style_apply, save_style, paste, extra_networks_button, token_counter, token_button, negative_token_counter, negative_token_button -def setup_progressbar(*args, **kwargs): +def setup_progressbar(*args, **kwargs): # pylint: disable=unused-argument pass def apply_setting(key, value): if value is None: return gr.update() - if shared.cmd_opts.freeze_settings: return gr.update() - # dont allow model to be swapped when model hash exists in prompt if key == "sd_model_checkpoint" and opts.disable_weights_auto_swap: return gr.update() - if key == "sd_model_checkpoint": ckpt_info = sd_models.get_closet_checkpoint_match(value) - if ckpt_info is not None: value = ckpt_info.title else: return gr.update() - comp_args = opts.data_labels[key].component_args if comp_args and isinstance(comp_args, dict) and comp_args.get('visible') is False: return - valtype = type(opts.data_labels[key].default) oldval = opts.data.get(key, None) opts.data[key] = valtype(value) if valtype != type(None) else value if oldval != value and opts.data_labels[key].onchange is not None: opts.data_labels[key].onchange() - opts.save(shared.config_filename) return getattr(opts, key) @@ -343,18 +281,12 @@ def create_refresh_button(refresh_component, refresh_method, refreshed_args, ele def refresh(): refresh_method() args = refreshed_args() if callable(refreshed_args) else refreshed_args - for k, v in args.items(): setattr(refresh_component, k, v) - return gr.update(**(args or {})) refresh_button = ToolButton(value=refresh_symbol, elem_id=elem_id) - refresh_button.click( - fn=refresh, - inputs=[], - outputs=[refresh_component] - ) + refresh_button.click(fn=refresh, inputs=[], outputs=[refresh_component]) return refresh_button @@ -366,126 +298,98 @@ def create_sampler_and_steps_selection(choices, tabname): with FormRow(elem_id=f"sampler_selection_{tabname}"): sampler_index = gr.Dropdown(label='Sampling method', elem_id=f"{tabname}_sampling", choices=[x.name for x in choices], value="UniPC" if tabname == 'txt2img' else "Euler a", type="index") steps = gr.Slider(minimum=1, maximum=150, step=1, elem_id=f"{tabname}_steps", label="Sampling steps", value=10 if tabname == 'txt2img' else 20) - return steps, sampler_index def ordered_ui_categories(): user_order = {x.strip(): i * 2 + 1 for i, x in enumerate(shared.opts.ui_reorder.split(","))} - for i, category in sorted(enumerate(shared.ui_reorder_categories), key=lambda x: user_order.get(x[1], x[0] * 2 + 0)): yield category def get_value_for_setting(key): value = getattr(opts, key) - info = opts.data_labels[key] args = info.component_args() if callable(info.component_args) else info.component_args or {} args = {k: v for k, v in args.items() if k not in {'precision'}} - return gr.update(value=value, **args) -def create_override_settings_dropdown(tabname, row): +def create_override_settings_dropdown(tabname, row): # pylint: disable=unused-argument dropdown = gr.Dropdown([], label="Override settings", visible=False, elem_id=f"{tabname}_override_settings", multiselect=True) - - dropdown.change( - fn=lambda x: gr.Dropdown.update(visible=len(x) > 0), - inputs=[dropdown], - outputs=[dropdown], - ) - + dropdown.change(fn=lambda x: gr.Dropdown.update(visible=len(x) > 0), inputs=[dropdown], outputs=[dropdown]) return dropdown def create_ui(): - import modules.img2img - import modules.txt2img - + import modules.img2img # pylint: disable=redefined-outer-name + import modules.txt2img # pylint: disable=redefined-outer-name reload_javascript() - parameters_copypaste.reset() - modules.scripts.scripts_current = modules.scripts.scripts_txt2img modules.scripts.scripts_txt2img.initialize_scripts(is_img2img=False) - with gr.Blocks(analytics_enabled=False) as txt2img_interface: txt2img_prompt, txt2img_prompt_styles, txt2img_negative_prompt, submit, _, _, txt2img_prompt_style_apply, txt2img_save_style, txt2img_paste, extra_networks_button, token_counter, token_button, negative_token_counter, negative_token_button = create_toprow(is_img2img=False) - dummy_component = gr.Label(visible=False) txt_prompt_img = gr.File(label="", elem_id="txt2img_prompt_image", file_count="single", type="binary", visible=False) - - with FormRow(variant='compact', elem_id="txt2img_extra_networks", visible=False) as extra_networks: + with FormRow(variant='compact', elem_id="txt2img_extra_networks", visible=False) as extra_networks_ui: from modules import ui_extra_networks - extra_networks_ui = ui_extra_networks.create_ui(extra_networks, extra_networks_button, 'txt2img') - + extra_networks_ui = ui_extra_networks.create_ui(extra_networks_ui, extra_networks_button, 'txt2img') with gr.Row().style(equal_height=False): with gr.Column(variant='compact', elem_id="txt2img_settings"): for category in ordered_ui_categories(): if category == "sampler": steps, sampler_index = create_sampler_and_steps_selection(samplers, "txt2img") - elif category == "dimensions": with FormRow(): with gr.Column(elem_id="txt2img_column_size", scale=4): with FormRow(elem_id="txt2img_row_dimension"): width = gr.Slider(minimum=64, maximum=2048, step=8, label="Width", value=512, elem_id="txt2img_width") height = gr.Slider(minimum=64, maximum=2048, step=8, label="Height", value=512, elem_id="txt2img_height") - with gr.Column(elem_id="txt2img_dimensions_row", scale=1, elem_classes="dimensions-tools"): res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="txt2img_res_switch_btn") - with gr.Column(elem_id="txt2img_column_batch"): with FormRow(elem_id="txt2img_row_batch"): batch_count = gr.Slider(minimum=1, step=1, label='Batch count', value=1, elem_id="txt2img_batch_count") batch_size = gr.Slider(minimum=1, maximum=32, step=1, label='Batch size', value=1, elem_id="txt2img_batch_size") - elif category == "cfg": with FormRow(): cfg_scale = gr.Slider(minimum=1.0, maximum=30.0, step=0.5, label='CFG Scale', value=7.0, elem_id="txt2img_cfg_scale") clip_skip = gr.Slider(label='CLIP Skip', value=1, minimum=1, maximum=4, step=1, elem_id='txt2img_clip_skip', interactive=True) clip_skip.change(fn=change_clip_skip, show_progress=False, inputs=clip_skip) - elif category == "seed": seed, reuse_seed, subseed, reuse_subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, seed_checkbox = create_seed_inputs('txt2img') - elif category == "checkboxes": with FormRow(elem_classes="checkboxes-row", variant="compact"): restore_faces = gr.Checkbox(label='Restore faces', value=False, visible=len(shared.face_restorers) > 1, elem_id="txt2img_restore_faces") tiling = gr.Checkbox(label='Tiling', value=False, elem_id="txt2img_tiling") enable_hr = gr.Checkbox(label='Hires fix', value=False, elem_id="txt2img_enable_hr") hr_final_resolution = FormHTML(value="", elem_id="txtimg_hr_finalres", label="Upscaled resolution", interactive=False) - elif category == "hires_fix": with FormGroup(visible=False, elem_id="txt2img_hires_fix") as hr_options: with FormRow(elem_id="txt2img_hires_fix_row1", variant="compact"): hr_upscaler = gr.Dropdown(label="Upscaler", elem_id="txt2img_hr_upscaler", choices=[*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]], value=shared.latent_upscale_default_mode) hr_second_pass_steps = gr.Slider(minimum=0, maximum=150, step=1, label='Hires steps', value=0, elem_id="txt2img_hires_steps") denoising_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Denoising strength', value=0.7, elem_id="txt2img_denoising_strength") - with FormRow(elem_id="txt2img_hires_fix_row2", variant="compact"): hr_scale = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label="Upscale by", value=2.0, elem_id="txt2img_hr_scale") hr_resize_x = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize width to", value=0, elem_id="txt2img_hr_resize_x") hr_resize_y = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize height to", value=0, elem_id="txt2img_hr_resize_y") - elif category == "override_settings": with FormRow(elem_id="txt2img_override_settings_row") as row: override_settings = create_override_settings_dropdown('txt2img', row) - elif category == "scripts": with FormGroup(elem_id="txt2img_script_container"): custom_inputs = modules.scripts.scripts_txt2img.setup_ui() - hr_resolution_preview_inputs = [enable_hr, width, height, hr_scale, hr_resize_x, hr_resize_y] - for input in hr_resolution_preview_inputs: - input.change( + for preview_input in hr_resolution_preview_inputs: + preview_input.change( fn=calc_resolution_hires, inputs=hr_resolution_preview_inputs, outputs=[hr_final_resolution], show_progress=False, ) - input.change( + preview_input.change( None, _js="onCalcResolutionHires", inputs=hr_resolution_preview_inputs, @@ -494,7 +398,6 @@ def create_ui(): ) txt2img_gallery, generation_info, html_info, html_log = create_output_panel("txt2img", opts.outdir_txt2img_samples) - connect_reuse_seed(seed, reuse_seed, generation_info, dummy_component, is_subseed=False) connect_reuse_seed(subseed, reuse_subseed, generation_info, dummy_component, is_subseed=True) @@ -614,9 +517,9 @@ def create_ui(): img2img_prompt_img = gr.File(label="", elem_id="img2img_prompt_image", file_count="single", type="binary", visible=False) - with FormRow(variant='compact', elem_id="img2img_extra_networks", visible=False) as extra_networks: + with FormRow(variant='compact', elem_id="img2img_extra_networks", visible=False) as extra_networks_ui: from modules import ui_extra_networks - extra_networks_ui_img2img = ui_extra_networks.create_ui(extra_networks, extra_networks_button, 'img2img') + extra_networks_ui_img2img = ui_extra_networks.create_ui(extra_networks_ui, extra_networks_button, 'img2img') with FormRow().style(equal_height=False): with gr.Column(variant='compact', elem_id="img2img_settings"): @@ -767,7 +670,7 @@ def create_ui(): for i, elem in enumerate([tab_img2img, tab_sketch, tab_inpaint, tab_inpaint_color, tab_inpaint_upload, tab_batch]): elem.select( - fn=lambda tab=i: select_img2img_tab(tab), + fn=lambda tab=i: select_img2img_tab(tab), # pylint: disable=cell-var-from-loop inputs=[], outputs=[inpaint_controls, mask_alpha], ) @@ -926,31 +829,6 @@ def create_ui(): with gr.Blocks(analytics_enabled=False) as extras_interface: ui_postprocessing.create_ui() - """ - with gr.Blocks(analytics_enabled=False) as pnginfo_interface: - with gr.Row().style(equal_height=False): - with gr.Column(variant='panel'): - image = gr.Image(elem_id="pnginfo_image", label="Source", source="upload", interactive=True, type="pil") - - with gr.Column(variant='panel'): - html = gr.HTML() - generation_info = gr.Textbox(visible=False, elem_id="pnginfo_generation_info") - html2 = gr.HTML() - with gr.Row(): - buttons = parameters_copypaste.create_buttons(["txt2img", "img2img", "inpaint", "extras"]) - - for tabname, button in buttons.items(): - parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding( - paste_button=button, tabname=tabname, source_text_component=generation_info, source_image_component=image, - )) - - image.change( - fn=wrap_gradio_call(modules.extras.run_pnginfo), - inputs=[image], - outputs=[html, generation_info, html2], - ) - """ - def update_interp_description(value): interp_description_css = "

{}

" interp_descriptions = { @@ -1322,9 +1200,7 @@ def create_ui(): info = opts.data_labels[key] t = type(info.default) - args = info.component_args() if callable(info.component_args) else info.component_args - if info.component is not None: comp = info.component elif t == str: @@ -1334,10 +1210,8 @@ def create_ui(): elif t == bool: comp = gr.Checkbox else: - raise Exception(f'bad options item type: {str(t)} for key {key}') - + raise ValueError(f'bad options item type: {str(t)} for key {key}') elem_id = "setting_"+key - if info.refresh is not None: if is_quicksettings: res = comp(label=info.label, value=fun(), elem_id=elem_id, **(args or {})) @@ -1348,7 +1222,6 @@ def create_ui(): create_refresh_button(res, info.refresh, info.component_args, "refresh_" + key) else: res = comp(label=info.label, value=fun(), elem_id=elem_id, **(args or {})) - return res components = [] @@ -1433,7 +1306,7 @@ def create_ui(): current_tab.__exit__() request_notifications = gr.Button(value='Request browser notifications', elem_id="request_notifications", visible=False) - show_all_pages = gr.Button(value="Show all pages", variant='primary', elem_id="settings_show_all_pages") + _show_all_pages = gr.Button(value="Show all pages", variant='primary', elem_id="settings_show_all_pages") with gr.TabItem("Licenses"): gr.HTML(shared.html("licenses.html"), elem_id="licenses") @@ -1507,7 +1380,7 @@ def create_ui(): parameters_copypaste.connect_paste_params_buttons() - with gr.Tabs(elem_id="tabs") as tabs: + with gr.Tabs(elem_id="tabs") as _tabs: for interface, label, ifid in interfaces: if label in shared.opts.hidden_tabs: continue diff --git a/modules/ui_common.py b/modules/ui_common.py index 12908005e..cb9870196 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -66,10 +66,12 @@ def save_files(js_data, images, do_make_zip, index): for image_index, filedata in enumerate(images, start_index): image = image_from_url_text(filedata) - is_grid = image_index < p.index_of_first_image i = 0 if is_grid else (image_index - p.index_of_first_image) - + if len(p.all_seeds) <= i: + p.all_seeds.append(p.seed) + if len(p.all_prompts) <= i: + p.all_prompts.append(p.prompt) fullfn, txt_fullfn = modules.images.save_image(image, path, "", seed=p.all_seeds[i], prompt=p.all_prompts[i], extension=extension, info=p.infotexts[image_index], grid=is_grid, p=p, save_to_dirs=save_to_dirs) filename = os.path.relpath(fullfn, path) diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index d5c5bb34f..a8e3851b1 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -20,37 +20,28 @@ close_symbol = '\U0000274C' # ❌ def register_page(page): """registers extra networks page for the UI; recommend doing it in on_before_ui() callback for extensions""" - extra_pages.append(page) allowed_dirs.clear() allowed_dirs.update(set(sum([x.allowed_directories_for_previews() for x in extra_pages], []))) def fetch_file(filename: str = ""): - from starlette.responses import FileResponse - + from starlette.responses import FileResponse, JSONResponse if not any([Path(x).absolute() in Path(filename).absolute().parents for x in allowed_dirs]): - raise ValueError(f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages.") - - ext = os.path.splitext(filename)[1].lower() - if ext not in (".png", ".jpg", ".webp"): - raise ValueError(f"File cannot be fetched: {filename}. Only png and jpg and webp.") - - # would profit from returning 304 + return JSONResponse({"error": f"File cannot be fetched: {filename}. Must be in one of directories registered by extra pages."}) + if os.path.splitext(filename)[1].lower() not in (".png", ".jpg", ".webp"): + return JSONResponse({"error": f"File cannot be fetched: {filename}. Only png and jpg and webp."}) return FileResponse(filename, headers={"Accept-Ranges": "bytes"}) def get_metadata(page: str = "", item: str = ""): from starlette.responses import JSONResponse - page = next(iter([x for x in extra_pages if x.name == page]), None) if page is None: return JSONResponse({}) - metadata = page.metadata.get(item) if metadata is None: return JSONResponse({}) - return JSONResponse({"metadata": metadata}) @@ -75,58 +66,44 @@ class ExtraNetworksPage: def search_terms_from_path(self, filename, possible_directories=None): abspath = os.path.abspath(filename) - for parentdir in (possible_directories if possible_directories is not None else self.allowed_directories_for_previews()): parentdir = os.path.abspath(parentdir) if abspath.startswith(parentdir): return abspath[len(parentdir):].replace('\\', '/') - return "" def create_html(self, tabname): view = shared.opts.extra_networks_default_view items_html = '' - self.metadata = {} - subdirs = {} for parentdir in [os.path.abspath(x) for x in self.allowed_directories_for_previews()]: for x in glob.glob(os.path.join(parentdir, '**/*'), recursive=True): if not os.path.isdir(x): continue - subdir = os.path.abspath(x)[len(parentdir):].replace("\\", "/") while subdir.startswith("/"): subdir = subdir[1:] - is_empty = len(os.listdir(x)) == 0 if not is_empty and not subdir.endswith("/"): subdir = subdir + "/" - subdirs[subdir] = 1 - if subdirs: subdirs = {"": 1, **subdirs} - subdirs_html = "".join([f""" """ for subdir in subdirs]) - for item in self.list_items(): metadata = item.get("metadata") if metadata: self.metadata[item["name"]] = metadata - items_html += self.create_html_for_item(item, tabname) - if items_html == '': dirs = "".join([f"
  • {x}
  • " for x in self.allowed_directories_for_previews()]) items_html = shared.html("extra-networks-no-cards.html").format(dirs=dirs) - self_name_id = self.name.replace(" ", "_") - res = f"""
    {subdirs_html} @@ -135,7 +112,6 @@ class ExtraNetworksPage: {items_html}
    """ - return res def list_items(self): @@ -146,11 +122,9 @@ class ExtraNetworksPage: def create_html_for_item(self, item, tabname): preview = item.get("preview", None) - onclick = item.get("onclick", None) if onclick is None: onclick = '"' + html.escape(f"""return cardClicked({json.dumps(tabname)}, {item["prompt"]}, {"true" if self.allow_negative_prompt else "false"})""") + '"' - height = f"height: {shared.opts.extra_networks_card_height}px;" if shared.opts.extra_networks_card_height else '' width = f"width: {shared.opts.extra_networks_card_width}px;" if shared.opts.extra_networks_card_width else '' background_image = f"background-image: url(\"{html.escape(preview)}\");" if preview else '' @@ -158,7 +132,6 @@ class ExtraNetworksPage: metadata = item.get("metadata") if metadata: metadata_button = f"" - args = { "style": f"'{height}{width}{background_image}'", "prompt": item.get("prompt", None), @@ -167,28 +140,25 @@ class ExtraNetworksPage: "name": item["name"], "description": (item.get("description") or ""), "card_clicked": onclick, + "save_card_description": '"' + html.escape(f"""return saveCardDescription(event, {json.dumps(tabname)}, {json.dumps(item["local_preview"])})""") + '"', "save_card_preview": '"' + html.escape(f"""return saveCardPreview(event, {json.dumps(tabname)}, {json.dumps(item["local_preview"])})""") + '"', + "read_card_description": '"' + html.escape(f"""return readCardDescription(event, {json.dumps(tabname)}, {json.dumps(item["local_preview"])}, {json.dumps(item["description"])})""") + '"', "search_term": item.get("search_term", ""), "metadata_button": metadata_button, } - return self.card_page.format(**args) def find_preview(self, path): """ Find a preview PNG for a given path (without extension) and call link_preview on it. """ - preview_extensions = ["png", "jpg", "webp"] if shared.opts.samples_format not in preview_extensions: preview_extensions.append(shared.opts.samples_format) - potential_files = sum([[path + "." + ext, path + ".preview." + ext] for ext in preview_extensions], []) - for file in potential_files: if os.path.isfile(file): return self.link_preview(file) - return None def find_description(self, path): @@ -212,26 +182,24 @@ class ExtraNetworksUi: def __init__(self): self.pages = None self.stored_extra_pages = None - self.button_save_preview = None self.preview_target_filename = None - + self.button_save_description = None + self.button_read_description = None + self.description_target_filename = None + self.description_input = None self.tabname = None def pages_in_preferred_order(pages): tab_order = [x.lower().strip() for x in shared.opts.ui_extra_networks_tab_reorder.split(",")] - def tab_name_score(name): name = name.lower() for i, possible_match in enumerate(tab_order): if possible_match in name: return i - return len(pages) - tab_scores = {page.name: (tab_name_score(page.name), original_index) for original_index, page in enumerate(pages)} - return sorted(pages, key=lambda x: tab_scores[x.name]) @@ -240,47 +208,43 @@ def create_ui(container, button, tabname): ui.pages = [] ui.stored_extra_pages = pages_in_preferred_order(extra_pages.copy()) ui.tabname = tabname - - with gr.Tabs(elem_id=tabname+"_extra_tabs") as tabs: + with gr.Tabs(elem_id=tabname+"_extra_tabs"): for page in ui.stored_extra_pages: with gr.Tab(page.title): - page_elem = gr.HTML(page.create_html(ui.tabname)) ui.pages.append(page_elem) - - filter = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", placeholder="Search...", visible=False) + _filter = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", placeholder="Search...", visible=False) + ui.description_input = gr.TextArea('', show_label=False, elem_id=tabname+"_description_input", placeholder="Save/Replace Extra Network Description...", lines=2) button_refresh = ToolButton(refresh_symbol, elem_id=tabname+"_extra_refresh") button_close = ToolButton(close_symbol, elem_id=tabname+"_extra_close") - ui.button_save_preview = gr.Button('Save preview', elem_id=tabname+"_save_preview", visible=False) ui.preview_target_filename = gr.Textbox('Preview save filename', elem_id=tabname+"_preview_filename", visible=False) + ui.button_save_description = gr.Button('Save description', elem_id=tabname+"_save_description", visible=False) + ui.button_read_description = gr.Button('Read description', elem_id=tabname+"_read_description", visible=False) + ui.description_target_filename = gr.Textbox('Description save filename', elem_id=tabname+"_description_filename", visible=False) def toggle_visibility(is_visible): is_visible = not is_visible return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary")) - state_visible = gr.State(value=False) + state_visible = gr.State(value=False) # pylint: disable=abstract-class-instantiated button.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container, button]) button_close.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container]) def refresh(): res = [] - for pg in ui.stored_extra_pages: pg.refresh() res.append(pg.create_html(ui.tabname)) - return res button_refresh.click(fn=refresh, inputs=[], outputs=ui.pages) - return ui def path_is_parent(parent_path, child_path): parent_path = os.path.abspath(parent_path) child_path = os.path.abspath(child_path) - return child_path.startswith(parent_path) @@ -289,30 +253,24 @@ def setup_ui(ui, gallery): if len(images) == 0: print("There is no image in gallery to save as a preview.") return [page.create_html(ui.tabname) for page in ui.stored_extra_pages] - index = int(index) index = 0 if index < 0 else index index = len(images) - 1 if index >= len(images) else index - img_info = images[index if index >= 0 else 0] image = image_from_url_text(img_info) - geninfo, items = read_info_from_image(image) - + geninfo, _items = read_info_from_image(image) is_allowed = False for extra_page in ui.stored_extra_pages: if any([path_is_parent(x, filename) for x in extra_page.allowed_directories_for_previews()]): is_allowed = True break - assert is_allowed, f'writing to {filename} is not allowed' - if geninfo: pnginfo_data = PngImagePlugin.PngInfo() pnginfo_data.add_text('parameters', geninfo) image.save(filename, pnginfo=pnginfo_data) else: image.save(filename) - return [page.create_html(ui.tabname) for page in ui.stored_extra_pages] ui.button_save_preview.click( @@ -321,3 +279,24 @@ def setup_ui(ui, gallery): inputs=[ui.preview_target_filename, gallery, ui.preview_target_filename], outputs=[*ui.pages] ) + + # write description to a file + def save_description(filename,descrip): + lastDotIndex = filename.rindex('.') + filename = filename[0:lastDotIndex]+".description.txt" + if descrip != "": + try: + f = open(filename,'w', encoding='utf-8') + except OSError: + print ("Could not open file to write: " + filename) + with f: + f.write(descrip) + f.close() + return [page.create_html(ui.tabname) for page in ui.stored_extra_pages] + + ui.button_save_description.click( + fn=save_description, + _js="function(x,y){return [x,y]}", + inputs=[ui.description_target_filename, ui.description_input], + outputs=[*ui.pages] + ) diff --git a/modules/ui_postprocessing.py b/modules/ui_postprocessing.py index 243631d88..c5ddbfae5 100644 --- a/modules/ui_postprocessing.py +++ b/modules/ui_postprocessing.py @@ -41,8 +41,14 @@ def create_ui(): for tabname, button in buttons.items(): parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=generation_info, source_image_component=extras_image)) - def pretty_geninfo(generation_info): - return generation_info.replace(', ', '\n') + def pretty_geninfo(generation_info: str): + if generation_info is None: + return '' + sections = generation_info.split('Steps:') + if len(sections) > 1: + param = sections[0].strip() + '\nSteps:' + sections[1].strip().replace(', ', '\n') + return param + return generation_info tab_single.select(fn=lambda: 0, inputs=[], outputs=[tab_index]) tab_batch.select(fn=lambda: 1, inputs=[], outputs=[tab_index]) diff --git a/modules/upscaler.py b/modules/upscaler.py index 0376d256c..9ca6e4596 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -54,7 +54,7 @@ class Upscaler: dest_w = int(img.width * scale) dest_h = int(img.height * scale) - for i in range(3): + for _i in range(3): shape = (img.width, img.height) img = self.do_upscale(img, selected_model) @@ -74,7 +74,7 @@ class Upscaler: def load_model(self, path: str): pass - def find_models(self, ext_filter=None) -> list: + def find_models(self, ext_filter=None) -> list: # pylint: disable=unused-argument return modelloader.load_models(model_path=self.model_path, model_url=self.model_url, command_path=self.user_path) def update_status(self, prompt): @@ -107,7 +107,7 @@ class UpscalerNone(Upscaler): def do_upscale(self, img, selected_model=None): return img - def __init__(self, dirname=None): + def __init__(self, dirname=None): # pylint: disable=unused-argument super().__init__(False) self.scalers = [UpscalerData("None", None, self)] @@ -121,7 +121,7 @@ class UpscalerLanczos(Upscaler): def load_model(self, _): pass - def __init__(self, dirname=None): + def __init__(self, dirname=None): # pylint: disable=unused-argument super().__init__(False) self.name = "Lanczos" self.scalers = [UpscalerData("Lanczos", None, self)] @@ -136,7 +136,7 @@ class UpscalerNearest(Upscaler): def load_model(self, _): pass - def __init__(self, dirname=None): + def __init__(self, dirname=None): # pylint: disable=unused-argument super().__init__(False) self.name = "Nearest" self.scalers = [UpscalerData("Nearest", None, self)] diff --git a/script.js b/script.js index 03afe8445..9b0eebe03 100644 --- a/script.js +++ b/script.js @@ -1,7 +1,6 @@ function gradioApp() { const elems = document.getElementsByTagName('gradio-app') const elem = elems.length == 0 ? document : elems[0] - if (elem !== document) elem.getElementById = function(id){ return document.getElementById(id) } return elem.shadowRoot ? elem.shadowRoot : elem } @@ -34,12 +33,10 @@ function onOptionsChanged(callback){ } function runCallback(x, m){ - try { - x(m) - } catch (e) { - (console.error || console.log).call(console, e.message, e); - } + try { x(m) + } catch (e) { (console.error || console.log).call(console, e.message, e); } } + function executeCallbacks(queue, m) { queue.forEach(function(x){runCallback(x, m)}) } @@ -52,7 +49,6 @@ document.addEventListener("DOMContentLoaded", function() { executedOnLoaded = true; executeCallbacks(uiLoadedCallbacks); } - executeCallbacks(uiUpdateCallbacks, m); const newTab = get_uiCurrentTab(); if ( newTab && ( newTab !== uiCurrentTab ) ) { @@ -75,9 +71,7 @@ document.addEventListener('keydown', function(e) { } if (handled) { button = get_uiCurrentTabContent().querySelector('button[id$=_generate]'); - if (button) { - button.click(); - } + if (button) button.click(); e.preventDefault(); } }) @@ -87,18 +81,11 @@ document.addEventListener('keydown', function(e) { */ function uiElementIsVisible(el) { let isVisible = !el.closest('.\\!hidden'); - if ( ! isVisible ) { - return false; - } - + if (!isVisible) return false; while( isVisible = el.closest('.tabitem')?.style.display !== 'none' ) { - if ( ! isVisible ) { - return false; - } else if ( el.parentElement ) { - el = el.parentElement - } else { - break; - } + if ( ! isVisible ) return false; + else if ( el.parentElement ) el = el.parentElement + else break; } return isVisible; } diff --git a/scripts/img2imgalt.py b/scripts/img2imgalt.py index bb00fb3f1..3066100a2 100644 --- a/scripts/img2imgalt.py +++ b/scripts/img2imgalt.py @@ -1,19 +1,15 @@ from collections import namedtuple - import numpy as np from tqdm import trange - -import modules.scripts as scripts -import gradio as gr - -from modules import processing, shared, sd_samplers, sd_samplers_common - import torch import k_diffusion as K +import gradio as gr +import modules.scripts as scripts +from modules import processing, shared, sd_samplers, sd_samplers_common + def find_noise_for_image(p, cond, uncond, cfg_scale, steps): x = p.init_latent - s_in = x.new_ones([x.shape[0]]) if shared.sd_model.parameterization == "v": dnw = K.external.CompVisVDenoiser(shared.sd_model) @@ -22,40 +18,29 @@ def find_noise_for_image(p, cond, uncond, cfg_scale, steps): dnw = K.external.CompVisDenoiser(shared.sd_model) skip = 0 sigmas = dnw.get_sigmas(steps).flip(0) - shared.state.sampling_steps = steps for i in trange(1, len(sigmas)): shared.state.sampling_step += 1 - x_in = torch.cat([x] * 2) sigma_in = torch.cat([sigmas[i] * s_in] * 2) cond_in = torch.cat([uncond, cond]) - image_conditioning = torch.cat([p.image_conditioning] * 2) cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} - c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] t = dnw.sigma_to_t(sigma_in) - eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) - denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale - d = (x - denoised) / sigmas[i] dt = sigmas[i] - sigmas[i - 1] - x = x + d * dt - sd_samplers_common.store_latent(x) - # This shouldn't be necessary, but solved some VRAM issues del x_in, sigma_in, cond_in, c_out, c_in, t, del eps, denoised_uncond, denoised_cond, denoised, d, dt shared.state.nextjob() - return x / x.std() @@ -65,7 +50,6 @@ Cached = namedtuple("Cached", ["noise", "cfg_scale", "steps", "latent", "origina # Based on changes suggested by briansemrau in https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/736 def find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg_scale, steps): x = p.init_latent - s_in = x.new_ones([x.shape[0]]) if shared.sd_model.parameterization == "v": dnw = K.external.CompVisVDenoiser(shared.sd_model) @@ -79,42 +63,31 @@ def find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg_scale, steps): for i in trange(1, len(sigmas)): shared.state.sampling_step += 1 - x_in = torch.cat([x] * 2) sigma_in = torch.cat([sigmas[i - 1] * s_in] * 2) cond_in = torch.cat([uncond, cond]) - image_conditioning = torch.cat([p.image_conditioning] * 2) cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} - c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] - if i == 1: t = dnw.sigma_to_t(torch.cat([sigmas[i] * s_in] * 2)) else: t = dnw.sigma_to_t(sigma_in) - eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) - denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale - if i == 1: d = (x - denoised) / (2 * sigmas[i]) else: d = (x - denoised) / sigmas[i - 1] - dt = sigmas[i] - sigmas[i - 1] x = x + d * dt - sd_samplers_common.store_latent(x) - # This shouldn't be necessary, but solved some VRAM issues del x_in, sigma_in, cond_in, c_out, c_in, t, del eps, denoised_uncond, denoised_cond, denoised, d, dt shared.state.nextjob() - return x / sigmas[-1] @@ -123,7 +96,7 @@ class Script(scripts.Script): self.cache = None def title(self): - return "img2img alternative test" + return "Alternative" def show(self, is_img2img): return is_img2img @@ -132,24 +105,19 @@ class Script(scripts.Script): info = gr.Markdown(''' * `CFG Scale` should be 2 or lower. ''') - override_sampler = gr.Checkbox(label="Override `Sampling method` to Euler?(this method is built for it)", value=True, elem_id=self.elem_id("override_sampler")) - override_prompt = gr.Checkbox(label="Override `prompt` to the same value as `original prompt`?(and `negative prompt`)", value=True, elem_id=self.elem_id("override_prompt")) original_prompt = gr.Textbox(label="Original prompt", lines=1, elem_id=self.elem_id("original_prompt")) original_negative_prompt = gr.Textbox(label="Original negative prompt", lines=1, elem_id=self.elem_id("original_negative_prompt")) - override_steps = gr.Checkbox(label="Override `Sampling Steps` to the same value as `Decode steps`?", value=True, elem_id=self.elem_id("override_steps")) st = gr.Slider(label="Decode steps", minimum=1, maximum=150, step=1, value=50, elem_id=self.elem_id("st")) - override_strength = gr.Checkbox(label="Override `Denoising strength` to 1?", value=True, elem_id=self.elem_id("override_strength")) - cfg = gr.Slider(label="Decode CFG scale", minimum=0.0, maximum=15.0, step=0.1, value=1.0, elem_id=self.elem_id("cfg")) randomness = gr.Slider(label="Randomness", minimum=0.0, maximum=1.0, step=0.01, value=0.0, elem_id=self.elem_id("randomness")) sigma_adjustment = gr.Checkbox(label="Sigma adjustment for finding noise for image", value=False, elem_id=self.elem_id("sigma_adjustment")) return [ - info, + info, override_sampler, override_prompt, original_prompt, original_negative_prompt, override_steps, st, @@ -171,13 +139,11 @@ class Script(scripts.Script): def sample_extra(conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts): lat = (p.init_latent.cpu().numpy() * 10).astype(int) - same_params = self.cache is not None and self.cache.cfg_scale == cfg and self.cache.steps == st \ and self.cache.original_prompt == original_prompt \ and self.cache.original_negative_prompt == original_negative_prompt \ and self.cache.sigma_adjustment == sigma_adjustment same_everything = same_params and self.cache.latent.shape == lat.shape and np.abs(self.cache.latent-lat).sum() < 100 - if same_everything: rec_noise = self.cache.noise else: @@ -191,28 +157,20 @@ class Script(scripts.Script): self.cache = Cached(rec_noise, cfg, st, lat, original_prompt, original_negative_prompt, sigma_adjustment) rand_noise = processing.create_random_tensors(p.init_latent.shape[1:], seeds=seeds, subseeds=subseeds, subseed_strength=p.subseed_strength, seed_resize_from_h=p.seed_resize_from_h, seed_resize_from_w=p.seed_resize_from_w, p=p) - combined_noise = ((1 - randomness) * rec_noise + randomness * rand_noise) / ((randomness**2 + (1-randomness)**2) ** 0.5) - sampler = sd_samplers.create_sampler(p.sampler_name, p.sd_model) - sigmas = sampler.model_wrap.get_sigmas(p.steps) - noise_dt = combined_noise - (p.init_latent / sigmas[0]) - p.seed = p.seed + 1 - return sampler.sample_img2img(p, p.init_latent, noise_dt, conditioning, unconditional_conditioning, image_conditioning=p.image_conditioning) p.sample = sample_extra - p.extra_generation_params["Decode prompt"] = original_prompt p.extra_generation_params["Decode negative prompt"] = original_negative_prompt p.extra_generation_params["Decode CFG scale"] = cfg p.extra_generation_params["Decode steps"] = st p.extra_generation_params["Randomness"] = randomness p.extra_generation_params["Sigma Adjustment"] = sigma_adjustment - processed = processing.process_images(p) return processed diff --git a/scripts/outpainting_mk_2.py b/scripts/outpainting_mk_2.py index 670bb8ace..4e764fee4 100644 --- a/scripts/outpainting_mk_2.py +++ b/scripts/outpainting_mk_2.py @@ -120,7 +120,7 @@ def get_matched_noise(_np_src_image, np_mask_rgb, noise_q=1, color_variation=0.0 class Script(scripts.Script): def title(self): - return "Outpainting mk2" + return "Outpainting" def show(self, is_img2img): return is_img2img diff --git a/scripts/poor_mans_outpainting.py b/scripts/poor_mans_outpainting.py index ddcbd2d3a..e80478b7c 100644 --- a/scripts/poor_mans_outpainting.py +++ b/scripts/poor_mans_outpainting.py @@ -11,7 +11,7 @@ from modules.shared import opts, cmd_opts, state class Script(scripts.Script): def title(self): - return "Poor man's outpainting" + return "Outpainting alternative" def show(self, is_img2img): return is_img2img diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py index 7152797d2..b2f9c7408 100644 --- a/scripts/postprocessing_upscale.py +++ b/scripts/postprocessing_upscale.py @@ -4,8 +4,8 @@ import numpy as np from modules import scripts_postprocessing, shared import gradio as gr -from modules.ui_components import FormRow - +from modules.ui_components import FormRow, ToolButton +from modules.ui import switch_values_symbol upscale_cache = {} @@ -17,22 +17,28 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): def ui(self): selected_tab = gr.State(value=0) - with gr.Tabs(elem_id="extras_resize_mode"): - with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: - with FormRow(): - upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") - extras_upscaler_1 = gr.Dropdown(label='Upscaler', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value="SwinIR_4x") + with gr.Column(): + with FormRow(): + with gr.Tabs(elem_id="extras_resize_mode"): + with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") - with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: - with FormRow(): - upscaling_resize_w = gr.Number(label="Width", value=512, precision=0, elem_id="extras_upscaling_resize_w") - upscaling_resize_h = gr.Number(label="Height", value=512, precision=0, elem_id="extras_upscaling_resize_h") - upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") + with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: + with FormRow(): + with gr.Row(elem_id="upscaling_column_size", scale=4): + upscaling_resize_w = gr.Slider(minimum=64, maximum=4096, step=8, label="Width", value=512, elem_id="extras_upscaling_resize_w") + upscaling_resize_h = gr.Slider(minimum=64, maximum=4096, step=8, label="Height", value=512, elem_id="extras_upscaling_resize_h") + upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn") + upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") - with FormRow(): - extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, visible=False) - extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + with FormRow(): + extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + with FormRow(): + extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + + upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False) tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab]) tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab]) diff --git a/scripts/prompts_from_file.py b/scripts/prompts_from_file.py index e6d5c5a39..fe30d4b07 100644 --- a/scripts/prompts_from_file.py +++ b/scripts/prompts_from_file.py @@ -109,7 +109,7 @@ def load_prompt_file(file): class Script(scripts.Script): def title(self): - return "Prompts from file or textbox" + return "Prompts from file" def ui(self, is_img2img): checkbox_iterate = gr.Checkbox(label="Iterate seed every line", value=False, elem_id=self.elem_id("checkbox_iterate")) @@ -130,8 +130,6 @@ class Script(scripts.Script): lines = [x.strip() for x in prompt_txt.splitlines()] lines = [x for x in lines if len(x) > 0] - p.do_not_save_grid = True - job_count = 0 jobs = [] diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 52ae1c6e1..748fe7bbc 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -1,26 +1,18 @@ +import re +import csv +import random from collections import namedtuple from copy import copy from itertools import permutations, chain -import random -import csv from io import StringIO from PIL import Image import numpy as np - -import modules.scripts as scripts import gradio as gr - -from modules import images, paths, sd_samplers, processing, sd_models, sd_vae -from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img -from modules.shared import opts, cmd_opts, state +import modules.scripts as scripts import modules.shared as shared -import modules.sd_samplers -import modules.sd_models -import modules.sd_vae -import glob -import os -import re - +from modules import images, sd_samplers, processing, sd_models, sd_vae +from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img +from modules.shared import opts, state from modules.ui_components import ToolButton fill_values_symbol = "\U0001f4d2" # 📒 @@ -83,15 +75,15 @@ def confirm_samplers(p, xs): def apply_checkpoint(p, x, xs): - info = modules.sd_models.get_closet_checkpoint_match(x) + info = sd_models.get_closet_checkpoint_match(x) if info is None: raise RuntimeError(f"Unknown checkpoint: {x}") - modules.sd_models.reload_model_weights(shared.sd_model, info) + sd_models.reload_model_weights(shared.sd_model, info) def confirm_checkpoints(p, xs): for x in xs: - if modules.sd_models.get_closet_checkpoint_match(x) is None: + if sd_models.get_closet_checkpoint_match(x) is None: raise RuntimeError(f"Unknown checkpoint: {x}") @@ -108,26 +100,34 @@ def apply_upscale_latent_space(p, x, xs): def find_vae(name: str): if name.lower() in ['auto', 'automatic']: - return modules.sd_vae.unspecified + return sd_vae.unspecified if name.lower() == 'none': return None else: - choices = [x for x in sorted(modules.sd_vae.vae_dict, key=lambda x: len(x)) if name.lower().strip() in x.lower()] + choices = [x for x in sorted(sd_vae.vae_dict, key=lambda x: len(x)) if name.lower().strip() in x.lower()] if len(choices) == 0: print(f"No VAE found for {name}; using automatic") - return modules.sd_vae.unspecified + return sd_vae.unspecified else: - return modules.sd_vae.vae_dict[choices[0]] + return sd_vae.vae_dict[choices[0]] def apply_vae(p, x, xs): - modules.sd_vae.reload_vae_weights(shared.sd_model, vae_file=find_vae(x)) + sd_vae.reload_vae_weights(shared.sd_model, vae_file=find_vae(x)) def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): p.styles.extend(x.split(',')) +def apply_fallback(p, x, xs): + sampler_name = sd_samplers.samplers_map.get(x.lower(), None) + if sampler_name is None: + raise RuntimeError(f"Unknown sampler: {x}") + + opts.data["xyz_fallback_sampler"] = sampler_name + + def apply_uni_pc_order(p, x, xs): opts.data["uni_pc_order"] = min(x, p.steps - 1) @@ -220,6 +220,7 @@ axis_options = [ AxisOption("Clip skip", int, apply_clip_skip), AxisOption("Denoising", float, apply_field("denoising_strength")), AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), + AxisOptionTxt2Img("Fallback latent upscaler sampler", str, apply_fallback, format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers]), AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: list(sd_vae.vae_dict)), AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), @@ -332,7 +333,6 @@ def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend if draw_legend: z_grid = images.draw_grid_annotations(z_grid, sub_grid_size[0], sub_grid_size[1], title_texts, [[images.GridAnnotation()]]) processed_result.images.insert(0, z_grid) - #TODO: Deeper aspects of the program rely on grid info being misaligned between metadata arrays, which is not ideal. #processed_result.all_prompts.insert(0, processed_result.all_prompts[0]) #processed_result.all_seeds.insert(0, processed_result.all_seeds[0]) processed_result.infotexts.insert(0, processed_result.infotexts[0]) @@ -345,12 +345,12 @@ class SharedSettingsStackHelper(object): self.CLIP_stop_at_last_layers = opts.CLIP_stop_at_last_layers self.vae = opts.sd_vae self.uni_pc_order = opts.uni_pc_order - + def __exit__(self, exc_type, exc_value, tb): opts.data["sd_vae"] = self.vae opts.data["uni_pc_order"] = self.uni_pc_order - modules.sd_models.reload_model_weights() - modules.sd_vae.reload_vae_weights() + sd_models.reload_model_weights() + sd_vae.reload_vae_weights() opts.data["CLIP_stop_at_last_layers"] = self.CLIP_stop_at_last_layers @@ -390,15 +390,13 @@ class Script(scripts.Script): fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False) with gr.Row(variant="compact", elem_id="axis_options"): - with gr.Column(): - draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) - no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) - with gr.Column(): - include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) - include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) - with gr.Column(): - margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) - + draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) + include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + with gr.Row(variant="compact", elem_id="axis_options"): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + with gr.Row(variant="compact", elem_id="swap_axes"): swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button") swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button") @@ -428,6 +426,9 @@ class Script(scripts.Script): current_values = axis_values_dropdown if has_choices: choices = choices() + if len(choices) > 12: + has_choices = False + if has_choices: if isinstance(current_values,str): current_values = current_values.split(",") current_values = list(filter(lambda x: x in choices, current_values)) @@ -459,7 +460,7 @@ class Script(scripts.Script): def run(self, p, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size): if not no_fixed_seeds: - modules.processing.fix_seed(p) + processing.fix_seed(p) if not opts.return_grid: p.batch_size = 1 @@ -489,7 +490,7 @@ class Script(scripts.Script): start = int(mc.group(1)) end = int(mc.group(2)) num = int(mc.group(3)) if mc.group(3) is not None else 1 - + valslist_ext += [int(x) for x in np.linspace(start=start, stop=end, num=num).tolist()] else: valslist_ext.append(val) @@ -511,7 +512,7 @@ class Script(scripts.Script): start = float(mc.group(1)) end = float(mc.group(2)) num = int(mc.group(3)) if mc.group(3) is not None else 1 - + valslist_ext += np.linspace(start=start, stop=end, num=num).tolist() else: valslist_ext.append(val) @@ -699,13 +700,12 @@ class Script(scripts.Script): # Auto-save main and sub-grids: grid_count = z_count + 1 if z_count > 1 else 1 for g in range(grid_count): - #TODO: See previous comment about intentional data misalignment. adj_g = g-1 if g > 0 else g images.save_image(processed.images[g], p.outpath_grids, "xyz_grid", info=processed.infotexts[g], extension=opts.grid_format, prompt=processed.all_prompts[adj_g], seed=processed.all_seeds[adj_g], grid=True, p=processed) if not include_sub_grids: # Done with sub-grids, drop all related information: - for sg in range(z_count): + for _sg in range(z_count): del processed.images[1] del processed.all_prompts[1] del processed.all_seeds[1] diff --git a/setup.py b/setup.py index dda7a4423..615ac49d4 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ def setup_logging(clean=False): rh.set_name(logging.DEBUG if args.debug else logging.INFO) log.addHandler(rh) + # check if package is installed def installed(package, friendly: str = None): import pkg_resources @@ -392,7 +393,8 @@ def set_environment(): os.environ.setdefault('GRADIO_ANALYTICS_ENABLED', 'False') os.environ.setdefault('SAFETENSORS_FAST_GPU', '1') os.environ.setdefault('NUMEXPR_MAX_THREADS', '16') - os.environ.setdefault('PYTORCH_ENABLE_MPS_FALLBACK', '1') + if sys.platform == 'darwin': + os.environ.setdefault('PYTORCH_ENABLE_MPS_FALLBACK', '1') def check_extensions(): @@ -405,6 +407,9 @@ def check_extensions(): for ext in extensions: newest = 0 extension_dir = os.path.join(folder, ext) + if not os.path.isdir(extension_dir): + log.debug(f'Extension listed as installed but folder missing: {extension_dir}') + continue for f in os.listdir(extension_dir): if '.json' in f or '.csv' in f or '__pycache__' in f: continue @@ -496,12 +501,9 @@ def check_timestamp(): return ok -def parse_args(): - # command line args - # parser = argparse.ArgumentParser(description = 'Setup for SD WebUI') - if vars(parser)['_option_string_actions'].get('--debug', None) is not None: - return - parser.add_argument('--debug', default = False, action='store_true', help = "Run installer with debug logging, default: %(default)s") +def add_args(): + if vars(parser)['_option_string_actions'].get('--debug', None) is None: + parser.add_argument('--debug', default = False, action='store_true', help = "Run installer with debug logging, default: %(default)s") parser.add_argument('--reset', default = False, action='store_true', help = "Reset main repository to latest version, default: %(default)s") parser.add_argument('--upgrade', default = False, action='store_true', help = "Upgrade main repository to latest version, default: %(default)s") parser.add_argument('--noupdate', default = False, action='store_true', help = "Skip update of extensions and submodules, default: %(default)s") @@ -510,6 +512,10 @@ def parse_args(): parser.add_argument('--skip-extensions', default = False, action='store_true', help = "Skips running individual extension installers, default: %(default)s") parser.add_argument('--skip-git', default = False, action='store_true', help = "Skips running all GIT operations, default: %(default)s") parser.add_argument('--experimental', default = False, action='store_true', help = "Allow unsupported versions of libraries, default: %(default)s") + + +def parse_args(): + # command line args global args # pylint: disable=global-statement args = parser.parse_args() diff --git a/style.css b/style.css index 845f6832d..996ad15d2 100644 --- a/style.css +++ b/style.css @@ -1,165 +1,36 @@ - -/* general gradio fixes */ - -:root, .dark{ - --checkbox-label-gap: 0.25em 0.1em; - --section-header-text-size: 12pt; - --block-background-fill: transparent; -} - -.block.padded:not(.gradio-accordion) { - padding: 0 !important; -} - -div.gradio-container{ - max-width: unset !important; -} - -.hidden{ - display: none; -} - -.compact{ - background: transparent !important; - padding: 0 !important; -} - -div.form{ - border-width: 0; - box-shadow: none; - background: transparent; - overflow: visible; - gap: 0.5em; -} - -.block.gradio-dropdown, -.block.gradio-slider, -.block.gradio-checkbox, -.block.gradio-textbox, -.block.gradio-radio, -.block.gradio-checkboxgroup, -.block.gradio-number, -.block.gradio-colorpicker -{ - border-width: 0 !important; - box-shadow: none !important; -} - -.gap.compact{ - padding: 0; - gap: 0.2em 0; -} - -div.compact{ - gap: 1em; -} - -.gradio-dropdown label span:not(.has-info), -.gradio-textbox label span:not(.has-info), -.gradio-number label span:not(.has-info) -{ - margin-bottom: 0; -} - -.gradio-dropdown ul.options{ - z-index: 3000; - min-width: fit-content; - max-width: inherit; - white-space: nowrap; -} - -.gradio-dropdown ul.options li.item { - padding: 0.05em 0; -} - -.gradio-dropdown ul.options li.item:not(:has(.hide)) { - background-color: var(--neutral-100); -} - -.dark .gradio-dropdown ul.options li.item:not(:has(.hide)) { - background-color: var(--neutral-900); -} - -.gradio-dropdown div.wrap.wrap.wrap.wrap{ - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); -} - -.gradio-dropdown:not(.multiselect) .wrap-inner.wrap-inner.wrap-inner{ - flex-wrap: unset; -} - -.gradio-dropdown .single-select{ - white-space: nowrap; - overflow: hidden; -} - -.gradio-dropdown .token-remove.remove-all.remove-all{ - display: none; -} - -.gradio-dropdown.multiselect .token-remove.remove-all.remove-all{ - display: flex; -} - -.gradio-slider input[type="number"]{ - width: 6em; -} - -.block.gradio-checkbox { - margin: 0.75em 1.5em 0 0; -} - -.gradio-html div.wrap{ - height: 100%; -} -div.gradio-html.min{ - min-height: 0; -} - -.block.gradio-gallery{ - background: var(--input-background-fill); -} - -.gradio-container .prose a, .gradio-container .prose a:visited{ - color: unset; - text-decoration: none; -} - - +:root, .dark{ --checkbox-label-gap: 0.25em 0.1em; --section-header-text-size: 12pt; --block-background-fill: transparent;} +.block.padded:not(.gradio-accordion) { padding: 0 !important; } +div.gradio-container{ max-width: unset !important; } +.hidden{ display: none; } +.compact{ background: transparent !important; padding: 0 !important; } +div.form{ border-width: 0; box-shadow: none; background: transparent; overflow: visible; gap: 0.5em; } +.block.gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important;} +.gap.compact{ padding: 0; gap: 0.2em 0; } +div.compact{ gap: 1em; } +.gradio-dropdown label span:not(.has-info), .gradio-textbox label span:not(.has-info), .gradio-number label span:not(.has-info) { margin-bottom: 0; } +.gradio-dropdown ul.options{ z-index: 3000; min-width: fit-content; max-width: inherit; white-space: nowrap; } +.gradio-dropdown ul.options li.item { padding: 0.05em 0; } +.gradio-dropdown ul.options li.item:not(:has(.hide)) { background-color: var(--neutral-100); } +.dark .gradio-dropdown ul.options li.item:not(:has(.hide)) { background-color: var(--neutral-900); } +.gradio-dropdown div.wrap.wrap.wrap.wrap{ box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } +.gradio-dropdown:not(.multiselect) .wrap-inner.wrap-inner.wrap-inner{ flex-wrap: unset; } +.gradio-dropdown .single-select{ white-space: nowrap; overflow: hidden; } +.gradio-dropdown .token-remove.remove-all.remove-all{ display: none; } +.gradio-dropdown.multiselect .token-remove.remove-all.remove-all{ display: flex; } +.gradio-slider input[type="number"]{ width: 6em; } +.block.gradio-checkbox { margin: 0.75em 1.5em 0 0; } +.gradio-html div.wrap{ height: 100%; } +div.gradio-html.min{ min-height: 0; } +.block.gradio-gallery{ background: var(--input-background-fill); } +.gradio-container .prose a, .gradio-container .prose a:visited{ color: unset; text-decoration: none; } /* general styled components */ - -.gradio-button.tool{ - max-width: 2.2em; - min-width: 2.2em !important; - height: 2.4em; - align-self: end; - line-height: 1em; - border-radius: 0.5em; -} - -.gradio-button.secondary-down{ - background: var(--button-secondary-background-fill); - color: var(--button-secondary-text-color); -} -.gradio-button.secondary-down, .gradio-button.secondary-down:hover{ - box-shadow: 1px 1px 1px rgba(0,0,0,0.25) inset, 0px 0px 3px rgba(0,0,0,0.15) inset; -} -.gradio-button.secondary-down:hover{ - background: var(--button-secondary-background-fill-hover); - color: var(--button-secondary-text-color-hover); -} - -.checkboxes-row{ - margin-bottom: 0.5em; - margin-left: 0em; -} -.checkboxes-row > div{ - flex: 0; - white-space: nowrap; - min-width: auto; -} - +.gradio-button.tool{ max-width: 2.2em; min-width: 2.2em !important; height: 2.4em; align-self: end; line-height: 1em; border-radius: 0.5em; } +.gradio-button.secondary-down{ background: var(--button-secondary-background-fill); color: var(--button-secondary-text-color); } +.gradio-button.secondary-down, .gradio-button.secondary-down:hover{ box-shadow: 1px 1px 1px rgba(0,0,0,0.25) inset, 0px 0px 3px rgba(0,0,0,0.15) inset; } +.gradio-button.secondary-down:hover{ background: var(--button-secondary-background-fill-hover); color: var(--button-secondary-text-color-hover); } +.checkboxes-row{ margin-bottom: 0.5em; margin-left: 0em; } +.checkboxes-row > div{ flex: 0; white-space: nowrap; min-width: auto; } button.custom-button{ border-radius: var(--button-large-radius); padding: var(--button-large-padding); @@ -176,9 +47,7 @@ button.custom-button{ text-align: center; } - /* txt2img/img2img specific */ - .block.token-counter{ position: absolute; display: inline-block; @@ -201,13 +70,8 @@ button.custom-button{ border: 2px solid rgba(255,0,0,0.4) !important; } -.block.token-counter div{ - display: inline; -} - -.block.token-counter span{ - padding: 0.1em 0.75em; -} +.block.token-counter div{ display: inline; } +.block.token-counter span{ padding: 0.1em 0.75em; } [id$=_subseed_show]{ min-width: auto !important; @@ -309,6 +173,10 @@ div.dimensions-tools{ align-content: center; } +div#extras_scale_to_tab div.form{ + flex-direction: row; +} + #mode_img2img .gradio-image > div.fixed-height, #mode_img2img .gradio-image > div.fixed-height img{ height: 480px !important; max-height: 480px !important; @@ -638,47 +506,15 @@ footer { } /* extra networks UI */ +.extra-networks > div > [id *= '_extra_']{ margin: 0.3em; } +.extra-network-subdirs{ padding: 0.2em 0.35em; } +.extra-network-subdirs button{ margin: 0 0.15em; } +.extra-networks .tab-nav .search{ display: inline-block; max-width: 16em; margin: 0.3em; align-self: center; width: 16em; } +#txt2img_extra_view, #img2img_extra_view { width: auto; } +.extra-network-cards .nocards, .extra-network-thumbs .nocards{ margin: 1.25em 0.5em 0.5em 0.5em; } +.extra-network-cards .nocards h1, .extra-network-thumbs .nocards h1{ font-size: 1.5em; margin-bottom: 1em; } +.extra-network-cards .nocards li, .extra-network-thumbs .nocards li{ margin-left: 0.5em; } -.extra-networks > div > [id *= '_extra_']{ - margin: 0.3em; -} - -.extra-network-subdirs{ - padding: 0.2em 0.35em; -} - -.extra-network-subdirs button{ - margin: 0 0.15em; -} -.extra-networks .tab-nav .search{ - display: inline-block; - max-width: 16em; - margin: 0.3em; - align-self: center; - width: 16em; -} - -#txt2img_extra_view, #img2img_extra_view { - width: auto; -} - -.extra-network-cards .nocards, .extra-network-thumbs .nocards{ - margin: 1.25em 0.5em 0.5em 0.5em; -} - -.extra-network-cards .nocards h1, .extra-network-thumbs .nocards h1{ - font-size: 1.5em; - margin-bottom: 1em; -} - -.extra-network-cards .nocards li, .extra-network-thumbs .nocards li{ - margin-left: 0.5em; -} - - -.extra-network-cards .card .metadata-button:before, .extra-network-thumbs .card .metadata-button:before{ - content: "🛈"; -} .extra-network-cards .card .metadata-button, .extra-network-thumbs .card .metadata-button{ display: none; position: absolute; @@ -689,23 +525,14 @@ footer { font-size: 22pt; width: 1.5em; } -.extra-network-cards .card:hover .metadata-button, .extra-network-thumbs .card:hover .metadata-button{ - display: inline-block; -} -.extra-network-cards .card .metadata-button:hover, .extra-network-thumbs .card .metadata-button:hover{ - color: red; -} - - -.extra-network-thumbs { - display: flex; - flex-flow: row wrap; - gap: 10px; -} +.extra-network-cards .card:hover .metadata-button, .extra-network-thumbs .card:hover .metadata-button{ display: inline-block; } +.extra-network-thumbs { display: flex; flex-flow: row wrap; gap: 10px; } +.extra-network-cards .card .additional a:hover, .extra-network-thumbs .card .additional a:hover { color: darkorange } .extra-network-thumbs .card { - height: 6em; - width: 6em; + display: inline-block; + height: 9em; + width: 9em; cursor: pointer; background-image: url('./file=html/card-no-preview.png'); background-size: cover; @@ -713,25 +540,8 @@ footer { position: relative; } -.extra-network-thumbs .card:hover .additional a { - display: inline-block; -} - -.extra-network-thumbs .actions .additional a { - background-image: url('./file=html/image-update.svg'); - background-repeat: no-repeat; - background-size: cover; - background-position: center center; - position: absolute; - top: 0; - left: 0; - width: 24px; - height: 24px; - display: none; - font-size: 0; - text-align: -9999; -} - +.extra-network-cards .card .additional, .extra-network-thumbs .card .additional { white-space: nowrap; overflow: hidden; } +.extra-network-thumbs .card:hover .additional a { display: inline-block; } .extra-network-thumbs .actions .name { position: absolute; bottom: 0; @@ -745,11 +555,7 @@ footer { color: white; } -.extra-network-thumbs .card:hover .actions .name { - white-space: normal; - word-break: break-all; -} - +.extra-network-thumbs .card:hover .actions .name { white-space: normal; word-break: break-all; } .extra-network-cards .card{ display: inline-block; margin: 0.5em; @@ -758,22 +564,15 @@ footer { box-shadow: 0 0 5px rgba(128, 128, 128, 0.5); border-radius: 0.2em; position: relative; - background-size: auto 100%; background-position: center; overflow: hidden; cursor: pointer; - background-image: url('./file=html/card-no-preview.png') } -.extra-network-cards .card:hover{ - box-shadow: 0 0 2px 0.3em rgba(0, 128, 255, 0.35); -} - -.extra-network-cards .card .actions .additional{ - display: none; -} +.extra-network-cards .card:hover { box-shadow: 0 0 2px 0.3em rgba(0, 128, 255, 0.35); } +.extra-network-cards .card .actions .additional, .extra-network-thumbs .card .actions .additional{ display: none; } .extra-network-cards .card .actions{ position: absolute; @@ -786,58 +585,13 @@ footer { text-shadow: 0 0 0.2em black; } -.extra-network-cards .card .actions *{ - color: white; -} - -.extra-network-cards .card .actions:hover{ - box-shadow: 0 0 0.75em 0.75em rgba(0,0,0,0.5) !important; -} - -.extra-network-cards .card .actions .name{ - font-size: 1.7em; - font-weight: bold; - line-break: anywhere; -} - -.extra-network-cards .card .actions .description { - display: block; - max-height: 3em; - white-space: pre-wrap; - line-height: 1.1; -} - -.extra-network-cards .card .actions .description:hover { - max-height: none; -} - -.extra-network-cards .card .actions:hover .additional{ - display: block; -} - -.extra-network-cards .card ul{ - margin: 0.25em 0 0.75em 0.25em; - cursor: unset; -} - -.extra-network-cards .card ul a{ - cursor: pointer; -} - -.extra-network-cards .card ul a:hover{ - color: red; -} - -.theme-preview { - display: none; - position: fixed; - border: 4px solid var(--neutral-600); - box-shadow: 2px 2px 2px 2px var(--neutral-700); - top: 0; - bottom: 0; - left: 0; - right: 0; - margin: auto; - max-width: 75vw; - z-index: 999; -} +.extra-network-cards .card .actions *{ color: white; } +.extra-network-cards .card .actions:hover { box-shadow: 0 0 0.75em 0.75em rgba(0,0,0,0.5) !important; } +.extra-network-cards .card .actions .name { font-size: 1.7em; font-weight: bold; line-break: anywhere; } +.extra-network-cards .card .actions .description { display: block; max-height: 3em; white-space: pre-wrap; line-height: 1.1; } +.extra-network-cards .card .actions .description:hover { max-height: none; } +.extra-network-cards .card .actions:hover .additional, .extra-network-thumbs .card:hover .additional{ display: block; } +.extra-network-cards .card ul{ margin: 0.25em 0 0.75em 0.25em; cursor: unset; } +.extra-network-cards .card ul a{ cursor: pointer; } +.extra-network-cards .card ul a:hover{ color: red; } +.theme-preview { display: none; position: fixed; border: 4px solid var(--neutral-600); box-shadow: 2px 2px 2px 2px var(--neutral-700); top: 0; bottom: 0; left: 0; right: 0; margin: auto; max-width: 75vw; z-index: 999; } diff --git a/webui.bat b/webui.bat index 209d972bd..6a6edcf10 100755 --- a/webui.bat +++ b/webui.bat @@ -2,10 +2,7 @@ if not defined PYTHON (set PYTHON=python) if not defined VENV_DIR (set "VENV_DIR=%~dp0%venv") - - set ERROR_REPORTING=FALSE - mkdir tmp 2>NUL %PYTHON% -c "" >tmp/stdout.txt 2>tmp/stderr.txt @@ -38,14 +35,14 @@ goto :show_stdout_stderr :activate_venv set PYTHON="%VENV_DIR%\Scripts\Python.exe" -echo venv %PYTHON% +echo Using VENV: %VENV_DIR% :skip_venv if [%ACCELERATE%] == ["True"] goto :accelerate goto :launch :accelerate -echo Checking for accelerate +echo Checking for accelerate: %ACCELERATE% set ACCELERATE="%VENV_DIR%\Scripts\accelerate.exe" if EXIST %ACCELERATE% goto :accelerate_launch @@ -56,7 +53,7 @@ exit /b :accelerate_launch echo Accelerating -%ACCELERATE% launch --num_cpu_threads_per_process=6 launch.py +%ACCELERATE% launch --num_cpu_threads_per_process=6 launch.py %* pause exit /b diff --git a/webui.py b/webui.py index ac097efc0..54d159d24 100644 --- a/webui.py +++ b/webui.py @@ -1,9 +1,11 @@ import os import re +import sys import time import signal -import warnings +import asyncio import logging +import warnings from rich import print # pylint: disable=W0622 from modules import timer, errors @@ -66,10 +68,13 @@ else: def check_rollback_vae(): if shared.cmd_opts.rollback_vae: - if not torch.__version__.startswith('2.1'): + if not torch.cuda.is_available(): + print("Rollback VAE functionality requires compatible GPU") + shared.cmd_opts.rollback_vae = False + elif not torch.__version__.startswith('2.1'): print("Rollback VAE functionality requires Torch 2.1 or higher") shared.cmd_opts.rollback_vae = False - if 0 < torch.cuda.get_device_capability()[0] < 8: + elif 0 < torch.cuda.get_device_capability()[0] < 8: print('Rollback VAE functionality device capabilities not met') shared.cmd_opts.rollback_vae = False @@ -90,9 +95,6 @@ def initialize(): gfpgan.setup_model(opts.gfpgan_models_path) startup_timer.record("gfpgan") - modelloader.list_builtin_upscalers() - startup_timer.record("upscalers") - modules.scripts.load_scripts() startup_timer.record("scripts") @@ -163,9 +165,25 @@ def create_api(app): return api +def async_policy(): + _BasePolicy = asyncio.WindowsSelectorEventLoopPolicy if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy") else asyncio.DefaultEventLoopPolicy + + class AnyThreadEventLoopPolicy(_BasePolicy): + def get_event_loop(self) -> asyncio.AbstractEventLoop: + try: + return super().get_event_loop() + except (RuntimeError, AssertionError): + loop = self.new_event_loop() + self.set_event_loop(loop) + return loop + + asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy()) + + def start_ui(): logging.disable(logging.INFO) create_paths(opts) + async_policy() initialize() if shared.opts.clean_temp_dir_at_start: ui_tempdir.cleanup_tmpdr() diff --git a/webui.sh b/webui.sh index ebbe586fb..ff9c58b4d 100755 --- a/webui.sh +++ b/webui.sh @@ -4,34 +4,19 @@ # change the variables in webui-user.sh instead # ################################################# -# If run from macOS, load defaults from webui-macos-env.sh -if [[ "$OSTYPE" == "darwin"* ]]; then - if [[ -f webui-macos-env.sh ]] - then - source ./webui-macos-env.sh - fi -fi +# change to local directory +cd -- "$(dirname -- "$0")" + +can_run_as_root=0 +export ERROR_REPORTING=FALSE +export PIP_IGNORE_INSTALLED=0 # Read variables from webui-user.sh -# shellcheck source=/dev/null if [[ -f webui-user.sh ]] then source ./webui-user.sh fi -# Set defaults -# Install directory without trailing slash -if [[ -z "${install_dir}" ]] -then - install_dir="${HOME}" -fi - -# Name of the subdirectory (defaults to stable-diffusion-webui) -if [[ -z "${clone_dir}" ]] -then - clone_dir="stable-diffusion-webui" -fi - # python3 executable if [[ -z "${python_cmd}" ]] then @@ -44,19 +29,11 @@ then export GIT="git" fi -# python3 venv without trailing slash (defaults to ${install_dir}/${clone_dir}/venv) if [[ -z "${venv_dir}" ]] then venv_dir="venv" fi -if [[ -z "${LAUNCH_SCRIPT}" ]] -then - LAUNCH_SCRIPT="launch.py" -fi - -# this script cannot be run as root by default -can_run_as_root=0 # read any command line flags to the webui.sh script while getopts "f" flag > /dev/null 2>&1 @@ -67,102 +44,48 @@ do esac done -# Disable sentry logging -export ERROR_REPORTING=FALSE - -# Do not reinstall existing pip packages on Debian/Ubuntu -export PIP_IGNORE_INSTALLED=0 - -# Pretty print -delimiter="################################################################" - -printf "\n%s\n" "${delimiter}" -printf "\e[1m\e[32mInstall script for stable-diffusion + Web UI\n" -printf "\e[1m\e[34mTested on Debian 11 (Bullseye)\e[0m" -printf "\n%s\n" "${delimiter}" - # Do not run as root if [[ $(id -u) -eq 0 && can_run_as_root -eq 0 ]] then - printf "\n%s\n" "${delimiter}" - printf "\e[1m\e[31mERROR: This script must not be launched as root, aborting...\e[0m" - printf "\n%s\n" "${delimiter}" + echo "Cannot run as root" exit 1 -else - printf "\n%s\n" "${delimiter}" - printf "Running on \e[1m\e[32m%s\e[0m user" "$(whoami)" - printf "\n%s\n" "${delimiter}" -fi - -if [[ -d .git ]] -then - printf "\n%s\n" "${delimiter}" - printf "Repo already cloned, using it as install directory" - printf "\n%s\n" "${delimiter}" - install_dir="${PWD}/../" - clone_dir="${PWD##*/}" fi for preq in "${GIT}" "${python_cmd}" do if ! hash "${preq}" &>/dev/null then - printf "\n%s\n" "${delimiter}" - printf "\e[1m\e[31mERROR: %s is not installed, aborting...\e[0m" "${preq}" - printf "\n%s\n" "${delimiter}" + printf "Error: %s is not installed, aborting...\n" "${preq}" exit 1 fi done if ! "${python_cmd}" -c "import venv" &>/dev/null then - printf "\n%s\n" "${delimiter}" - printf "\e[1m\e[31mERROR: python3-venv is not installed, aborting...\e[0m" - printf "\n%s\n" "${delimiter}" + echo "Error: python3-venv is not installed" exit 1 fi -cd "${install_dir}"/ || { printf "\e[1m\e[31mERROR: Can't cd to %s/, aborting...\e[0m" "${install_dir}"; exit 1; } -if [[ -d "${clone_dir}" ]] -then - cd "${clone_dir}"/ || { printf "\e[1m\e[31mERROR: Can't cd to %s/%s/, aborting...\e[0m" "${install_dir}" "${clone_dir}"; exit 1; } -else - printf "\n%s\n" "${delimiter}" - printf "Clone stable-diffusion-webui" - printf "\n%s\n" "${delimiter}" - "${GIT}" clone https://github.com/vladmandic/automatic.git "${clone_dir}" - cd "${clone_dir}"/ || { printf "\e[1m\e[31mERROR: Can't cd to %s/%s/, aborting...\e[0m" "${install_dir}" "${clone_dir}"; exit 1; } -fi - -printf "\n%s\n" "${delimiter}" -printf "Create and activate python venv" -printf "\n%s\n" "${delimiter}" -cd "${install_dir}"/"${clone_dir}"/ || { printf "\e[1m\e[31mERROR: Can't cd to %s/%s/, aborting...\e[0m" "${install_dir}" "${clone_dir}"; exit 1; } +echo "Create and activate python venv" if [[ ! -d "${venv_dir}" ]] then "${python_cmd}" -m venv "${venv_dir}" first_launch=1 fi -# shellcheck source=/dev/null + if [[ -f "${venv_dir}"/bin/activate ]] then source "${venv_dir}"/bin/activate else - printf "\n%s\n" "${delimiter}" - printf "\e[1m\e[31mERROR: Cannot activate python venv, aborting...\e[0m" - printf "\n%s\n" "${delimiter}" + echo "Error: Cannot activate python venv" exit 1 fi if [[ ! -z "${ACCELERATE}" ]] && [ ${ACCELERATE}="True" ] && [ -x "$(command -v accelerate)" ] then - printf "\n%s\n" "${delimiter}" - printf "Accelerating launch.py..." - printf "\n%s\n" "${delimiter}" - exec accelerate launch --num_cpu_threads_per_process=6 "${LAUNCH_SCRIPT}" "$@" + echo "Accelerating launch.py..." + exec accelerate launch --num_cpu_threads_per_process=6 launch.py "$@" else - printf "\n%s\n" "${delimiter}" - printf "Launching launch.py..." - printf "\n%s\n" "${delimiter}" - exec "${python_cmd}" "${LAUNCH_SCRIPT}" "$@" + echo "Launching launch.py..." + exec "${python_cmd}" launch.py "$@" fi