diff --git a/CODEOWNERS b/CODEOWNERS index 7438c9bc69d..4eb946b009d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,12 +1 @@ -* @AUTOMATIC1111 - -# if you were managing a localization and were removed from this file, this is because -# the intended way to do localizations now is via extensions. See: -# https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Developing-extensions -# Make a repo with your localization and since you are still listed as a collaborator -# you can add it to the wiki page yourself. This change is because some people complained -# the git commit log is cluttered with things unrelated to almost everyone and -# because I believe this is the best overall for the project to handle localizations almost -# entirely without my oversight. - - +* @AUTOMATIC1111 @w-e-w @catboxanon diff --git a/README.md b/README.md index bc62945c0c5..007f590d249 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ python_cmd="python3.11" 2. Navigate to the directory you would like the webui to be installed and execute the following command: ```bash wget -q https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui/master/webui.sh +chmod +x webui.sh ``` Or just clone the repo wherever you want: ```bash diff --git a/configs/sd_xl_v.yaml b/configs/sd_xl_v.yaml new file mode 100644 index 00000000000..9fbcbbac8b2 --- /dev/null +++ b/configs/sd_xl_v.yaml @@ -0,0 +1,98 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.13025 + disable_first_stage_autocast: True + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.DiscreteDenoiser + params: + num_idx: 1000 + + weighting_config: + target: sgm.modules.diffusionmodules.denoiser_weighting.VWeighting + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.VScaling + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization + + network_config: + target: sgm.modules.diffusionmodules.openaimodel.UNetModel + params: + adm_in_channels: 2816 + num_classes: sequential + use_checkpoint: False + in_channels: 4 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2] + num_res_blocks: 2 + channel_mult: [1, 2, 4] + num_head_channels: 64 + use_spatial_transformer: True + use_linear_in_transformer: True + transformer_depth: [1, 2, 10] # note: the first is unused (due to attn_res starting at 2) 32, 16, 8 --> 64, 32, 16 + context_dim: 2048 + spatial_transformer_attn_type: softmax-xformers + legacy: False + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + # crossattn cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenCLIPEmbedder + params: + layer: hidden + layer_idx: 11 + # crossattn and vector cond + - is_trainable: False + input_key: txt + target: sgm.modules.encoders.modules.FrozenOpenCLIPEmbedder2 + params: + arch: ViT-bigG-14 + version: laion2b_s39b_b160k + freeze: True + layer: penultimate + always_return_pooled: True + legacy: False + # vector cond + - is_trainable: False + input_key: original_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: crop_coords_top_left + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + # vector cond + - is_trainable: False + input_key: target_size_as_tuple + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 # multiplied by two + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKLInferenceWrapper + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: true + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js index 7807f7f6185..b40c860275b 100644 --- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js +++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js @@ -816,7 +816,7 @@ onUiLoaded(async() => { // Increase or decrease brush size based on scroll direction adjustBrushSize(elemId, e.deltaY); } - }); + }, {passive: false}); // Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element. function handleMoveKeyDown(e) { diff --git a/extensions-builtin/hypertile/hypertile.py b/extensions-builtin/hypertile/hypertile.py index 0f40e2d3925..2da71583871 100644 --- a/extensions-builtin/hypertile/hypertile.py +++ b/extensions-builtin/hypertile/hypertile.py @@ -1,7 +1,7 @@ """ Hypertile module for splitting attention layers in SD-1.5 U-Net and SD-1.5 VAE Warn: The patch works well only if the input image has a width and height that are multiples of 128 -Original author: @tfernd Github: https://github.com/tfernd/HyperTile +Original author: @tfernd GitHub: https://github.com/tfernd/HyperTile """ from __future__ import annotations diff --git a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py index 7e674989814..c8ea7e0e061 100644 --- a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py +++ b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_autosized_crop.py @@ -34,14 +34,14 @@ def ui(self): with ui_components.InputAccordion(False, label="Auto-sized crop") as enable: gr.Markdown('Each image is center-cropped with an automatically chosen width and height.') with gr.Row(): - mindim = gr.Slider(minimum=64, maximum=2048, step=8, label="Dimension lower bound", value=384, elem_id="postprocess_multicrop_mindim") - maxdim = gr.Slider(minimum=64, maximum=2048, step=8, label="Dimension upper bound", value=768, elem_id="postprocess_multicrop_maxdim") + mindim = gr.Slider(minimum=64, maximum=2048, step=8, label="Dimension lower bound", value=384, elem_id=self.elem_id_suffix("postprocess_multicrop_mindim")) + maxdim = gr.Slider(minimum=64, maximum=2048, step=8, label="Dimension upper bound", value=768, elem_id=self.elem_id_suffix("postprocess_multicrop_maxdim")) with gr.Row(): - minarea = gr.Slider(minimum=64 * 64, maximum=2048 * 2048, step=1, label="Area lower bound", value=64 * 64, elem_id="postprocess_multicrop_minarea") - maxarea = gr.Slider(minimum=64 * 64, maximum=2048 * 2048, step=1, label="Area upper bound", value=640 * 640, elem_id="postprocess_multicrop_maxarea") + minarea = gr.Slider(minimum=64 * 64, maximum=2048 * 2048, step=1, label="Area lower bound", value=64 * 64, elem_id=self.elem_id_suffix("postprocess_multicrop_minarea")) + maxarea = gr.Slider(minimum=64 * 64, maximum=2048 * 2048, step=1, label="Area upper bound", value=640 * 640, elem_id=self.elem_id_suffix("postprocess_multicrop_maxarea")) with gr.Row(): - objective = gr.Radio(["Maximize area", "Minimize error"], value="Maximize area", label="Resizing objective", elem_id="postprocess_multicrop_objective") - threshold = gr.Slider(minimum=0, maximum=1, step=0.01, label="Error threshold", value=0.1, elem_id="postprocess_multicrop_threshold") + objective = gr.Radio(["Maximize area", "Minimize error"], value="Maximize area", label="Resizing objective", elem_id=self.elem_id_suffix("postprocess_multicrop_objective")) + threshold = gr.Slider(minimum=0, maximum=1, step=0.01, label="Error threshold", value=0.1, elem_id=self.elem_id_suffix("postprocess_multicrop_threshold")) return { "enable": enable, diff --git a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py index cff1dbc5470..db835fb6b61 100644 --- a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py +++ b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_focal_crop.py @@ -11,10 +11,10 @@ class ScriptPostprocessingFocalCrop(scripts_postprocessing.ScriptPostprocessing) def ui(self): with ui_components.InputAccordion(False, label="Auto focal point crop") as enable: - face_weight = gr.Slider(label='Focal point face weight', value=0.9, minimum=0.0, maximum=1.0, step=0.05, elem_id="postprocess_focal_crop_face_weight") - entropy_weight = gr.Slider(label='Focal point entropy weight', value=0.15, minimum=0.0, maximum=1.0, step=0.05, elem_id="postprocess_focal_crop_entropy_weight") - edges_weight = gr.Slider(label='Focal point edges weight', value=0.5, minimum=0.0, maximum=1.0, step=0.05, elem_id="postprocess_focal_crop_edges_weight") - debug = gr.Checkbox(label='Create debug image', elem_id="train_process_focal_crop_debug") + face_weight = gr.Slider(label='Focal point face weight', value=0.9, minimum=0.0, maximum=1.0, step=0.05, elem_id=self.elem_id_suffix("postprocess_focal_crop_face_weight")) + entropy_weight = gr.Slider(label='Focal point entropy weight', value=0.15, minimum=0.0, maximum=1.0, step=0.05, elem_id=self.elem_id_suffix("postprocess_focal_crop_entropy_weight")) + edges_weight = gr.Slider(label='Focal point edges weight', value=0.5, minimum=0.0, maximum=1.0, step=0.05, elem_id=self.elem_id_suffix("postprocess_focal_crop_edges_weight")) + debug = gr.Checkbox(label='Create debug image', elem_id=self.elem_id_suffix("train_process_focal_crop_debug")) return { "enable": enable, diff --git a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py index 133e199b838..b569a06e373 100644 --- a/extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py +++ b/extensions-builtin/postprocessing-for-training/scripts/postprocessing_split_oversized.py @@ -35,8 +35,8 @@ class ScriptPostprocessingSplitOversized(scripts_postprocessing.ScriptPostproces def ui(self): with ui_components.InputAccordion(False, label="Split oversized images") as enable: with gr.Row(): - split_threshold = gr.Slider(label='Threshold', value=0.5, minimum=0.0, maximum=1.0, step=0.05, elem_id="postprocess_split_threshold") - overlap_ratio = gr.Slider(label='Overlap ratio', value=0.2, minimum=0.0, maximum=0.9, step=0.05, elem_id="postprocess_overlap_ratio") + split_threshold = gr.Slider(label='Threshold', value=0.5, minimum=0.0, maximum=1.0, step=0.05, elem_id=self.elem_id_suffix("postprocess_split_threshold")) + overlap_ratio = gr.Slider(label='Overlap ratio', value=0.2, minimum=0.0, maximum=0.9, step=0.05, elem_id=self.elem_id_suffix("postprocess_overlap_ratio")) return { "enable": enable, diff --git a/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js b/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js index 114cf94ccbf..a59e584c26d 100644 --- a/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js +++ b/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js @@ -4,11 +4,11 @@ // If there's a mismatch, the keyword counter turns red and if you hover on it, a tooltip tells you what's wrong. function checkBrackets(textArea, counterElt) { - var counts = {}; - (textArea.value.match(/[(){}[\]]/g) || []).forEach(bracket => { - counts[bracket] = (counts[bracket] || 0) + 1; + const counts = {}; + textArea.value.matchAll(/(? { + counts[bracket[1]] = (counts[bracket[1]] || 0) + 1; }); - var errors = []; + const errors = []; function checkPair(open, close, kind) { if (counts[open] !== counts[close]) { diff --git a/html/footer.html b/html/footer.html index 69b2372c755..2bedddbc19d 100644 --- a/html/footer.html +++ b/html/footer.html @@ -1,7 +1,7 @@
API  •  - Github + GitHub  •  Gradio  •  diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js index e01fd67e80e..661343d524b 100644 --- a/javascript/contextMenus.js +++ b/javascript/contextMenus.js @@ -104,7 +104,7 @@ var contextMenuInit = function() { e.preventDefault(); } }); - }); + }, {passive: false}); }); eventListenerApplied = true; diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index c5cced97399..8e8adad0bae 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -201,7 +201,7 @@ function setupExtraNetworks() { setupExtraNetworksForTab('img2img'); } -var re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/; +var re_extranet = /<([^:^>]+:[^:]+):[\d.]+>(.*)/s; var re_extranet_g = /<([^:^>]+:[^:]+):[\d.]+>/g; var re_extranet_neg = /\(([^:^>]+:[\d.]+)\)/; diff --git a/javascript/imageviewer.js b/javascript/imageviewer.js index 9b23f4700b3..979d05de5ba 100644 --- a/javascript/imageviewer.js +++ b/javascript/imageviewer.js @@ -13,6 +13,7 @@ function showModal(event) { if (modalImage.style.display === 'none') { lb.style.setProperty('background-image', 'url(' + source.src + ')'); } + updateModalImage(); lb.style.display = "flex"; lb.focus(); @@ -31,21 +32,26 @@ function negmod(n, m) { return ((n % m) + m) % m; } +function updateModalImage() { + const modalImage = gradioApp().getElementById("modalImage"); + let currentButton = selected_gallery_button(); + let preview = gradioApp().querySelectorAll('.livePreview > img'); + if (opts.js_live_preview_in_modal_lightbox && preview.length > 0) { + // show preview image if available + modalImage.src = preview[preview.length - 1].src; + } else if (currentButton?.children?.length > 0 && modalImage.src != currentButton.children[0].src) { + modalImage.src = currentButton.children[0].src; + if (modalImage.style.display === 'none') { + const modal = gradioApp().getElementById("lightboxModal"); + modal.style.setProperty('background-image', `url(${modalImage.src})`); + } + } +} + function updateOnBackgroundChange() { const modalImage = gradioApp().getElementById("modalImage"); if (modalImage && modalImage.offsetParent) { - let currentButton = selected_gallery_button(); - let preview = gradioApp().querySelectorAll('.livePreview > img'); - if (opts.js_live_preview_in_modal_lightbox && preview.length > 0) { - // show preview image if available - modalImage.src = preview[preview.length - 1].src; - } else if (currentButton?.children?.length > 0 && modalImage.src != currentButton.children[0].src) { - modalImage.src = currentButton.children[0].src; - if (modalImage.style.display === 'none') { - const modal = gradioApp().getElementById("lightboxModal"); - modal.style.setProperty('background-image', `url(${modalImage.src})`); - } - } + updateModalImage(); } } diff --git a/javascript/progressbar.js b/javascript/progressbar.js index 23dea64ceda..c81a41e9a47 100644 --- a/javascript/progressbar.js +++ b/javascript/progressbar.js @@ -79,11 +79,12 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre var wakeLock = null; var requestWakeLock = async function() { - if (!opts.prevent_screen_sleep_during_generation || wakeLock) return; + if (!opts.prevent_screen_sleep_during_generation || wakeLock !== null) return; try { wakeLock = await navigator.wakeLock.request('screen'); } catch (err) { console.error('Wake Lock is not supported.'); + wakeLock = false; } }; diff --git a/javascript/resizeHandle.js b/javascript/resizeHandle.js index 4aeb14b41f3..0283f21bfe1 100644 --- a/javascript/resizeHandle.js +++ b/javascript/resizeHandle.js @@ -124,7 +124,7 @@ } else { R.screenX = evt.changedTouches[0].screenX; } - }); + }, {passive: false}); }); resizeHandle.addEventListener('dblclick', onDoubleClick); diff --git a/modules/api/api.py b/modules/api/api.py index 97ec7514ea1..db2fc648019 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -122,7 +122,7 @@ def encode_pil_to_base64(image): if opts.samples_format.lower() in ("jpg", "jpeg"): image.save(output_bytes, format="JPEG", exif = exif_bytes, quality=opts.jpeg_quality) else: - image.save(output_bytes, format="WEBP", exif = exif_bytes, quality=opts.jpeg_quality) + image.save(output_bytes, format="WEBP", exif = exif_bytes, quality=opts.jpeg_quality, lossless=opts.webp_lossless) else: raise HTTPException(status_code=500, detail="Invalid image format") diff --git a/modules/dat_model.py b/modules/dat_model.py index 495d5f4937d..e256a5a3282 100644 --- a/modules/dat_model.py +++ b/modules/dat_model.py @@ -1,7 +1,7 @@ import os from modules import modelloader, errors -from modules.shared import cmd_opts, opts +from modules.shared import cmd_opts, opts, hf_endpoint from modules.upscaler import Upscaler, UpscalerData from modules.upscaler_utils import upscale_with_model @@ -49,7 +49,18 @@ def load_model(self, path): scaler.local_data_path = modelloader.load_file_from_url( scaler.data_path, model_dir=self.model_download_path, + hash_prefix=scaler.sha256, ) + + if os.path.getsize(scaler.local_data_path) < 200: + # Re-download if the file is too small, probably an LFS pointer + scaler.local_data_path = modelloader.load_file_from_url( + scaler.data_path, + model_dir=self.model_download_path, + hash_prefix=scaler.sha256, + re_download=True, + ) + if not os.path.exists(scaler.local_data_path): raise FileNotFoundError(f"DAT data missing: {scaler.local_data_path}") return scaler @@ -60,20 +71,23 @@ def get_dat_models(scaler): return [ UpscalerData( name="DAT x2", - path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x2.pth", + path=f"{hf_endpoint}/w-e-w/DAT/resolve/main/experiments/pretrained_models/DAT/DAT_x2.pth", scale=2, upscaler=scaler, + sha256='7760aa96e4ee77e29d4f89c3a4486200042e019461fdb8aa286f49aa00b89b51', ), UpscalerData( name="DAT x3", - path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x3.pth", + path=f"{hf_endpoint}/w-e-w/DAT/resolve/main/experiments/pretrained_models/DAT/DAT_x3.pth", scale=3, upscaler=scaler, + sha256='581973e02c06f90d4eb90acf743ec9604f56f3c2c6f9e1e2c2b38ded1f80d197', ), UpscalerData( name="DAT x4", - path="https://github.com/n0kovo/dat_upscaler_models/raw/main/DAT/DAT_x4.pth", + path=f"{hf_endpoint}/w-e-w/DAT/resolve/main/experiments/pretrained_models/DAT/DAT_x4.pth", scale=4, upscaler=scaler, + sha256='391a6ce69899dff5ea3214557e9d585608254579217169faf3d4c353caff049e', ), ] diff --git a/modules/extras.py b/modules/extras.py index 2a310ae3f25..adc88ca558b 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -23,7 +23,7 @@ def run_pnginfo(image): info = '' for key, text in items.items(): info += f""" -
+

{plaintext_to_html(str(key))}

{plaintext_to_html(str(text))}

diff --git a/modules/modelloader.py b/modules/modelloader.py index 36e7415af43..f5a2ff79c30 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -10,6 +10,7 @@ from modules import shared from modules.upscaler import Upscaler, UpscalerLanczos, UpscalerNearest, UpscalerNone +from modules.util import load_file_from_url # noqa, backwards compatibility if TYPE_CHECKING: import spandrel @@ -17,30 +18,6 @@ logger = logging.getLogger(__name__) -def load_file_from_url( - url: str, - *, - model_dir: str, - progress: bool = True, - file_name: str | None = None, - hash_prefix: str | None = None, -) -> str: - """Download a file from `url` into `model_dir`, using the file present if possible. - - Returns the path to the downloaded file. - """ - os.makedirs(model_dir, exist_ok=True) - if not file_name: - parts = urlparse(url) - file_name = os.path.basename(parts.path) - cached_file = os.path.abspath(os.path.join(model_dir, file_name)) - if not os.path.exists(cached_file): - print(f'Downloading: "{url}" to {cached_file}\n') - from torch.hub import download_url_to_file - download_url_to_file(url, cached_file, progress=progress, hash_prefix=hash_prefix) - return cached_file - - def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None, hash_prefix=None) -> list: """ A one-and done loader to try finding the desired models in specified directories. diff --git a/modules/models/sd3/sd3_cond.py b/modules/models/sd3/sd3_cond.py index 325c512d594..6a43f569bea 100644 --- a/modules/models/sd3/sd3_cond.py +++ b/modules/models/sd3/sd3_cond.py @@ -24,7 +24,7 @@ def __getitem__(self, key): return self.file.get_tensor(key) -CLIPL_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_l.safetensors" +CLIPL_URL = f"{shared.hf_endpoint}/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_l.safetensors" CLIPL_CONFIG = { "hidden_act": "quick_gelu", "hidden_size": 768, @@ -33,7 +33,7 @@ def __getitem__(self, key): "num_hidden_layers": 12, } -CLIPG_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_g.safetensors" +CLIPG_URL = f"{shared.hf_endpoint}/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/clip_g.safetensors" CLIPG_CONFIG = { "hidden_act": "gelu", "hidden_size": 1280, @@ -43,7 +43,7 @@ def __getitem__(self, key): "textual_inversion_key": "clip_g", } -T5_URL = "https://huggingface.co/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/t5xxl_fp16.safetensors" +T5_URL = f"{shared.hf_endpoint}/AUTOMATIC/stable-diffusion-3-medium-text-encoders/resolve/main/t5xxl_fp16.safetensors" T5_CONFIG = { "d_ff": 10240, "d_model": 4096, diff --git a/modules/processing.py b/modules/processing.py index 7535b56e18c..92c3582cc66 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -1259,7 +1259,10 @@ def init(self, all_prompts, all_seeds, all_subseeds): if self.hr_checkpoint_info is None: raise Exception(f'Could not find checkpoint with name {self.hr_checkpoint_name}') - self.extra_generation_params["Hires checkpoint"] = self.hr_checkpoint_info.short_title + if shared.sd_model.sd_checkpoint_info == self.hr_checkpoint_info: + self.hr_checkpoint_info = None + else: + self.extra_generation_params["Hires checkpoint"] = self.hr_checkpoint_info.short_title if self.hr_sampler_name is not None and self.hr_sampler_name != self.sampler_name: self.extra_generation_params["Hires sampler"] = self.hr_sampler_name diff --git a/modules/scripts_auto_postprocessing.py b/modules/scripts_auto_postprocessing.py index d63078de50e..cf981625034 100644 --- a/modules/scripts_auto_postprocessing.py +++ b/modules/scripts_auto_postprocessing.py @@ -13,6 +13,7 @@ def show(self, is_img2img): return scripts.AlwaysVisible def ui(self, is_img2img): + self.script.tab_name = '_img2img' if is_img2img else '_txt2img' self.postprocessing_controls = self.script.ui() return self.postprocessing_controls.values() @@ -33,7 +34,7 @@ def create_auto_preprocessing_script_data(): 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: + if script is None or script.script_class.extra_only: continue constructor = lambda s=script: ScriptPostprocessingForMainUI(s.script_class()) diff --git a/modules/scripts_postprocessing.py b/modules/scripts_postprocessing.py index 4b3b7afda1c..70270c658d9 100644 --- a/modules/scripts_postprocessing.py +++ b/modules/scripts_postprocessing.py @@ -1,3 +1,4 @@ +import re import dataclasses import os import gradio as gr @@ -59,6 +60,10 @@ class ScriptPostprocessing: args_from = None args_to = None + # define if the script should be used only in extras or main UI + extra_only = None + main_ui_only = None + order = 1000 """scripts will be ordred by this value in postprocessing UI""" @@ -97,6 +102,31 @@ def process_firstpass(self, pp: PostprocessedImage, **args): def image_changed(self): pass + tab_name = '' # used by ScriptPostprocessingForMainUI + replace_pattern = re.compile(r'\s') + rm_pattern = re.compile(r'[^a-z_0-9]') + + def elem_id(self, item_id): + """ + Helper function to generate id for a HTML element + constructs final id out of script name and user-supplied item_id + 'script_extras_{self.name.lower()}_{item_id}' + {tab_name} will append to the end of the id if set + tab_name will be set to '_img2img' or '_txt2img' if use by ScriptPostprocessingForMainUI + + Extensions should use this function to generate element IDs + """ + return self.elem_id_suffix(f'extras_{self.name.lower()}_{item_id}') + + def elem_id_suffix(self, base_id): + """ + Append tab_name to the base_id + + Extensions that already have specific there element IDs and wish to keep their IDs the same when possible should use this function + """ + base_id = self.rm_pattern.sub('', self.replace_pattern.sub('_', base_id)) + return f'{base_id}{self.tab_name}' + def wrap_call(func, filename, funcname, *args, default=None, **kwargs): try: @@ -119,10 +149,6 @@ def initialize_scripts(self, scripts_data): for script_data in scripts_data: script: ScriptPostprocessing = script_data.script_class() script.filename = script_data.path - - if script.name == "Simple Upscale": - continue - self.scripts.append(script) def create_script_ui(self, script, inputs): @@ -152,7 +178,7 @@ def script_score(name): return len(self.scripts) - filtered_scripts = [script for script in self.scripts if script.name not in scripts_filter_out] + filtered_scripts = [script for script in self.scripts if script.name not in scripts_filter_out and not script.main_ui_only] script_scores = {script.name: (script_score(script.name), script.order, script.name, original_index) for original_index, script in enumerate(filtered_scripts)} return sorted(filtered_scripts, key=lambda x: script_scores[x.name]) diff --git a/modules/sd_disable_initialization.py b/modules/sd_disable_initialization.py index 273a7edd8b4..3750e85e906 100644 --- a/modules/sd_disable_initialization.py +++ b/modules/sd_disable_initialization.py @@ -76,7 +76,7 @@ def transformers_modeling_utils_load_pretrained_model(*args, **kwargs): def transformers_utils_hub_get_file_from_cache(original, url, *args, **kwargs): # this file is always 404, prevent making request - if url == 'https://huggingface.co/openai/clip-vit-large-patch14/resolve/main/added_tokens.json' or url == 'openai/clip-vit-large-patch14' and args[0] == 'added_tokens.json': + if url == f'{shared.hf_endpoint}/openai/clip-vit-large-patch14/resolve/main/added_tokens.json' or url == 'openai/clip-vit-large-patch14' and args[0] == 'added_tokens.json': return None try: diff --git a/modules/sd_models.py b/modules/sd_models.py index 55bd9ca5e43..1c7d370e97b 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -159,7 +159,7 @@ def list_models(): model_url = None expected_sha256 = None else: - model_url = f"{shared.hf_endpoint}/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors" + model_url = f"{shared.hf_endpoint}/stable-diffusion-v1-5/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors" expected_sha256 = '6ce0161689b3853acaa03779ec93eafe75a02f4ced659bee03f50797806fa2fa' model_list = modelloader.load_models(model_path=model_path, model_url=model_url, command_path=shared.cmd_opts.ckpt_dir, ext_filter=[".ckpt", ".safetensors"], download_name="v1-5-pruned-emaonly.safetensors", ext_blacklist=[".vae.ckpt", ".vae.safetensors"], hash_prefix=expected_sha256) @@ -423,6 +423,10 @@ def load_model_weights(model, checkpoint_info: CheckpointInfo, state_dict, timer set_model_type(model, state_dict) set_model_fields(model) + if 'ztsnr' in state_dict: + model.ztsnr = True + else: + model.ztsnr = False if model.is_sdxl: sd_models_xl.extend_sdxl(model) @@ -661,7 +665,7 @@ def apply_alpha_schedule_override(sd_model, p=None): p.extra_generation_params['Downcast alphas_cumprod'] = opts.use_downcasted_alpha_bar sd_model.alphas_cumprod = sd_model.alphas_cumprod.half().to(shared.device) - if opts.sd_noise_schedule == "Zero Terminal SNR": + if opts.sd_noise_schedule == "Zero Terminal SNR" or (hasattr(sd_model, 'ztsnr') and sd_model.ztsnr): if p is not None: p.extra_generation_params['Noise Schedule'] = opts.sd_noise_schedule sd_model.alphas_cumprod = rescale_zero_terminal_snr_abar(sd_model.alphas_cumprod).to(shared.device) @@ -783,7 +787,7 @@ def get_obj_from_str(string, reload=False): return getattr(importlib.import_module(module, package=None), cls) -def load_model(checkpoint_info=None, already_loaded_state_dict=None): +def load_model(checkpoint_info=None, already_loaded_state_dict=None, checkpoint_config=None): from modules import sd_hijack checkpoint_info = checkpoint_info or select_checkpoint() @@ -801,7 +805,8 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None): else: state_dict = get_checkpoint_state_dict(checkpoint_info, timer) - checkpoint_config = sd_models_config.find_checkpoint_config(state_dict, checkpoint_info) + if not checkpoint_config: + checkpoint_config = sd_models_config.find_checkpoint_config(state_dict, checkpoint_info) clip_is_included_into_sd = any(x for x in [sd1_clip_weight, sd2_clip_weight, sdxl_clip_weight, sdxl_refiner_clip_weight] if x in state_dict) timer.record("find config") @@ -974,7 +979,7 @@ def reload_model_weights(sd_model=None, info=None, forced_reload=False): if sd_model is not None: send_model_to_trash(sd_model) - load_model(checkpoint_info, already_loaded_state_dict=state_dict) + load_model(checkpoint_info, already_loaded_state_dict=state_dict, checkpoint_config=checkpoint_config) return model_data.sd_model try: diff --git a/modules/sd_models_config.py b/modules/sd_models_config.py index fb44c5a8d98..3c1e4a1518f 100644 --- a/modules/sd_models_config.py +++ b/modules/sd_models_config.py @@ -14,6 +14,7 @@ config_sd2v = os.path.join(sd_repo_configs_path, "v2-inference-v.yaml") config_sd2_inpainting = os.path.join(sd_repo_configs_path, "v2-inpainting-inference.yaml") config_sdxl = os.path.join(sd_xl_repo_configs_path, "sd_xl_base.yaml") +config_sdxlv = os.path.join(sd_configs_path, "sd_xl_v.yaml") config_sdxl_refiner = os.path.join(sd_xl_repo_configs_path, "sd_xl_refiner.yaml") config_sdxl_inpainting = os.path.join(sd_configs_path, "sd_xl_inpaint.yaml") config_depth_model = os.path.join(sd_repo_configs_path, "v2-midas-inference.yaml") @@ -81,6 +82,9 @@ def guess_model_config_from_state_dict(sd, filename): if diffusion_model_input.shape[1] == 9: return config_sdxl_inpainting else: + if ('v_pred' in sd): + del sd['v_pred'] + return config_sdxlv return config_sdxl if sd.get('conditioner.embedders.0.model.ln_final.weight', None) is not None: diff --git a/modules/shared_items.py b/modules/shared_items.py index 11f10b3f7b1..3aaf0649028 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -16,10 +16,12 @@ def dat_models_names(): return [x.name for x in modules.dat_model.get_dat_models(None)] -def postprocessing_scripts(): +def postprocessing_scripts(filter_out_extra_only=False, filter_out_main_ui_only=False): import modules.scripts - - return modules.scripts.scripts_postproc.scripts + return list(filter( + lambda s: (not filter_out_extra_only or not s.extra_only) and (not filter_out_main_ui_only or not s.main_ui_only), + modules.scripts.scripts_postproc.scripts, + )) def sd_vae_items(): diff --git a/modules/shared_options.py b/modules/shared_options.py index 9f4520274b1..78089cbec05 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -231,7 +231,7 @@ options_templates.update(options_section(('optimizations', "Optimizations", "sd"), { "cross_attention_optimization": OptionInfo("Automatic", "Cross attention optimization", gr.Dropdown, lambda: {"choices": shared_items.cross_attention_optimizations()}), - "s_min_uncond": OptionInfo(0.0, "Negative Guidance minimum sigma", gr.Slider, {"minimum": 0.0, "maximum": 15.0, "step": 0.01}, infotext='NGMS').link("PR", "https://github.com/AUTOMATIC1111/stablediffusion-webui/pull/9177").info("skip negative prompt for some steps when the image is almost ready; 0=disable, higher=faster"), + "s_min_uncond": OptionInfo(0.0, "Negative Guidance minimum sigma", gr.Slider, {"minimum": 0.0, "maximum": 15.0, "step": 0.01}, infotext='NGMS').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/9177").info("skip negative prompt for some steps when the image is almost ready; 0=disable, higher=faster"), "s_min_uncond_all": OptionInfo(False, "Negative Guidance minimum sigma all steps", infotext='NGMS all steps').info("By default, NGMS above skips every other step; this makes it skip all steps"), "token_merging_ratio": OptionInfo(0.0, "Token merging ratio", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}, infotext='Token merging ratio').link("PR", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/pull/9256").info("0=disable, higher=faster"), "token_merging_ratio_img2img": OptionInfo(0.0, "Token merging ratio for img2img", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}).info("only applies if non-zero and overrides above"), @@ -291,6 +291,7 @@ "textual_inversion_print_at_load": OptionInfo(False, "Print a list of Textual Inversion embeddings when loading model"), "textual_inversion_add_hashes_to_infotext": OptionInfo(True, "Add Textual Inversion hashes to infotext"), "sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": ["None", *shared.hypernetworks]}, refresh=shared_items.reload_hypernetworks), + "textual_inversion_image_embedding_data_cache": OptionInfo(False, 'Cache the data of image embeddings').info('potentially increase TI load time at the cost some disk space'), })) options_templates.update(options_section(('ui_prompt_editing', "Prompt editing", "ui"), { @@ -404,15 +405,15 @@ 'uni_pc_order': OptionInfo(3, "UniPC order", gr.Slider, {"minimum": 1, "maximum": 50, "step": 1}, infotext='UniPC order').info("must be < sampling steps"), 'uni_pc_lower_order_final': OptionInfo(True, "UniPC lower order final", infotext='UniPC lower order final'), 'sd_noise_schedule': OptionInfo("Default", "Noise schedule for sampling", gr.Radio, {"choices": ["Default", "Zero Terminal SNR"]}, infotext="Noise Schedule").info("for use with zero terminal SNR trained models"), - 'skip_early_cond': OptionInfo(0.0, "Ignore negative prompt during early sampling", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}, infotext="Skip Early CFG").info("disables CFG on a proportion of steps at the beginning of generation; 0=skip none; 1=skip all; can both improve sample diversity/quality and speed up sampling"), + 'skip_early_cond': OptionInfo(0.0, "Ignore negative prompt during early sampling", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}, infotext="Skip Early CFG").info("disables CFG on a proportion of steps at the beginning of generation; 0=skip none; 1=skip all; can both improve sample diversity/quality and speed up sampling; XYZ plot: Skip Early CFG"), 'beta_dist_alpha': OptionInfo(0.6, "Beta scheduler - alpha", gr.Slider, {"minimum": 0.01, "maximum": 1.0, "step": 0.01}, infotext='Beta scheduler alpha').info('Default = 0.6; the alpha parameter of the beta distribution used in Beta sampling'), 'beta_dist_beta': OptionInfo(0.6, "Beta scheduler - beta", gr.Slider, {"minimum": 0.01, "maximum": 1.0, "step": 0.01}, infotext='Beta scheduler beta').info('Default = 0.6; the beta parameter of the beta distribution used in Beta sampling'), })) options_templates.update(options_section(('postprocessing', "Postprocessing", "postprocessing"), { - 'postprocessing_enable_in_main_ui': OptionInfo([], "Enable postprocessing operations in txt2img and img2img tabs", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), - 'postprocessing_disable_in_extras': OptionInfo([], "Disable postprocessing operations in extras tab", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), - 'postprocessing_operation_order': OptionInfo([], "Postprocessing operation order", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), + 'postprocessing_enable_in_main_ui': OptionInfo([], "Enable postprocessing operations in txt2img and img2img tabs", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts(filter_out_extra_only=True)]}), + 'postprocessing_disable_in_extras': OptionInfo([], "Disable postprocessing operations in extras tab", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts(filter_out_main_ui_only=True)]}), + 'postprocessing_operation_order': OptionInfo([], "Postprocessing operation order", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts(filter_out_main_ui_only=True)]}), 'upscaling_max_images_in_cache': OptionInfo(5, "Maximum number of images in upscaling cache", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), 'postprocessing_existing_caption_action': OptionInfo("Ignore", "Action for existing captions", gr.Radio, {"choices": ["Ignore", "Keep", "Prepend", "Append"]}).info("when generating captions using postprocessing; Ignore = use generated; Keep = use original; Prepend/Append = combine both"), })) diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index dc7833e9394..f209b883450 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -12,7 +12,7 @@ import numpy as np from PIL import Image, PngImagePlugin -from modules import shared, devices, sd_hijack, sd_models, images, sd_samplers, sd_hijack_checkpoint, errors, hashes +from modules import shared, devices, sd_hijack, sd_models, images, sd_samplers, sd_hijack_checkpoint, errors, hashes, cache import modules.textual_inversion.dataset from modules.textual_inversion.learn_schedule import LearnRateScheduler @@ -116,6 +116,7 @@ def __init__(self): self.expected_shape = -1 self.embedding_dirs = {} self.previously_displayed_embeddings = () + self.image_embedding_cache = cache.cache('image-embedding') def add_embedding_dir(self, path): self.embedding_dirs[path] = DirWithTextualInversionEmbeddings(path) @@ -154,6 +155,31 @@ def get_expected_shape(self): vec = shared.sd_model.cond_stage_model.encode_embedding_init_text(",", 1) return vec.shape[1] + def read_embedding_from_image(self, path, name): + try: + ondisk_mtime = os.path.getmtime(path) + + if (cache_embedding := self.image_embedding_cache.get(path)) and ondisk_mtime == cache_embedding.get('mtime', 0): + # cache will only be used if the file has not been modified time matches + return cache_embedding.get('data', None), cache_embedding.get('name', None) + + embed_image = Image.open(path) + if hasattr(embed_image, 'text') and 'sd-ti-embedding' in embed_image.text: + data = embedding_from_b64(embed_image.text['sd-ti-embedding']) + name = data.get('name', name) + elif data := extract_image_data_embed(embed_image): + name = data.get('name', name) + + if data is None or shared.opts.textual_inversion_image_embedding_data_cache: + # data of image embeddings only will be cached if the option textual_inversion_image_embedding_data_cache is enabled + # results of images that are not embeddings will allways be cached to reduce unnecessary future disk reads + self.image_embedding_cache[path] = {'data': data, 'name': None if data is None else name, 'mtime': ondisk_mtime} + + return data, name + except Exception: + errors.report(f"Error loading embedding {path}", exc_info=True) + return None, None + def load_from_file(self, path, filename): name, ext = os.path.splitext(filename) ext = ext.upper() @@ -163,17 +189,10 @@ def load_from_file(self, path, filename): if second_ext.upper() == '.PREVIEW': return - embed_image = Image.open(path) - if hasattr(embed_image, 'text') and 'sd-ti-embedding' in embed_image.text: - data = embedding_from_b64(embed_image.text['sd-ti-embedding']) - name = data.get('name', name) - else: - data = extract_image_data_embed(embed_image) - if data: - name = data.get('name', name) - else: - # if data is None, means this is not an embedding, just a preview image - return + data, name = self.read_embedding_from_image(path, name) + if data is None: + return + elif ext in ['.BIN', '.PT']: data = torch.load(path, map_location="cpu") elif ext in ['.SAFETENSORS']: @@ -191,7 +210,6 @@ def load_from_file(self, path, filename): else: print(f"Unable to load Textual inversion embedding due to data issue: '{name}'.") - def load_from_dir(self, embdir): if not os.path.isdir(embdir.path): return diff --git a/modules/ui.py b/modules/ui.py index f48638f69ad..9a76b5fcd90 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -44,6 +44,9 @@ mimetypes.add_type('image/webp', '.webp') mimetypes.add_type('image/avif', '.avif') +# override potentially incorrect mimetypes +mimetypes.add_type('text/css', '.css') + if not cmd_opts.share and not cmd_opts.listen: # fix gradio phoning home gradio.utils.version_check = lambda: None diff --git a/modules/ui_components.py b/modules/ui_components.py index 9cf67722a3d..3e3ea54bafe 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -91,6 +91,7 @@ class InputAccordion(gr.Checkbox): Actually just a hidden checkbox, but creates an accordion that follows and is followed by the state of the checkbox. """ + accordion_id_set = set() global_index = 0 def __init__(self, value, **kwargs): @@ -99,6 +100,18 @@ def __init__(self, value, **kwargs): self.accordion_id = f"input-accordion-{InputAccordion.global_index}" InputAccordion.global_index += 1 + if not InputAccordion.accordion_id_set: + from modules import script_callbacks + script_callbacks.on_script_unloaded(InputAccordion.reset) + + if self.accordion_id in InputAccordion.accordion_id_set: + count = 1 + while (unique_id := f'{self.accordion_id}-{count}') in InputAccordion.accordion_id_set: + count += 1 + self.accordion_id = unique_id + + InputAccordion.accordion_id_set.add(self.accordion_id) + kwargs_checkbox = { **kwargs, "elem_id": f"{self.accordion_id}-checkbox", @@ -143,3 +156,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def get_block_name(self): return "checkbox" + @classmethod + def reset(cls): + cls.global_index = 0 + cls.accordion_id_set.clear() diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 6e9ec164552..1f19bd36d12 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -177,10 +177,8 @@ def add_pages_to_demo(app): app.add_api_route("/sd_extra_networks/get-single-card", get_single_card, methods=["GET"]) -def quote_js(s): - s = s.replace('\\', '\\\\') - s = s.replace('"', '\\"') - return f'"{s}"' +def quote_js(s: str): + return json.dumps(s, ensure_ascii=False) class ExtraNetworksPage: diff --git a/modules/ui_loadsave.py b/modules/ui_loadsave.py index 0cc1ab82af4..95a776d947e 100644 --- a/modules/ui_loadsave.py +++ b/modules/ui_loadsave.py @@ -176,7 +176,7 @@ def iter_changes(self, current_ui_settings, values): if new_value == old_value: continue - if old_value is None and new_value == '' or new_value == []: + if old_value is None and (new_value == '' or new_value == []): continue yield path, old_value, new_value diff --git a/modules/upscaler.py b/modules/upscaler.py index 507881fede2..12ab3547cf6 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -93,13 +93,14 @@ class UpscalerData: scaler: Upscaler = None model: None - def __init__(self, name: str, path: str, upscaler: Upscaler = None, scale: int = 4, model=None): + def __init__(self, name: str, path: str, upscaler: Upscaler = None, scale: int = 4, model=None, sha256: str = None): self.name = name self.data_path = path self.local_data_path = path self.scaler = upscaler self.scale = scale self.model = model + self.sha256 = sha256 def __repr__(self): return f"" diff --git a/modules/util.py b/modules/util.py index 7911b0db72c..baeba2fa271 100644 --- a/modules/util.py +++ b/modules/util.py @@ -211,3 +211,80 @@ def open_folder(path): subprocess.Popen(["explorer.exe", subprocess.check_output(["wslpath", "-w", path])]) else: subprocess.Popen(["xdg-open", path]) + + +def load_file_from_url( + url: str, + *, + model_dir: str, + progress: bool = True, + file_name: str | None = None, + hash_prefix: str | None = None, + re_download: bool = False, +) -> str: + """Download a file from `url` into `model_dir`, using the file present if possible. + Returns the path to the downloaded file. + + file_name: if specified, it will be used as the filename, otherwise the filename will be extracted from the url. + file is downloaded to {file_name}.tmp then moved to the final location after download is complete. + hash_prefix: sha256 hex string, if provided, the hash of the downloaded file will be checked against this prefix. + if the hash does not match, the temporary file is deleted and a ValueError is raised. + re_download: forcibly re-download the file even if it already exists. + """ + from urllib.parse import urlparse + import requests + try: + from tqdm import tqdm + except ImportError: + class tqdm: + def __init__(self, *args, **kwargs): + pass + + def update(self, n=1, *args, **kwargs): + pass + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + if not file_name: + parts = urlparse(url) + file_name = os.path.basename(parts.path) + + cached_file = os.path.abspath(os.path.join(model_dir, file_name)) + + if re_download or not os.path.exists(cached_file): + os.makedirs(model_dir, exist_ok=True) + temp_file = os.path.join(model_dir, f"{file_name}.tmp") + print(f'\nDownloading: "{url}" to {cached_file}') + response = requests.get(url, stream=True) + response.raise_for_status() + total_size = int(response.headers.get('content-length', 0)) + with tqdm(total=total_size, unit='B', unit_scale=True, desc=file_name, disable=not progress) as progress_bar: + with open(temp_file, 'wb') as file: + for chunk in response.iter_content(chunk_size=1024): + if chunk: + file.write(chunk) + progress_bar.update(len(chunk)) + + if hash_prefix and not compare_sha256(temp_file, hash_prefix): + print(f"Hash mismatch for {temp_file}. Deleting the temporary file.") + os.remove(temp_file) + raise ValueError(f"File hash does not match the expected hash prefix {hash_prefix}!") + + os.rename(temp_file, cached_file) + return cached_file + + +def compare_sha256(file_path: str, hash_prefix: str) -> bool: + """Check if the SHA256 hash of the file matches the given prefix.""" + import hashlib + hash_sha256 = hashlib.sha256() + blksize = 1024 * 1024 + + with open(file_path, "rb") as f: + for chunk in iter(lambda: f.read(blksize), b""): + hash_sha256.update(chunk) + return hash_sha256.hexdigest().startswith(hash_prefix.strip().lower()) diff --git a/requirements_versions.txt b/requirements_versions.txt index 0306ce94fda..389e2af33b0 100644 --- a/requirements_versions.txt +++ b/requirements_versions.txt @@ -22,7 +22,7 @@ protobuf==3.20.0 psutil==5.9.5 pytorch_lightning==1.9.4 resize-right==0.0.2 -safetensors==0.4.2 +safetensors==0.4.5 scikit-image==0.21.0 spandrel==0.3.4 spandrel-extra-arches==0.1.1 diff --git a/scripts/postprocessing_codeformer.py b/scripts/postprocessing_codeformer.py index 53a0cc44cde..f86e99a04db 100644 --- a/scripts/postprocessing_codeformer.py +++ b/scripts/postprocessing_codeformer.py @@ -12,8 +12,8 @@ class ScriptPostprocessingCodeFormer(scripts_postprocessing.ScriptPostprocessing def ui(self): with ui_components.InputAccordion(False, label="CodeFormer") as enable: with gr.Row(): - codeformer_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_codeformer_visibility") - codeformer_weight = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Weight (0 = maximum effect, 1 = minimum effect)", value=0, elem_id="extras_codeformer_weight") + codeformer_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id=self.elem_id_suffix("extras_codeformer_visibility")) + codeformer_weight = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Weight (0 = maximum effect, 1 = minimum effect)", value=0, elem_id=self.elem_id_suffix("extras_codeformer_weight")) return { "enable": enable, diff --git a/scripts/postprocessing_gfpgan.py b/scripts/postprocessing_gfpgan.py index 57e3623995c..3a130fd6336 100644 --- a/scripts/postprocessing_gfpgan.py +++ b/scripts/postprocessing_gfpgan.py @@ -11,7 +11,7 @@ class ScriptPostprocessingGfpGan(scripts_postprocessing.ScriptPostprocessing): def ui(self): with ui_components.InputAccordion(False, label="GFPGAN") as enable: - gfpgan_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_gfpgan_visibility") + gfpgan_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id=self.elem_id_suffix("extras_gfpgan_visibility")) return { "enable": enable, diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py index 2409fd2073e..838872bf1b9 100644 --- a/scripts/postprocessing_upscale.py +++ b/scripts/postprocessing_upscale.py @@ -30,31 +30,31 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): def ui(self): selected_tab = gr.Number(value=0, visible=False) - with InputAccordion(True, label="Upscale", elem_id="extras_upscale") as upscale_enabled: + with InputAccordion(True, label="Upscale", elem_id=self.elem_id_suffix("extras_upscale")) as upscale_enabled: 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) + extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id=self.elem_id_suffix("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") + extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id=self.elem_id_suffix("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=self.elem_id_suffix("extras_upscaler_2_visibility")) 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: + with gr.Tabs(elem_id=self.elem_id_suffix("extras_resize_mode")): + with gr.TabItem('Scale by', elem_id=self.elem_id_suffix("extras_scale_by_tab")) as tab_scale_by: with gr.Row(): with gr.Column(scale=4): - upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id=self.elem_id_suffix("extras_upscaling_resize")) with gr.Column(scale=1, min_width=160): - max_side_length = gr.Number(label="Max side length", value=0, elem_id="extras_upscale_max_side_length", tooltip="If any of two sides of the image ends up larger than specified, will downscale it to fit. 0 = no limit.", min_width=160, step=8, minimum=0) + max_side_length = gr.Number(label="Max side length", value=0, elem_id=self.elem_id_suffix("extras_upscale_max_side_length"), tooltip="If any of two sides of the image ends up larger than specified, will downscale it to fit. 0 = no limit.", min_width=160, step=8, minimum=0) - with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: + with gr.TabItem('Scale to', elem_id=self.elem_id_suffix("extras_scale_to_tab")) as tab_scale_to: with FormRow(): - with gr.Column(elem_id="upscaling_column_size", scale=4): - upscaling_resize_w = gr.Slider(minimum=64, maximum=8192, step=8, label="Width", value=512, elem_id="extras_upscaling_resize_w") - upscaling_resize_h = gr.Slider(minimum=64, maximum=8192, step=8, label="Height", value=512, elem_id="extras_upscaling_resize_h") - with gr.Column(elem_id="upscaling_dimensions_row", scale=1, elem_classes="dimensions-tools"): - upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn", tooltip="Switch width/height") - upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") + with gr.Column(elem_id=self.elem_id_suffix("upscaling_column_size"), scale=4): + upscaling_resize_w = gr.Slider(minimum=64, maximum=8192, step=8, label="Width", value=512, elem_id=self.elem_id_suffix("extras_upscaling_resize_w")) + upscaling_resize_h = gr.Slider(minimum=64, maximum=8192, step=8, label="Height", value=512, elem_id=self.elem_id_suffix("extras_upscaling_resize_h")) + with gr.Column(elem_id=self.elem_id_suffix("upscaling_dimensions_row"), scale=1, elem_classes="dimensions-tools"): + upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id=self.elem_id_suffix("upscaling_res_switch_btn"), tooltip="Switch width/height") + upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id=self.elem_id_suffix("extras_upscaling_crop")) def on_selected_upscale_method(upscale_method): if not shared.opts.set_scale_by_when_changing_upscaler: @@ -169,6 +169,7 @@ def image_changed(self): class ScriptPostprocessingUpscaleSimple(ScriptPostprocessingUpscale): name = "Simple Upscale" order = 900 + main_ui_only = True def ui(self): with FormRow(): diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 6a42a04d9a3..5778d5f4c8d 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -20,7 +20,7 @@ import modules.sd_vae import re -from modules.ui_components import ToolButton +from modules.ui_components import ToolButton, InputAccordion fill_values_symbol = "\U0001f4d2" # 📒 @@ -259,6 +259,7 @@ def __init__(self, *args, **kwargs): AxisOption("Schedule min sigma", float, apply_override("sigma_min")), AxisOption("Schedule max sigma", float, apply_override("sigma_max")), AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Skip Early CFG", float, apply_override('skip_early_cond')), AxisOption("Beta schedule alpha", float, apply_override("beta_dist_alpha")), AxisOption("Beta schedule beta", float, apply_override("beta_dist_beta")), AxisOption("Eta", float, apply_field("eta")), @@ -284,7 +285,7 @@ def __init__(self, *args, **kwargs): ] -def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size): +def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size, draw_grid): hor_texts = [[images.GridAnnotation(x)] for x in x_labels] ver_texts = [[images.GridAnnotation(y)] for y in y_labels] title_texts = [[images.GridAnnotation(z)] for z in z_labels] @@ -369,29 +370,30 @@ def index(ix, iy, iz): print("Unexpected error: draw_xyz_grid failed to return even a single processed image") return Processed(p, []) - z_count = len(zs) + if draw_grid: + z_count = len(zs) - for i in range(z_count): - start_index = (i * len(xs) * len(ys)) + i - end_index = start_index + len(xs) * len(ys) - grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) + for i in range(z_count): + start_index = (i * len(xs) * len(ys)) + i + end_index = start_index + len(xs) * len(ys) + grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) + if draw_legend: + grid_max_w, grid_max_h = map(max, zip(*(img.size for img in processed_result.images[start_index:end_index]))) + grid = images.draw_grid_annotations(grid, grid_max_w, grid_max_h, hor_texts, ver_texts, margin_size) + processed_result.images.insert(i, grid) + processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) + processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) + processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) + + z_grid = images.image_grid(processed_result.images[:z_count], rows=1) + z_sub_grid_max_w, z_sub_grid_max_h = map(max, zip(*(img.size for img in processed_result.images[:z_count]))) if draw_legend: - grid_max_w, grid_max_h = map(max, zip(*(img.size for img in processed_result.images[start_index:end_index]))) - grid = images.draw_grid_annotations(grid, grid_max_w, grid_max_h, hor_texts, ver_texts, margin_size) - processed_result.images.insert(i, grid) - processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) - processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) - processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) - - z_grid = images.image_grid(processed_result.images[:z_count], rows=1) - z_sub_grid_max_w, z_sub_grid_max_h = map(max, zip(*(img.size for img in processed_result.images[:z_count]))) - if draw_legend: - z_grid = images.draw_grid_annotations(z_grid, z_sub_grid_max_w, z_sub_grid_max_h, 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]) + z_grid = images.draw_grid_annotations(z_grid, z_sub_grid_max_w, z_sub_grid_max_h, 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]) return processed_result @@ -441,7 +443,6 @@ def ui(self, is_img2img): 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.Row(): vary_seeds_x = gr.Checkbox(label='Vary seeds for X', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_x"), tooltip="Use different seeds for images along X axis.") @@ -449,9 +450,12 @@ def ui(self, is_img2img): vary_seeds_z = gr.Checkbox(label='Vary seeds for Z', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_z"), tooltip="Use different seeds for images along Z axis.") 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")) csv_mode = gr.Checkbox(label='Use text inputs instead of dropdowns', value=False, elem_id=self.elem_id("csv_mode")) - with gr.Column(): + + with InputAccordion(True, label='Draw grid', elem_id=self.elem_id('draw_grid')) as draw_grid: + with gr.Row(): + include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) 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"): @@ -533,9 +537,9 @@ def get_dropdown_update_from_params(axis, params): (z_values_dropdown, lambda params: get_dropdown_update_from_params("Z", params)), ) - return [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, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode] + return [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, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode, draw_grid] - 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, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode): + 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, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode, draw_grid): x_type, y_type, z_type = x_type or 0, y_type or 0, z_type or 0 # if axle type is None set to 0 if not no_fixed_seeds: @@ -780,7 +784,8 @@ def cell(x, y, z, ix, iy, iz): include_sub_grids=include_sub_grids, first_axes_processed=first_axes_processed, second_axes_processed=second_axes_processed, - margin_size=margin_size + margin_size=margin_size, + draw_grid=draw_grid, ) if not processed.images: @@ -789,14 +794,15 @@ def cell(x, y, z, ix, iy, iz): z_count = len(zs) - # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) - processed.infotexts[:1 + z_count] = grid_infotext[:1 + z_count] + if draw_grid: + # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) + processed.infotexts[:1 + z_count] = grid_infotext[:1 + z_count] if not include_lone_images: # Don't need sub-images anymore, drop from list: - processed.images = processed.images[:z_count + 1] + processed.images = processed.images[:z_count + 1] if draw_grid else [] - if opts.grid_save: + if draw_grid and opts.grid_save: # Auto-save main and sub-grids: grid_count = z_count + 1 if z_count > 1 else 1 for g in range(grid_count): @@ -806,7 +812,7 @@ def cell(x, y, z, ix, iy, iz): if not include_sub_grids: # if not include_sub_grids then skip saving after the first grid break - if not include_sub_grids: + if draw_grid and not include_sub_grids: # Done with sub-grids, drop all related information: for _ in range(z_count): del processed.images[1] diff --git a/webui.bat b/webui.bat index 7b162ce27cc..f29a40b36e9 100644 --- a/webui.bat +++ b/webui.bat @@ -4,7 +4,16 @@ if exist webui.settings.bat ( call webui.settings.bat ) -if not defined PYTHON (set PYTHON=python) +if not defined PYTHON ( + for /f "delims=" %%A in ('where python ^| findstr /n . ^| findstr ^^1:') do ( + if /i "%%~xA" == ".exe" ( + set PYTHON=python + ) else ( + set PYTHON=call python + ) + ) +) + if defined GIT (set "GIT_PYTHON_GIT_EXECUTABLE=%GIT%") if not defined VENV_DIR (set "VENV_DIR=%~dp0%venv") diff --git a/webui.py b/webui.py index 2c417168aa6..421e3b8334f 100644 --- a/webui.py +++ b/webui.py @@ -45,6 +45,44 @@ def api_only(): ) +def warning_if_invalid_install_dir(): + """ + Shows a warning if the webui is installed under a path that contains a leading dot in any of its parent directories. + + Gradio '/file=' route will block access to files that have a leading dot in the path segments. + We use this route to serve files such as JavaScript and CSS to the webpage, + if those files are blocked, the webpage will not function properly. + See https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/13292 + + This is a security feature was added to Gradio 3.32.0 and is removed in later versions, + this function replicates Gradio file access blocking logic. + + This check should be removed when it's no longer applicable. + """ + from packaging.version import parse + from pathlib import Path + import gradio + + if parse('3.32.0') <= parse(gradio.__version__) < parse('4'): + + def abspath(path): + """modified from Gradio 3.41.2 gradio.utils.abspath()""" + if path.is_absolute(): + return path + is_symlink = path.is_symlink() or any(parent.is_symlink() for parent in path.parents) + return Path.cwd() / path if (is_symlink or path == path.resolve()) else path.resolve() + + webui_root = Path(__file__).parent + if any(part.startswith(".") for part in abspath(webui_root).parts): + print(f'''{"!"*25} Warning {"!"*25} +WebUI is installed in a directory that has a leading dot (.) in one of its parent directories. +This will prevent WebUI from functioning properly. +Please move the installation to a different directory. +Current path: "{webui_root}" +For more information see: https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/13292 +{"!"*25} Warning {"!"*25}''') + + def webui(): from modules.shared_cmd_options import cmd_opts @@ -53,6 +91,8 @@ def webui(): from modules import shared, ui_tempdir, script_callbacks, ui, progress, ui_extra_networks + warning_if_invalid_install_dir() + while 1: if shared.opts.clean_temp_dir_at_start: ui_tempdir.cleanup_tmpdr()