diff --git a/.gitmodules b/.gitmodules index a6d3e9bf1..cc6c17569 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,34 +1,36 @@ [submodule "wiki"] path = wiki url = https://github.com/vladmandic/automatic.wiki -[submodule "extensions-builtin/sd-extension-system-info"] - path = extensions-builtin/sd-extension-system-info - url = https://github.com/vladmandic/sd-extension-system-info -[submodule "extensions-builtin/stable-diffusion-webui-images-browser"] - path = extensions-builtin/stable-diffusion-webui-images-browser - url = https://github.com/AlUlkesh/stable-diffusion-webui-images-browser - ignore = dirty -[submodule "modules/lora"] - path = modules/lora - url = https://github.com/kohya-ss/sd-scripts - ignore = dirty -[submodule "extensions-builtin/sd-webui-controlnet"] - path = extensions-builtin/sd-webui-controlnet - url = https://github.com/Mikubill/sd-webui-controlnet - ignore = dirty -[submodule "extensions-builtin/stable-diffusion-webui-rembg"] - path = extensions-builtin/stable-diffusion-webui-rembg - url = https://github.com/vladmandic/sd-extension-rembg - ignore = dirty -[submodule "extensions-builtin/sd-webui-agent-scheduler"] - path = extensions-builtin/sd-webui-agent-scheduler - url = https://github.com/ArtVentureX/sd-webui-agent-scheduler - ignore = dirty -[submodule "extensions-builtin/sd-extension-chainner"] - path = extensions-builtin/sd-extension-chainner - url = https://github.com/vladmandic/sd-extension-chainner ignore = dirty [submodule "modules/k-diffusion"] path = modules/k-diffusion url = https://github.com/crowsonkb/k-diffusion ignore = dirty +[submodule "modules/lora"] + path = modules/lora + url = https://github.com/kohya-ss/sd-scripts + ignore = dirty +[submodule "extensions-builtin/sd-extension-system-info"] + path = extensions-builtin/sd-extension-system-info + url = https://github.com/vladmandic/sd-extension-system-info + ignore = dirty +[submodule "extensions-builtin/sd-extension-chainner"] + path = extensions-builtin/sd-extension-chainner + url = https://github.com/vladmandic/sd-extension-chainner + ignore = dirty +[submodule "extensions-builtin/stable-diffusion-webui-rembg"] + path = extensions-builtin/stable-diffusion-webui-rembg + url = https://github.com/vladmandic/sd-extension-rembg + ignore = dirty +[submodule "extensions-builtin/stable-diffusion-webui-images-browser"] + path = extensions-builtin/stable-diffusion-webui-images-browser + url = https://github.com/AlUlkesh/stable-diffusion-webui-images-browser + ignore = dirty +[submodule "extensions-builtin/sd-webui-controlnet"] + path = extensions-builtin/sd-webui-controlnet + url = https://github.com/Mikubill/sd-webui-controlnet + ignore = dirty +[submodule "extensions-builtin/sd-webui-agent-scheduler"] + path = extensions-builtin/sd-webui-agent-scheduler + url = https://github.com/ArtVentureX/sd-webui-agent-scheduler + ignore = dirty diff --git a/CHANGELOG.md b/CHANGELOG.md index 41055f528..43167176f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,47 @@ # Change Log for SD.Next +## Update for 2023-11-10 + +- **Diffusers** + - **LCM** support for any *SD 1.5* or *SD-XL* model! + - download [lcm-lora-sd15](https://huggingface.co/latent-consistency/lcm-lora-sdv1-5/tree/main) and/or [lcm-lora-sdxl](https://huggingface.co/latent-consistency/lcm-lora-sdxl/tree/main) + - load for favorite *SD 1.5* or *SD-XL* model *(original LCM was SD 1.5 only, this is both)* + - load **lcm lora** + - set **sampler** to **LCM** + - set number of steps to some low number, for SD-XL 6-7 steps is normally sufficient + note: LCM scheduler does not support steps higher than 50 + - set cfg to 1 or 2 + - Add `cli/lcm-convert.py` script to convert any SD 1.5 or SD-XL model to LCM model + by baking in LORA and uploading to Huggingface, thanks @Disty0 + - Add additional pipeline types for manual model loads when loading from `safetensors` + - Updated logic for calculating **steps** when using base/hires/refiner workflows + - Safe model offloading for non-standard models + - Fix **DPM SDE** scheduler + - Update to `diffusers==0.23.0` +- **Extra networks** + - Use multi-threading for 5x load speedup +- **General**: + - Reworked parser when pasting previously generated images/prompts + includes all `txt2img`, `img2img` and `override` params + - Add refiner options to XYZ Grid + - Support custom upscalers in subfolders + - Support `--ckpt none` to skip loading a model +- **Fixes** + - Fix inpaint + - Fix manual grid image save + - Fix img2img init image save + - More uniform models paths + - Safe scripts callback execution + - Improve extension compatibility + - Improve BF16 support + ## Update for 2023-11-06 Another pretty big release, this time with focus on new models (3 new model types), new backends and optimizations Plus quite a few fixes Also, [Wiki](https://github.com/vladmandic/automatic/wiki) has been updated with new content, so check it out! -Some highlights: [OpenVINO](https://github.com/vladmandic/automatic/wiki/OpenVINO), [IntelArc](https://github.com/vladmandic/automatic/wiki/Intel-ARC), [DirectML](https://github.com/vladmandic/automatic/wiki/DirectML), [ONNX/Olive>](https://github.com/vladmandic/automatic/wiki/ONNX-Runtime) +Some highlights: [OpenVINO](https://github.com/vladmandic/automatic/wiki/OpenVINO), [IntelArc](https://github.com/vladmandic/automatic/wiki/Intel-ARC), [DirectML](https://github.com/vladmandic/automatic/wiki/DirectML), [ONNX/Olive](https://github.com/vladmandic/automatic/wiki/ONNX-Olive) - **Diffusers** - since now **SD.Next** supports **12** different model types, we've added reference model for each type in diff --git a/README.md b/README.md index 08afaf6c1..7958c1b5d 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ General goals: ### **Sponsors**
-Allan GrantMichael HarrisBrent OzarToniXMatthew RunoHELLO WORLD SASSalad TechnologiesGym Dreams • GymDreams8a.v.mantzaris +Allan GrantMichael HarrisBrent OzarToniXMatthew RunoHELLO WORLD SASSalad Technologiesa.v.mantzaris

diff --git a/cli/lcm-convert.py b/cli/lcm-convert.py new file mode 100644 index 000000000..c2d7c266b --- /dev/null +++ b/cli/lcm-convert.py @@ -0,0 +1,55 @@ +import os +import argparse +import torch +from diffusers import StableDiffusionPipeline, StableDiffusionXLPipeline, AutoPipelineForText2Image, LCMScheduler + +parser = argparse.ArgumentParser("lcm_convert") +parser.add_argument("--name", help="Name of the new LCM model", type=str) +parser.add_argument("--model", help="A model to convert", type=str) +parser.add_argument("--lora-scale", default=1.0, help="Strenght of the LCM", type=float) +parser.add_argument("--huggingface", action="store_true", help="Use Hugging Face models instead of safetensors models") +parser.add_argument("--upload", action="store_true", help="Upload the new LCM model to Hugging Face") +parser.add_argument("--no-half", action="store_true", help="Convert the new LCM model to FP32") +parser.add_argument("--no-save", action="store_true", help="Don't save the new LCM model to local disk") +parser.add_argument("--sdxl", action="store_true", help="Use SDXL models") +parser.add_argument("--ssd-1b", action="store_true", help="Use SSD-1B models") + +args = parser.parse_args() + +if args.huggingface: + pipeline = AutoPipelineForText2Image.from_pretrained(args.model, torch_dtype=torch.float16, variant="fp16") +else: + if args.sdxl or args.ssd_1b: + pipeline = StableDiffusionXLPipeline.from_single_file(args.model) + else: + pipeline = StableDiffusionPipeline.from_single_file(args.model) + +pipeline.scheduler = LCMScheduler.from_config(pipeline.scheduler.config) +if args.sdxl: + pipeline.load_lora_weights("latent-consistency/lcm-lora-sdxl") +elif args.ssd_1b: + pipeline.load_lora_weights("latent-consistency/lcm-lora-ssd-1b") +else: + pipeline.load_lora_weights("latent-consistency/lcm-lora-sdv1-5") +pipeline.fuse_lora(lora_scale=args.lora_scale) + +#components = pipeline.components +#pipeline = LatentConsistencyModelPipeline(**components) + +if args.no_half: + pipeline = pipeline.to(dtype=torch.float32) +else: + pipeline = pipeline.to(dtype=torch.float16) +print(pipeline) + +if not args.no_save: + os.makedirs(f"models--local--{args.name}/snapshots") + if args.no_half: + pipeline.save_pretrained(f"models--local--{args.name}/snapshots/{args.name}") + else: + pipeline.save_pretrained(f"models--local--{args.name}/snapshots/{args.name}", variant="fp16") +if args.upload: + if args.no_half: + pipeline.push_to_hub(args.name) + else: + pipeline.push_to_hub(args.name, variant="fp16") diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index 249f84890..7df667acb 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -1,5 +1,6 @@ import os import json +import concurrent import network import networks from modules import shared, ui_extra_networks @@ -8,6 +9,7 @@ from modules import shared, ui_extra_networks class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): def __init__(self): super().__init__('Lora') + self.list_time = 0 def refresh(self): networks.list_available_networks() @@ -74,10 +76,12 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): return None def list_items(self): - for _index, name in enumerate(networks.available_networks): - item = self.create_item(name) - if item is not None: - yield item + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + future_items = {executor.submit(self.create_item, net): net for net in networks.available_networks} + for future in concurrent.futures.as_completed(future_items): + item = future.result() + if item is not None: + yield item def allowed_directories_for_previews(self): return [shared.cmd_opts.lora_dir, shared.cmd_opts.lyco_dir] diff --git a/extensions-builtin/sd-webui-agent-scheduler b/extensions-builtin/sd-webui-agent-scheduler index 99b2cafbc..8970f485b 160000 --- a/extensions-builtin/sd-webui-agent-scheduler +++ b/extensions-builtin/sd-webui-agent-scheduler @@ -1 +1 @@ -Subproject commit 99b2cafbc2b4a2fc93ffcabd56b0ff915396d1f1 +Subproject commit 8970f485b767929cb36b5fee0df1d98840023f2a diff --git a/extensions-builtin/sd-webui-controlnet b/extensions-builtin/sd-webui-controlnet index fce6775a6..05ef0b1cd 160000 --- a/extensions-builtin/sd-webui-controlnet +++ b/extensions-builtin/sd-webui-controlnet @@ -1 +1 @@ -Subproject commit fce6775a6dddef52ecd658259e909687d9dedf72 +Subproject commit 05ef0b1cd1374cf285dd8d5ccd7db9997549893c diff --git a/extensions-builtin/stable-diffusion-webui-rembg b/extensions-builtin/stable-diffusion-webui-rembg index 7f5772962..b73dee3f3 160000 --- a/extensions-builtin/stable-diffusion-webui-rembg +++ b/extensions-builtin/stable-diffusion-webui-rembg @@ -1 +1 @@ -Subproject commit 7f57729626503837a70ad9eed92313bc36db7bf3 +Subproject commit b73dee3f3fa99b1e7c7ee8dc6dad0176cb74e24c diff --git a/html/locale_en.json b/html/locale_en.json index f0c84af87..65f239e10 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -61,7 +61,7 @@ {"id":"","label":"Skip","localized":"","hint":"Stop processing current job and continue processing"}, {"id":"","label":"Interrupt","localized":"","hint":"Interrupt current processing job"}, {"id":"","label":"Pause","localized":"","hint":"Pause processing"}, - {"id":"","label":"Restore","localized":"","hint":"Restore parameters from last known generated image"}, + {"id":"","label":"Restore","localized":"","hint":"Restore parameters from current prompt or last known generated image"}, {"id":"","label":"Clear","localized":"","hint":"Clear prompts"}, {"id":"","label":"Networks","localized":"","hint":"Open extra network interface"}, {"id":"","label":"Interrogate\nCLIP","localized":"","hint":"Run interrogate using CLIP model"}, diff --git a/html/reference.json b/html/reference.json index 8c6d752d2..49f85b189 100644 --- a/html/reference.json +++ b/html/reference.json @@ -24,7 +24,12 @@ "desc": "Segmind's Tiny-SD offers a compact, efficient, and distilled version of Realistic Vision 4.0 and is up to 80% faster than SD1.5", "preview": "segmind--tiny-sd.jpg" }, - "LCM Dreamshaper 7": { + "LCM SD-XL": { + "path": "latent-consistency/lcm-sdxl", + "desc": "Latent Consistencey Models enable swift inference with minimal steps on any pre-trained LDMs, including Stable Diffusion. By distilling classifier-free guidance into the model's input, LCM can generate high-quality images in very short inference time. LCM can generate quality images in as few as 3-4 steps, making it blazingly fast.", + "preview": "latent-consistency--lcm-sdxl.jpg" + }, + "LCM SD-1.5 Dreamshaper 7": { "path": "SimianLuo/LCM_Dreamshaper_v7", "desc": "Latent Consistencey Models enable swift inference with minimal steps on any pre-trained LDMs, including Stable Diffusion. By distilling classifier-free guidance into the model's input, LCM can generate high-quality images in very short inference time. LCM can generate quality images in as few as 3-4 steps, making it blazingly fast.", "preview": "simianluo--lcm_dreamshaper_v7.jpg" diff --git a/installer.py b/installer.py index 01a48ad8e..adb57121f 100644 --- a/installer.py +++ b/installer.py @@ -201,7 +201,7 @@ def installed(package, friendly: str = None, reload = False, quiet = False): def uninstall(package): - if installed(package, package): + if installed(package, package, quiet=True): log.warning(f'Uninstalling: {package}') pip(f"uninstall {package} --yes --quiet", ignore=True, quiet=True) diff --git a/javascript/black-teal.css b/javascript/black-teal.css index 70e1807ac..b2cd0b984 100644 --- a/javascript/black-teal.css +++ b/javascript/black-teal.css @@ -38,7 +38,7 @@ html { font-size: var(--font-size); font-family: var(--font); } body, button, input, select, textarea { font-family: var(--font); } button { font-size: 1.2rem; max-width: 400px; } img { background-color: var(--background-color); } -input[type=range] { height: var(--line-sm) !important; appearance: none !important; margin-top: 0 !important; min-width: 100% !important; +input[type=range] { height: var(--line-sm) !important; appearance: none !important; margin-top: 0 !important; min-width: max(4em, 100%) !important; background-color: var(--background-color) !important; width: 100% !important; background: transparent !important; } input[type=range]::-webkit-slider-runnable-track { width: 100% !important; height: var(--line-sm) !important; cursor: pointer !important; box-shadow: 2px 2px 3px #111111 !important; background: var(--input-background-fill) !important; border-radius: var(--radius-lg) !important; border: 0px solid #222222 !important; } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 06c0cf819..8415d2d11 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -40,6 +40,7 @@ const setENState = (state) => { // methods function showCardDetails(event) { + console.log('showCardDetails', event) const tabname = getENActiveTab(); const btn = gradioApp().getElementById(`${tabname}_extra_details_btn`); btn.click(); diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 61aebf947..94652f34f 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -30,7 +30,7 @@ textarea { overflow-y: auto !important; } .gradio-column { min-width: min(160px, 100%) !important; } .gradio-container { max-width: unset !important; padding: var(--block-label-padding) !important; } .gradio-container .prose a, .gradio-container .prose a:visited{ color: unset; text-decoration: none; } -.gradio-dropdown { margin-right: var(--spacing-sm) !important; } +.gradio-dropdown { margin-right: var(--spacing-sm) !important; min-width:160px; max-width:fit-content } .gradio-dropdown ul.options { z-index: 1000; min-width: fit-content; max-height: 33vh !important; white-space: nowrap; } .gradio-dropdown ul.options li.item { padding: var(--spacing-xs); } .gradio-dropdown ul.options li.item:not(:has(.hide)) { background-color: var(--primary-500); } @@ -106,11 +106,10 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } #quicksettings > button { padding: 0 1em 0 0; align-self: end; margin-bottom: var(--text-sm); } #settings { display: flex; gap: var(--layout-gap); } #settings div { border: none; gap: 0; margin: 0 0 var(--layout-gap) 0px; padding: 0; } -#settings .gr-group { max-width: 70em; } #settings > div.tab-content { flex: 10 0 75%; display: grid; } #settings > div.tab-content > div { border: none; padding: 0; } #settings > div.tab-content > div > div > div > div > div { flex-direction: unset; } -#settings > div.tab-nav { display: grid; grid-template-columns: repeat(auto-fill, .5em minmax(10em, 1fr)); flex: 1 0 auto; width: 12em; align-self: flex-start; gap: var(--spacing-lg); } +#settings > div.tab-nav { display: grid; grid-template-columns: repeat(auto-fill, .5em minmax(10em, 1fr)); flex: 1 0 auto; width: 12em; align-self: flex-start; gap: var(--spacing-xxl); } #settings > div.tab-nav button { display: block; border: none; text-align: left; white-space: initial; padding: 0; } #settings > div.tab-nav > #settings_show_all_pages { padding: var(--size-2) var(--size-4); } #settings .block.gradio-checkbox { margin: 0; width: auto; } diff --git a/javascript/settings.js b/javascript/settings.js index dacc8bd87..93e4bebce 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -77,7 +77,7 @@ function markIfModified(setting_name, value) { tab_nav_indicator.classList.toggle('saved', saved.size > 0); if (changed_items.size > 0) tab_nav_indicator.title += `click to reset ${changed_items.size} unapplied changes in this tab\n`; if (saved.size > 0) tab_nav_indicator.title += `${saved.size} custom values\n${unsaved.size} default values}`; - elem.scrollIntoView({ behavior: 'smooth', block: 'center' }); // TODO why is scroll happening on every change if all pages are visible? + // elem.scrollIntoView({ behavior: 'smooth', block: 'center' }); // TODO why is scroll happening on every change if all pages are visible? } onAfterUiUpdate(async () => { @@ -105,9 +105,10 @@ onAfterUiUpdate(async () => { }, }); - const settings_search = gradioApp().querySelectorAll('#settings_search > label > textarea')[0]; - settings_search.oninput = (e) => { + const settingsSearch = gradioApp().querySelectorAll('#settings_search > label > textarea')[0]; + settingsSearch.oninput = (e) => { setTimeout(() => { + log('settingsSearch', e.target.value) showAllSettings(); gradioApp().querySelectorAll('#tab_settings .tabitem').forEach((section) => { section.querySelectorAll('.dirtyable').forEach((setting) => { @@ -131,23 +132,23 @@ onOptionsChanged(() => { function initSettings() { if (settingsInitialized) return; settingsInitialized = true; - const tab_nav_element = gradioApp().querySelector('#settings > .tab-nav'); - const tab_nav_buttons = gradioApp().querySelectorAll('#settings > .tab-nav > button'); - const tab_elements = gradioApp().querySelectorAll('#settings > div:not(.tab-nav)'); + const tabNavElements = gradioApp().querySelector('#settings > .tab-nav'); + const tabNavButtons = gradioApp().querySelectorAll('#settings > .tab-nav > button'); + const tabElements = gradioApp().querySelectorAll('#settings > div:not(.tab-nav)'); const observer = new MutationObserver((mutations) => { - const show_all_pages_dummy = gradioApp().getElementById('settings_show_all_pages'); - if (show_all_pages_dummy.style.display === 'none') { return; } - const mutation_on_style = (mut) => mut.type === 'attributes' && mut.attributeName === 'style'; - if (mutations.some(mutation_on_style)) showAllSettings(); + const showAllPages = gradioApp().getElementById('settings_show_all_pages'); + if (showAllPages.style.display === 'none') return; + const mutation = (mut) => mut.type === 'attributes' && mut.attributeName === 'style' + if (mutations.some(mutation)) showAllSettings(); }); - const tab_content_wrapper = document.createElement('div'); - tab_content_wrapper.className = 'tab-content'; - tab_nav_element.parentElement.insertBefore(tab_content_wrapper, tab_nav_element.nextSibling); - tab_elements.forEach((elem, index) => { - const tab_name = elem.id.replace('settings_', ''); - const indicator = gradioApp().getElementById(`modification_indicator_${tab_name}`); - tab_nav_element.insertBefore(indicator, tab_nav_buttons[index]); - tab_content_wrapper.appendChild(elem); + const tabContentWrapper = document.createElement('div'); + tabContentWrapper.className = 'tab-content'; + tabNavElements.parentElement.insertBefore(tabContentWrapper, tabNavElements.nextSibling); + tabElements.forEach((elem, index) => { + const tabName = elem.id.replace('settings_', ''); + const indicator = gradioApp().getElementById(`modification_indicator_${tabName}`); + tabNavElements.insertBefore(indicator, tabNavButtons[index]); + tabContentWrapper.appendChild(elem); observer.observe(elem, { attributes: true, attributeFilter: ['style'] }); }); log('initSettings'); diff --git a/javascript/ui.js b/javascript/ui.js index d15ac24de..daffbe352 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -170,7 +170,7 @@ function submit_postprocessing(...args) { return args; } -const submit = submit_txt2img; +window.submit = submit_txt2img; function modelmerger(...args) { const id = randomId(); diff --git a/models/Reference/latent-consistency--lcm-sdxl.jpg b/models/Reference/latent-consistency--lcm-sdxl.jpg new file mode 100644 index 000000000..34e6b2394 Binary files /dev/null and b/models/Reference/latent-consistency--lcm-sdxl.jpg differ diff --git a/modules/deepbooru.py b/modules/deepbooru.py index de50853d6..24f970a7d 100644 --- a/modules/deepbooru.py +++ b/modules/deepbooru.py @@ -16,9 +16,10 @@ class DeepDanbooru: def load(self): if self.model is not None: return - + model_path = os.path.join(paths.models_path, "DeepDanbooru") + shared.log.debug(f'Loading interrogate model: type=DeepDanbooru folder={model_path}') files = modelloader.load_models( - model_path=os.path.join(paths.models_path, "torch_deepdanbooru"), + model_path=model_path, model_url='https://github.com/AUTOMATIC1111/TorchDeepDanbooru/releases/download/v1/model-resnet_custom_v3.pt', ext_filter=[".pt"], download_name='model-resnet_custom_v3.pt', diff --git a/modules/dml/hijack/diffusers.py b/modules/dml/hijack/diffusers.py index 7ad66336f..881ac44fd 100644 --- a/modules/dml/hijack/diffusers.py +++ b/modules/dml/hijack/diffusers.py @@ -1,5 +1,8 @@ import torch import diffusers +import diffusers.utils.torch_utils +from typing import Optional, Union, Tuple + def PNDMScheduler__get_prev_sample(self, sample: torch.FloatTensor, timestep, prev_timestep, model_output): # See formula (9) of PNDM paper https://arxiv.org/pdf/2202.09778.pdf @@ -45,8 +48,10 @@ def PNDMScheduler__get_prev_sample(self, sample: torch.FloatTensor, timestep, pr return prev_sample + diffusers.PNDMScheduler._get_prev_sample = PNDMScheduler__get_prev_sample # pylint: disable=protected-access + def UniPCMultistepScheduler_multistep_uni_p_bh_update( self: diffusers.UniPCMultistepScheduler, model_output: torch.FloatTensor, @@ -153,4 +158,103 @@ def UniPCMultistepScheduler_multistep_uni_p_bh_update( x_t = x_t.to(x.dtype) return x_t + diffusers.UniPCMultistepScheduler.multistep_uni_p_bh_update = UniPCMultistepScheduler_multistep_uni_p_bh_update + + +def LCMScheduler_step( + self, + model_output: torch.FloatTensor, + timestep: int, + sample: torch.FloatTensor, + generator: Optional[torch.Generator] = None, + return_dict: bool = True, + ) -> Union[diffusers.schedulers.scheduling_lcm.LCMSchedulerOutput, Tuple]: + """ + Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion + process from the learned model outputs (most often the predicted noise). + + Args: + model_output (`torch.FloatTensor`): + The direct output from learned diffusion model. + timestep (`float`): + The current discrete timestep in the diffusion chain. + sample (`torch.FloatTensor`): + A current instance of a sample created by the diffusion process. + generator (`torch.Generator`, *optional*): + A random number generator. + return_dict (`bool`, *optional*, defaults to `True`): + Whether or not to return a [`~schedulers.scheduling_lcm.LCMSchedulerOutput`] or `tuple`. + Returns: + [`~schedulers.scheduling_utils.LCMSchedulerOutput`] or `tuple`: + If return_dict is `True`, [`~schedulers.scheduling_lcm.LCMSchedulerOutput`] is returned, otherwise a + tuple is returned where the first element is the sample tensor. + """ + if self.num_inference_steps is None: + raise ValueError( + "Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler" + ) + + if self.step_index is None: + self._init_step_index(timestep) + + # 1. get previous step value + prev_step_index = self.step_index + 1 + if prev_step_index < len(self.timesteps): + prev_timestep = self.timesteps[prev_step_index] + else: + prev_timestep = timestep + + # 2. compute alphas, betas + sample.__str__() + alpha_prod_t = self.alphas_cumprod[timestep] + alpha_prod_t_prev = self.alphas_cumprod[prev_timestep] if prev_timestep >= 0 else self.final_alpha_cumprod + + beta_prod_t = 1 - alpha_prod_t + beta_prod_t_prev = 1 - alpha_prod_t_prev + + # 3. Get scalings for boundary conditions + c_skip, c_out = self.get_scalings_for_boundary_condition_discrete(timestep) + + # 4. Compute the predicted original sample x_0 based on the model parameterization + if self.config.prediction_type == "epsilon": # noise-prediction + predicted_original_sample = (sample - beta_prod_t.sqrt() * model_output) / alpha_prod_t.sqrt() + elif self.config.prediction_type == "sample": # x-prediction + predicted_original_sample = model_output + elif self.config.prediction_type == "v_prediction": # v-prediction + predicted_original_sample = alpha_prod_t.sqrt() * sample - beta_prod_t.sqrt() * model_output + else: + raise ValueError( + f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample` or" + " `v_prediction` for `LCMScheduler`." + ) + + # 5. Clip or threshold "predicted x_0" + if self.config.thresholding: + predicted_original_sample = self._threshold_sample(predicted_original_sample) + elif self.config.clip_sample: + predicted_original_sample = predicted_original_sample.clamp( + -self.config.clip_sample_range, self.config.clip_sample_range + ) + + # 6. Denoise model output using boundary conditions + denoised = c_out * predicted_original_sample + c_skip * sample + + # 7. Sample and inject noise z ~ N(0, I) for MultiStep Inference + # Noise is not used for one-step sampling. + if len(self.timesteps) > 1: + noise = diffusers.utils.torch_utils.randn_tensor(model_output.shape, generator=generator, device=model_output.device) + prev_sample = alpha_prod_t_prev.sqrt() * denoised + beta_prod_t_prev.sqrt() * noise + else: + prev_sample = denoised + + # upon completion increase step index by one + self._step_index += 1 + + if not return_dict: + return (prev_sample, denoised) + + return diffusers.schedulers.scheduling_lcm.LCMSchedulerOutput(prev_sample=prev_sample, denoised=denoised) + + +diffusers.LCMScheduler.step = LCMScheduler_step diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index e2cc4f92f..e742e32fc 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -3,20 +3,20 @@ import io import os import re import json - from PIL import Image import gradio as gr from modules.paths import data_path from modules import shared, ui_tempdir, script_callbacks, images + re_param_code = r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)' re_param = re.compile(re_param_code) re_imagesize = re.compile(r"^(\d+)x(\d+)$") re_hypernet_hash = re.compile("\(([0-9a-f]+)\)$") # pylint: disable=anomalous-backslash-in-string type_of_gr_update = type(gr.update()) - paste_fields = {} registered_param_bindings = [] +debug = shared.log.info if os.environ.get('SD_PASTE_DEBUG', None) is not None else lambda *args, **kwargs: None class ParamBinding: @@ -203,37 +203,22 @@ def find_hypernetwork_key(hypernet_name, hypernet_hash=None): def parse_generation_parameters(x: str): - """parses generation parameters string, the one you see in text field under the picture in UI: -``` -girl with an artist's beret, determined, blue eyes, desert scene, computer monitors, heavy makeup, by Alphonse Mucha and Charlie Bowater, ((eyeshadow)), (coquettish), detailed, intricate -Negative prompt: ugly, fat, obese, chubby, (((deformed))), [blurry], bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), messy drawing -Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model hash: 45dee52b -``` - - returns a dict with field values - """ - if x is None: - return {} res = {} - prompt = "" - negative_prompt = "" - done_with_prompt = False - *lines, lastline = x.strip().split("\n") - if len(re_param.findall(lastline)) < 3: - lines.append(lastline) - lastline = '' - for line in lines: - line = line.strip() - if line.startswith("Negative prompt:"): - done_with_prompt = True - line = line[16:].strip() - if done_with_prompt: - negative_prompt += ("" if negative_prompt == "" else "\n") + line - else: - prompt += ("" if prompt == "" else "\n") + line - res["Prompt"] = prompt - res["Negative prompt"] = negative_prompt - for k, v in re_param.findall(lastline): + if x is None: + return res + remaining = x.replace('\n', ' ').strip() + if len(remaining) == 0: + return res + remaining = x[7:] if x.startswith('Prompt: ') else x + remaining = x[11:] if x.startswith('parameters: ') else x + prompt, remaining = remaining.strip().split('Negative prompt: ', maxsplit=1) if 'Negative prompt: ' in remaining else (remaining, '') + res["Prompt"] = prompt.strip() + negative, remaining = remaining.strip().split('Steps: ', maxsplit=1) if 'Steps: ' in remaining else (remaining, None) + res["Negative prompt"] = negative.strip() + if remaining is None: + return res + remaining = f'Steps: {remaining}' + for k, v in re_param.findall(remaining.strip()): try: if v[0] == '"' and v[-1] == '"': v = unquote(v) @@ -245,16 +230,8 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model res[k] = v except Exception: pass - - # Missing CLIP skip means it was set to 1 (the default) - if "Clip skip" not in res: - res["Clip skip"] = "1" - hypernet = res.get("Hypernet", None) - if hypernet is not None: - res["Prompt"] += f"""""" - if "Hires resize-1" not in res: - res["Hires resize-1"] = 0 - res["Hires resize-2"] = 0 + res["Full quality"] = res.get('VAE', None) != 'TAESD' + debug(f"Parse prompt: {res}") return res @@ -262,7 +239,7 @@ settings_map = {} infotext_to_setting_name_mapping = [ - ('Backed', 'sd_backend'), + ('Backend', 'sd_backend'), ('Model hash', 'sd_model_checkpoint'), ('Refiner', 'sd_model_refiner'), ('VAE', 'sd_vae'), @@ -305,13 +282,6 @@ infotext_to_setting_name_mapping = [ def create_override_settings_dict(text_pairs): - """creates processing's override_settings parameters from gradio's multiselect - Example input: - ['Clip skip: 2', 'Model hash: e6e99610c4', 'ENSD: 31337'] - - Example output: - {'CLIP_stop_at_last_layers': 2, 'sd_model_checkpoint': 'e6e99610c4', 'eta_noise_seed_delta': 31337} - """ res = {} params = {} for pair in text_pairs: @@ -328,19 +298,20 @@ def create_override_settings_dict(text_pairs): def connect_paste(button, local_paste_fields, input_comp, override_settings_component, tabname): def paste_func(prompt): - if prompt is not None and 'Negative prompt' not in prompt and 'Steps' not in prompt: - prompt = None - if not prompt and not shared.cmd_opts.hide_ui_dir_config: + if prompt is None or len(prompt.strip()) == 0 and not shared.cmd_opts.hide_ui_dir_config: filename = os.path.join(data_path, "params.txt") if os.path.exists(filename): with open(filename, "r", encoding="utf8") as file: prompt = file.read() + shared.log.debug(f'Paste prompt: type="params" prompt="{prompt}"') else: prompt = '' - shared.log.debug(f'Paste prompt: {prompt}') + else: + shared.log.debug(f'Paste prompt: type="current" prompt="{prompt}"') params = parse_generation_parameters(prompt) script_callbacks.infotext_pasted_callback(prompt, params) res = [] + applied = {} for output, key in local_paste_fields: if callable(key): v = key(params) @@ -350,6 +321,7 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp res.append(gr.update()) elif isinstance(v, type_of_gr_update): res.append(v) + applied[key] = v else: try: valtype = type(output.value) @@ -358,8 +330,10 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp else: val = valtype(v) res.append(gr.update(value=val)) + applied[key] = val except Exception: res.append(gr.update()) + debug(f"Parse apply: {applied}") return res if override_settings_component is not None: @@ -380,6 +354,7 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp continue vals[param_name] = v vals_pairs = [f"{k}: {v}" for k, v in vals.items()] + shared.log.debug(f'Settings overrides: {vals_pairs}') return gr.Dropdown.update(value=vals_pairs, choices=vals_pairs, visible=len(vals_pairs) > 0) local_paste_fields = local_paste_fields + [(override_settings_component, paste_settings)] diff --git a/modules/images.py b/modules/images.py index 2c4ecefe4..28046e159 100644 --- a/modules/images.py +++ b/modules/images.py @@ -532,6 +532,8 @@ def atomically_save_image(): file.write(exifinfo) if shared.opts.save_log_fn != '' and len(exifinfo) > 0: fn = os.path.join(paths.data_path, shared.opts.save_log_fn) + if not fn.endswith('.json'): + fn += '.json' entries = shared.readfile(fn) idx = len(list(entries)) if idx == 0: @@ -547,7 +549,7 @@ save_thread = threading.Thread(target=atomically_save_image, daemon=True) save_thread.start() -def save_image(image, path, basename = '', seed=None, prompt=None, extension=shared.opts.samples_format, info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix="", save_to_dirs=None): # pylint: disable=unused-argument +def save_image(image, path, basename='', seed=None, prompt=None, extension=shared.opts.samples_format, info=None, short_filename=False, no_prompt=False, grid=False, pnginfo_section_name='parameters', p=None, existing_info=None, forced_filename=None, suffix='', save_to_dirs=None): # pylint: disable=unused-argument if image is None: shared.log.warning('Image is none') return None, None @@ -556,26 +558,30 @@ def save_image(image, path, basename = '', seed=None, prompt=None, extension=sha if path is None or len(path) == 0: # set default path to avoid errors when functions are triggered manually or via api and param is not set path = shared.opts.outdir_save namegen = FilenameGenerator(p, seed, prompt, image, grid=grid) + suffix = suffix if suffix is not None else '' + basename = basename if basename is not None else '' if shared.opts.save_to_dirs: dirname = namegen.apply(shared.opts.directories_filename_pattern or "[prompt_words]") path = os.path.join(path, dirname) if forced_filename is None: - if short_filename or seed is None: - file_decoration = "" if shared.opts.samples_filename_pattern and len(shared.opts.samples_filename_pattern) > 0: file_decoration = shared.opts.samples_filename_pattern else: file_decoration = "[seq]-[prompt_words]" file_decoration = namegen.apply(file_decoration) - file_decoration += suffix - filename = os.path.join(path, f"{file_decoration}.{extension}") if basename == '' else os.path.join(path, f"{basename}-{file_decoration}.{extension}") + file_decoration += suffix if suffix is not None else '' + filename = os.path.join(path, f"{file_decoration}.{extension}") if basename == '' else os.path.join(path, f"{basename}-{file_decoration}.{extension}") + else: + forced_filename += suffix if suffix is not None else '' + filename = os.path.join(path, f"{forced_filename}.{extension}") if basename == '' else os.path.join(path, f"{basename}-{forced_filename}.{extension}") pnginfo = existing_info or {} if info is not None: pnginfo[pnginfo_section_name] = info params = script_callbacks.ImageSaveParams(image, p, filename, pnginfo) params.filename = namegen.sanitize(filename) dirname = os.path.dirname(params.filename) - os.makedirs(dirname, exist_ok=True) + if dirname is not None and len(dirname) > 0: + os.makedirs(dirname, exist_ok=True) # sequence if shared.opts.save_images_add_number or '[seq]' in params.filename: if '[seq]' not in params.filename: diff --git a/modules/img2img.py b/modules/img2img.py index b05254434..0152cf9d5 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -211,6 +211,11 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s p.extra_generation_params['Resize mode'] = resize_mode if mask: p.extra_generation_params["Mask blur"] = mask_blur + p.extra_generation_params["Mask alpha"] = mask_alpha + p.extra_generation_params["Mask invert"] = inpainting_mask_invert + p.extra_generation_params["Mask content"] = inpainting_fill + p.extra_generation_params["Mask area"] = inpaint_full_res + p.extra_generation_params["Mask padding"] = inpaint_full_res_padding p.is_batch = mode == 5 if p.is_batch: process_batch(p, img2img_batch_files, img2img_batch_input_dir, img2img_batch_output_dir, img2img_batch_inpaint_mask_dir, args) diff --git a/modules/interrogate.py b/modules/interrogate.py index e2c6f9577..76685dae9 100644 --- a/modules/interrogate.py +++ b/modules/interrogate.py @@ -87,9 +87,10 @@ class InterrogateModels: def load_blip_model(self): self.create_fake_fairscale() import models.blip # pylint: disable=no-name-in-module - + model_path = os.path.join(paths.models_path, "BLIP") + shared.log.debug(f'Loading interrogate model: type=BLIP folder={model_path}') files = modelloader.load_models( - model_path=os.path.join(paths.models_path, "BLIP"), + model_path=model_path, model_url='https://storage.googleapis.com/sfr-vision-language-research/BLIP/models/model_base_caption_capfilt_large.pth', ext_filter=[".pth"], download_name='model_base_caption_capfilt_large.pth', diff --git a/modules/modelloader.py b/modules/modelloader.py index 4d4858cec..f0fd4241e 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -334,23 +334,23 @@ def load_reference(name: str): return True -modelloader_directories = {} +cache_folders = {} cache_last = 0 cache_time = 1 -def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: # pylint: disable=redefined-builtin +def directory_updated(path:str, *, recursive:bool=True) -> bool: # pylint: disable=redefined-builtin try: - dir = os.path.abspath(dir) - if dir not in modelloader_directories: + path = os.path.abspath(path) + if path not in cache_folders: return True if cache_last > (time.time() - cache_time): return False - if not (os.path.exists(dir) and os.path.isdir(dir) and os.path.getmtime(dir) == modelloader_directories[dir][0]): + if not (os.path.exists(path) and os.path.isdir(path) and os.path.getmtime(path) == cache_folders[path][0]): return True if recursive: - for _dir in modelloader_directories: - if _dir.startswith(dir) and _dir != dir and not (os.path.exists(_dir) and os.path.isdir(_dir) and os.path.getmtime(_dir) == modelloader_directories[_dir][0]): + for folder in cache_folders: + if folder.startswith(path) and folder != path and not (os.path.exists(folder) and os.path.isdir(folder) and os.path.getmtime(folder) == cache_folders[folder][0]): return True except Exception as e: shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") @@ -358,44 +358,48 @@ def directory_has_changed(dir:str, *, recursive:bool=True) -> bool: # pylint: di return False -def directory_directories(dir:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: # pylint: disable=redefined-builtin - dir = os.path.abspath(dir) - if directory_has_changed(dir, recursive=recursive): - for _dir in list(modelloader_directories): - if os.path.exists(_dir) or os.path.isdir(_dir): +def directory_list(path:str, *, recursive:bool=True) -> dict[str,tuple[float,list[str]]]: # pylint: disable=redefined-builtin + path = os.path.abspath(path) + res = {} + if not os.path.exists(path): + return res + if directory_updated(path, recursive=recursive): + for folder in list(cache_folders): + del cache_folders[folder] + if os.path.exists(folder) or os.path.isdir(folder): + continue + for folder, files in walk(path, lambda e, path: shared.log.debug(f"FS walk error: {e} {path}")): + if not os.path.exists(folder): continue - del modelloader_directories[_dir] - for _dir, _files in walk(dir, lambda e, path: shared.log.debug(f"FS walk error: {e} {path}")): try: - mtime = os.path.getmtime(_dir) - if _dir not in modelloader_directories or mtime != modelloader_directories[_dir][0]: - modelloader_directories[_dir] = (mtime, [os.path.join(_dir, fn) for fn in _files]) + mtime = os.path.getmtime(folder) + if folder not in cache_folders or mtime != cache_folders[folder][0]: + cache_folders[folder] = (mtime, [os.path.join(folder, fn) for fn in files]) except Exception as e: shared.log.error(f"Filesystem Error: {e.__class__.__name__}({e})") - del modelloader_directories[_dir] - res = {} - for _dir in modelloader_directories: - if _dir == dir or (recursive and _dir.startswith(dir)): - res[_dir] = modelloader_directories[_dir] + del cache_folders[folder] + for folder in cache_folders: + if folder == path or (recursive and folder.startswith(path)): + res[folder] = cache_folders[folder] if not recursive: break return res -def directory_mtime(dir:str, *, recursive:bool=True) -> float: # pylint: disable=redefined-builtin - return float(max(0, *[mtime for mtime, _ in directory_directories(dir, recursive=recursive).values()])) +def directory_mtime(path:str, *, recursive:bool=True) -> float: # pylint: disable=redefined-builtin + return float(max(0, *[mtime for mtime, _ in directory_list(path, recursive=recursive).values()])) def directories_file_paths(directories:dict) -> list[str]: return sum([dat[1] for dat in directories.values()],[]) -def unique_directories(directories:list[str], *, recursive:bool=True) -> list[str]: +def directories_unique(directories:list[str], *, recursive:bool=True) -> list[str]: '''Ensure no empty, or duplicates''' - directories = { os.path.abspath(dir): True for dir in directories if dir }.keys() + directories = { os.path.abspath(path): True for path in directories if path }.keys() if recursive: '''If we are going recursive, then directories that are children of other directories are redundant''' - directories = [dir for dir in directories if not any(_dir != dir and dir.startswith(os.path.join(_dir,'')) for _dir in directories)] + directories = [path for path in directories if not any(d != path and path.startswith(os.path.join(d,'')) for d in directories)] return directories @@ -404,7 +408,7 @@ def unique_paths(paths:list[str]) -> list[str]: def directory_files(*directories:list[str], recursive:bool=True) -> list[str]: - return unique_paths(sum([[*directories_file_paths(directory_directories(dir, recursive=recursive))] for dir in unique_directories(directories, recursive=recursive)],[])) + return unique_paths(sum([[*directories_file_paths(directory_list(d, recursive=recursive))] for d in directories_unique(directories, recursive=recursive)],[])) def extension_filter(ext_filter=None, ext_blacklist=None): @@ -485,7 +489,7 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None @param ext_filter: An optional list of filename extensions to filter by @return: A list of paths containing the desired model(s) """ - places = unique_directories([model_path, command_path]) + places = directories_unique([model_path, command_path]) output = [] try: output:list = [*filter(extension_filter(ext_filter, ext_blacklist), directory_files(*places))] diff --git a/modules/processing.py b/modules/processing.py index b0213db25..19655eb36 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -7,6 +7,7 @@ import random import warnings from contextlib import nullcontext from typing import Any, Dict, List +from dataclasses import dataclass, field import torch import numpy as np import cv2 @@ -115,6 +116,7 @@ def txt2img_image_conditioning(sd_model, x, width, height): return x.new_zeros(x.shape[0], 5, 1, 1, dtype=x.dtype, device=x.device) +@dataclass(repr=False) class StableDiffusionProcessing: """ The first set of paramaters: sd_models -> do_not_reload_embeddings represent the minimum required to create a StableDiffusionProcessing @@ -164,11 +166,6 @@ class StableDiffusionProcessing: self.disable_extra_networks = False self.token_merging_ratio = 0 self.token_merging_ratio_hr = 0 - if not seed_enable_extras: - self.subseed = -1 - self.subseed_strength = 0 - self.seed_resize_from_h = 0 - self.seed_resize_from_w = 0 self.scripts = None self.script_args = script_args or [] self.per_script_args = {} @@ -203,11 +200,39 @@ class StableDiffusionProcessing: self.all_hr_negative_prompts = [] self.comments = {} self.is_api = False + self.scripts_value: modules.scripts.ScriptRunner = field(default=None, init=False) + self.script_args_value: list = field(default=None, init=False) + self.scripts_setup_complete: bool = field(default=False, init=False) + @property def sd_model(self): return shared.sd_model + @property + def scripts(self): + return self.scripts_value + + @scripts.setter + def scripts(self, value): + self.scripts_value = value + if self.scripts_value and self.script_args_value and not self.scripts_setup_complete: + self.setup_scripts() + + @property + def script_args(self): + return self.script_args_value + + @script_args.setter + def script_args(self, value): + self.script_args_value = value + if self.scripts_value and self.script_args_value and not self.scripts_setup_complete: + self.setup_scripts() + + def setup_scripts(self): + self.scripts_setup_complete = True + self.scripts.setup_scrips(self, is_ui=not self.is_api) + def comment(self, text): self.comments[text] = 1 @@ -558,7 +583,7 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No args["Denoising strength"] = p.denoising_strength args["Latent sampler"] = p.latent_sampler args["Image CFG scale"] = p.image_cfg_scale - args["CFG rescale"] = p.diffusers_guidance_rescale if shared.backend == shared.Backend.DIFFUSERS else None + args["CFG rescale"] = p.diffusers_guidance_rescale if 'refine' in p.ops: args["Second pass"] = p.enable_hr args["Refiner"] = None if (not shared.opts.add_model_name_to_info) or (not shared.sd_refiner) or (not shared.sd_refiner.sd_checkpoint_info.model_name) else shared.sd_refiner.sd_checkpoint_info.model_name.replace(',', '').replace(':', '') @@ -567,12 +592,13 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts=None, all_seeds=No args['Refiner start'] = p.refiner_start args["Hires steps"] = p.hr_second_pass_steps args["Latent sampler"] = p.latent_sampler - args["CFG rescale"] = p.diffusers_guidance_rescale if shared.backend == shared.Backend.DIFFUSERS else None + args["CFG rescale"] = p.diffusers_guidance_rescale if 'img2img' in p.ops or 'inpaint' in p.ops: args["Init image size"] = f"{getattr(p, 'init_img_width', 0)}x{getattr(p, 'init_img_height', 0)}" args["Init image hash"] = getattr(p, 'init_img_hash', None) args["Mask weight"] = getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None args['Resize mode'] = getattr(p, 'resize_mode', None) + args['Resize scale'] = getattr(p, 'scale_by', None) args["Mask blur"] = p.mask_blur if getattr(p, 'mask', None) is not None and getattr(p, 'mask_blur', 0) > 0 else None args["Denoising strength"] = getattr(p, 'denoising_strength', None) if 'face' in p.ops: @@ -651,7 +677,7 @@ def print_profile(profile, msg: str): def process_images(p: StableDiffusionProcessing) -> Processed: if not hasattr(p.sd_model, 'sd_checkpoint_info'): return None - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.scripts.before_process(p) stored_opts = {} for k, v in p.override_settings.copy().items(): @@ -726,7 +752,13 @@ def process_images(p: StableDiffusionProcessing) -> Processed: return res -def validate_sample(sample): +def validate_sample(tensor): + if tensor.dtype == torch.bfloat16: # numpy does not support bf16 + tensor = tensor.to(torch.float16) + if shared.backend == shared.Backend.ORIGINAL: + sample = 255.0 * np.moveaxis(tensor.cpu().numpy(), 0, 2) + else: + sample = 255. * tensor with warnings.catch_warnings(record=True) as w: cast = sample.astype(np.uint8) if len(w) > 0: @@ -771,7 +803,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.all_subseeds = [int(subseed) + x for x in range(len(p.all_prompts))] if os.path.exists(shared.opts.embeddings_dir) and not p.do_not_reload_embeddings and shared.backend == shared.Backend.ORIGINAL: modules.sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings(force_reload=False) - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.scripts.process(p) infotexts = [] output_images = [] @@ -809,7 +841,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n + 1) * p.batch_size] p.seeds = p.all_seeds[n * p.batch_size:(n + 1) * p.batch_size] p.subseeds = p.all_subseeds[n * p.batch_size:(n + 1) * p.batch_size] - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.scripts.before_process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) if len(p.prompts) == 0: break @@ -817,7 +849,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if not p.disable_extra_networks: with devices.autocast(): modules.extra_networks.activate(p, extra_network_data) - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.scripts.process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) if n == 0: with open(os.path.join(modules.paths.data_path, "params.txt"), "w", encoding="utf8") as file: @@ -866,9 +898,9 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if shared.cmd_opts.lowvram or shared.cmd_opts.medvram and shared.backend == shared.Backend.ORIGINAL: modules.lowvram.send_everything_to_cpu() devices.torch_gc() - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.scripts.postprocess_batch(p, x_samples_ddim, batch_number=n) - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): p.prompts = p.all_prompts[n * p.batch_size:(n + 1) * p.batch_size] p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n + 1) * p.batch_size] batch_params = modules.scripts.PostprocessBatchListArgs(list(x_samples_ddim)) @@ -884,7 +916,6 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: image = x_sample x_sample = np.array(x_sample) else: - x_sample = 255. * (np.moveaxis(x_sample.cpu().numpy(), 0, 2) if shared.backend == shared.Backend.ORIGINAL else x_sample) x_sample = validate_sample(x_sample) image = Image.fromarray(x_sample) if p.restore_faces: @@ -897,7 +928,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.ops.append('face') x_sample = modules.face_restoration.restore_faces(x_sample) image = Image.fromarray(x_sample) - if p.scripts is not None: + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner): pp = modules.scripts.PostprocessImageArgs(image) p.scripts.postprocess_image(p, pp) image = pp.image @@ -962,7 +993,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: index_of_first_image=index_of_first_image, infotexts=infotexts, ) - if p.scripts is not None and not (shared.state.interrupted or shared.state.skipped): + if p.scripts is not None and isinstance(p.scripts, modules.scripts.ScriptRunner) and not (shared.state.interrupted or shared.state.skipped): p.scripts.postprocess(p, res) return res @@ -1088,7 +1119,6 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): decoded_samples = decode_first_stage(self.sd_model, samples.to(dtype=devices.dtype_vae), self.full_quality) decoded_samples = torch.clamp((decoded_samples + 1.0) / 2.0, min=0.0, max=1.0) for i, x_sample in enumerate(decoded_samples): - x_sample = 255. * np.moveaxis(x_sample.cpu().numpy(), 0, 2) x_sample = validate_sample(x_sample) image = Image.fromarray(x_sample) bak_extra_generation_params, bak_restore_faces = self.extra_generation_params, self.restore_faces @@ -1104,7 +1134,6 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): decoded_samples = torch.clamp((decoded_samples + 1.0) / 2.0, min=0.0, max=1.0) batch_images = [] for _i, x_sample in enumerate(decoded_samples): - x_sample = 255. * np.moveaxis(x_sample.cpu().numpy(), 0, 2) x_sample = validate_sample(x_sample) image = Image.fromarray(x_sample) image = images.resize_image(1, image, target_width, target_height, upscaler_name=self.hr_upscaler) @@ -1185,7 +1214,7 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): def init(self, all_prompts, all_seeds, all_subseeds): if shared.backend == shared.Backend.DIFFUSERS and self.image_mask is not None: shared.sd_model = modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.INPAINTING) - self.sd_model.dtype = self.sd_model.unet.dtype + # self.sd_model.dtype = self.sd_model.unet.dtype elif shared.backend == shared.Backend.DIFFUSERS and self.image_mask is None: shared.sd_model = modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.IMAGE_2_IMAGE) diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 3dedcb85b..bde6efdbc 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -89,12 +89,12 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro def full_vae_decode(latents, model): t0 = time.time() - if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False): + if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'): shared.log.debug('Moving to CPU: model=UNet') unet_device = model.unet.device model.unet.to(devices.cpu) devices.torch_gc() - if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload: + if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload and hasattr(model, 'vae'): model.vae.to(devices.device) latents.to(model.vae.device) @@ -104,7 +104,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro latents = latents.to(next(iter(model.vae.post_quant_conv.parameters())).dtype) decoded = model.vae.decode(latents / model.vae.config.scaling_factor, return_dict=False)[0] - if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False): + if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'): model.unet.to(unet_device) t1 = time.time() shared.log.debug(f'VAE decode: name={sd_vae.loaded_vae_file if sd_vae.loaded_vae_file is not None else "baked"} dtype={model.vae.dtype} upcast={upcast} images={latents.shape[0]} latents={latents.shape} time={round(t1-t0, 3)}') @@ -112,15 +112,15 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro def full_vae_encode(image, model): shared.log.debug(f'VAE encode: name={sd_vae.loaded_vae_file if sd_vae.loaded_vae_file is not None else "baked"} dtype={model.vae.dtype} upcast={model.vae.config.get("force_upcast", None)}') - if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False): + if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'): shared.log.debug('Moving to CPU: model=UNet') unet_device = model.unet.device model.unet.to(devices.cpu) devices.torch_gc() - if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload: + if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload and hasattr(model, 'vae'): model.vae.to(devices.device) encoded = model.vae.encode(image.to(model.vae.device, model.vae.dtype)).latent_dist.sample() - if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False): + if shared.opts.diffusers_move_unet and not getattr(model, 'has_accelerate', False) and hasattr(model, 'unet'): model.unet.to(unet_device) return encoded @@ -263,6 +263,11 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro args['negative_pooled_prompt_embeds'] = negative_pooled else: args['negative_prompt'] = negative_prompts + if hasattr(model, 'scheduler') and hasattr(model.scheduler, 'noise_sampler_seed') and hasattr(model.scheduler, 'noise_sampler'): + model.scheduler.noise_sampler = None # noise needs to be reset instead of using cached values + model.scheduler.noise_sampler_seed = seeds[0] # some schedulers have internal noise generator and do not use pipeline generator + if 'noise_sampler_seed' in possible: + args['noise_sampler_seed'] = seeds[0] if 'guidance_scale' in possible: args['guidance_scale'] = p.cfg_scale if 'generator' in possible: @@ -294,6 +299,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro clean.pop('callback', None) clean.pop('callback_steps', None) clean.pop('callback_on_step_end', None) + clean.pop('callback_on_step_end_tensor_inputs', None) if 'latents' in clean: clean['latents'] = clean['latents'].shape if 'image' in clean: @@ -371,29 +377,35 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro if shared.opts.diffusers_move_base and not getattr(shared.sd_model, 'has_accelerate', False): shared.sd_model.to(devices.device) - is_img2img = bool(sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.IMAGE_2_IMAGE or - sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.INPAINTING) + is_img2img = bool(sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.IMAGE_2_IMAGE or sd_models.get_diffusers_task(shared.sd_model) == sd_models.DiffusersTaskType.INPAINTING) use_refiner_start = bool(is_refiner_enabled and not p.is_hr_pass and not is_img2img and p.refiner_start > 0 and p.refiner_start < 1) use_denoise_start = bool(is_img2img and p.refiner_start > 0 and p.refiner_start < 1) def calculate_base_steps(): + steps = p.steps if use_refiner_start: - return int(p.steps // p.refiner_start + 1) if shared.sd_model_type == 'sdxl' else p.steps - elif use_denoise_start and shared.sd_model_type == 'sdxl': - return int(p.steps // (1 - p.refiner_start)) - elif is_img2img: - return int(p.steps // p.denoising_strength + 1) - else: - return p.steps + steps = (p.steps // (1.0 - p.refiner_start)) if shared.sd_model_type == 'sdxl' else p.steps + if os.environ.get('SD_STEPS_DEBUG', None) is not None: + shared.log.debug(f'Steps: type=base input={p.steps} output={steps} refiner={use_refiner_start}') + return max(2, int(steps)) + + def calculate_hires_steps(): + # denoising strength is applied to steps by diffusers so this is no-op + # steps = (p.hr_second_pass_steps * p.denoising_strength) if p.hr_second_pass_steps > 0 else (p.steps * p.denoising_strength) + steps = p.hr_second_pass_steps if p.hr_second_pass_steps > 0 else p.steps + if os.environ.get('SD_STEPS_DEBUG', None) is not None: + shared.log.debug(f'Steps: type=hires input={p.hr_second_pass_steps} output={steps} denoise={p.denoising_strength}') + return max(2, int(steps)) def calculate_refiner_steps(): - refiner_is_sdxl = bool("StableDiffusionXL" in shared.sd_refiner.__class__.__name__) - if p.refiner_start > 0 and p.refiner_start < 1 and refiner_is_sdxl: - refiner_steps = int(p.refiner_steps // (1 - p.refiner_start)) + # diffusers apply additional math to refiner steps, but we leave numbers as-is without correction + if p.refiner_start > 0 and p.refiner_start < 1: + steps = ((1 - p.refiner_start) * p.refiner_steps) if p.refiner_steps > 0 else ((1 - p.refiner_start) * p.steps) else: - refiner_steps = int(p.refiner_steps // p.denoising_strength + 1) if refiner_is_sdxl else p.refiner_steps - p.refiner_steps = min(99, refiner_steps) - return p.refiner_steps + steps = (p.denoising_strength * p.refiner_steps) if p.refiner_steps > 0 else (p.denoising_strength * p.steps) + if os.environ.get('SD_STEPS_DEBUG', None) is not None: + shared.log.debug(f'Steps: type=refiner input={p.refiner_steps} output={steps} start={p.refiner_start} denoise={p.denoising_strength}') + return max(2, int(steps)) # pipeline type is set earlier in processing, but check for sanity if sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.TEXT_2_IMAGE and len(getattr(p, 'init_images' ,[])) == 0: # reset pipeline @@ -451,15 +463,13 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) recompile_model(hires=True) update_sampler(shared.sd_model, second_pass=True) - if p.hr_second_pass_steps == 0: - p.hr_second_pass_steps = p.steps hires_args = set_pipeline_args( model=shared.sd_model, prompts=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts, negative_prompts=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts, prompts_2=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts, negative_prompts_2=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts, - num_inference_steps=int(p.hr_second_pass_steps // p.denoising_strength + 1), + num_inference_steps=calculate_hires_steps(), eta=shared.opts.scheduler_eta, guidance_scale=p.image_cfg_scale if p.image_cfg_scale is not None else p.cfg_scale, guidance_rescale=p.diffusers_guidance_rescale, @@ -511,12 +521,11 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro image = vae_decode(latents=image, model=shared.sd_model, full_quality=p.full_quality, output_type='pil') p.extra_generation_params['Noise level'] = noise_level output_type = 'np' - calculate_refiner_steps() refiner_args = set_pipeline_args( model=shared.sd_refiner, prompts=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts[i], negative_prompts=[p.refiner_negative] if len(p.refiner_negative) > 0 else negative_prompts[i], - num_inference_steps=p.refiner_steps, + num_inference_steps=calculate_refiner_steps(), eta=shared.opts.scheduler_eta, # strength=p.denoising_strength, noise_level=noise_level, # StableDiffusionUpscalePipeline only diff --git a/modules/sd_models.py b/modules/sd_models.py index c627cea7b..0c6db835a 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -268,7 +268,7 @@ def select_checkpoint(op='model'): shared.log.info(f'Select: {op}="{checkpoint_info.title if checkpoint_info is not None else None}"') return checkpoint_info if len(checkpoints_list) == 0 and not shared.cmd_opts.no_download: - shared.log.error("Cannot generate without a checkpoint") + shared.log.warning("Cannot generate without a checkpoint") shared.log.info("Set system paths to use existing folders in a different location") shared.log.info("Or use --ckpt to force using existing checkpoint") return None @@ -646,6 +646,14 @@ def detect_pipeline(f: str, op: str = 'model'): guess = 'Stable Diffusion XL Instruct' else: guess = 'Stable Diffusion' + if 'LCM_' in f or 'LCM-' in f: + if shared.backend == shared.Backend.ORIGINAL: + shared.log.warning(f'Model detected as LCM model, but attempting to load using backend=original: {op}={f} size={size} MB') + guess = 'Latent Consistency Model' + if 'PixArt' in f: + if shared.backend == shared.Backend.ORIGINAL: + shared.log.warning(f'Model detected as PixArt Alpha model, but attempting to load using backend=original: {op}={f} size={size} MB') + guess = 'PixArt Alpha' pipeline = shared_items.get_pipelines().get(guess, None) shared.log.info(f'Autodetect: {op}="{guess}" class={pipeline.__name__} file="{f}" size={size}MB') except Exception as e: @@ -777,7 +785,7 @@ def set_diffuser_options(sd_model, vae, op: str): shared.log.debug(f'Setting {op} VAE: name={sd_vae.loaded_vae_file} upcast={sd_model.vae.config.get("force_upcast", None)}') if shared.opts.cross_attention_optimization == "xFormers" and hasattr(sd_model, 'enable_xformers_memory_efficient_attention'): sd_model.enable_xformers_memory_efficient_attention() - if shared.opts.opt_channelslast: + if shared.opts.opt_channelslast and hasattr(sd_model, 'unet'): shared.log.debug(f'Setting {op}: enable channels last') sd_model.unet.to(memory_format=torch.channels_last) diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index db9ad937f..62f0c8a73 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -53,14 +53,14 @@ def create_sampler(name, model): sampler.config = config sampler.initialize(p=None) sampler.name = name - shared.log.debug(f'Sampler: sampler={sampler.name} config={sampler.config.options}') + shared.log.debug(f'Sampler: sampler="{sampler.name}" config={sampler.config.options}') return sampler elif shared.backend == shared.Backend.DIFFUSERS: sampler = config.constructor(model) if not hasattr(model, 'scheduler_config'): model.scheduler_config = sampler.sampler.config.copy() model.scheduler = sampler.sampler - shared.log.debug(f'Sampler: sampler={sampler.name} config={sampler.config}') + shared.log.debug(f'Sampler: sampler="{sampler.name}" config={sampler.config}') return sampler.sampler else: return None diff --git a/modules/sd_samplers_diffusers.py b/modules/sd_samplers_diffusers.py index b613fca99..9cd3ee806 100644 --- a/modules/sd_samplers_diffusers.py +++ b/modules/sd_samplers_diffusers.py @@ -17,6 +17,7 @@ try: UniPCMultistepScheduler, LMSDiscreteScheduler, KDPM2AncestralDiscreteScheduler, + LCMScheduler, ) except Exception as e: import diffusers @@ -40,6 +41,7 @@ config = { 'LMSD': { 'use_karras_sigmas': False, 'timestep_spacing': 'linspace', 'steps_offset': 0 }, 'PNDM': { 'skip_prk_steps': False, 'set_alpha_to_one': False, 'steps_offset': 0 }, 'UniPC': { 'solver_order': 2, 'thresholding': False, 'sample_max_value': 1.0, 'predict_x0': 'bh2', 'lower_order_final': True }, + 'LCM': { 'num_train_timesteps': 1000, 'beta_start': 0.00085, 'beta_end': 0.012, 'beta_schedule': "scaled_linear", 'set_alpha_to_one': True, 'rescale_betas_zero_snr': False }, } samplers_data_diffusers = [ @@ -58,6 +60,7 @@ samplers_data_diffusers = [ sd_samplers_common.SamplerData('Euler', lambda model: DiffusionSampler('Euler', EulerDiscreteScheduler, model), [], {}), sd_samplers_common.SamplerData('Euler a', lambda model: DiffusionSampler('Euler a', EulerAncestralDiscreteScheduler, model), [], {}), sd_samplers_common.SamplerData('Heun', lambda model: DiffusionSampler('Heun', HeunDiscreteScheduler, model), [], {}), + sd_samplers_common.SamplerData('LCM', lambda model: DiffusionSampler('LCM', LCMScheduler, model), [], {}), ] class DiffusionSampler: @@ -70,8 +73,10 @@ class DiffusionSampler: return for key, value in config.get('All', {}).items(): # apply global defaults self.config[key] = value + # shared.log.debug(f'Sampler: name={name} type=all config={self.config}') for key, value in config.get(name, {}).items(): # apply diffusers per-scheduler defaults self.config[key] = value + # shared.log.debug(f'Sampler: name={name} type=scheduler config={self.config}') if hasattr(model.scheduler, 'scheduler_config'): # find model defaults orig_config = model.scheduler.scheduler_config else: @@ -79,9 +84,11 @@ class DiffusionSampler: for key, value in orig_config.items(): # apply model defaults if key in self.config: self.config[key] = value + # shared.log.debug(f'Sampler: name={name} type=model config={self.config}') for key, value in kwargs.items(): # apply user args, if any if key in self.config: self.config[key] = value + # shared.log.debug(f'Sampler: name={name} type=user config={self.config}') # finally apply user preferences if shared.opts.schedulers_prediction_type != 'default': self.config['prediction_type'] = shared.opts.schedulers_prediction_type diff --git a/modules/shared_items.py b/modules/shared_items.py index 85fba87a3..49387e56e 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -27,7 +27,7 @@ def list_crossattention(): def get_pipelines(): import diffusers from installer import log - pipelines = { + pipelines = { # note: not all pipelines can be used manually as they require prior pipeline next to decoder pipeline 'Autodetect': None, 'Stable Diffusion': getattr(diffusers, 'StableDiffusionPipeline', None), 'Stable Diffusion Img2Img': getattr(diffusers, 'StableDiffusionImg2ImgPipeline', None), @@ -37,9 +37,15 @@ def get_pipelines(): 'Stable Diffusion XL Img2Img': getattr(diffusers, 'StableDiffusionXLImg2ImgPipeline', None), 'Stable Diffusion XL Inpaint': getattr(diffusers, 'StableDiffusionXLInpaintPipeline', None), 'Stable Diffusion XL Instruct': getattr(diffusers, 'StableDiffusionXLInstructPix2PixPipeline', None), + 'Latent Consistency Model': getattr(diffusers, 'LatentConsistencyModelPipeline', None), + 'PixArt Alpha': getattr(diffusers, 'PixArtAlphaPipeline', None), + 'UniDiffuser': getattr(diffusers, 'UniDiffuserPipeline', None), + 'Wuerstchen': getattr(diffusers, 'WuerstchenCombinedPipeline', None), + 'Kandinsky 2.1': getattr(diffusers, 'KandinskyPipeline', None), + 'Kandinsky 2.2': getattr(diffusers, 'KandinskyV22Pipeline', None), + 'DeepFloyd IF': getattr(diffusers, 'IFPipeline', None), 'Custom Diffusers Pipeline': getattr(diffusers, 'DiffusionPipeline', None), - # 'Test': getattr(diffusers, 'TestPipeline', None), - # 'Kandinsky V1', 'Kandinsky V2', 'DeepFloyd IF', 'Shap-E', 'Kandinsky V1 Img2Img', 'Kandinsky V2 Img2Img', 'DeepFloyd IF Img2Img', 'Shap-E Img2Img', + # Segmind SSD-1B, Segmind Tiny } for k, v in pipelines.items(): if k != 'Autodetect' and v is None: diff --git a/modules/styles.py b/modules/styles.py index 053cfd6fb..427e76ea9 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -5,7 +5,6 @@ import os import csv import json from installer import log -from modules import paths class Style(): @@ -66,6 +65,8 @@ def apply_styles_to_extra(p, style: Style): class StyleDatabase: def __init__(self, opts): + from modules import paths + self.no_style = Style("None") self.styles = {} self.path = opts.styles_dir @@ -135,18 +136,33 @@ class StyleDatabase: return found[0] if len(found) > 0 else self.no_style def get_style_prompts(self, styles): + if styles is None or not isinstance(styles, list): + log.error(f'Invalid styles: {styles}') + return [] return [self.find_style(x).prompt for x in styles] def get_negative_style_prompts(self, styles): + if styles is None or not isinstance(styles, list): + log.error(f'Invalid styles: {styles}') + return [] return [self.find_style(x).negative_prompt for x in styles] def apply_styles_to_prompt(self, prompt, styles): + if styles is None or not isinstance(styles, list): + log.error(f'Invalid styles: {styles}') + return prompt return apply_styles_to_prompt(prompt, [self.find_style(x).prompt for x in styles]) def apply_negative_styles_to_prompt(self, prompt, styles): + if styles is None or not isinstance(styles, list): + log.error(f'Invalid styles: {styles}') + return prompt return apply_styles_to_prompt(prompt, [self.find_style(x).negative_prompt for x in styles]) def apply_styles_to_extra(self, p): + if p.styles is None or not isinstance(p.styles, list): + log.error(f'Invalid styles: {p.styles}') + return for style in p.styles: s = self.find_style(style) apply_styles_to_extra(p, s) @@ -172,19 +188,25 @@ class StyleDatabase: log.error(f'Failed to save style: name={name} file={path} error={e}') count = len(list(self.styles)) if count > 0: - log.debug(f'Saved styles: {path} {count}') + log.debug(f'Saved styles: folder="{path}" items={count}') def load_csv(self, legacy_file): if not os.path.isfile(legacy_file): return with open(legacy_file, "r", encoding="utf-8-sig", newline='') as file: reader = csv.DictReader(file, skipinitialspace=True) + num = 0 for row in reader: try: - self.styles[row["name"]] = Style(row["name"], row["prompt"] if "prompt" in row else row["text"], row.get("negative_prompt", "")) + name = row["name"] + prompt = row["prompt"] if "prompt" in row else row["text"] + negative = row.get("negative_prompt", "") if "negative_prompt" in row else row.get("negative", "") + self.styles[name] = Style(name, desc=name, prompt=prompt, negative_prompt=negative, extra="") + log.debug(f'Migrated style: {self.styles[name].__dict__}') + num += 1 except Exception: - log.error(f'Styles error: file={legacy_file} row={row}') - log.debug(f'Load legacy styles: file={legacy_file} items={len(self.styles.keys())}') + log.error(f'Styles error: file="{legacy_file}" row={row}') + log.info(f'Load legacy styles: file="{legacy_file}" loaded={num} created={len(list(self.styles))}') """ def save_csv(self, path: str) -> None: diff --git a/modules/ui.py b/modules/ui.py index 262507000..ee490a87d 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -511,43 +511,48 @@ def create_ui(startup_timer = None): txt_prompt_img.change(fn=modules.images.image_data, inputs=[txt_prompt_img], outputs=[txt2img_prompt, txt_prompt_img]) txt2img_paste_fields = [ + # prompt (txt2img_prompt, "Prompt"), (txt2img_negative_prompt, "Negative prompt"), - # (txt2img_prompt_styles, "Styles"), - (steps, "Steps"), - (seed, "Seed"), - (sampler_index, "Sampler"), - (cfg_scale, "CFG scale"), + # main (width, "Size-1"), (height, "Size-2"), + # sampler + (sampler_index, "Sampler"), + (steps, "Steps"), + # batch + (batch_count, "Batch-1"), + (batch_size, "Batch-2"), + # seed + (seed, "Seed"), (subseed, "Variation seed"), (subseed_strength, "Variation strength"), + # advanced + (cfg_scale, "CFG scale"), (clip_skip, "Clip skip"), - (latent_index, "Latent sampler"), - (latent_index, "Secondary sampler"), - (denoising_strength, "Denoising strength"), - (refiner_steps, "Refiner steps"), - (refiner_start, "Refiner start"), + (image_cfg_scale, "Image CFG scale"), + (diffusers_guidance_rescale, "CFG rescale"), (full_quality, "Full quality"), (restore_faces, "Face restoration"), - (batch_size, "Batch size"), - (batch_count, "Batch count"), - (seed_resize_from_w, "Seed resize from-1"), - (seed_resize_from_h, "Seed resize from-2"), + (tiling, "Tiling"), + # second pass (enable_hr, "Second pass"), - (hr_force, "Hires force"), - (hr_scale, "Hires upscale"), + (latent_index, "Latent sampler"), + (denoising_strength, "Denoising strength"), (hr_upscaler, "Hires upscaler"), + (hr_force, "Hires force"), (hr_second_pass_steps, "Hires steps"), + (hr_scale, "Hires upscale"), (hr_resize_x, "Hires resize-1"), (hr_resize_y, "Hires resize-2"), - (diffusers_guidance_rescale, "CFG rescale"), - (image_cfg_scale, "Image CFG scale"), - (refiner_steps, "Refiner steps"), + # refiner (refiner_start, "Refiner start"), - (tiling, "Tiling"), - (refiner_negative, "Negative2"), + (refiner_steps, "Refiner steps"), (refiner_prompt, "Prompt2"), + (refiner_negative, "Negative2"), + # hidden + (seed_resize_from_w, "Seed resize from-1"), + (seed_resize_from_h, "Seed resize from-2"), *modules.scripts.scripts_txt2img.infotext_fields ] parameters_copypaste.add_paste_fields("txt2img", None, txt2img_paste_fields, override_settings) @@ -838,46 +843,45 @@ def create_ui(startup_timer = None): ui_extra_networks.setup_ui(extra_networks_ui_img2img, img2img_gallery) img2img_paste_fields = [ + # prompt (img2img_prompt, "Prompt"), (img2img_negative_prompt, "Negative prompt"), - # (img2img_prompt_styles, "Styles"), - (steps, "Steps"), - (seed, "Seed"), + # sampler (sampler_index, "Sampler"), - (cfg_scale, "CFG scale"), + (steps, "Steps"), + # resize + (resize_mode, "Resize mode"), (width, "Size-1"), (height, "Size-2"), + (scale_by, "Resize scale"), + # batch + (batch_count, "Batch-1"), + (batch_size, "Batch-2"), + # seed + (seed, "Seed"), (subseed, "Variation seed"), (subseed_strength, "Variation strength"), - (full_quality, "Full quality"), - (clip_skip, "Clip skip"), - (latent_index, "Latent sampler"), - (latent_index, "Secondary sampler"), + # denoise (denoising_strength, "Denoising strength"), + (refiner_start, "Refiner start"), + # advanced + (cfg_scale, "CFG scale"), + (image_cfg_scale, "Image CFG scale"), + (clip_skip, "Clip skip"), + (diffusers_guidance_rescale, "CFG rescale"), + (full_quality, "Full quality"), (restore_faces, "Face restoration"), - (batch_size, "Batch size"), - (batch_count, "Batch count"), + (tiling, "Tiling"), + # inpaint + (mask_blur, "Mask blur"), + (mask_alpha, "Mask alpha"), + (inpainting_mask_invert, "Mask invert"), + (inpainting_fill, "Masked content"), + (inpaint_full_res, "Mask area"), + (inpaint_full_res_padding, "Masked padding"), + # hidden (seed_resize_from_w, "Seed resize from-1"), (seed_resize_from_h, "Seed resize from-2"), - (resize_mode, "Resize mode"), - (image_cfg_scale, "Image CFG scale"), - (diffusers_guidance_rescale, "CFG rescale"), - (tiling, "Tiling"), - (mask_blur, "Mask blur"), - # TODO scale_by add to paste fields - (scale_by, "UNKNOWN"), - # from txt2img - (hr_force, "Hires force"), - (hr_scale, "Hires upscale"), - (hr_upscaler, "Hires upscaler"), - (hr_second_pass_steps, "Hires steps"), - (hr_second_pass_steps, "Hires steps"), - (hr_resize_x, "Hires resize-1"), - (hr_resize_y, "Hires resize-2"), - (refiner_steps, "Refiner steps"), - (refiner_start, "Refiner start"), - (refiner_prompt, "Prompt2"), - (refiner_negative, "Negative2"), *modules.scripts.scripts_img2img.infotext_fields ] parameters_copypaste.add_paste_fields("img2img", init_img, img2img_paste_fields, override_settings) diff --git a/modules/ui_common.py b/modules/ui_common.py index 8b4733b73..eff3d91ac 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -93,6 +93,7 @@ def save_files(js_data, images, html_info, index): self.index_of_first_image = getattr(self, 'index_of_first_image', 0) self.infotexts = getattr(self, 'infotexts', [html_info]) self.infotext = self.infotexts[0] if len(self.infotexts) > 0 else html_info + self.outpath_grids = shared.opts.outdir_grids or shared.opts.outdir_txt2img_grids try: data = json.loads(js_data) except Exception: diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index 1fafce4bf..6a3e1e6b1 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -319,6 +319,7 @@ def create_html(search_text, sort_column): for ext in sorted(extensions_list, key=sort_function, reverse=sort_reverse): installed = get_installed(ext) author = '' + updated = datetime.timestamp(datetime.now()) try: if 'github' in ext['url']: author = ext['url'].split('/')[-2].split(':')[-1] if '/' in ext['url'] else ext['url'].split(':')[1].split('/')[0] diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index ff48c82ac..4815e8210 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -40,9 +40,11 @@ card_full = ''' ''' card_list = '''
- 🛈  -
{title}
  -
+
+ 🛈  +
{title}
  +
+
''' @@ -222,6 +224,8 @@ class ExtraNetworksPage: self.items = [] shared.log.error(f'Extra networks error listing items: class={self.__class__.__name__} tab={tabname} {e}') for item in self.items: + if item is None: + continue self.metadata[item["name"]] = item.get("metadata", {}) t1 = time.time() debug(f'EN create-items: page={self.name} items={len(self.items)} time={t1-t0:.2f}') @@ -237,7 +241,7 @@ class ExtraNetworksPage: return f"
Extra network page not ready
Click refresh to try again
" subdirs = {} allowed_folders = [os.path.abspath(x) for x in self.allowed_directories_for_previews()] - for parentdir, dirs in {d: modelloader.directory_directories(d) for d in allowed_folders}.items(): + for parentdir, dirs in {d: modelloader.directory_list(d) for d in allowed_folders}.items(): for tgt in dirs.keys(): if shared.backend == shared.Backend.DIFFUSERS: if os.path.join(paths.models_path, 'Reference') in tgt: @@ -265,7 +269,8 @@ class ExtraNetworksPage: self.create_items(tabname) self.create_xyz_grid() htmls = [] - self.items.sort(key=lambda x: x["mtime"], reverse=True) + if len(self.items) > 0 and self.items[0].get('mtime', None) is not None: + self.items.sort(key=lambda x: x["mtime"], reverse=True) for item in self.items: htmls.append(self.create_html(item, tabname)) self.html += ''.join(htmls) @@ -318,7 +323,7 @@ class ExtraNetworksPage: return 'html/card-no-preview.png' if shared.opts.diffusers_dir in path: path = os.path.relpath(path, shared.opts.diffusers_dir) - ref = os.path.join(paths.models_path, 'Reference') + ref = os.path.join('models', 'Reference') fn = os.path.join(ref, path.replace('models--', '').replace('\\', '/').split('/')[0]) files = listdir(ref) else: @@ -539,9 +544,11 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): refresh_time = time.time() threads = [] for page in get_pages(): - # page.create_items(ui.tabname) - threads.append(threading.Thread(target=page.create_items, args=[ui.tabname])) - threads[-1].start() + if os.environ.get('SD_EN_DEBUG', None) is not None: + threads.append(threading.Thread(target=page.create_items, args=[ui.tabname])) + threads[-1].start() + else: + page.create_items(ui.tabname) for thread in threads: thread.join() for page in get_pages(): diff --git a/modules/ui_extra_networks_checkpoints.py b/modules/ui_extra_networks_checkpoints.py index f31604079..01902b376 100644 --- a/modules/ui_extra_networks_checkpoints.py +++ b/modules/ui_extra_networks_checkpoints.py @@ -1,9 +1,11 @@ +import os import html import json -import os -from modules import shared, ui_extra_networks, sd_models, paths +import concurrent +from modules import shared, ui_extra_networks, sd_models -reference_dir = os.path.join(paths.models_path, 'Reference') + +reference_dir = os.path.join('models', 'Reference') class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): def __init__(self): @@ -12,7 +14,7 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): def refresh(self): shared.refresh_checkpoints() - def list_reference(self): + def list_reference(self): # pylint: disable=inconsistent-return-statements if shared.backend != shared.Backend.DIFFUSERS: return [] reference_models = shared.readfile(os.path.join('html', 'reference.json')) @@ -36,31 +38,38 @@ class ExtraNetworksPageCheckpoints(ui_extra_networks.ExtraNetworksPage): "description": v.get('desc', ''), } + def create_item(self, name): + record = None + try: + checkpoint: sd_models.CheckpointInfo = sd_models.checkpoints_list.get(name) + exists = os.path.exists(checkpoint.filename) + record = { + "type": 'Model', + "name": checkpoint.name, + "title": checkpoint.title, + "filename": checkpoint.filename, + "hash": checkpoint.shorthash, + "search_term": self.search_terms_from_path(checkpoint.title), + "preview": self.find_preview(checkpoint.filename), + "local_preview": f"{os.path.splitext(checkpoint.filename)[0]}.{shared.opts.samples_format}", + "metadata": checkpoint.metadata, + "onclick": '"' + html.escape(f"""return selectCheckpoint({json.dumps(name)})""") + '"', + "mtime": os.path.getmtime(checkpoint.filename) if exists else 0, + "size": os.path.getsize(checkpoint.filename) if exists else 0, + } + record["info"] = self.find_info(checkpoint.filename) + record["description"] = self.find_description(checkpoint.filename, record["info"]) + except Exception as e: + shared.log.debug(f"Extra networks error: type=model file={name} {e}") + return record + def list_items(self): - checkpoint: sd_models.CheckpointInfo - checkpoints = sd_models.checkpoints_list.copy() - for name, checkpoint in checkpoints.items(): - try: - exists = os.path.exists(checkpoint.filename) - record = { - "type": 'Model', - "name": checkpoint.name, - "title": checkpoint.title, - "filename": checkpoint.filename, - "hash": checkpoint.shorthash, - "search_term": self.search_terms_from_path(checkpoint.title), - "preview": self.find_preview(checkpoint.filename), - "local_preview": f"{os.path.splitext(checkpoint.filename)[0]}.{shared.opts.samples_format}", - "metadata": checkpoint.metadata, - "onclick": '"' + html.escape(f"""return selectCheckpoint({json.dumps(name)})""") + '"', - "mtime": os.path.getmtime(checkpoint.filename) if exists else 0, - "size": os.path.getsize(checkpoint.filename) if exists else 0, - } - record["info"] = self.find_info(checkpoint.filename) - record["description"] = self.find_description(checkpoint.filename, record["info"]) - yield record - except Exception as e: - shared.log.debug(f"Extra networks error: type=model file={name} {e}") + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + future_items = {executor.submit(self.create_item, cp): cp for cp in list(sd_models.checkpoints_list.copy())} + for future in concurrent.futures.as_completed(future_items): + item = future.result() + if item is not None: + yield item for record in self.list_reference(): yield record diff --git a/modules/ui_extra_networks_hypernets.py b/modules/ui_extra_networks_hypernets.py index 2fde91634..28189dad4 100644 --- a/modules/ui_extra_networks_hypernets.py +++ b/modules/ui_extra_networks_hypernets.py @@ -13,7 +13,6 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage): def list_items(self): for name, path in shared.hypernetworks.items(): try: - fn = os.path.splitext(path)[0] name = os.path.relpath(os.path.splitext(path)[0], shared.opts.hypernetwork_dir) yield { "type": 'Hypernetwork', diff --git a/modules/ui_extra_networks_styles.py b/modules/ui_extra_networks_styles.py index 49a34affb..12f5f0a53 100644 --- a/modules/ui_extra_networks_styles.py +++ b/modules/ui_extra_networks_styles.py @@ -1,6 +1,7 @@ import os import html import json +import concurrent from modules import shared, extra_networks, ui_extra_networks, styles @@ -62,35 +63,44 @@ class ExtraNetworksPageStyles(ui_extra_networks.ExtraNetworksPage): } return item - def list_items(self): - for k, style in shared.prompt_styles.styles.items(): - try: - fn = os.path.splitext(getattr(style, 'filename', ''))[0] - name = getattr(style, 'name', '') - if name == '': - continue - txt = f'Prompt: {getattr(style, "prompt", "")}' - if len(getattr(style, 'negative_prompt', '')) > 0: - txt += f'\nNegative: {style.negative_prompt}' - yield { - "type": 'Style', - "name": name, - "title": k, - "filename": style.filename, - "search_term": f'{txt} {self.search_terms_from_path(name)}', - "preview": style.preview if getattr(style, 'preview', None) is not None and style.preview.startswith('data:') else self.find_preview(fn), - "description": style.description if getattr(style, 'description', None) is not None and len(style.description) > 0 else txt, - "prompt": getattr(style, 'prompt', ''), - "negative": getattr(style, 'negative_prompt', ''), - "extra": getattr(style, 'extra', ''), - "local_preview": f"{fn}.{shared.opts.samples_format}", - "onclick": '"' + html.escape(f"""return selectStyle({json.dumps(name)})""") + '"', - "mtime": getattr(style, 'mtime', 0), - "size": os.path.getsize(style.filename), - } - except Exception as e: - shared.log.debug(f"Extra networks error: type=style file={k} {e}") + def create_item(self, k): + item = None + try: + style = shared.prompt_styles.styles.get(k) + fn = os.path.splitext(getattr(style, 'filename', ''))[0] + name = getattr(style, 'name', '') + if name == '': + return item + txt = f'Prompt: {getattr(style, "prompt", "")}' + if len(getattr(style, 'negative_prompt', '')) > 0: + txt += f'\nNegative: {style.negative_prompt}' + item = { + "type": 'Style', + "name": name, + "title": k, + "filename": style.filename, + "search_term": f'{txt} {self.search_terms_from_path(name)}', + "preview": style.preview if getattr(style, 'preview', None) is not None and style.preview.startswith('data:') else self.find_preview(fn), + "description": style.description if getattr(style, 'description', None) is not None and len(style.description) > 0 else txt, + "prompt": getattr(style, 'prompt', ''), + "negative": getattr(style, 'negative_prompt', ''), + "extra": getattr(style, 'extra', ''), + "local_preview": f"{fn}.{shared.opts.samples_format}", + "onclick": '"' + html.escape(f"""return selectStyle({json.dumps(name)})""") + '"', + "mtime": getattr(style, 'mtime', 0), + "size": os.path.getsize(style.filename), + } + except Exception as e: + shared.log.debug(f"Extra networks error: type=style file={k} {e}") + return item + def list_items(self): + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + future_items = {executor.submit(self.create_item, style): style for style in list(shared.prompt_styles.styles)} + for future in concurrent.futures.as_completed(future_items): + item = future.result() + if item is not None: + yield item def allowed_directories_for_previews(self): return [v for v in [shared.opts.styles_dir] if v is not None] + ['html'] diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index 004e4cecc..f67031029 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -1,5 +1,6 @@ import json import os +import concurrent from modules import shared, sd_hijack, sd_models, ui_extra_networks from modules.textual_inversion.textual_inversion import Embedding @@ -8,6 +9,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): def __init__(self): super().__init__('Embedding') self.allow_negative_prompt = True + self.embeddings = [] def refresh(self): if sd_models.model_data.sd_model is None: @@ -17,51 +19,61 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): elif hasattr(sd_models.model_data.sd_model, 'embedding_db'): sd_models.model_data.sd_model.embedding_db.load_textual_inversion_embeddings(force_reload=True) + def create_item(self, embedding: Embedding): + record = None + try: + path, _ext = os.path.splitext(embedding.filename) + tags = {} + if embedding.tag is not None: + tags[embedding.tag]=1 + name = os.path.splitext(embedding.basename)[0] + record = { + "type": 'Embedding', + "name": name, + "filename": embedding.filename, + "preview": self.find_preview(embedding.filename), + "search_term": self.search_terms_from_path(name), + "prompt": json.dumps(f" {os.path.splitext(embedding.name)[0]}"), + "local_preview": f"{path}.{shared.opts.samples_format}", + "tags": tags, + "mtime": os.path.getmtime(embedding.filename), + "size": os.path.getsize(embedding.filename), + } + record["info"] = self.find_info(embedding.filename) + record["description"] = self.find_description(embedding.filename, record["info"]) + except Exception as e: + shared.log.debug(f"Extra networks error: type=embedding file={embedding.filename} {e}") + return record + def list_items(self): + def list_folder(folder): for filename in os.listdir(folder): fn = os.path.join(folder, filename) if os.path.isfile(fn) and (fn.lower().endswith(".pt") or fn.lower().endswith(".safetensors")): embedding = Embedding(vec=0, name=os.path.basename(fn), filename=fn) embedding.filename = fn - embeddings.append(embedding) + self.embeddings.append(embedding) elif os.path.isdir(fn) and not fn.startswith('.'): list_folder(fn) if sd_models.model_data.sd_model is None: - embeddings = [] + self.embeddings = [] list_folder(shared.opts.embeddings_dir) elif shared.backend == shared.Backend.ORIGINAL: - embeddings = list(sd_hijack.model_hijack.embedding_db.word_embeddings.values()) + self.embeddings = list(sd_hijack.model_hijack.embedding_db.word_embeddings.values()) elif hasattr(sd_models.model_data.sd_model, 'embedding_db'): - embeddings = list(sd_models.model_data.sd_model.embedding_db.word_embeddings.values()) + self.embeddings = list(sd_models.model_data.sd_model.embedding_db.word_embeddings.values()) else: - embeddings = [] - embeddings = sorted(embeddings, key=lambda emb: emb.filename) - for embedding in embeddings: - try: - path, _ext = os.path.splitext(embedding.filename) - tags = {} - if embedding.tag is not None: - tags[embedding.tag]=1 - name = os.path.splitext(embedding.basename)[0] - record = { - "type": 'Embedding', - "name": name, - "filename": embedding.filename, - "preview": self.find_preview(embedding.filename), - "search_term": self.search_terms_from_path(name), - "prompt": json.dumps(f" {os.path.splitext(embedding.name)[0]}"), - "local_preview": f"{path}.{shared.opts.samples_format}", - "tags": tags, - "mtime": os.path.getmtime(embedding.filename), - "size": os.path.getsize(embedding.filename), - } - record["info"] = self.find_info(embedding.filename) - record["description"] = self.find_description(embedding.filename, record["info"]) - yield record - except Exception as e: - shared.log.debug(f"Extra networks error: type=embedding file={embedding.filename} {e}") + self.embeddings = [] + self.embeddings = sorted(self.embeddings, key=lambda emb: emb.filename) + + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + future_items = {executor.submit(self.create_item, net): net for net in self.embeddings} + for future in concurrent.futures.as_completed(future_items): + item = future.result() + if item is not None: + yield item def allowed_directories_for_previews(self): return list(sd_hijack.model_hijack.embedding_db.embedding_dirs) diff --git a/modules/ui_interrogate.py b/modules/ui_interrogate.py index 3adc5b72c..b9bb074ea 100644 --- a/modules/ui_interrogate.py +++ b/modules/ui_interrogate.py @@ -35,7 +35,7 @@ class BatchWriter: def load(clip_model_name): global ci # pylint: disable=global-statement if ci is None: - config = Config(device=devices.get_optimal_device(), cache_path=os.path.join(paths.models_path, 'clip-interrogator'), clip_model_name=clip_model_name, quiet=True) + config = Config(device=devices.get_optimal_device(), cache_path=os.path.join(paths.models_path, 'Interrogator'), clip_model_name=clip_model_name, quiet=True) if low_vram: config.apply_low_vram_defaults() shared.log.info(f'Interrogate load: config={config}') diff --git a/modules/upscaler.py b/modules/upscaler.py index e60f9a46c..52c50ff45 100644 --- a/modules/upscaler.py +++ b/modules/upscaler.py @@ -51,6 +51,23 @@ class Upscaler: except Exception: pass + def find_folder(self, folder, scalers, loaded): + for fn in os.listdir(folder): # from folder + file_name = os.path.join(folder, fn) + if os.path.isdir(file_name): + self.find_folder(file_name, scalers, loaded) + continue + if not file_name.endswith('.pth') and not file_name.endswith('.pt'): + continue + if file_name not in loaded: + model_name = os.path.splitext(fn)[0] + scaler = UpscalerData(name=f'{self.name} {model_name}', path=file_name, upscaler=self) + scaler.custom = True + scalers.append(scaler) + loaded.append(file_name) + modules.shared.log.debug(f'Upscaler type={self.name} folder="{folder}" model="{model_name}" path="{file_name}"') + print(f'Upscaler type={self.name} folder="{folder}" model="{model_name}" path="{file_name}"') + def find_scalers(self): scalers = [] loaded = [] @@ -66,6 +83,8 @@ class Upscaler: # modules.shared.log.debug(f'Upscaler type={self.name} folder="{self.user_path}" model="{model[0]}" path="{model_path}"') if not os.path.exists(self.user_path): return scalers + self.find_folder(self.user_path, scalers, loaded) + """ for fn in os.listdir(self.user_path): # from folder if not fn.endswith('.pth') and not fn.endswith('.pt'): continue @@ -77,6 +96,7 @@ class Upscaler: scalers.append(scaler) loaded.append(file_name) # modules.shared.log.debug(f'Upscaler type={self.name} folder="{self.user_path}" model="{model_name}" path="{file_name}"') + """ return scalers @abstractmethod diff --git a/repositories/codeformer/facelib/detection/__init__.py b/repositories/codeformer/facelib/detection/__init__.py index 5d1f8fc21..1c021d410 100644 --- a/repositories/codeformer/facelib/detection/__init__.py +++ b/repositories/codeformer/facelib/detection/__init__.py @@ -1,14 +1,16 @@ import os +from copy import deepcopy import torch from torch import nn -from copy import deepcopy - from facelib.utils import load_file_from_url from facelib.utils import download_pretrained_models from facelib.detection.yolov5face.models.common import Conv - from .retinaface.retinaface import RetinaFace from .yolov5face.face_detector import YoloDetector +from modules import paths + + +model_dir = os.path.join(paths.models_path, 'Codeformer') def init_detection_model(model_name, half=False, device='cuda'): @@ -32,7 +34,7 @@ def init_retinaface_model(model_name, half=False, device='cuda'): else: raise NotImplementedError(f'{model_name} is not implemented.') - model_path = load_file_from_url(url=model_url, model_dir='weights/facelib', progress=True, file_name=None) + model_path = load_file_from_url(url=model_url, model_dir=model_dir, progress=True, file_name=None) load_net = torch.load(model_path, map_location=lambda storage, loc: storage) # remove unnecessary 'module.' for k, v in deepcopy(load_net).items(): @@ -55,8 +57,8 @@ def init_yolov5face_model(model_name, device='cuda'): model_url = 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5n-face.pth' else: raise NotImplementedError(f'{model_name} is not implemented.') - - model_path = load_file_from_url(url=model_url, model_dir='weights/facelib', progress=True, file_name=None) + + model_path = load_file_from_url(url=model_url, model_dir=model_dir, progress=True, file_name=None) load_net = torch.load(model_path, map_location=lambda storage, loc: storage) model.detector.load_state_dict(load_net, strict=True) model.detector.eval() @@ -69,32 +71,3 @@ def init_yolov5face_model(model_name, device='cuda'): m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility return model - - -# Download from Google Drive -# def init_yolov5face_model(model_name, device='cuda'): -# if model_name == 'YOLOv5l': -# model = YoloDetector(config_name='facelib/detection/yolov5face/models/yolov5l.yaml', device=device) -# f_id = {'yolov5l-face.pth': '131578zMA6B2x8VQHyHfa6GEPtulMCNzV'} -# elif model_name == 'YOLOv5n': -# model = YoloDetector(config_name='facelib/detection/yolov5face/models/yolov5n.yaml', device=device) -# f_id = {'yolov5n-face.pth': '1fhcpFvWZqghpGXjYPIne2sw1Fy4yhw6o'} -# else: -# raise NotImplementedError(f'{model_name} is not implemented.') - -# model_path = os.path.join('weights/facelib', list(f_id.keys())[0]) -# if not os.path.exists(model_path): -# download_pretrained_models(file_ids=f_id, save_path_root='weights/facelib') - -# load_net = torch.load(model_path, map_location=lambda storage, loc: storage) -# model.detector.load_state_dict(load_net, strict=True) -# model.detector.eval() -# model.detector = model.detector.to(device).float() - -# for m in model.detector.modules(): -# if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]: -# m.inplace = True # pytorch 1.7.0 compatibility -# elif isinstance(m, Conv): -# m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility - -# return model \ No newline at end of file diff --git a/repositories/codeformer/scripts/download_pretrained_models.py b/repositories/codeformer/scripts/download_pretrained_models.py index daa6e8ca1..16ff6e170 100644 --- a/repositories/codeformer/scripts/download_pretrained_models.py +++ b/repositories/codeformer/scripts/download_pretrained_models.py @@ -1,40 +1,26 @@ -import argparse import os -from os import path as osp - +from modules import paths from basicsr.utils.download_util import load_file_from_url -def download_pretrained_models(method, file_urls): - save_path_root = f'./weights/{method}' - os.makedirs(save_path_root, exist_ok=True) +urls = { + 'CodeFormer': { + 'codeformer.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth' + }, + 'facelib': { + # 'yolov5l-face.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5l-face.pth', + 'detection_Resnet50_Final.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/detection_Resnet50_Final.pth', + 'parsing_parsenet.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth' + } +} + +def download_pretrained_models(file_urls): + model_dir = os.path.join(paths.models_path, 'Codeformer') for file_name, file_url in file_urls.items(): - save_path = load_file_from_url(url=file_url, model_dir=save_path_root, progress=True, file_name=file_name) + load_file_from_url(url=file_url, model_dir=model_dir, progress=True, file_name=file_name) if __name__ == '__main__': - parser = argparse.ArgumentParser() - - parser.add_argument( - 'method', - type=str, - help=("Options: 'CodeFormer' 'facelib'. Set to 'all' to download all the models.")) - args = parser.parse_args() - - file_urls = { - 'CodeFormer': { - 'codeformer.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth' - }, - 'facelib': { - # 'yolov5l-face.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/yolov5l-face.pth', - 'detection_Resnet50_Final.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/detection_Resnet50_Final.pth', - 'parsing_parsenet.pth': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth' - } - } - - if args.method == 'all': - for method in file_urls.keys(): - download_pretrained_models(method, file_urls[method]) - else: - download_pretrained_models(args.method, file_urls[args.method]) \ No newline at end of file + for method in urls.keys(): + download_pretrained_models(urls[method]) diff --git a/repositories/codeformer/scripts/download_pretrained_models_from_gdrive.py b/repositories/codeformer/scripts/download_pretrained_models_from_gdrive.py index 7df5be6fc..5a5c6bd44 100644 --- a/repositories/codeformer/scripts/download_pretrained_models_from_gdrive.py +++ b/repositories/codeformer/scripts/download_pretrained_models_from_gdrive.py @@ -1,18 +1,16 @@ -import argparse import os +from modules import paths from os import path as osp - -# from basicsr.utils.download_util import download_file_from_google_drive import gdown -def download_pretrained_models(method, file_ids): - save_path_root = f'./weights/{method}' - os.makedirs(save_path_root, exist_ok=True) +model_dir = os.path.join(paths.models_path, 'Codeformer') + +def download_pretrained_models(file_ids): for file_name, file_id in file_ids.items(): file_url = 'https://drive.google.com/uc?id='+file_id - save_path = osp.abspath(osp.join(save_path_root, file_name)) + save_path = osp.abspath(osp.join(model_dir, file_name)) if osp.exists(save_path): user_response = input(f'{file_name} already exist. Do you want to cover it? Y/N\n') if user_response.lower() == 'y': @@ -29,21 +27,13 @@ def download_pretrained_models(method, file_ids): # download_file_from_google_drive(file_id, save_path) if __name__ == '__main__': - parser = argparse.ArgumentParser() - - parser.add_argument( - 'method', - type=str, - help=("Options: 'CodeFormer' 'facelib'. Set to 'all' to download all the models.")) - args = parser.parse_args() - # file name: file id # 'dlib': { # 'mmod_human_face_detector-4cb19393.dat': '1qD-OqY8M6j4PWUP_FtqfwUPFPRMu6ubX', # 'shape_predictor_5_face_landmarks-c4b1e980.dat': '1vF3WBUApw4662v9Pw6wke3uk1qxnmLdg', # 'shape_predictor_68_face_landmarks-fbdc2cb8.dat': '1tJyIVdCHaU6IDMDx86BZCxLGZfsWB8yq' # } - file_ids = { + urls = { 'CodeFormer': { 'codeformer.pth': '1v_E_vZvP-dQPF55Kc5SRCjaKTQXDz-JB' }, @@ -52,9 +42,5 @@ if __name__ == '__main__': 'parsing_parsenet.pth': '16pkohyZZ8ViHGBk3QtVqxLZKzdo466bK' } } - - if args.method == 'all': - for method in file_ids.keys(): - download_pretrained_models(method, file_ids[method]) - else: - download_pretrained_models(args.method, file_ids[args.method]) \ No newline at end of file + for method in urls.keys(): + download_pretrained_models(urls[method]) diff --git a/repositories/codeformer/web-demos/hugging_face/app.py b/repositories/codeformer/web-demos/hugging_face/app.py deleted file mode 100644 index 7da0fc947..000000000 --- a/repositories/codeformer/web-demos/hugging_face/app.py +++ /dev/null @@ -1,280 +0,0 @@ -""" -This file is used for deploying hugging face demo: -https://huggingface.co/spaces/sczhou/CodeFormer -""" - -import sys -sys.path.append('CodeFormer') -import os -import cv2 -import torch -import torch.nn.functional as F -import gradio as gr - -from torchvision.transforms.functional import normalize - -from basicsr.utils import imwrite, img2tensor, tensor2img -from basicsr.utils.download_util import load_file_from_url -from facelib.utils.face_restoration_helper import FaceRestoreHelper -from facelib.utils.misc import is_gray -from basicsr.archs.rrdbnet_arch import RRDBNet -from basicsr.utils.realesrgan_utils import RealESRGANer - -from basicsr.utils.registry import ARCH_REGISTRY - - -os.system("pip freeze") - -pretrain_model_url = { - 'codeformer': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/codeformer.pth', - 'detection': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/detection_Resnet50_Final.pth', - 'parsing': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/parsing_parsenet.pth', - 'realesrgan': 'https://github.com/sczhou/CodeFormer/releases/download/v0.1.0/RealESRGAN_x2plus.pth' -} -# download weights -if not os.path.exists('CodeFormer/weights/CodeFormer/codeformer.pth'): - load_file_from_url(url=pretrain_model_url['codeformer'], model_dir='CodeFormer/weights/CodeFormer', progress=True, file_name=None) -if not os.path.exists('CodeFormer/weights/facelib/detection_Resnet50_Final.pth'): - load_file_from_url(url=pretrain_model_url['detection'], model_dir='CodeFormer/weights/facelib', progress=True, file_name=None) -if not os.path.exists('CodeFormer/weights/facelib/parsing_parsenet.pth'): - load_file_from_url(url=pretrain_model_url['parsing'], model_dir='CodeFormer/weights/facelib', progress=True, file_name=None) -if not os.path.exists('CodeFormer/weights/realesrgan/RealESRGAN_x2plus.pth'): - load_file_from_url(url=pretrain_model_url['realesrgan'], model_dir='CodeFormer/weights/realesrgan', progress=True, file_name=None) - -# download images -torch.hub.download_url_to_file( - 'https://replicate.com/api/models/sczhou/codeformer/files/fa3fe3d1-76b0-4ca8-ac0d-0a925cb0ff54/06.png', - '01.png') -torch.hub.download_url_to_file( - 'https://replicate.com/api/models/sczhou/codeformer/files/a1daba8e-af14-4b00-86a4-69cec9619b53/04.jpg', - '02.jpg') -torch.hub.download_url_to_file( - 'https://replicate.com/api/models/sczhou/codeformer/files/542d64f9-1712-4de7-85f7-3863009a7c3d/03.jpg', - '03.jpg') -torch.hub.download_url_to_file( - 'https://replicate.com/api/models/sczhou/codeformer/files/a11098b0-a18a-4c02-a19a-9a7045d68426/010.jpg', - '04.jpg') -torch.hub.download_url_to_file( - 'https://replicate.com/api/models/sczhou/codeformer/files/7cf19c2c-e0cf-4712-9af8-cf5bdbb8d0ee/012.jpg', - '05.jpg') - -def imread(img_path): - img = cv2.imread(img_path) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - return img - -# set enhancer with RealESRGAN -def set_realesrgan(): - half = True if torch.cuda.is_available() else False - model = RRDBNet( - num_in_ch=3, - num_out_ch=3, - num_feat=64, - num_block=23, - num_grow_ch=32, - scale=2, - ) - upsampler = RealESRGANer( - scale=2, - model_path="CodeFormer/weights/realesrgan/RealESRGAN_x2plus.pth", - model=model, - tile=400, - tile_pad=40, - pre_pad=0, - half=half, - ) - return upsampler - -upsampler = set_realesrgan() -device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') -codeformer_net = ARCH_REGISTRY.get("CodeFormer")( - dim_embd=512, - codebook_size=1024, - n_head=8, - n_layers=9, - connect_list=["32", "64", "128", "256"], -).to(device) -ckpt_path = "CodeFormer/weights/CodeFormer/codeformer.pth" -checkpoint = torch.load(ckpt_path)["params_ema"] -codeformer_net.load_state_dict(checkpoint) -codeformer_net.eval() - -os.makedirs('output', exist_ok=True) - -def inference(image, background_enhance, face_upsample, upscale, codeformer_fidelity): - """Run a single prediction on the model""" - try: # global try - # take the default setting for the demo - has_aligned = False - only_center_face = False - draw_box = False - detection_model = "retinaface_resnet50" - print('Inp:', image, background_enhance, face_upsample, upscale, codeformer_fidelity) - - img = cv2.imread(str(image), cv2.IMREAD_COLOR) - print('\timage size:', img.shape) - - upscale = int(upscale) # convert type to int - if upscale > 4: # avoid memory exceeded due to too large upscale - upscale = 4 - if upscale > 2 and max(img.shape[:2])>1000: # avoid memory exceeded due to too large img resolution - upscale = 2 - if max(img.shape[:2]) > 1500: # avoid memory exceeded due to too large img resolution - upscale = 1 - background_enhance = False - face_upsample = False - - face_helper = FaceRestoreHelper( - upscale, - face_size=512, - crop_ratio=(1, 1), - det_model=detection_model, - save_ext="png", - use_parse=True, - device=device, - ) - bg_upsampler = upsampler if background_enhance else None - face_upsampler = upsampler if face_upsample else None - - if has_aligned: - # the input faces are already cropped and aligned - img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_LINEAR) - face_helper.is_gray = is_gray(img, threshold=5) - if face_helper.is_gray: - print('\tgrayscale input: True') - face_helper.cropped_faces = [img] - else: - face_helper.read_image(img) - # get face landmarks for each face - num_det_faces = face_helper.get_face_landmarks_5( - only_center_face=only_center_face, resize=640, eye_dist_threshold=5 - ) - print(f'\tdetect {num_det_faces} faces') - # align and warp each face - face_helper.align_warp_face() - - # face restoration for each cropped face - for idx, cropped_face in enumerate(face_helper.cropped_faces): - # prepare data - cropped_face_t = img2tensor( - cropped_face / 255.0, bgr2rgb=True, float32=True - ) - normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) - cropped_face_t = cropped_face_t.unsqueeze(0).to(device) - - try: - with torch.no_grad(): - output = codeformer_net( - cropped_face_t, w=codeformer_fidelity, adain=True - )[0] - restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1)) - del output - torch.cuda.empty_cache() - except RuntimeError as error: - print(f"Failed inference for CodeFormer: {error}") - restored_face = tensor2img( - cropped_face_t, rgb2bgr=True, min_max=(-1, 1) - ) - - restored_face = restored_face.astype("uint8") - face_helper.add_restored_face(restored_face) - - # paste_back - if not has_aligned: - # upsample the background - if bg_upsampler is not None: - # Now only support RealESRGAN for upsampling background - bg_img = bg_upsampler.enhance(img, outscale=upscale)[0] - else: - bg_img = None - face_helper.get_inverse_affine(None) - # paste each restored face to the input image - if face_upsample and face_upsampler is not None: - restored_img = face_helper.paste_faces_to_input_image( - upsample_img=bg_img, - draw_box=draw_box, - face_upsampler=face_upsampler, - ) - else: - restored_img = face_helper.paste_faces_to_input_image( - upsample_img=bg_img, draw_box=draw_box - ) - - # save restored img - save_path = f'output/out.png' - imwrite(restored_img, str(save_path)) - - restored_img = cv2.cvtColor(restored_img, cv2.COLOR_BGR2RGB) - return restored_img, save_path - except Exception as error: - print('Global exception', error) - return None, None - - -title = "CodeFormer: Robust Face Restoration and Enhancement Network" -description = r"""
CodeFormer logo
-Official Gradio demo for Towards Robust Blind Face Restoration with Codebook Lookup Transformer (NeurIPS 2022).
-🔥 CodeFormer is a robust face restoration algorithm for old photos or AI-generated faces.
-🤗 Try CodeFormer for improved stable-diffusion generation!
-""" -article = r""" -If CodeFormer is helpful, please help to ⭐ the Github Repo. Thanks! -[![GitHub Stars](https://img.shields.io/github/stars/sczhou/CodeFormer?style=social)](https://github.com/sczhou/CodeFormer) - ---- - -📝 **Citation** - -If our work is useful for your research, please consider citing: -```bibtex -@inproceedings{zhou2022codeformer, - author = {Zhou, Shangchen and Chan, Kelvin C.K. and Li, Chongyi and Loy, Chen Change}, - title = {Towards Robust Blind Face Restoration with Codebook Lookup TransFormer}, - booktitle = {NeurIPS}, - year = {2022} -} -``` - -📋 **License** - -This project is licensed under S-Lab License 1.0. -Redistribution and use for non-commercial purposes should follow this license. - -📧 **Contact** - -If you have any questions, please feel free to reach me out at shangchenzhou@gmail.com. - -
- 🤗 Find Me: - Twitter Follow - Github Follow -
- -
visitors
-""" - -demo = gr.Interface( - inference, [ - gr.inputs.Image(type="filepath", label="Input"), - gr.inputs.Checkbox(default=True, label="Background_Enhance"), - gr.inputs.Checkbox(default=True, label="Face_Upsample"), - gr.inputs.Number(default=2, label="Rescaling_Factor (up to 4)"), - gr.Slider(0, 1, value=0.5, step=0.01, label='Codeformer_Fidelity (0 for better quality, 1 for better identity)') - ], [ - gr.outputs.Image(type="numpy", label="Output"), - gr.outputs.File(label="Download the output") - ], - title=title, - description=description, - article=article, - examples=[ - ['01.png', True, True, 2, 0.7], - ['02.jpg', True, True, 2, 0.7], - ['03.jpg', True, True, 2, 0.7], - ['04.jpg', True, True, 2, 0.1], - ['05.jpg', True, True, 2, 0.1] - ] - ) - -demo.queue(concurrency_count=2) -demo.launch() \ No newline at end of file diff --git a/repositories/codeformer/web-demos/replicate/cog.yaml b/repositories/codeformer/web-demos/replicate/cog.yaml deleted file mode 100644 index 3f4589690..000000000 --- a/repositories/codeformer/web-demos/replicate/cog.yaml +++ /dev/null @@ -1,30 +0,0 @@ -""" -This file is used for deploying replicate demo: -https://replicate.com/sczhou/codeformer -""" - -build: - gpu: true - cuda: "11.3" - python_version: "3.8" - system_packages: - - "libgl1-mesa-glx" - - "libglib2.0-0" - python_packages: - - "ipython==8.4.0" - - "future==0.18.2" - - "lmdb==1.3.0" - - "scikit-image==0.19.3" - - "torch==1.11.0 --extra-index-url=https://download.pytorch.org/whl/cu113" - - "torchvision==0.12.0 --extra-index-url=https://download.pytorch.org/whl/cu113" - - "scipy==1.9.0" - - "gdown==4.5.1" - - "pyyaml==6.0" - - "tb-nightly==2.11.0a20220906" - - "tqdm==4.64.1" - - "yapf==0.32.0" - - "lpips==0.1.4" - - "Pillow==9.2.0" - - "opencv-python==4.6.0.66" - -predict: "predict.py:Predictor" diff --git a/repositories/codeformer/web-demos/replicate/predict.py b/repositories/codeformer/web-demos/replicate/predict.py deleted file mode 100644 index 61935e9e7..000000000 --- a/repositories/codeformer/web-demos/replicate/predict.py +++ /dev/null @@ -1,189 +0,0 @@ -""" -This file is used for deploying replicate demo: -https://replicate.com/sczhou/codeformer -running: cog predict -i image=@inputs/whole_imgs/04.jpg -i codeformer_fidelity=0.5 -i upscale=2 -push: cog push r8.im/sczhou/codeformer -""" - -import tempfile -import cv2 -import torch -from torchvision.transforms.functional import normalize -try: - from cog import BasePredictor, Input, Path -except Exception: - print('please install cog package') - -from basicsr.utils import imwrite, img2tensor, tensor2img -from basicsr.archs.rrdbnet_arch import RRDBNet -from basicsr.utils.realesrgan_utils import RealESRGANer -from basicsr.utils.registry import ARCH_REGISTRY -from facelib.utils.face_restoration_helper import FaceRestoreHelper - - -class Predictor(BasePredictor): - def setup(self): - """Load the model into memory to make running multiple predictions efficient""" - self.device = "cuda:0" - self.upsampler = set_realesrgan() - self.net = ARCH_REGISTRY.get("CodeFormer")( - dim_embd=512, - codebook_size=1024, - n_head=8, - n_layers=9, - connect_list=["32", "64", "128", "256"], - ).to(self.device) - ckpt_path = "weights/CodeFormer/codeformer.pth" - checkpoint = torch.load(ckpt_path)[ - "params_ema" - ] # update file permission if cannot load - self.net.load_state_dict(checkpoint) - self.net.eval() - - def predict( - self, - image: Path = Input(description="Input image"), - codeformer_fidelity: float = Input( - default=0.5, - ge=0, - le=1, - description="Balance the quality (lower number) and fidelity (higher number).", - ), - background_enhance: bool = Input( - description="Enhance background image with Real-ESRGAN", default=True - ), - face_upsample: bool = Input( - description="Upsample restored faces for high-resolution AI-created images", - default=True, - ), - upscale: int = Input( - description="The final upsampling scale of the image", - default=2, - ), - ) -> Path: - """Run a single prediction on the model""" - - # take the default setting for the demo - has_aligned = False - only_center_face = False - draw_box = False - detection_model = "retinaface_resnet50" - - self.face_helper = FaceRestoreHelper( - upscale, - face_size=512, - crop_ratio=(1, 1), - det_model=detection_model, - save_ext="png", - use_parse=True, - device=self.device, - ) - - bg_upsampler = self.upsampler if background_enhance else None - face_upsampler = self.upsampler if face_upsample else None - - img = cv2.imread(str(image), cv2.IMREAD_COLOR) - - if has_aligned: - # the input faces are already cropped and aligned - img = cv2.resize(img, (512, 512), interpolation=cv2.INTER_LINEAR) - self.face_helper.cropped_faces = [img] - else: - self.face_helper.read_image(img) - # get face landmarks for each face - num_det_faces = self.face_helper.get_face_landmarks_5( - only_center_face=only_center_face, resize=640, eye_dist_threshold=5 - ) - print(f"\tdetect {num_det_faces} faces") - # align and warp each face - self.face_helper.align_warp_face() - - # face restoration for each cropped face - for idx, cropped_face in enumerate(self.face_helper.cropped_faces): - # prepare data - cropped_face_t = img2tensor( - cropped_face / 255.0, bgr2rgb=True, float32=True - ) - normalize(cropped_face_t, (0.5, 0.5, 0.5), (0.5, 0.5, 0.5), inplace=True) - cropped_face_t = cropped_face_t.unsqueeze(0).to(self.device) - - try: - with torch.no_grad(): - output = self.net( - cropped_face_t, w=codeformer_fidelity, adain=True - )[0] - restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1)) - del output - torch.cuda.empty_cache() - except Exception as error: - print(f"\tFailed inference for CodeFormer: {error}") - restored_face = tensor2img( - cropped_face_t, rgb2bgr=True, min_max=(-1, 1) - ) - - restored_face = restored_face.astype("uint8") - self.face_helper.add_restored_face(restored_face) - - # paste_back - if not has_aligned: - # upsample the background - if bg_upsampler is not None: - # Now only support RealESRGAN for upsampling background - bg_img = bg_upsampler.enhance(img, outscale=upscale)[0] - else: - bg_img = None - self.face_helper.get_inverse_affine(None) - # paste each restored face to the input image - if face_upsample and face_upsampler is not None: - restored_img = self.face_helper.paste_faces_to_input_image( - upsample_img=bg_img, - draw_box=draw_box, - face_upsampler=face_upsampler, - ) - else: - restored_img = self.face_helper.paste_faces_to_input_image( - upsample_img=bg_img, draw_box=draw_box - ) - - # save restored img - out_path = Path(tempfile.mkdtemp()) / 'output.png' - imwrite(restored_img, str(out_path)) - - return out_path - - -def imread(img_path): - img = cv2.imread(img_path) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - return img - - -def set_realesrgan(): - if not torch.cuda.is_available(): # CPU - import warnings - - warnings.warn( - "The unoptimized RealESRGAN is slow on CPU. We do not use it. " - "If you really want to use it, please modify the corresponding codes.", - category=RuntimeWarning, - ) - upsampler = None - else: - model = RRDBNet( - num_in_ch=3, - num_out_ch=3, - num_feat=64, - num_block=23, - num_grow_ch=32, - scale=2, - ) - upsampler = RealESRGANer( - scale=2, - model_path="./weights/realesrgan/RealESRGAN_x2plus.pth", - model=model, - tile=400, - tile_pad=40, - pre_pad=0, - half=True, - ) - return upsampler diff --git a/repositories/codeformer/weights/CodeFormer/.gitkeep b/repositories/codeformer/weights/CodeFormer/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/repositories/codeformer/weights/README.md b/repositories/codeformer/weights/README.md deleted file mode 100644 index 67ad334bd..000000000 --- a/repositories/codeformer/weights/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Weights - -Put the downloaded pre-trained models to this folder. \ No newline at end of file diff --git a/repositories/codeformer/weights/facelib/.gitkeep b/repositories/codeformer/weights/facelib/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/requirements.txt b/requirements.txt index c4a0005f5..b62383f18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,7 +51,7 @@ requests==2.31.0 tqdm==4.66.1 accelerate==0.20.3 opencv-python-headless==4.7.0.72 -diffusers==0.22.0 +diffusers==0.23.0 einops==0.4.1 gradio==3.43.2 huggingface_hub==0.18.0 diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py index 0086a3abc..e97ac9b45 100644 --- a/scripts/postprocessing_upscale.py +++ b/scripts/postprocessing_upscale.py @@ -16,7 +16,7 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): with FormRow(elem_id="extras_upscale"): with gr.Tabs(elem_id="extras_resize_mode"): with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: - upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=2.0, elem_id="extras_upscaling_resize") with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: with FormRow(): diff --git a/scripts/sd_upscale.py b/scripts/sd_upscale.py index 0d35e262b..8e22febbe 100644 --- a/scripts/sd_upscale.py +++ b/scripts/sd_upscale.py @@ -18,7 +18,7 @@ class Script(scripts.Script): info = gr.HTML("

Will upscale the image by the selected scale factor; use width and height sliders to set tile size

") overlap = gr.Slider(minimum=0, maximum=256, step=16, label='Tile overlap', value=64, elem_id=self.elem_id("overlap")) scale_factor = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label='Scale Factor', value=2.0, elem_id=self.elem_id("scale_factor")) - upscaler_index = gr.Radio(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index", elem_id=self.elem_id("upscaler_index")) + upscaler_index = gr.Dropdown(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index", elem_id=self.elem_id("upscaler_index")) return [info, overlap, upscaler_index, scale_factor] diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 5e5c00938..912638777 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -86,6 +86,17 @@ def apply_checkpoint(p, x, xs): p.override_settings['sd_model_checkpoint'] = info.name +def apply_refiner(p, x, xs): + if x == shared.opts.sd_model_refiner: + return + info = sd_models.get_closet_checkpoint_match(x) + if info is None: + shared.log.warning(f"XYZ grid: apply refiner unknown checkpoint: {x}") + else: + sd_models.reload_model_weights(shared.sd_refiner, info) + p.override_settings['sd_model_refiner'] = info.name + + def apply_dict(p, x, xs): if x == shared.opts.sd_model_dict: return @@ -240,11 +251,12 @@ axis_options = [ AxisOption("[Second pass] upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), AxisOption("[Second pass] sampler", str, apply_latent_sampler, fmt=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers]), AxisOption("[Second pass] denoising Strength", float, apply_field("denoising_strength")), - AxisOption("[Second pass] steps", int, apply_field("hr_second_pass_steps")), + AxisOption("[Second pass] hires steps", int, apply_field("hr_second_pass_steps")), AxisOption("[Second pass] CFG scale", float, apply_field("image_cfg_scale")), AxisOption("[Second pass] guidance rescale", float, apply_field("diffusers_guidance_rescale")), - AxisOption("[Second pass] refiner start", float, apply_field("refiner_start")), - AxisOption("[Second pass] refiner start", float, apply_field("refiner_start")), + AxisOption("[Refiner] model", str, apply_refiner, fmt=format_value, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list)), + AxisOption("[Refiner] refiner start", float, apply_field("refiner_start")), + AxisOption("[Refiner] refiner steps", float, apply_field("refiner_steps")), AxisOption("[TOME] Token merging ratio (txt2img)", float, apply_override('token_merging_ratio')), AxisOption("[TOME] Token merging ratio (hires)", float, apply_override('token_merging_ratio_hr')), AxisOption("[FreeU] 1st stage backbone factor", float, apply_setting('freeu_b1')), diff --git a/webui.py b/webui.py index 2e3c76f80..8397aced3 100644 --- a/webui.py +++ b/webui.py @@ -156,7 +156,7 @@ def initialize(): def load_model(): - if opts.sd_checkpoint_autoload: + if opts.sd_checkpoint_autoload and (shared.cmd_opts.ckpt is not None and shared.cmd_opts.ckpt.lower() != 'none'): shared.state.begin('load') thread_model = Thread(target=lambda: shared.sd_model) thread_model.start() diff --git a/wiki b/wiki index e999774e3..cd040c02e 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit e999774e3096ceb89a264548fdfaaa76d891c0df +Subproject commit cd040c02e4a477135ce08efe4d06672b57456c31