•
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()