diff --git a/CHANGELOG.md b/CHANGELOG.md index 7976779d1..46f4be47c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,17 +6,15 @@ - Quick apply style - Add refine workflow in img2img - Control API/CLI -- Model load from dropdown select variant -- VAE preview - SC LoRA ## Update for 2024-03-23 - **Features**: - **Gallery**: - implemented as infinite-scroll with client-side-caching and lazy-loading while being fully async and non-blocking - search or sort by path, name, size, width, height, mtime or any image metadata item, also with extended syntax like *width > 1000* - *settings*: optional additional user-defined folders, thumbnails in fixed or variable aspect-ratio + implemented as infinite-scroll with client-side-caching and lazy-loading while being fully async and non-blocking + search or sort by path, name, size, width, height, mtime or any image metadata item, also with extended syntax like *width > 1000* + *settings*: optional additional user-defined folders, thumbnails in fixed or variable aspect-ratio - **Changes**: - Removed built-in extensions: *ControlNet* and *Image-Browser* as both *image-browser* and *controlnet* have native equivalents @@ -24,11 +22,13 @@ - **Improvements**: - Styles apply wildcards to params - Make metadata in full screen viewer optional + - Add VAE civitai scan metadata/preview - **Fixes**: - Prompt params parser - Fix image save without metadata - fix ROCm compatibility, thanks @Disty0 - Fix API generate save metadata + - Enumerate diffusers model with multiple variants ## Update for 2024-03-19 diff --git a/javascript/gallery.js b/javascript/gallery.js index 172023615..b73fd512f 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -68,6 +68,27 @@ async function createThumb(img) { return dataURL; } +async function addSeparators() { + document.querySelectorAll('.gallery-separator').forEach((node) => el.files.removeChild(node)); + const all = Array.from(el.files.children); + let lastDir; + for (const f of all) { + let dir = f.name.match(/(.*)[\/\\]/); + if (!dir) dir = ''; + else dir = dir[1]; + if (dir !== lastDir) { + lastDir = dir; + if (dir.length > 0) { + const sep = document.createElement('div'); + sep.className = 'gallery-separator'; + sep.innerText = dir; + sep.title = dir; + el.files.insertBefore(sep, f); + } + } + } +} + async function delayFetchThumb(fn) { while (outstanding > 16) await new Promise((resolve) => setTimeout(resolve, 50)); // eslint-disable-line no-promise-executor-return outstanding++; @@ -102,6 +123,7 @@ class GalleryFile extends HTMLElement { } async connectedCallback() { + if (this.shadow.children.length > 0) return; const ext = this.name.split('.').pop().toLowerCase(); if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) return; this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define @@ -243,7 +265,6 @@ async function gallerySearch(evt) { async function gallerySort(btn) { const t0 = performance.now(); - document.querySelectorAll('.gallery-separator').forEach((node) => el.files.removeChild(node)); // cannot sort separators const arr = Array.from(el.files.children); const fragment = document.createDocumentFragment(); el.files.innerHTML = ''; @@ -292,6 +313,7 @@ async function gallerySort(btn) { break; } el.files.appendChild(fragment); + addSeparators(); const t1 = performance.now(); el.status.innerText = `Sort | ${arr.length.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`; } @@ -315,21 +337,13 @@ async function fetchFiles(evt) { // fetch file-by-file list over websockets ws.close(); } else { const json = JSON.parse(event.data); - const dir = json.file.match(/(.*)[\/\\]/) || ''; - if (dir?.[1] !== lastDir) { // create separator - lastDir = dir[1]; - const sep = document.createElement('div'); - sep.className = 'gallery-separator'; - sep.innerText = lastDir; - sep.title = lastDir; - el.files.appendChild(sep); - } const file = new GalleryFile(json); fragment.appendChild(file); if (numFiles % 100 === 0) { el.files.appendChild(fragment); fragment = document.createDocumentFragment(); } + addSeparators(); el.status.innerText = `Folder | ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`; } }; diff --git a/modules/control/units/xs_model.py b/modules/control/units/xs_model.py index 43d992575..fcb6d99b4 100644 --- a/modules/control/units/xs_model.py +++ b/modules/control/units/xs_model.py @@ -436,15 +436,8 @@ class ControlNetXSModel(ModelMixin, ConfigMixin): norm_num_groups = unet.config.norm_num_groups else: norm_num_groups = min(block_out_channels) - - if group_norms_match_channel_sizes(norm_num_groups, block_out_channels): - print( - f"`norm_num_groups` was set to `min(block_out_channels)` (={norm_num_groups}) so it divides all block_out_channels` ({block_out_channels}). Set it explicitly to remove this information." - ) - else: - raise ValueError( - f"`block_out_channels` ({block_out_channels}) don't match the base models `norm_num_groups` ({unet.config.norm_num_groups}). Setting `norm_num_groups` to `min(block_out_channels)` ({norm_num_groups}) didn't fix this. Pass `norm_num_groups` explicitly so it divides all block_out_channels." - ) + if not group_norms_match_channel_sizes(norm_num_groups, block_out_channels): + raise ValueError(f'ControlNetXSModel mismatch: block_out_channels={block_out_channels} norm_num_groups={unet.config.norm_num_groups}') def get_time_emb_input_dim(unet: UNet2DConditionModel): return unet.time_embedding.linear_1.in_features diff --git a/modules/dml/hijack/realesrgan_model.py b/modules/dml/hijack/realesrgan_model.py index bee137a45..a8011beb5 100644 --- a/modules/dml/hijack/realesrgan_model.py +++ b/modules/dml/hijack/realesrgan_model.py @@ -1,6 +1,7 @@ import math import torch from modules.postprocess.realesrgan_model_arch import RealESRGANer +from modules.shared import log # DML Solution: Some of contents of output tensor turn to 0 after Extended Slices. Move it to cpu. @@ -36,16 +37,15 @@ def tile_process(self): # input tile dimensions input_tile_width = input_end_x - input_start_x input_tile_height = input_end_y - input_start_y - tile_idx = y * tiles_x + x + 1 + _tile_idx = y * tiles_x + x + 1 input_tile = self.img[0:self.img.shape[0], 0:self.img.shape[1], input_start_y_pad:input_end_y_pad, input_start_x_pad:input_end_x_pad] # upscale tile try: with torch.no_grad(): output_tile = self.model(input_tile) - except RuntimeError as error: - print('Error', error) - print(f'\tTile {tile_idx}/{tiles_x * tiles_y}') + except Exception as e: + log.error(f'Upscale error: type=R-ESRGAN {e}') # output tile area on total image output_start_x = input_start_x * self.scale @@ -63,4 +63,5 @@ def tile_process(self): # put tile into output image self.output[0:self.output.shape[0], 0:self.output.shape[1], output_start_y:output_end_y, output_start_x:output_end_x] = output_tile.cpu()[0:output_tile.shape[0], 0:output_tile.shape[1], output_start_y_tile:output_end_y_tile, output_start_x_tile:output_end_x_tile] self.output = self.output.to(output_tile.device) + RealESRGANer.tile_process = tile_process diff --git a/modules/hypernetworks/hypernetwork.py b/modules/hypernetworks/hypernetwork.py index 6e13ece24..1bb0c8ade 100644 --- a/modules/hypernetworks/hypernetwork.py +++ b/modules/hypernetworks/hypernetwork.py @@ -229,16 +229,6 @@ class Hypernetwork: # Dropout structure should have same length as layer structure, Every digits should be in [0,1), and last digit must be 0. if self.dropout_structure is None: self.dropout_structure = parse_dropout_structure(self.layer_structure, self.use_dropout, self.last_layer_dropout) - if shared.opts.print_hypernet_extra: - if self.optional_info is not None: - print(f" INFO:\n {self.optional_info}\n") - print(f" Layer structure: {self.layer_structure}") - print(f" Activation function: {self.activation_func}") - print(f" Weight initialization: {self.weight_init}") - print(f" Layer norm: {self.add_layer_norm}") - print(f" Dropout usage: {self.use_dropout}" ) - print(f" Activate last layer: {self.activate_output}") - print(f" Dropout structure: {self.dropout_structure}") optimizer_saved_dict = torch.load(self.filename + '.optim', map_location='cpu') if os.path.exists(self.filename + '.optim') else {} if self.shorthash() == optimizer_saved_dict.get('hash', None): self.optimizer_state_dict = optimizer_saved_dict.get('optimizer_state_dict', None) @@ -246,13 +236,8 @@ class Hypernetwork: self.optimizer_state_dict = None if self.optimizer_state_dict: self.optimizer_name = optimizer_saved_dict.get('optimizer_name', 'AdamW') - if shared.opts.print_hypernet_extra: - print("Load existing optimizer from checkpoint") - print(f"Optimizer name is {self.optimizer_name}") else: self.optimizer_name = "AdamW" - if shared.opts.print_hypernet_extra: - print("No saved optimizer exists in checkpoint") for size, sd in state_dict.items(): if type(size) == int: self.layers[size] = ( diff --git a/modules/modelloader.py b/modules/modelloader.py index 50f5623dc..5a8b27c33 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -235,7 +235,6 @@ def load_diffusers_models(clear=True): place = os.path.join(models_path, 'Diffusers') if clear: diffuser_repos.clear() - output = [] try: for folder in os.listdir(place): try: @@ -253,20 +252,20 @@ def load_diffusers_models(clear=True): if len(snapshots) == 0: shared.log.warning(f"Diffusers folder has no snapshots: location={place} folder={folder} name={name}") continue - commit = os.path.join(folder, 'snapshots', snapshots[-1]) - mtime = os.path.getmtime(commit) - info = os.path.join(commit, "model_info.json") - diffuser_repos.append({ 'name': name, 'filename': name, 'friendly': friendly, 'folder': folder, 'path': commit, 'hash': commit, 'mtime': mtime, 'model_info': info }) - if os.path.exists(os.path.join(folder, 'hidden')): - continue - output.append(name) - except Exception: - # shared.log.error(f"Error analyzing diffusers model: {folder} {e}") - pass + for snapshot in snapshots: + commit = os.path.join(folder, 'snapshots', snapshot) + mtime = os.path.getmtime(commit) + info = os.path.join(commit, "model_info.json") + repo = { 'name': name, 'filename': name, 'friendly': friendly, 'folder': folder, 'path': commit, 'hash': snapshot, 'mtime': mtime, 'model_info': info } + diffuser_repos.append(repo) + if os.path.exists(os.path.join(folder, 'hidden')): + continue + except Exception as e: + debug(f"Error analyzing diffusers model: {folder} {e}") except Exception as e: shared.log.error(f"Error listing diffusers: {place} {e}") - shared.log.debug(f'Scanning diffusers cache: folder={place} items={len(output)} time={time.time()-t0:.2f}') - return output + shared.log.debug(f'Scanning diffusers cache: folder={place} items={len(list(diffuser_repos))} time={time.time()-t0:.2f}') + return diffuser_repos def find_diffuser(name: str): diff --git a/modules/postprocess/swinir_model_arch.py b/modules/postprocess/swinir_model_arch.py index 73898b3a5..d5ae4dd32 100644 --- a/modules/postprocess/swinir_model_arch.py +++ b/modules/postprocess/swinir_model_arch.py @@ -848,19 +848,3 @@ class SwinIR(nn.Module): flops += H * W * 3 * self.embed_dim * self.embed_dim flops += self.upsample.flops() return flops - - -if __name__ == '__main__': - upscale = 4 - window_size = 8 - height = (1024 // upscale // window_size + 1) * window_size - width = (720 // upscale // window_size + 1) * window_size - model = SwinIR(upscale=2, img_size=(height, width), - window_size=window_size, img_range=1., depths=[6, 6, 6, 6], - embed_dim=60, num_heads=[6, 6, 6, 6], mlp_ratio=2, upsampler='pixelshuffledirect') - print(model) - print(height, width, model.flops() / 1e9) - - x = torch.randn((1, 3, height, width)) - x = model(x) - print(x.shape) diff --git a/modules/postprocess/swinir_model_arch_v2.py b/modules/postprocess/swinir_model_arch_v2.py index 71c246c3f..ca69e2969 100644 --- a/modules/postprocess/swinir_model_arch_v2.py +++ b/modules/postprocess/swinir_model_arch_v2.py @@ -997,19 +997,3 @@ class Swin2SR(nn.Module): flops += H * W * 3 * self.embed_dim * self.embed_dim flops += self.upsample.flops() return flops - - -if __name__ == '__main__': - upscale = 4 - window_size = 8 - height = (1024 // upscale // window_size + 1) * window_size - width = (720 // upscale // window_size + 1) * window_size - model = Swin2SR(upscale=2, img_size=(height, width), - window_size=window_size, img_range=1., depths=[6, 6, 6, 6], - embed_dim=60, num_heads=[6, 6, 6, 6], mlp_ratio=2, upsampler='pixelshuffledirect') - print(model) - print(height, width, model.flops() / 1e9) - - x = torch.randn((1, 3, height, width)) - x = model(x) - print(x.shape) diff --git a/modules/sd_models.py b/modules/sd_models.py index 9f578ee93..8da3b2645 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -41,9 +41,9 @@ debug_load = os.environ.get('SD_LOAD_DEBUG', None) class CheckpointInfo: - def __init__(self, filename): + def __init__(self, filename, sha=None): self.name = None - self.hash = None + self.hash = sha self.filename = filename self.type = '' relname = filename @@ -77,9 +77,11 @@ class CheckpointInfo: self.filename = filename self.sha256 = hashes.sha256_from_cache(self.filename, f"checkpoint/{relname}") self.type = ext - # self.model_name = os.path.splitext(name.replace("/", "_").replace("\\", "_"))[0] else: # maybe a diffuser - repo = [r for r in modelloader.diffuser_repos if filename == r['name']] + if self.hash is None: + repo = [r for r in modelloader.diffuser_repos if self.filename == r['name']] + else: + repo = [r for r in modelloader.diffuser_repos if self.hash == r['hash']] if len(repo) == 0: self.name = relname self.filename = filename @@ -140,17 +142,17 @@ def list_models(): global checkpoints_list # pylint: disable=global-statement checkpoints_list.clear() checkpoint_aliases.clear() - if shared.opts.sd_disable_ckpt or shared.backend == shared.Backend.DIFFUSERS: - ext_filter = [".safetensors"] - else: - ext_filter = [".ckpt", ".safetensors"] + ext_filter = [".safetensors"] if shared.opts.sd_disable_ckpt or shared.backend == shared.Backend.DIFFUSERS else [".ckpt", ".safetensors"] model_list = list(modelloader.load_models(model_path=model_path, model_url=None, command_path=shared.opts.ckpt_dir, ext_filter=ext_filter, download_name=None, ext_blacklist=[".vae.ckpt", ".vae.safetensors"])) - if shared.backend == shared.Backend.DIFFUSERS: - model_list += modelloader.load_diffusers_models(clear=True) for filename in sorted(model_list, key=str.lower): checkpoint_info = CheckpointInfo(filename) if checkpoint_info.name is not None: checkpoint_info.register() + if shared.backend == shared.Backend.DIFFUSERS: + for repo in modelloader.load_diffusers_models(clear=True): + checkpoint_info = CheckpointInfo(repo['name'], sha=repo['hash']) + if checkpoint_info.name is not None: + checkpoint_info.register() if shared.cmd_opts.ckpt is not None: if not os.path.exists(shared.cmd_opts.ckpt) and shared.backend == shared.Backend.ORIGINAL: if shared.cmd_opts.ckpt.lower() != "none": @@ -920,18 +922,18 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No try: # this is horrible special-case handling for stable-cascade multi-stage pipeline with variants and non-standard revision diffusers_load_config.pop("vae", None) diffusers_load_config["variant"] = 'bf16' - if 'lite' in checkpoint_info.name: - decoder_unet = diffusers.models.StableCascadeUNet.from_pretrained("stabilityai/stable-cascade", subfolder="decoder_lite", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/44", **diffusers_load_config) - decoder = diffusers.StableCascadeDecoderPipeline.from_pretrained("stabilityai/stable-cascade", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/44", decoder=decoder_unet, **diffusers_load_config) + if 'lite' in checkpoint_info.name or 'abc818bb0d' in checkpoint_info.hash: + decoder_unet = diffusers.models.StableCascadeUNet.from_pretrained("stabilityai/stable-cascade", subfolder="decoder_lite", cache_dir=shared.opts.diffusers_dir, **diffusers_load_config) + decoder = diffusers.StableCascadeDecoderPipeline.from_pretrained("stabilityai/stable-cascade", cache_dir=shared.opts.diffusers_dir, decoder=decoder_unet, **diffusers_load_config) shared.log.debug(f'StableCascade lite decoder: scale={decoder.latent_dim_scale}') - prior_unet = diffusers.models.StableCascadeUNet.from_pretrained("stabilityai/stable-cascade-prior", subfolder="prior_lite", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/2", **diffusers_load_config) - prior = diffusers.StableCascadePriorPipeline.from_pretrained("stabilityai/stable-cascade-prior", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/2", prior=prior_unet, **diffusers_load_config) + prior_unet = diffusers.models.StableCascadeUNet.from_pretrained("stabilityai/stable-cascade-prior", subfolder="prior_lite", cache_dir=shared.opts.diffusers_dir, **diffusers_load_config) + prior = diffusers.StableCascadePriorPipeline.from_pretrained("stabilityai/stable-cascade-prior", cache_dir=shared.opts.diffusers_dir, prior=prior_unet, **diffusers_load_config) shared.log.debug(f'StableCascade lite prior: scale={prior.resolution_multiple}') else: - decoder = diffusers.StableCascadeDecoderPipeline.from_pretrained("stabilityai/stable-cascade", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/44", **diffusers_load_config) - shared.log.debug(f'StableCascade decoder: scale={decoder.latent_dim_scale}') - prior = diffusers.StableCascadePriorPipeline.from_pretrained("stabilityai/stable-cascade-prior", cache_dir=shared.opts.diffusers_dir, revision="refs/pr/2", **diffusers_load_config) - shared.log.debug(f'StableCascade prior: scale={prior.resolution_multiple}') + decoder = diffusers.StableCascadeDecoderPipeline.from_pretrained("stabilityai/stable-cascade", cache_dir=shared.opts.diffusers_dir, **diffusers_load_config) + shared.log.debug(f'StableCascade full decoder: scale={decoder.latent_dim_scale}') + prior = diffusers.StableCascadePriorPipeline.from_pretrained("stabilityai/stable-cascade-prior", cache_dir=shared.opts.diffusers_dir, **diffusers_load_config) + shared.log.debug(f'StableCascade full prior: scale={prior.resolution_multiple}') sd_model = diffusers.StableCascadeCombinedPipeline( tokenizer=decoder.tokenizer, text_encoder=decoder.text_encoder, diff --git a/modules/ui_img2img.py b/modules/ui_img2img.py index 1c43847df..982c1f0bd 100644 --- a/modules/ui_img2img.py +++ b/modules/ui_img2img.py @@ -150,7 +150,7 @@ def create_ui(): for i, elem in enumerate(img2img_tabs): elem.select(fn=lambda tab=i: select_img2img_tab(tab), inputs=[], outputs=[inpaint_controls, mask_alpha]) # pylint: disable=cell-var-from-loop - override_settings = ui_common.create_override_inputs('img2img') + override_settings = ui_common.create_override_inputs('img2img') with gr.Group(elem_id="img2img_script_container"): img2img_script_inputs = modules.scripts.scripts_img2img.setup_ui(parent='img2img', accordion=True)