diff --git a/.gitignore b/.gitignore index bde1bf3bd..362e9c257 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__ .ruff_cache /cache.json /*.json +/*.yaml /params.txt /styles.csv /user.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d0558fdf..8af1d23a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Change Log for SD.Next +## Update for 2023-09-13 + +Started as a mostly a service release with quite a few fixes, but then... +Major changes how **hires** works as well as support for a very interesting new model [Wuerstchen](https://huggingface.co/blog/wuertschen) + +- tons of fixes +- changes to **hires** + - enable non-latent upscale modes (standard upscalers) + - when using latent upscale, hires pass is run automatically + - when using non-latent upscalers, hires pass is skipped by default + enabled using **force hires** option in ui + hires was not designed to work with standard upscalers, but i understand this is a common workflow + - when using refiner, upscale/hires runs before refiner pass + - second pass can now also utilize full/quick vae quality + - note that when combining non-latent upscale, hires and refiner output quality is maximum, + but operations are really resource intensive as it includes: *base->decode->upscale->encode->hires->refine* + - all combinations of: decode full/quick + upscale none/latent/non-latent + hires on/off + refiner on/off + should be supported, but given the number of combinations, issues are possible + - all operations are captured in image medata +- diffusers: + - allow loading of sd/sdxl models from safetensors without online connectivity + - support for new model: [wuerstchen](https://huggingface.co/warp-ai/wuerstchen) + its a high-resolution model (1024px+) that nearly doubls performance of sd-xl with much lower resource requirements + go to *models -> huggingface -> search "warp-ai/wuerstchen" -> download* + its nearly 12gb in size, so be patient :) +- minor re-layout of the main ui +- update **ui hints** +- updated **models -> civitai** + - search and download loras + - find previews for already downloaded models or loras +- new option **inference mode** + - default is standard `torch.no_grad` + new option is `torch.inference_only` which is slightly faster and uses less vram, but only works on some gpus +- new cmdline param `--no-metadata` + skips reading metadata from models that are not already cached +- updated **gradio** +- **styles** support for subfolders +- **css** optimizations +- clean-up **logging** + - capture system info in startup log + - better diagnostic output + - capture extension output + - capture ldm output + - cleaner server restart + - custom exception handling + ## Update for 2023-09-06 One week later, another large update! diff --git a/README.md b/README.md index bb5725346..bc1f07692 100644 --- a/README.md +++ b/README.md @@ -46,20 +46,23 @@ All Individual features are not listed here, instead check [ChangeLog](CHANGELOG - **Original**: Based on [LDM](https://github.com/Stability-AI/stablediffusion) reference implementation and significantly expanded on by [A1111](https://github.com/AUTOMATIC1111/stable-diffusion-webui) This is the default backend and it is fully compatible with all existing functionality and extensions + It supports **SD 1.x** and **SD 2.x** models - **Diffusers**: Based on new [Huggingface Diffusers](https://huggingface.co/docs/diffusers/index) implementation - It is also the only backend that supports **Stable Diffusion XL** model + It supports All models listed below + It is also the *only backend* that supports **Stable Diffusion XL** model See [wiki article](https://github.com/vladmandic/automatic/wiki/Diffusers) for more information ## Model support Additional models will be added as they become available and there is public interest in them -- Stable Diffusion 1.x and 2.x *(all variants)* -- Stable Diffusion XL -- Kandinsky 2.1 and 2.2 -- DeepFloyd IF -- UniDiffusion -- SD-Distilled *(all variants)* +- [Stable Diffusion](https://github.com/Stability-AI/stablediffusion/) 1.x and 2.x *(all variants)* +- [Stable Diffusion XL](https://github.com/Stability-AI/generative-models) +- [Kandinsky](https://github.com/ai-forever/Kandinsky-2) 2.1 and 2.2 +- [DeepFloyd IF](https://github.com/deep-floyd/IF) +- [UniDiffusion](https://github.com/thu-ml/unidiffuser) +- [SD-Distilled](https://huggingface.co/blog/sd_distillation) *(all variants)* +- [Wuerstchen](https://huggingface.co/blog/wuertschen) ## Platform support diff --git a/TODO.md b/TODO.md index 58172fdbe..e3e463db8 100644 --- a/TODO.md +++ b/TODO.md @@ -22,7 +22,6 @@ Stuff to be added, in no particular order... - Port **A1111** stuff - Port `p.all_hr_prompts` - Import core repos to reduce dependencies - - Parse StabilityAI `modelspec` metadata - Non-technical: - Update Wiki - Get more high-quality upscalers @@ -30,14 +29,15 @@ Stuff to be added, in no particular order... - [Localization](https://app.transifex.com/signup/open-source/) - New Minor - Prompt padding for positive/negative + - Add EN provider for VAEs - XYZ grid upscalers - Built-in `motd`-style notifications - Docker PR - New Major - - Style editor (use json format instead of csv) - - Profile manager (for config.json and ui-config.json) + - Style editor + - Profile manager (for `config.json` and `ui-config.json`) - Multi-user support - - Add [SAG](https://huggingface.co/docs/diffusers/v0.19.3/en/api/pipelines/self_attention_guidance),(https://github.com/ashen-sensored/sd_webui_SAG) + - Add [SAG](https://huggingface.co/docs/diffusers/v0.19.3/en/api/pipelines/self_attention_guidance), [SAG](https://github.com/ashen-sensored/sd_webui_SAG) - Image phash and hdash using `imagehash` - Model merge using `git-rebasin` - Enable refiner-style workflow for `ldm` backend @@ -49,11 +49,9 @@ Stuff to be added, in no particular order... - Templates for SD-XL training - Lora train UI - Redesign - - Extensions reporting framework - New UI - New inpainting canvas controls (move from backend to purely frontend) - New image browser (move from backend to purely frontend) - - New extra networks (move from backend to purely frontend) - Change workflows from static/legacy to steps-based ## Investigate diff --git a/cli/validate-locale.py b/cli/validate-locale.py index 5eb574b84..b5dd48ba7 100755 --- a/cli/validate-locale.py +++ b/cli/validate-locale.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import os import sys import json from rich import print # pylint: disable=redefined-builtin @@ -7,6 +8,9 @@ from rich import print # pylint: disable=redefined-builtin if __name__ == "__main__": sys.argv.pop(0) fn = sys.argv[0] if len(sys.argv) > 0 else 'locale_en.json' + if not os.path.isfile(fn): + print(f'File not found: {fn}') + sys.exit(1) with open(fn, 'r', encoding="utf-8") as f: data = json.load(f) keys = [] diff --git a/extensions-builtin/Lora/lora.py b/extensions-builtin/Lora/lora.py index 607dd8e33..fbac7e8fc 100644 --- a/extensions-builtin/Lora/lora.py +++ b/extensions-builtin/Lora/lora.py @@ -77,19 +77,16 @@ class LoraOnDisk: self.filename = filename self.metadata = {} self.is_safetensors = os.path.splitext(filename)[1].lower() == ".safetensors" - if self.is_safetensors: try: self.metadata = sd_models.read_metadata_from_safetensors(filename) except Exception as e: errors.display(e, f"reading lora metadata: {filename}") - if self.metadata: m = {} for k, v in sorted(self.metadata.items(), key=lambda x: metadata_tags_order.get(x[0], 999)): m[k] = v self.metadata = m - self.ssmd_cover_images = self.metadata.pop('ssmd_cover_images', None) # those are cover images and they are too big to display in UI as text self.alias = self.metadata.get('ss_output_name', self.name) self.hash = None @@ -98,7 +95,7 @@ class LoraOnDisk: def set_hash(self, v): self.hash = v - self.shorthash = self.hash[0:12] + self.shorthash = self.hash[0:10] if self.shorthash: available_lora_hash_lookup[self.shorthash] = self @@ -442,19 +439,13 @@ def list_available_loras(): forbidden_lora_aliases.clear() available_lora_hash_lookup.clear() forbidden_lora_aliases.update({"none": 1, "Addams": 1}) - os.makedirs(shared.cmd_opts.lora_dir, exist_ok=True) - for filename in sorted([*filter(extension_filter(['.PT', '.CKPT', '.SAFETENSORS']), directory_files(shared.cmd_opts.lora_dir))], key=str.lower): - name = os.path.splitext(os.path.basename(filename))[0] entry = LoraOnDisk(name, filename) - available_loras[name] = entry - if entry.alias in available_lora_aliases: forbidden_lora_aliases[entry.alias.lower()] = 1 - available_lora_aliases[name] = entry available_lora_aliases[entry.alias] = entry diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index a06ab27d8..e99105142 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -13,32 +13,28 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): lora.list_available_loras() def list_items(self): - for name, lora_on_disk in lora.available_loras.items(): - path, _ext = os.path.splitext(lora_on_disk.filename) - alias = lora_on_disk.get_alias() + for name, l in lora.available_loras.items(): + path, _ext = os.path.splitext(l.filename) + alias = l.get_alias() prompt = f" " prompt = json.dumps(prompt) - metadata = json.dumps(lora_on_disk.metadata, indent=4) if lora_on_disk.metadata else None - possible_tags = lora_on_disk.metadata.get('ss_tag_frequency', {}) if lora_on_disk.metadata is not None else {} + metadata = json.dumps(l.metadata, indent=4) if l.metadata else None + possible_tags = l.metadata.get('ss_tag_frequency', {}) if l.metadata is not None else {} if isinstance(possible_tags, str): possible_tags = {} - shared.log.debug(f'Lora has invalid metadata: {path}') tags = {} - for tag in possible_tags.keys(): - if '_' not in tag: - tag = f'0_{tag}' - words = tag.split('_', 1) + for k, v in possible_tags.items(): + words = k.split('_', 1) if '_' in k else [v, k] tags[' '.join(words[1:])] = words[0] - # shared.log.debug(f'Lora: {path}: name={name} alias={alias} tags={tags}') yield { "name": name, "filename": path, - "fullname": lora_on_disk.filename, - "hash": lora_on_disk.shorthash, + "fullname": l.filename, + "hash": l.shorthash, "preview": self.find_preview(path), "description": self.find_description(path), "info": self.find_info(path), - "search_term": self.search_terms_from_path(lora_on_disk.filename) + ' '.join(tags.keys()), + "search_term": self.search_terms_from_path(l.filename) + ' '.join(tags.keys()), "prompt": prompt, "local_preview": f"{path}.{shared.opts.samples_format}", "metadata": metadata, diff --git a/extensions-builtin/sd-extension-system-info b/extensions-builtin/sd-extension-system-info index 761cf83c9..83dd4d8f6 160000 --- a/extensions-builtin/sd-extension-system-info +++ b/extensions-builtin/sd-extension-system-info @@ -1 +1 @@ -Subproject commit 761cf83c93d8ac64b212ccd4a93b8efc2eaae305 +Subproject commit 83dd4d8f65511af720ddb034125fe4ac0a46fb18 diff --git a/extensions-builtin/sd-webui-agent-scheduler b/extensions-builtin/sd-webui-agent-scheduler index 310bb4eac..097fe4e5c 160000 --- a/extensions-builtin/sd-webui-agent-scheduler +++ b/extensions-builtin/sd-webui-agent-scheduler @@ -1 +1 @@ -Subproject commit 310bb4eace9e7fc03074dd49a91797bd6df2c43b +Subproject commit 097fe4e5c97612520b3e4d80d44f36088c53e2c4 diff --git a/extensions-builtin/sd-webui-controlnet b/extensions-builtin/sd-webui-controlnet index 2b12b2760..e67e01773 160000 --- a/extensions-builtin/sd-webui-controlnet +++ b/extensions-builtin/sd-webui-controlnet @@ -1 +1 @@ -Subproject commit 2b12b2760dcde0e29c0d33640739ba3fe2c0cd32 +Subproject commit e67e017731aad05796b9615dc6eadce911298ea1 diff --git a/extensions-builtin/stable-diffusion-webui-images-browser b/extensions-builtin/stable-diffusion-webui-images-browser index b8f6e05d1..9f95e6d48 160000 --- a/extensions-builtin/stable-diffusion-webui-images-browser +++ b/extensions-builtin/stable-diffusion-webui-images-browser @@ -1 +1 @@ -Subproject commit b8f6e05d1d23b3d0d17b0a9cfbf824bf4a1f98e9 +Subproject commit 9f95e6d4812ca7acb3ec56cbf45fbc85aef0236d diff --git a/html/locale_en.json b/html/locale_en.json index 07aa68b55..2c6e77e49 100644 --- a/html/locale_en.json +++ b/html/locale_en.json @@ -9,7 +9,17 @@ {"id":"","label":"⏫","localized":"","hint":"Fill"}, {"id":"","label":"🎲️","localized":"","hint":"Use random seed"}, {"id":"","label":"♻️","localized":"","hint":"Reuse previous seed"}, - {"id":"","label":"⇅","localized":"","hint":"Swap values"} + {"id":"","label":"⇅","localized":"","hint":"Swap values"}, + {"id":"","label":"⇦","localized":"","hint":"Read generation parameters from prompt or last generation if prompt is empty into user interface"}, + {"id":"","label":"⊗","localized":"","hint":"Clear prompt"}, + {"id":"","label":"🗁","localized":"","hint":"Show/hide extra networks"}, + {"id":"","label":"⇰","localized":"","hint":"Apply selected styles to current prompt"}, + {"id":"","label":"⇩","localized":"","hint":"Save current prompt as style template"}, + {"id":"","label":"⟲","localized":"","hint":"Refresh"}, + {"id":"","label":"🗙","localized":"","hint":"Close"}, + {"id":"","label":"⊜","localized":"","hint":"Fill"}, + {"id":"","label":"📐","localized":"","hint":"Measure"}, + {"id":"","label":"🔍","localized":"","hint":"Search"} ], "prompts": [ {"id":"","label":"Prompt","localized":"","hint":"Type what you want to see in the image"}, @@ -31,11 +41,13 @@ {"id":"","label":"Train","localized":"","hint":"Run training or model merging"}, {"id":"","label":"Models","localized":"","hint":"Convert or merge your models"}, {"id":"","label":"Interrogator","localized":"","hint":"Run interrogate to get description of your image"}, - {"id":"","label":"System Info","localized":"","hint":"System information and benchmarking"}, + {"id":"","label":"System Info","localized":"","hint":"System information"}, {"id":"","label":"Agent Scheduler","localized":"","hint":"Enqueue your generate requests and run them in the background"}, {"id":"","label":"Image Browser","localized":"","hint":"Browse through your generated image database"}, + {"id":"","label":"System","localized":"","hint":"System settings and information"}, {"id":"","label":"Settings","localized":"","hint":"Application settings"}, - {"id":"","label":"Extensions","localized":"","hint":"Application extensions"} + {"id":"","label":"Extensions","localized":"","hint":"Application extensions"}, + {"id":"","label":"Script","localized":"","hint":"Addtional scripts to be used"} ], "action panel": [ {"id":"","label":"Generate","localized":"","hint":"Start processing"}, @@ -48,8 +60,10 @@ {"id":"","label":"Interrogate\nDeepBooru","localized":"","hint":"Run interrogate using DeepBooru model"} ], "extra networks": [ - {"id":"","label":"Extra networks tab order","localized":"","hint":"Comma-separated list of tab names; tabs listed here will appear in the extra networks UI first and in order listed"}, - {"id":"","label":"UI position","localized":"","hint":""}, + {"id":"","label":"UI position","localized":"","hint":"Location of extra networks"}, + {"id":"","label":"cover","localized":"","hint":"cover full area"}, + {"id":"","label":"inline","localized":"","hint":"inline with all additional elelemtns (scrollable)"}, + {"id":"","label":"sidebar","localized":"","hint":"sidebar on the right side of the screen"}, {"id":"","label":"UI height (%)","localized":"","hint":""}, {"id":"","label":"UI sidebar width (%)","localized":"","hint":""}, {"id":"","label":"UI card preview lazy loading","localized":"","hint":""}, @@ -58,20 +72,19 @@ {"id":"","label":"UI image contain method","localized":"","hint":""}, {"id":"","label":"Do not automatically build extra network pages","localized":"","hint":""}, {"id":"","label":"Use LyCoris handler for all Lora types","localized":"","hint":""}, - {"id":"","label":"Disable built-in Lora handler","localized":"","hint":""}, - {"id":"","label":"Use Kohya method for handling multiple Loras","localized":"","hint":""}, + {"id":"","label":"Use Kohya method for handling multiple LoRA","localized":"","hint":""}, {"id":"","label":"Multiplier for extra networks","localized":"","hint":"When adding extra network such as Hypernetwork or Lora to prompt, use this multiplier for it"}, {"id":"","label":"Add hypernetwork to prompt","localized":"","hint":""}, {"id":"","label":"Add Lora to prompt","localized":"","hint":""}, {"id":"","label":"shuffle tags by ',' when creating prompts.","localized":"","hint":""}, - {"id":"","label":"extra text to add before <...> when adding extra network to prompt","localized":"","hint":""}, {"id":"","label":"When adding to prompt, refer to Lora by","localized":"","hint":""}, {"id":"","label":"add lora hashes to infotext","localized":"","hint":""}, - {"id":"","label":"Checkpoints","localized":"","hint":""}, - {"id":"","label":"Lora","localized":"","hint":""}, - {"id":"","label":"LyCORIS","localized":"","hint":""}, - {"id":"","label":"Textual Inversion","localized":"","hint":""}, - {"id":"","label":"Hypernetworks","localized":"","hint":""}, + {"id":"","label":"Checkpoints","localized":"","hint":"Trained model checkpoints"}, + {"id":"","label":"Styles","localized":"","hint":"Additional styles to be applied on selected generation paramters"}, + {"id":"","label":"Lora","localized":"","hint":"LoRA: Low-Rank Adaptation. Fine-tuned model that is applied on top of a loaded model"}, + {"id":"","label":"LyCORIS","localized":"","hint":"LyCORIS: Lora beYond Conventional methods. Fine-tuned model that is applied on top of a loaded model"}, + {"id":"","label":"Textual Inversion","localized":"","hint":"Textual inversion embedding is a trained embedded information about the subject"}, + {"id":"","label":"Hypernetworks","localized":"","hint":"Small trained neural network that modifies behavior of the loaded model"}, {"id":"","label":"Save preview","localized":"","hint":"Save current image as extra network preview"}, {"id":"","label":"Save description","localized":"","hint":"Save current text as extra network description"}, {"id":"","label":"Read description","localized":"","hint":"Read stored extra network description"} @@ -102,26 +115,37 @@ {"id":"","label":"Apply changes & restart server","localized":"","hint":"Apply all changes and restart server"}, {"id":"","label":"install","localized":"","hint":"install this extension"}, {"id":"","label":"uninstall","localized":"","hint":"uninstall this extension"}, - {"id":"","label":"User interface defaults","localized":"","hint":"Review and set current values as default values for the user interface"}, + {"id":"","label":"UI Config","localized":"","hint":"Review and set current values as default values for the user interface"}, {"id":"","label":"View changes","localized":"","hint":"Review changes between default user interface values and current values"}, {"id":"","label":"Set new defaults","localized":"","hint":"Set current values as default values for the user interface"}, + {"id":"","label":"Benchmark","localized":"","hint":"Run benchmarks"}, + {"id":"","label":"Models & Networks","localized":"","hint":"View lists of all available models and networks"}, {"id":"","label":"Restore system defaults","localized":"","hint":"Restore default user interface values"} ], "txt2img tab": [ + {"id":"","label":"Batch","localized":"","hint":"Additional batching options"}, + {"id":"","label":"Seed details","localized":"","hint":"Additional options regarding initial seed used to produce images"}, + {"id":"","label":"Advanced","localized":"","hint":"Additional advanced options"}, {"id":"","label":"Sampling method","localized":"","hint":"Which algorithm to use to produce the image"}, {"id":"","label":"Sampling steps","localized":"","hint":"How many times to improve the generated image iteratively; higher values take longer; very low values can produce bad results"}, {"id":"","label":"Restore faces","localized":"","hint":"Use a pre-trained model to correct the generated faces. See GFPGAN or Codeformer."}, {"id":"","label":"Tiling","localized":"","hint":"Produce an image that can be tiled"}, - {"id":"","label":"Hires fix","localized":"","hint":"Use a similar process as image to image to upscale and add detail to the final image."}, + {"id":"","label":"full quality","localized":"","hint":"Use full quality VAE to decode latent samples"}, + {"id":"","label":"face restore","localized":"","hint":"Run processed image through additional face restoration model"}, + {"id":"","label":"denoise","localized":"","hint":"Denoising details for img2img"}, + {"id":"","label":"remove background","localized":"","hint":"Run processed image through additional background removal model"}, + {"id":"","label":"Second pass","localized":"","hint":"Use a similar process as image to image to upscale and/or add detail to the final image. Optionally uses refiner model to enhance image details."}, {"id":"","label":"Denoising strength","localized":"","hint":"Determines how little respect the algorithm should have for image's content. At 0, nothing will change, and at 1 you'll get an unrelated image. With values below 1.0, processing will take less steps than the Sampling Steps slider specifies"}, {"id":"","label":"Denoise start","localized":"","hint":"Override denoise strength by stating how early base model should finish and when refiner should start. Only applicable to refiner usage. If set to 0 or 1, denoising strength will be used"}, {"id":"","label":"Hires steps","localized":"","hint":"Number of sampling steps for upscaled picture. If 0, uses same as for original"}, {"id":"","label":"Upscaler","localized":"","hint":"Which pre-trained model to use for the upscaling process."}, {"id":"","label":"Upscale by","localized":"","hint":"Adjusts the size of the image by multiplying the original width and height by the selected value. Ignored if either Resize width to or Resize height to are non-zero"}, + {"id":"","label":"Force Hires","localized":"","hint":"Hires runs automatically when Latent upscale is selected, but its skipped when using non-latent upscalers. Enable force hires to run hires with non-latent upscalers"}, {"id":"","label":"Resize width to","localized":"","hint":"Resizes image to this width. If 0, width is inferred from either of two nearby sliders"}, {"id":"","label":"Resize height to","localized":"","hint":"Resizes image to this height. If 0, height is inferred from either of two nearby sliders"}, {"id":"","label":"Secondary sampler","localized":"","hint":"Use specific sampler as fallback sampler if primary is not supported for specific operation"}, - {"id":"","label":"Secondary steps","localized":"","hint":"Number of steps to use for second pass"}, + {"id":"","label":"Refiner start","localized":"","hint":"Refiner pass will start when base model is this much complete (set to 0 or 1 to run after full base model run)"}, + {"id":"","label":"Refiner steps","localized":"","hint":"Number of steps to use for refiner pass"}, {"id":"","label":"Secondary CFG Scale","localized":"","hint":"CFG scale used for refiner pass"}, {"id":"","label":"Guidance rescale","localized":"","hint":"Rescale CFG generated noise to avoid overexposed images"}, {"id":"","label":"Secondary Prompt","localized":"","hint":"Prompt used for both second encoder in base model (if it exists) and for refiner pass (if enabled)"}, @@ -130,7 +154,7 @@ {"id":"","label":"Height","localized":"","hint":"Image height"}, {"id":"","label":"Batch count","localized":"","hint":"How many batches of images to create (has no impact on generation performance or VRAM usage)"}, {"id":"","label":"Batch size","localized":"","hint":"How many image to create in a single batch (increases generation performance at cost of higher VRAM usage)"}, - {"id":"","label":"CFG Scale","localized":"","hint":"Classifier Free Guidance scale: how strongly the image should conform to prompt. Lower values produce more creative results, higher values make it follow the prompt more strictly; recommended values between 5-10"}, + {"id":"","label":"cfg scale","localized":"","hint":"Classifier Free Guidance scale: how strongly the image should conform to prompt. Lower values produce more creative results, higher values make it follow the prompt more strictly; recommended values between 5-10"}, {"id":"","label":"CLIP skip","localized":"","hint":"Clip skip is a feature that allows users to control the level of specificity of the prompt, the higher the CLIP skip value, the less deep the prompt will be interpreted. CLIP Skip 1 is typical while some anime models produce better results at CLIP skip 2"}, {"id":"","label":"Seed","localized":"","hint":"A value that determines the output of random number generator - if you create an image with same parameters and seed as another image, you'll get the same result"}, {"id":"","label":"Extra","localized":"","hint":"Show additional options"}, @@ -149,7 +173,7 @@ {"id":"","label":"Input directory","localized":"","hint":"Folder where the images are that you want to process"}, {"id":"","label":"Output directory","localized":"","hint":"Folder where the processed images should be saved to"}, {"id":"","label":"Show result images","localized":"","hint":"Enable to show the processed images in the image pane"}, - {"id":"","label":"Resize","localized":"","hint":"Factor for resizing 1x mean no upscale, 4x means 4 times upscale, high values might lead to memory issues on small graphics cards"}, + {"id":"","label":"Resize","localized":"","hint":"Resizing details. Higher resolutions require additional processing memory."}, {"id":"","label":"Crop to fit","localized":"","hint":"If the dimensions of your source image (e.g. 512x510) deviate from your target dimensions (e.g. 1024x768) this function will fit your upscaled image into your target size image. Excess will be cropped"}, {"id":"","label":"Secondary Upscaler","localized":"","hint":"Select secondary upscaler to run after initial upscaler"}, {"id":"","label":"Upscaler 2 visibility","localized":"","hint":"Strength of the secondary upscaler"}, @@ -167,24 +191,22 @@ {"id":"sett_reload_sd_model","label":"Reload checkpoint","localized":"","hint":"Reload currently selected model checkpoint"} ], "settings sections": [ - {"id":"","label":"Stable Diffusion","localized":"","hint":""}, + {"id":"","label":"execution & models","localized":"","hint":""}, {"id":"","label":"Optimizations","localized":"","hint":""}, {"id":"","label":"Compute Settings","localized":"","hint":""}, {"id":"","label":"Diffusers Settings","localized":"","hint":""}, {"id":"","label":"System Paths","localized":"","hint":""}, {"id":"","label":"Image Options","localized":"","hint":""}, - {"id":"","label":"Image Processing","localized":"","hint":""}, - {"id":"","label":"Image Paths","localized":"","hint":""}, + {"id":"","label":"image naming & paths","localized":"","hint":""}, {"id":"","label":"User Interface","localized":"","hint":""}, {"id":"","label":"Live Previews","localized":"","hint":""}, {"id":"","label":"Sampler Settings","localized":"","hint":""}, {"id":"","label":"Postprocessing","localized":"","hint":""}, {"id":"","label":"Training","localized":"","hint":""}, {"id":"","label":"Interrogate","localized":"","hint":""}, - {"id":"","label":"Upscaling","localized":"","hint":""}, {"id":"","label":"Extra Networks","localized":"","hint":""}, - {"id":"","label":"Licenses","localized":"","hint":""}, - {"id":"","label":"Show all pages","localized":"","hint":""}, + {"id":"","label":"Licenses","localized":"","hint":"View licenses of all additional included libraries"}, + {"id":"","label":"Show all pages","localized":"","hint":"Show all settings pages"}, {"id":"","label":"Request browser notifications","localized":"","hint":""} ], "img2img tabs": [ @@ -192,8 +214,7 @@ {"id":"","label":"Sketch","localized":"","hint":""}, {"id":"","label":"Inpaint","localized":"","hint":""}, {"id":"","label":"Inpaint sketch","localized":"","hint":""}, - {"id":"","label":"Inpaint upload","localized":"","hint":""}, - {"id":"","label":"Batch","localized":"","hint":""} + {"id":"","label":"Inpaint upload","localized":"","hint":""} ], "img2img tab": [ {"id":"","label":"Inpaint batch input directory","localized":"","hint":""}, @@ -207,7 +228,7 @@ {"id":"","label":"Mask transparency","localized":"","hint":""}, {"id":"","label":"Inpaint masked","localized":"","hint":""}, {"id":"","label":"Inpaint not masked","localized":"","hint":""}, - {"id":"","label":"fill","localized":"","hint":"fill it with colors of the image"}, + {"id":"","label":"fill","localized":"","hint":"fill"}, {"id":"","label":"original","localized":"","hint":"keep whatever was there originally"}, {"id":"","label":"latent noise","localized":"","hint":"fill it with latent space noise"}, {"id":"","label":"latent nothing","localized":"","hint":"fill it with latent space zeroes"}, @@ -318,20 +339,20 @@ {"id":"","label":"Original model","localized":"","hint":""} ], "settings": [ - {"id":"","label":"Stable Diffusion checkpoint","localized":"","hint":""}, - {"id":"","label":"Stable Diffusion refiner","localized":"","hint":""}, - {"id":"","label":"Stable Diffusion checkpoint autoload on server start","localized":"","hint":""}, - {"id":"","label":"stable diffusion checkpoint dict","localized":"","hint":""}, - {"id":"","label":"disallow usage of checkpoints in ckpt format","localized":"","hint":""}, + {"id":"","label":"base model","localized":"","hint":"Main model used for all operations"}, + {"id":"","label":"refiner model","localized":"","hint":"Refiner model used for second-pass operations"}, + {"id":"","label":"model autoload on server start","localized":"","hint":""}, + {"id":"","label":"use baseline data from a different model","localized":"","hint":""}, + {"id":"","label":"Disallow usage of models in ckpt format","localized":"","hint":""}, {"id":"","label":"model compile fullgraph","localized":"","hint":""}, {"id":"","label":"create zip archive when downloading multiple images","localized":"","hint":""}, {"id":"","label":"samplers solver order where applicable","localized":"","hint":""}, - {"id":"","label":"samplers should use karras sigmas where applicable","localized":"","hint":""}, - {"id":"","label":"samplers should use use lower-order solvers in the final steps where applicable","localized":"","hint":""}, - {"id":"","label":"samplers should use dynamic thresholding where applicable","localized":"","hint":""}, - {"id":"","label":"Number of cached model checkpoints","localized":"","hint":"The amount of models to store in RAM for quick access"}, - {"id":"","label":"Number of cached VAE checkpoints","localized":"","hint":"The amount of VAE files to store in RAM for quick access"}, - {"id":"","label":"Select VAE","localized":"","hint":"VAE helps with fine details in the final image and may also alter colors"}, + {"id":"","label":"samplers use karras sigmas where applicable","localized":"","hint":""}, + {"id":"","label":"samplers use simplified solvers in final steps where applicable","localized":"","hint":""}, + {"id":"","label":"samplers use dynamic thresholding where applicable","localized":"","hint":""}, + {"id":"","label":"Number of cached models","localized":"","hint":"The amount of models to store in RAM for quick access"}, + {"id":"","label":"Number of cached VAEs","localized":"","hint":"The amount of VAE files to store in RAM for quick access"}, + {"id":"","label":"VAE model","localized":"","hint":"VAE helps with fine details in the final image and may also alter colors"}, {"id":"","label":"Enable splitting of hires batch processing","localized":"","hint":"Reduces VRAM usage when using hires fix on batches of images"}, {"id":"","label":"Load models using stream loading method","localized":"","hint":"When loading models attempt stream loading optimized for slow or network storage"}, {"id":"","label":"When loading models attempt to reuse previous model dictionary","localized":"","hint":""}, @@ -355,14 +376,13 @@ {"id":"","label":"Disable conditional batching enabled on low memory systems","localized":"","hint":""}, {"id":"","label":"Enable samplers quantization for sharper and cleaner results","localized":"","hint":""}, {"id":"","label":"Prompt padding for long prompts","localized":"","hint":"Increase coherency by padding from the last comma within n tokens when using more than 75 tokens"}, - {"id":"","label":"Original","localized":"","hint":""}, - {"id":"","label":"Diffusers","localized":"","hint":""}, - {"id":"","label":"VRAM usage polls per second during generation","localized":"","hint":""}, - {"id":"","label":"Autocast","localized":"","hint":""}, - {"id":"","label":"Full","localized":"","hint":""}, - {"id":"","label":"FP32","localized":"","hint":""}, - {"id":"","label":"FP16","localized":"","hint":""}, - {"id":"","label":"BF16","localized":"","hint":""}, + {"id":"","label":"Original","localized":"","hint":"Original LDM backend"}, + {"id":"","label":"Diffusers","localized":"","hint":"Diffusers backend"}, + {"id":"","label":"Autocast","localized":"","hint":"Automatically determine precision during runtime"}, + {"id":"","label":"Full","localized":"","hint":"Always use full precision"}, + {"id":"","label":"FP32","localized":"","hint":"Use 32-bit floating point precision for calculations"}, + {"id":"","label":"FP16","localized":"","hint":"Use 16-bit floating point precision for calculations"}, + {"id":"","label":"BF16","localized":"","hint":"Use modified 16-bit floating point precision for calculations"}, {"id":"","label":"Use full precision for model (--no-half)","localized":"","hint":"Uses FP32 for the model. May produce better results while using more VRAM and slower generation"}, {"id":"","label":"Use full precision for VAE (--no-half-vae)","localized":"","hint":"Uses FP32 for the VAE. May produce better results while using more VRAM and slower generation"}, {"id":"","label":"Enable upcast sampling","localized":"","hint":"Usually produces similar results to --no-half with better performance while using less memory"}, @@ -371,9 +391,7 @@ {"id":"","label":"Attempt VAE roll back when produced NaN values (experimental)","localized":"","hint":"Requires Torch 2.1 and NaN check enabled"}, {"id":"","label":"Use channels last as torch memory format","localized":"","hint":""}, {"id":"","label":"Enable full-depth cuDNN benchmark feature","localized":"","hint":""}, - {"id":"","label":"Allow TF32 math ops","localized":"","hint":""}, - {"id":"","label":"Allow TF16 reduced precision math ops","localized":"","hint":""}, - {"id":"","label":"Enable model compile (experimental)","localized":"","hint":""}, + {"id":"","label":"Enable model compile","localized":"","hint":""}, {"id":"","label":"inductor","localized":"","hint":""}, {"id":"","label":"cudagraphs","localized":"","hint":""}, {"id":"","label":"aot_ts_nvfuser","localized":"","hint":""}, @@ -381,26 +399,25 @@ {"id":"","label":"ipex","localized":"","hint":""}, {"id":"","label":"Model compile verbose mode","localized":"","hint":""}, {"id":"","label":"Model compile suppress errors","localized":"","hint":""}, - {"id":"","label":"Disable Torch memory garbage collection","localized":"","hint":"Disable Torch memory garbage collection on each generation. CG will still run before & after model load as well when low GPU memory threshold is reached."}, {"id":"","label":"Directory for temporary images; leave empty for default","localized":"","hint":""}, {"id":"","label":"Enable IPEX Optimize for Intel GPUs","localized":"","hint":""}, {"id":"","label":"Cleanup non-default temporary directory when starting webui","localized":"","hint":""}, - {"id":"","label":"Path to directory with stable diffusion checkpoints","localized":"","hint":""}, - {"id":"","label":"Path to directory with stable diffusion diffusers","localized":"","hint":""}, - {"id":"","label":"Path to directory with VAE files","localized":"","hint":""}, - {"id":"","label":"Embeddings directory for textual inversion","localized":"","hint":""}, - {"id":"","label":"Hypernetwork directory","localized":"","hint":""}, - {"id":"","label":"Path to directory with codeformer model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with GFPGAN model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with ESRGAN model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with BSRGAN model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with RealESRGAN model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with ScuNET model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with SwinIR model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with LDSR model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with CLIP model file(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with Lora network(s)","localized":"","hint":""}, - {"id":"","label":"Path to directory with LyCORIS network(s)","localized":"","hint":""}, + {"id":"","label":"Folder with stable diffusion models","localized":"","hint":""}, + {"id":"","label":"Folder with stable diffusion diffusers","localized":"","hint":""}, + {"id":"","label":"Folder with VAE files","localized":"","hint":""}, + {"id":"","label":"Folder with textual inversion embeddings","localized":"","hint":""}, + {"id":"","label":"Folder with Hypernetwork models","localized":"","hint":""}, + {"id":"","label":"Folder with codeformer Folder","localized":"","hint":""}, + {"id":"","label":"Folder with GFPGAN models","localized":"","hint":""}, + {"id":"","label":"Folder with ESRGAN models","localized":"","hint":""}, + {"id":"","label":"Folder with BSRGAN models","localized":"","hint":""}, + {"id":"","label":"Folder with RealESRGAN models","localized":"","hint":""}, + {"id":"","label":"Folder with ScuNET models","localized":"","hint":""}, + {"id":"","label":"Folder with SwinIR models","localized":"","hint":""}, + {"id":"","label":"Folder with LDSR models","localized":"","hint":""}, + {"id":"","label":"Folder with CLIP models","localized":"","hint":""}, + {"id":"","label":"Folder with Lora networks","localized":"","hint":""}, + {"id":"","label":"Folder with LyCORIS networks","localized":"","hint":""}, {"id":"","label":"Path to user-defined styles file","localized":"","hint":""}, {"id":"","label":"Always save all generated images","localized":"","hint":""}, {"id":"","label":"File format for generated images","localized":"","hint":"Select file format for images"}, @@ -410,8 +427,6 @@ {"id":"","label":"Always save all generated image grids","localized":"","hint":""}, {"id":"","label":"File format for grids","localized":"","hint":""}, {"id":"","label":"Add extended info (seed, prompt) to filename when saving grid","localized":"","hint":""}, - {"id":"","label":"Do not save grids consisting of one picture","localized":"","hint":""}, - {"id":"","label":"Prevent empty spots in grid (when set to autodetect)","localized":"","hint":""}, {"id":"","label":"Grid row count","localized":"","hint":"Use -1 for autodetect and 0 for it to be same as batch size"}, {"id":"","label":"Create text file next to every image with generation parameters","localized":"","hint":""}, {"id":"","label":"Create JSON log file for each saved image","localized":"","hint":"Save image information to a JSON file"}, @@ -420,7 +435,7 @@ {"id":"","label":"Save copy of image before applying color correction","localized":"","hint":""}, {"id":"","label":"Save copy of the inpainting greyscale mask","localized":"","hint":""}, {"id":"","label":"Save copy of inpainting masked composite","localized":"","hint":""}, - {"id":"","label":"Save copy of processing init images","localized":"","hint":""}, + {"id":"","label":"Save copy of img2img init images","localized":"","hint":""}, {"id":"","label":"Quality for saved jpeg images","localized":"","hint":""}, {"id":"","label":"Use lossless compression for webp images","localized":"","hint":""}, {"id":"","label":"Maximum allowed image size in megapixels","localized":"","hint":""}, @@ -462,10 +477,7 @@ {"id":"","label":"Ctrl+up/down precision when editing ","localized":"","hint":""}, {"id":"","label":"Ctrl+up/down word delimiters","localized":"","hint":""}, {"id":"","label":"Quicksettings list","localized":"","hint":"List of setting names, separated by commas, for settings that should go to the quick access bar at the top instead the setting tab"}, - {"id":"","label":"Hidden UI tabs","localized":"","hint":""}, - {"id":"","label":"UI tabs order","localized":"","hint":""}, {"id":"","label":"UI scripts order","localized":"","hint":""}, - {"id":"","label":"txt2img/img2img UI item order","localized":"","hint":""}, {"id":"","label":"Show progressbar","localized":"","hint":""}, {"id":"","label":"Show live previews of the created image","localized":"","hint":""}, {"id":"","label":"Show previews of all images generated in a batch as a grid","localized":"","hint":""}, @@ -477,7 +489,7 @@ {"id":"","label":"Approximate simple","localized":"","hint":"Very cheap approximation. Very fast compared to VAE, but produces pictures with 8 times smaller horizontal/vertical resolution and extremely low quality"}, {"id":"","label":"TAESD","localized":"","hint":""}, {"id":"","label":"Combined","localized":"","hint":""}, - {"id":"","label":"Progressbar/preview update period, in milliseconds","localized":"","hint":""}, + {"id":"","label":"Progress update period","localized":"","hint":"Update period for UI progress bar and preview checks, in miliseconds"}, {"id":"","label":"Euler a","localized":"","hint":"Euler Ancestral - very creative, each can get a completely different picture depending on step count, setting steps higher than 30-40 does not help"}, {"id":"","label":"Euler","localized":"","hint":""}, {"id":"","label":"LMS","localized":"","hint":""}, @@ -499,7 +511,6 @@ {"id":"","label":"DPM++ 2M SDE Karras","localized":"","hint":""}, {"id":"","label":"DDIM","localized":"","hint":"Denoising Diffusion Implicit Models - best at inpainting"}, {"id":"","label":"UniPC","localized":"","hint":"Unified Predictor-Corrector Framework for Fast Sampling of Diffusion Models"}, - {"id":"","label":"Force latent upscaler sampler","localized":"","hint":"Force specific sampler for second pass operations"}, {"id":"","label":"Noise multiplier for ancestral samplers (eta)","localized":"","hint":""}, {"id":"","label":"Noise multiplier for DDIM (eta)","localized":"","hint":""}, {"id":"","label":"uniform","localized":"","hint":""}, @@ -553,7 +564,6 @@ {"id":"","label":"Tile overlap in pixels for ESRGAN upscalers","localized":"","hint":"Low values = visible seam"}, {"id":"","label":"Tile size for SCUNET upscalers","localized":"","hint":"0 = no tiling"}, {"id":"","label":"Tile overlap for SCUNET upscalers","localized":"","hint":" Low values = visible seam"}, - {"id":"","label":"Hires fix uses width & height to set final resolution","localized":"","hint":"Hires fix uses width & height to set final resolution rather than first pass"}, {"id":"","label":"Do not fix prompt schedule for second order samplers","localized":"","hint":""}, {"id":"","label":"CodeFormer","localized":"","hint":""}, {"id":"","label":"GFPGAN","localized":"","hint":"Restore low quality faces using GFPGAN neural network"}, @@ -562,30 +572,45 @@ {"id":"","label":"Token merging ratio","localized":"","hint":"Enable redundant token merging via tomesd for speed and memory improvements, 0=disabled"}, {"id":"","label":"Token merging ratio for img2img","localized":"","hint":"Enable redundant token merging for img2img via tomesd for speed and memory improvements, 0=disabled"}, {"id":"","label":"Token merging ratio for hires pass","localized":"","hint":"Enable redundant token merging for hires pass via tomesd for speed and memory improvements, 0=disabled"}, - {"id":"","label":"Select diffuser pipeline when loading from safetensors","localized":"","hint":""}, + {"id":"","label":"Diffusers pipeline","localized":"","hint":"If autodetect does not detect model automatically, select model type before loading a model"}, {"id":"","label":"Move base model to CPU when using refiner","localized":"","hint":""}, + {"id":"","label":"Move base model to CPU when using VAE","localized":"","hint":""}, {"id":"","label":"Move refiner model to CPU when not in use","localized":"","hint":""}, - {"id":"","label":"Move UNet to CPU while VAE decoding","localized":"","hint":""}, {"id":"","label":"Use model EMA weights when possible","localized":"","hint":""}, {"id":"","label":"Generator device","localized":"","hint":""}, - {"id":"","label":"Enable sequential CPU offload","localized":"","hint":"Reduces GPU memory usage by transferring weights to the CPU. Increases inference time approximately 10%"}, - {"id":"","label":"Enable model CPU offload","localized":"","hint":"Transferring of entire models to the CPU, negligible impact on inference time while still providing some memory savings"}, + {"id":"","label":"Enable sequential CPU offload (--lowvram)","localized":"","hint":"Reduces GPU memory usage by transferring weights to the CPU. Increases inference time approximately 10%"}, + {"id":"","label":"Enable model CPU offload (--medvram)","localized":"","hint":"Transferring of entire models to the CPU, negligible impact on inference time while still providing some memory savings"}, {"id":"","label":"Enable VAE slicing","localized":"","hint":"Decodes batch latents one image at a time with limited VRAM. Small performance boost in VAE decode on multi-image batches"}, {"id":"","label":"Enable VAE tiling","localized":"","hint":"Divide large images into overlapping tiles with limited VRAM. Results in a minor increase in processing time"}, {"id":"","label":"Enable attention slicing","localized":"","hint":"Performs attention computation in steps instead of all at once. Slower inference times, but greatly reduced memory usage"}, {"id":"","label":"Diffusers model loading variant","localized":"","hint":""}, {"id":"","label":"Diffusers VAE loading variant","localized":"","hint":""}, - {"id":"","label":"Diffusers LoRA loading variant","localized":"","hint":"'sequential apply' loads and applies each LoRA in order of appearance, 'merge and apply' loads all LoRAs and merges them in-memory before applying to model, 'diffusers default' uses single LoRA loading method"} + {"id":"","label":"Diffusers LoRA loading variant","localized":"","hint":"'sequential apply' loads and applies each LoRA in order of appearance, 'merge and apply' loads all LoRAs and merges them in-memory before applying to model, 'diffusers default' uses single LoRA loading method"}, + {"id":"","label":"Torch inference mode","localized":"","hint":"Use torch inference mode"}, + {"id":"","label":"inference-mode","localized":"","hint":"Use torch.inference_mode"}, + {"id":"","label":"no-grad","localized":"","hint":"Use torch.no_grad"}, + {"id":"","label":"vae slicing (original)","localized":"","hint":"Run VAE on sliced samples to reduce memory requirements when processing high resolution images"}, + {"id":"","label":"use fixed unet precision","localized":"","hint":""}, + {"id":"","label":"enable model compile","localized":"","hint":"Enable usage of torch.compile"}, + {"id":"","label":"reduce-overhead","localized":"","hint":""}, + {"id":"","label":"max-autotune","localized":"","hint":""}, + {"id":"","label":"model compile precompile","localized":"","hint":"Run model compile immediately on model load instead of first use"}, + {"id":"","label":"sequential apply","localized":"","hint":"When loading multiple LoRAs, apply each in order of loading"}, + {"id":"","label":"merge and apply","localized":"","hint":"When loading multiple LoRAs, load all and merge them before applying to model"}, + {"id":"","label":"force zeros for prompts when empty","localized":"","hint":"Force full zero tensor when prompt is empty to remove any residual noise"}, + {"id":"","label":"require aesthetics score","localized":"","hint":"Automatically guide model towards higher-pleasing results, applicable only to refiner model"}, + {"id":"","label":"include watermark in saved images","localized":"","hint":"Add invisible watermark to image by altering some pixel values"}, + {"id":"","label":"image watermark string","localized":"","hint":"Watermark string to add to image. Keep very short to avoid image corruption."}, + {"id":"","label":"show log view","localized":"","hint":"Show log view at the bottom of the main window"}, + {"id":"","label":"Log view update period","localized":"","hint":"Log view update period, in miliseconds"} ], "scripts": [ - {"id":"","label":"Script","localized":"","hint":""}, {"id":"","label":"Swap X/Y","localized":"","hint":""}, {"id":"","label":"Swap Y/Z","localized":"","hint":""}, {"id":"","label":"Swap X/Z","localized":"","hint":""}, {"id":"","label":"Resize to","localized":"","hint":""}, {"id":"","label":"Resize by","localized":"","hint":""}, {"id":"","label":"Use via API","localized":"","hint":""}, - {"id":"","label":"Styles","localized":"","hint":""}, {"id":"","label":"Put variable parts at start of prompt","localized":"","hint":""}, {"id":"","label":"Use different seed for each picture","localized":"","hint":""}, {"id":"","label":"positive","localized":"","hint":""}, diff --git a/html/locale_ko.json b/html/locale_ko.json index ca3653222..c36545816 100644 --- a/html/locale_ko.json +++ b/html/locale_ko.json @@ -518,7 +518,7 @@ {"id":"","label":"logSNR","localized":"","hint":""}, {"id":"","label":"UniPC order (must be < sampling steps)","localized":"","hint":""}, {"id":"","label":"UniPC lower order final","localized":"","hint":""}, - {"id":"","label":"Enable addtional postprocessing operations","localized":"추가 후처리 작업","hint":""}, + {"id":"","label":"Enable additional postprocessing operations","localized":"추가 후처리 작업","hint":""}, {"id":"","label":"Postprocessing operation order","localized":"후처리 작업 순서","hint":""}, {"id":"","label":"Maximum number of images in upscaling cache","localized":"","hint":""}, {"id":"","label":"Move VAE and CLIP to RAM when training if possible","localized":"가능하다면 학습 시 VAE와 CLIP 모델을 램으로 이동","hint":""}, diff --git a/installer.py b/installer.py index 435d2a654..1f853cc24 100644 --- a/installer.py +++ b/installer.py @@ -108,6 +108,22 @@ def setup_logging(): # logging.getLogger("DeepSpeed").handlers = log.handlers +def custom_excepthook(exc_type, exc_value, exc_traceback): + import traceback + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + log.error(f"Uncaught exception occurred: type={exc_type} value={exc_value}") + if exc_traceback: + format_exception = traceback.format_tb(exc_traceback) + for line in format_exception: + log.error(repr(line)) + + +def print_dict(d): + return ' '.join([f'{k}={v}' for k, v in d.items()]) + + def print_profile(profile: cProfile.Profile, msg: str): try: from rich import print # pylint: disable=redefined-builtin @@ -265,6 +281,26 @@ def clone(url, folder, commithash=None): git(f'-C "{folder}" checkout {commithash}') +def get_platform(): + try: + if platform.system() == 'Windows': + release = platform.platform(aliased = True, terse = False) + else: + release = platform.release() + return { + # 'host': platform.node(), + 'arch': platform.machine(), + 'cpu': platform.processor(), + 'system': platform.system(), + 'release': release, + # 'platform': platform.platform(aliased = True, terse = False), + # 'version': platform.version(), + 'python': platform.python_version(), + } + except Exception as e: + return { 'error': e } + + # check python version def check_python(): supported_minors = [9, 10, 11] @@ -580,7 +616,7 @@ def list_extensions_folder(folder, quiet=False): disabled_extensions = opts.get('disabled_extensions', []) enabled_extensions = [x for x in os.listdir(folder) if x not in disabled_extensions and not x.startswith('.')] if not quiet: - log.info(f'Enabled {name}: {enabled_extensions}') + log.info(f'Extensions: enabled={enabled_extensions} {name}') return enabled_extensions @@ -713,9 +749,9 @@ def check_extensions(): extension_folders = [extensions_builtin_dir] if args.safe else [extensions_builtin_dir, extensions_dir] disabled_extensions_all = opts.get('disable_all_extensions', 'none') if disabled_extensions_all != 'none': - log.info(f'Disabled extensions: {disabled_extensions_all}') + log.info(f'Extensions: disabled={disabled_extensions_all}') else: - log.info(f'Disabled extensions: {opts.get("disabled_extensions", [])}') + log.info(f'Extensions: disabled={opts.get("disabled_extensions", [])}') for folder in extension_folders: if not os.path.isdir(folder): continue @@ -736,6 +772,28 @@ def check_extensions(): return round(newest_all) +def get_version(): + version = None + if version is None: + try: + res = subprocess.run('git log --pretty=format:"%h %ad" -1 --date=short', stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=True, check=True) + ver = res.stdout.decode(encoding = 'utf8', errors='ignore') if len(res.stdout) > 0 else ' ' + githash, updated = ver.split(' ') + res = subprocess.run('git remote get-url origin', stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=True, check=True) + origin = res.stdout.decode(encoding = 'utf8', errors='ignore') if len(res.stdout) > 0 else '' + res = subprocess.run('git rev-parse --abbrev-ref HEAD', stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell=True, check=True) + branch_name = res.stdout.decode(encoding = 'utf8', errors='ignore') if len(res.stdout) > 0 else '' + version = { + 'app': 'sd.next', + 'updated': updated, + 'hash': githash, + 'url': origin.replace('\n', '') + '/tree/' + branch_name.replace('\n', '') + } + except Exception: + version = { 'app': 'sd.next', 'version': 'unknown' } + return version + + # check version of the main repo and optionally upgrade it def check_version(offline=False, reset=True): # pylint: disable=unused-argument if args.skip_all: @@ -744,8 +802,7 @@ def check_version(offline=False, reset=True): # pylint: disable=unused-argument log.error('Not a git repository') if not args.ignore: sys.exit(1) - ver = git('log -1 --pretty=format:"%h %ad"') - log.info(f'Version: {ver}') + log.info(f'Version: {print_dict(get_version())}') if args.version or args.skip_git: return commit = git('rev-parse HEAD') @@ -873,11 +930,13 @@ def extensions_preload(parser): from modules.script_loading import preload_extensions from modules.paths_internal import extensions_builtin_dir, extensions_dir extension_folders = [extensions_builtin_dir] if args.safe else [extensions_builtin_dir, extensions_dir] + preload_time = {} for ext_dir in extension_folders: t0 = time.time() preload_extensions(ext_dir, parser) t1 = time.time() - log.info(f'Extension preload: {round(t1 - t0, 1)}s {ext_dir}') + preload_time[ext_dir] = round(t1 - t0, 2) + log.info(f'Extension preload: {preload_time}') except Exception: log.error('Error running extension preloading') if args.profile: diff --git a/javascript/amethyst-nightfall.css b/javascript/amethyst-nightfall.css index ad370bb4d..265112d13 100644 --- a/javascript/amethyst-nightfall.css +++ b/javascript/amethyst-nightfall.css @@ -95,7 +95,6 @@ svg.feather.feather-image, .feather .feather-image { display: none } #img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; } #interrogate, #deepbooru { margin: 0 0px 10px 0px; max-width: 80px; max-height: 80px; font-weight: normal; font-size: 0.95em; } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } -#quicksettings > div, #quicksettings > fieldset { min-width: 24em; max-width: 26em; line-height: 1.6em; margin-top: 0.4em; } #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h { display: none; } #save-animation { border-radius: var(--radius-sm) !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } diff --git a/javascript/black-orange.css b/javascript/black-orange.css index d98a42141..39406d41c 100644 --- a/javascript/black-orange.css +++ b/javascript/black-orange.css @@ -95,7 +95,6 @@ svg.feather.feather-image, .feather .feather-image { display: none } #img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; } #interrogate, #deepbooru { margin: 0 0px 10px 0px; max-width: 80px; max-height: 80px; font-weight: normal; font-size: 0.95em; } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } -#quicksettings > div, #quicksettings > fieldset { line-height: 1.4em; margin-top: 0.4em; } #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h { display: none; } #save-animation { border-radius: var(--radius-sm) !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } diff --git a/javascript/black-teal.css b/javascript/black-teal.css index 207e5f2f8..9bbcc4276 100644 --- a/javascript/black-teal.css +++ b/javascript/black-teal.css @@ -77,13 +77,14 @@ svg.feather.feather-image, .feather .feather-image { display: none } .py-6 { padding-bottom: 0; } .tabs { background-color: var(--background-color); } .block.token-counter span { background-color: var(--input-background-fill) !important; box-shadow: 2px 2px 2px #111; border: none !important; font-size: 0.8rem; } -.tab-nav { zoom: 120%; margin-bottom: 10px; border-bottom: 2px solid var(--highlight-color) !important; padding-bottom: 2px; } +.tab-nav { zoom: 120%; margin-top: 10px; margin-bottom: 10px; border-bottom: 2px solid var(--highlight-color) !important; padding-bottom: 2px; } .label-wrap { margin: 16px 0px 8px 0px; } .gradio-slider input[type="number"] { width: 4.5em; font-size: 0.8rem; height: 20px; } .gradio-button.tool { border: none; background: none; box-shadow: none; filter: hue-rotate(340deg) saturate(0.5); } -#tab_extensions table td, #tab_extensions table th { border: none; padding: 0.5em; } -#tab_extensions table { width: 96vw } -#tab_extensions table thead { background-color: var(--neutral-700); } +#tab_extensions table td, #tab_extensions table th, #tab_config table td, #tab_config table th { border: none; padding: 0.5em; } +#tab_extensions table, #tab_config table { width: 96vw } +#tab_extensions table thead, #tab_config table thead { background-color: var(--neutral-700); } +#tab_extensions table, #tab_config table { background-color: #222222; } /* automatic style classes */ .progressDiv { border-radius: var(--radius-sm) !important; position: fixed; top: 44px; right: 26px; max-width: 262px; height: 48px; z-index: 99; box-shadow: var(--button-shadow); } @@ -93,6 +94,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } .extra-networks { border-left: 2px solid var(--highlight-color) !important; padding-left: 4px; } .image-buttons { gap: 10px !important; justify-content: center; } .image-buttons > button { max-width: 160px; } +.tooltip { background: var(--primary-300); color: black; border: none; border-radius: var(--radius-lg) } #system_row > button, #settings_row > button, #config_row > button { max-width: 190px; } /* gradio elements overrides */ @@ -103,12 +105,10 @@ svg.feather.feather-image, .feather .feather-image { display: none } #img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; } #interrogate, #deepbooru { margin: 0 0px 10px 0px; max-width: 80px; max-height: 80px; font-weight: normal; font-size: 0.95em; } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } -#quicksettings > div, #quicksettings > fieldset { line-height: 1.4em; margin-top: 0.4em; } #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h { display: none; } #save-animation { border-radius: var(--radius-sm) !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } #settings > div.flex-wrap { width: 15em; } -#tab_extensions table { background-color: #222222; } #txt2img_cfg_scale { min-width: 200px; } #txt2img_checkboxes, #img2img_checkboxes { background-color: transparent; } #txt2img_checkboxes, #img2img_checkboxes { margin-bottom: 0.2em; } diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 4d833d8f9..b59129103 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -153,24 +153,35 @@ function setupExtraNetworksForTab(tabname) { tabs.appendChild(div); div.appendChild(search); div.appendChild(description); + let searchTimer = null; search.addEventListener('input', (evt) => { - const searchTerm = search.value.toLowerCase(); - gradioApp().querySelectorAll(`#${tabname}_extra_tabs div.card`).forEach((elem) => { - let text = `${elem.querySelector('.name').textContent.toLowerCase()} ${elem.querySelector('.search_term').textContent.toLowerCase()}`; - text = text.replace('models--', 'Diffusers'); - elem.style.display = text.indexOf(searchTerm) === -1 ? 'none' : ''; - }); + if (searchTimer) clearTimeout(searchTimer); + searchTimer = setTimeout(() => { + const searchTerm = search.value.toLowerCase(); + gradioApp().querySelectorAll(`#${tabname}_extra_tabs div.card`).forEach((elem) => { + let text = `${elem.querySelector('.name').textContent.toLowerCase()} ${elem.querySelector('.search_term').textContent.toLowerCase()}`; + text = text.replace('models--', 'Diffusers'); + elem.style.display = text.indexOf(searchTerm) === -1 ? 'none' : ''; + }); + searchTimer = null; + }, 100); }); + let hoverTimer = null; gradioApp().getElementById(`${tabname}_extra_tabs`).onmouseover = (e) => { - const el = e?.target?.parentElement; - if (!el?.classList?.contains('card')) return; - if (el.title === previousCard) return; - readCardDescription(el.dataset.filename, el.dataset.description); - readCardTags(el, el.dataset.tags); - e.stopPropagation(); - e.preventDefault(); - previousCard = el.title; + const el = e.target.closest('.card'); // bubble-up to card + if (!el || (el.title === previousCard)) return; + if (!hoverTimer) { + hoverTimer = setTimeout(() => { + readCardDescription(el.dataset.filename, el.dataset.description); + readCardTags(el, el.dataset.tags); + previousCard = el.title; + }, 300); + } + el.onmouseout = () => { + clearTimeout(hoverTimer); + hoverTimer = null; + }; }; const intersectionObserver = new IntersectionObserver((entries) => { @@ -278,7 +289,7 @@ function extraNetworksSearchButton(event) { updateInput(searchTextarea); } -function extraNetworksRefreshButton() { +function getENActivePage() { const tabname = getENActiveTab(); const page = gradioApp().querySelector(`#${tabname}_extra_networks > .tabs > .tab-nav > .selected`); return page ? page.innerText : ''; diff --git a/javascript/light-teal.css b/javascript/light-teal.css index a7f285951..2e2779b47 100644 --- a/javascript/light-teal.css +++ b/javascript/light-teal.css @@ -77,7 +77,7 @@ svg.feather.feather-image, .feather .feather-image { display: none } .py-6 { padding-bottom: 0; } .tabs { background-color: var(--background-color); } .block.token-counter span { background-color: var(--input-background-fill) !important; box-shadow: 2px 2px 2px #111; border: none !important; font-size: 0.8rem; } -.tab-nav { zoom: 120%; margin-bottom: 10px; border-bottom: 2px solid var(--highlight-color) !important; padding-bottom: 2px; } +.tab-nav { zoom: 120%; margin-top: 10px; margin-bottom: 10px; border-bottom: 2px solid var(--highlight-color) !important; padding-bottom: 2px; } .label-wrap { margin: 16px 0px 8px 0px; } .gradio-slider input[type="number"] { width: 4.5em; font-size: 0.8rem; height: 20px; } .gradio-button.tool { border: none; background: none; box-shadow: none; filter: hue-rotate(340deg) saturate(0.5); } @@ -103,7 +103,6 @@ svg.feather.feather-image, .feather .feather-image { display: none } #img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; } #interrogate, #deepbooru { margin: 0 0px 10px 0px; max-width: 80px; max-height: 80px; font-weight: normal; font-size: 0.95em; } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } -#quicksettings > div, #quicksettings > fieldset { line-height: 1.4em; margin-top: 0.4em; } #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h { display: none; } #save-animation { border-radius: var(--radius-sm) !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } diff --git a/javascript/midnight-barbie.css b/javascript/midnight-barbie.css index b2c7130a9..a52980c27 100644 --- a/javascript/midnight-barbie.css +++ b/javascript/midnight-barbie.css @@ -95,7 +95,6 @@ svg.feather.feather-image, .feather .feather-image { display: none } #img2img_settings { min-width: calc(2 * var(--left-column)); max-width: calc(2 * var(--left-column)); background-color: #111111; padding-top: 16px; } #interrogate, #deepbooru { margin: 0 0px 10px 0px; max-width: 80px; max-height: 80px; font-weight: normal; font-size: 0.95em; } #quicksettings .gr-button-tool { font-size: 1.6rem; box-shadow: none; margin-left: -20px; margin-top: -2px; height: 2.4em; } -#quicksettings > div, #quicksettings > fieldset { line-height: 1.4em; margin-top: 0.4em; } #open_folder_extras, #footer, #style_pos_col, #style_neg_col, #roll_col, #extras_upscaler_2, #extras_upscaler_2_visibility, #txt2img_seed_resize_from_w, #txt2img_seed_resize_from_h { display: none; } #save-animation { border-radius: var(--radius-sm) !important; margin-bottom: 16px; background-color: #111111; } #script_list { padding: 4px; margin-top: 20px; margin-bottom: 20px; } diff --git a/javascript/promptBracketChecker.js b/javascript/promptChecker.js similarity index 83% rename from javascript/promptBracketChecker.js rename to javascript/promptChecker.js index f5aa1f79a..d18a030ec 100644 --- a/javascript/promptBracketChecker.js +++ b/javascript/promptChecker.js @@ -3,15 +3,17 @@ // Counts open and closed brackets (round, square, curly) in the prompt and negative prompt text boxes in the txt2img and img2img tabs. // If there's a mismatch, the keyword counter turns red and if you hover on it, a tooltip tells you what's wrong. +let promptCheckerInitialized = false; + function checkBrackets(textArea, counterElt) { const counts = {}; - (textArea.value.match(/[(){}[\]]/g) || []).forEach((bracket) => { counts[bracket] = (counts[bracket] || 0) + 1; }); const errors = []; function checkPair(open, close, kind) { if (counts[open] !== counts[close]) errors.push(`${open}...${close} - Detected ${counts[open] || 0} opening and ${counts[close] || 0} closing ${kind}.`); } + (textArea.value.match(/[(){}[\]]/g) || []).forEach((bracket) => { counts[bracket] = (counts[bracket] || 0) + 1; }); checkPair('(', ')', 'round brackets'); checkPair('[', ']', 'square brackets'); checkPair('{', '}', 'curly brackets'); @@ -22,10 +24,14 @@ function checkBrackets(textArea, counterElt) { function setupBracketChecking(idPrompt, idCounter) { const textarea = gradioApp().querySelector(`#${idPrompt} > label > textarea`); const counter = gradioApp().getElementById(idCounter); - if (textarea && counter) textarea.addEventListener('input', () => checkBrackets(textarea, counter)); + if (!textarea || !counter) return; + if (!promptCheckerInitialized) log('initPromptChecker'); + promptCheckerInitialized = true; + textarea.addEventListener('input', () => checkBrackets(textarea, counter)); } onAfterUiUpdate(() => { + if (promptCheckerInitialized) return; setupBracketChecking('txt2img_prompt', 'txt2img_token_counter'); setupBracketChecking('txt2img_neg_prompt', 'txt2img_negative_token_counter'); setupBracketChecking('img2img_prompt', 'img2img_token_counter'); diff --git a/javascript/setHints.js b/javascript/setHints.js index da98175ca..7398c7a5e 100644 --- a/javascript/setHints.js +++ b/javascript/setHints.js @@ -52,6 +52,7 @@ async function setHints() { const res = await fetch('/file=html/locale_en.json'); const json = await res.json(); localeData.data = Object.values(json).flat(); + for (const e of localeData.data) e.label = e.label.toLowerCase().trim(); } const elements = [ ...Array.from(gradioApp().querySelectorAll('button')), @@ -65,7 +66,7 @@ async function setHints() { localeData.finished = true; const t0 = performance.now(); for (const el of elements) { - const found = localeData.data.find((l) => l.label === el.textContent.trim()); + const found = localeData.data.find((l) => l.label === el.textContent.toLowerCase().trim()); if (found?.localized?.length > 0) { localized++; el.textContent = found.localized; @@ -87,7 +88,7 @@ async function setHints() { log('setHints', { type: localeData.type, elements: elements.length, localized, hints, data: localeData.data.length, time: t1 - t0 }); // sortUIElements(); removeSplash(); - // validateHints(elements, localeData.data) + // validateHints(elements, localeData.data); } onAfterUiUpdate(async () => { diff --git a/javascript/style.css b/javascript/style.css index 381598f15..e06d61642 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -6,7 +6,7 @@ div.tabitem { padding: 0 !important; } div.form { border-width: 0; box-shadow: none; background: transparent; overflow: visible; gap: 0.5em 1em; flex-grow: 1 !important; } div.compact{ gap: 1em; } div.gradio-html.min{ min-height: 0; } -.block.gradio-checkbox { margin: 0.75em 1.5em 0 0; } +.block.gradio-checkbox { margin: 0.75em 1.5em 0 0; align-self: center; } .block.gradio-dropdown, .block.gradio-slider, .block.gradio-checkbox, .block.gradio-textbox, .block.gradio-radio, .block.gradio-checkboxgroup, .block.gradio-number, .block.gradio-colorpicker { border-width: 0 !important; box-shadow: none !important;} .block.padded:not(.gradio-accordion) { padding: 0 !important; margin-right: 0; min-width: 100px !important; } .compact{ background: transparent !important; padding: 0 !important; } @@ -19,21 +19,22 @@ div.gradio-html.min{ min-height: 0; } .gradio-dropdown label span:not(.has-info), .gradio-textbox label span:not(.has-info), .gradio-number label span:not(.has-info) { margin-bottom: 0; } .gradio-dropdown ul.options li.item { padding: 0.05em 0; } .gradio-dropdown ul.options li.item:not(:has(.hide)) { background-color: var(--neutral-100); } -.gradio-dropdown ul.options{ z-index: 3000; min-width: fit-content; max-width: inherit; white-space: nowrap; } +.gradio-dropdown ul.options { z-index: 3000; min-width: fit-content; max-height: 25vh !important; white-space: nowrap; } .gradio-dropdown:not(.multiselect) .wrap-inner.wrap-inner.wrap-inner{ flex-wrap: unset; } .gradio-dropdown.multiselect .token-remove.remove-all.remove-all{ display: flex; } .gradio-dropdown.multiselect div.wrap-inner { overflow-x: hidden; overflow-y: auto; max-height: 50vh; overflow-wrap: anywhere; } .gradio-html div.wrap{ height: 100%; } .gradio-slider input[type="number"]{ width: 6em; margin-left: 0.5em; } +.gradio-accordion { padding-top: var(--spacing-md) !important; padding-right: 0 !important; padding-bottom: 0 !important; color: var(--body-text-color); } .hidden { display: none; } footer { display: none; } td { border-bottom: none !important; } /* general styled components */ -.gradio-button.tool{ max-width: 1em; min-width: 1em !important; align-self: end; font-size: 1.4em } -.gradio-button.secondary-down{ background: var(--button-secondary-background-fill); color: var(--button-secondary-text-color); } -.gradio-button.secondary-down, .gradio-button.secondary-down:hover{ box-shadow: 1px 1px 1px rgba(0,0,0,0.25) inset, 0px 0px 3px rgba(0,0,0,0.15) inset; } -.gradio-button.secondary-down:hover{ background: var(--button-secondary-background-fill-hover); color: var(--button-secondary-text-color-hover); } +.gradio-button.tool { max-width: min-content; min-width: min-content !important; align-self: end; font-size: 1.4em; color: var(--body-text-color) !important; } +.gradio-button.secondary-down { background: var(--button-secondary-background-fill); color: var(--button-secondary-text-color); } +.gradio-button.secondary-down, .gradio-button.secondary-down:hover { box-shadow: 1px 1px 1px rgba(0,0,0,0.25) inset, 0px 0px 3px rgba(0,0,0,0.15) inset; } +.gradio-button.secondary-down:hover { background: var(--button-secondary-background-fill-hover); color: var(--button-secondary-text-color-hover); } .checkboxes-row { margin-bottom: 1em; gap: 0 !important; justify-content: space-around; flex-wrap: unset !important; } .checkboxes-row > div{ flex: 0; white-space: nowrap; min-width: auto; } @@ -75,7 +76,7 @@ button.custom-button{ #txt2img_generate_line2, #img2img_generate_line2 { display: flex; } #txt2img_generate_line2 > button, #img2img_generate_line2 > button, #extras_generate_box > button { height: 2.2em; line-height: 0; min-width: unset; display: block !important; } #txt2img_tools > div, #img2img_tools > div { justify-content: space-around; margin-top: 0.5em; margin-bottom: 0em; } -#txt2img_tools > div > button, #img2img_tools > div > button { scale: 120%; } +#txt2img_tools > div > button, #img2img_tools > div > button { scale: 120%; min-width: 1em !important; min-height: 1em !important; } #txt2img_prompt, #txt2img_neg_prompt, #img2img_prompt, #img2img_neg_prompt { display: contents; } .interrogate-col{ min-width: 0 !important; max-width: fit-content; gap: 0.5em; } .interrogate-col > button{ flex: 1; } @@ -115,12 +116,11 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } /* settings */ #si-sparkline-memo, #si-sparkline-load { background-color: #111; } .licenses { display: block !important; } -#quicksettings { width: fit-content; align-items: end; } -#quicksettings > div, #quicksettings > fieldset{ max-width: 20em; min-width: 24em; padding: 0; border: none; box-shadow: none; background: none; } -#quicksettings > button { margin-left: -0.5em; } +#quicksettings { width: fit-content; margin-top: 1em; } +#quicksettings > button { padding: 0 1em 0 0 } #settings { display: flex; gap: var(--layout-gap); } -#settings div { border: none; justify-content: normal; gap: 0.5em; } +#settings div { border: none; gap: 0.5em; } #settings > div.tab-content { flex: 10 0 75%; display: grid; } #settings > div.tab-content > div { border: none; padding: 0; } @@ -142,7 +142,7 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } .progressDiv{ position: relative; height: 20px; background: #b4c0cc; margin-bottom: -3px; } .dark .progressDiv{ background: #424c5b; } .progressDiv .progress{ width: 0%; height: 20px; background: #0060df; color: white; font-weight: bold; line-height: 20px; padding: 0 8px 0 0; text-align: right; overflow: visible; white-space: nowrap; padding: 0 0.5em; } -.livePreview { position: absolute; z-index: 300; background-color: transparent; width: -webkit-fill-available; } +.livePreview { position: absolute; z-index: 300; background-color: transparent; width: -moz-available; width: -webkit-fill-available; } .livePreview img { position: absolute; object-fit: contain; width: 100%; height: 100%; } .dark .livePreview { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } .popup-metadata { color: white; background: #0000; display: inline-block; white-space: pre-wrap; font-size: 0.75em; } @@ -150,7 +150,6 @@ div#extras_scale_to_tab div.form{ flex-direction: row; } .global-popup-close:before { content: "×"; } .global-popup-close{ position: fixed; right: 0.5em; top: 0; cursor: pointer; color: white; font-size: 32pt; } .global-popup-inner{ display: inline-block; margin: auto; padding: 2em; } -.ui-defaults-none{ color: #aaa !important; } /* fullpage image viewer */ @@ -211,8 +210,8 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt .context-menu-items a:hover { background: #a55000; } /* extensions */ -#tab_extensions table{ border-collapse: collapse; } -#tab_extensions table td, #tab_extensions table th { border: 1px solid #ccc; padding: 0.25em 0.5em; } +#tab_extensions table, #tab_config table{ border-collapse: collapse; } +#tab_extensions table td, #tab_extensions table th, #tab_config table td, #tab_config table th { border: 1px solid #ccc; padding: 0.25em 0.5em; } #tab_extensions table input[type="checkbox"] { margin-right: 0.5em; appearance: checkbox; } #tab_extensions button{ max-width: 16em; } #tab_extensions input[disabled="disabled"]{ opacity: 0.5; } @@ -226,20 +225,20 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt /* extra networks */ .extra-networks > div { margin: 0; gap: 0.2em; border-bottom: none !important; } -.extra-networks .second-line { display: flex; width: -webkit-fill-available; gap: 0.3em; box-shadow: var(--input-shadow); } +.extra-networks .second-line { display: flex; width: -moz-available; width: -webkit-fill-available; gap: 0.3em; box-shadow: var(--input-shadow); } .extra-networks .search { flex: 1; } .extra-networks .description { flex: 3; } .extra-networks .tab-nav > button { margin-right: 0; height: 24px; padding: 2px 4px 2px 4px; } .extra-networks-tab { padding: 0 !important; } -.extra-network-subdirs { background: var(--input-background-fill); overflow-x: hidden; overflow-y: auto; min-width: 120px; padding-top: 0.5em; } +.extra-network-subdirs { background: var(--input-background-fill); overflow-x: hidden; overflow-y: auto; min-width: max(20%, 120px); padding-top: 0.5em; } .extra-networks-page { display: flex } .extra-networks .custom-button { width: 120px; width: 100%; background: none; justify-content: left; text-align: left; padding: 2px 8px 2px 16px; text-indent: -8px; box-shadow: none; line-break: auto; } .extra-networks .custom-button:hover { background: var(--button-primary-background-fill) } -.extra-network-cards { display: flex; flex-wrap: wrap; overflow-y: auto; overflow-x: hidden; align-content: flex-start; width: -webkit-fill-available; } +.extra-network-cards { display: flex; flex-wrap: wrap; overflow-y: auto; overflow-x: hidden; align-content: flex-start; width: -moz-available; width: -webkit-fill-available; } .extra-network-cards .card { height: fit-content; margin: 0 0 0.5em 0.5em; position: relative; scroll-snap-align: start; scroll-margin-top: 0; } .extra-network-cards .card .overlay { position: absolute; bottom: 0; padding: 0.2em; z-index: 10; width: 100%; background: none; } .extra-network-cards .card:hover .overlay { background: rgba(0, 0, 0, 0.40); } -.extra-network-cards .card .overlay .name { font-size: 1.1em; font-weight: bold; text-shadow: 1px 1px black; color: white; } +.extra-network-cards .card .overlay .name { font-size: 1.1em; font-weight: bold; text-shadow: 1px 1px black; color: white; overflow-wrap: break-word; } .extra-network-cards .card .overlay .tags { margin: 4px; display: none; overflow-wrap: break-word; } .extra-network-cards .card .overlay .tag { padding: 2px; margin: 2px; background: var(--neutral-700); cursor: pointer; display: inline-block; } .extra-network-cards .card .overlay .actions { font-size: 2.2em; display: none; text-align-last: center; cursor: pointer; font-variant: unicase; height: 0.8em } @@ -267,7 +266,11 @@ div.controlnet_main_options { display: grid; grid-template-columns: 1fr 1fr; gri /* specific elements */ #modelmerger_interp_description { margin-top: 1em; margin-bottom: 1em; } -#scripts_alwayson_txt2img, #scripts_alwayson_img2img { display: grid } +#scripts_alwayson_txt2img, #scripts_alwayson_img2img { display: grid; padding: 0 } +#scripts_alwayson_txt2img > .label-wrap, #scripts_alwayson_img2img > .label-wrap { background: var(--input-background-fill); padding: 0; margin: 0; border-radius: var(--radius-lg); } +#scripts_alwayson_txt2img > .label-wrap > span, #scripts_alwayson_img2img > .label-wrap > span { padding: var(--spacing-xxl); } +#script_txt2img_agent_scheduler { display: none; } + #extras_generate, #extras_interrupt, #extras_skip { display: block !important; position: relative; height: 36px; } #extras_upscale { margin-top: 10px } #refresh_tac_refreshTempFiles { display: none; } @@ -278,6 +281,9 @@ div.controlnet_main_options { display: grid; grid-template-columns: 1fr 1fr; gri .log-monitor { display: none; justify-content: unset !important; overflow: hidden; padding: 0; margin-top: auto; font-family: monospace; font-size: 0.85em; } .log-monitor td, .log-monitor th { padding-left: 1em; } +/* custom component */ +.folder-selector textarea { height: 2em !important; padding: 6px !important; } + /* Workaround for Gradio dropdowns capturing clicks during and after fadeout */ .gradio-dropdown > label > div > div:first-child:not(.showOptions) ~ ul.options { pointer-events: none; } diff --git a/javascript/ui.js b/javascript/ui.js index 001d4df30..232bb5eb1 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -361,19 +361,27 @@ function create_theme_element() { return el; } -async function preview_theme() { +function previewTheme() { let name = gradioApp().getElementById('setting_gradio_theme').querySelectorAll('input')?.[0].value || ''; - const res = await fetch('/file=html/themes.json'); - const themes = await res.json(); - const theme = themes.find((t) => t.id === name); - if (theme) { - window.open(theme.subdomain, '_blank'); - } else { - const el = document.getElementById('theme-preview') || create_theme_element(); - el.style.display = el.style.display === 'block' ? 'none' : 'block'; - name = name.replace('/', '-'); - el.src = `/file=html/${name}.jpg`; - } + fetch('/file=html/themes.json').then((res) => { + res.json().then((themes) => { + const theme = themes.find((t) => t.id === name); + if (theme) { + window.open(theme.subdomain, '_blank'); + } else { + const el = document.getElementById('theme-preview') || create_theme_element(); + el.style.display = el.style.display === 'block' ? 'none' : 'block'; + name = name.replace('/', '-'); + el.src = `/file=html/${name}.jpg`; + } + }); + }); +} + +async function browseFolder() { + const f = await window.showDirectoryPicker(); + if (f && f.kind === 'directory') return f.name; + return null; } async function reconnectUI() { diff --git a/launch.py b/launch.py index 6afbd7458..fd06897e5 100644 --- a/launch.py +++ b/launch.py @@ -40,7 +40,7 @@ def get_custom_args(): current = getattr(args, arg) if current != default: custom[arg] = getattr(args, arg) - installer.log.info(f'Command line args: {custom}') + installer.log.info(f'Command line args: {sys.argv[1:]} {installer.print_dict(custom)}') @lru_cache() @@ -121,7 +121,7 @@ def get_memory_stats(): process = psutil.Process(os.getpid()) res = process.memory_info() ram_total = 100 * res.rss / process.memory_percent() - return f'used: {gb(res.rss)} total: {gb(ram_total)}' + return f'used={gb(res.rss)} total={gb(ram_total)}' def start_server(immediate=True, server=None): @@ -137,25 +137,26 @@ def start_server(immediate=True, server=None): collected = gc.collect() if not immediate: time.sleep(3) - installer.log.debug(f'Memory {get_memory_stats()} Collected {collected}') + if collected > 0: + installer.log.debug(f'Memory {get_memory_stats()} Collected {collected}') module_spec = importlib.util.spec_from_file_location('webui', 'webui.py') # installer.log.debug(f'Loading module: {module_spec}') server = importlib.util.module_from_spec(module_spec) installer.log.debug(f'Starting module: {server}') - installer.log.info(f"Server arguments: {sys.argv[1:]}") get_custom_args() module_spec.loader.exec_module(server) + uvicorn = None if args.test: installer.log.info("Test only") server.wants_restart = False else: if args.api_only: - server = server.api_only() + uvicorn = server.api_only() else: - server = server.webui(restart=not immediate) + uvicorn = server.webui(restart=not immediate) if args.profile: installer.print_profile(pr, 'WebUI') - return server + return uvicorn, server if __name__ == "__main__": @@ -164,6 +165,7 @@ if __name__ == "__main__": installer.args = args installer.setup_logging() installer.log.info('Starting SD.Next') + sys.excepthook = installer.custom_excepthook installer.read_options() if args.skip_all: args.quick = True @@ -173,6 +175,7 @@ if __name__ == "__main__": if args.skip_git: installer.log.info('Skipping GIT operations') installer.check_version() + installer.log.info(f'Platform: {installer.print_dict(installer.get_platform())}') installer.set_environment() installer.check_torch() installer.check_modified_files() @@ -207,20 +210,22 @@ if __name__ == "__main__": # installer.log.debug(f"Args: {vars(args)}") logging.disable(logging.NOTSET if args.debug else logging.DEBUG) - instance = start_server(immediate=True, server=None) + uv, instance = start_server(immediate=True, server=None) while True: try: - alive = instance.thread.is_alive() - requests = instance.server_state.total_requests if hasattr(instance, 'server_state') else 0 + alive = uv.thread.is_alive() + requests = uv.server_state.total_requests if hasattr(uv, 'server_state') else 0 except Exception: alive = False requests = 0 if round(time.time()) % 120 == 0: - installer.log.debug(f'Server alive={alive} requests={requests} memory {get_memory_stats()} ') + state = f'job="{instance.state.job}" {instance.state.job_no}/{instance.state.job_count}' if instance.state.job != '' or instance.state.job_no != 0 or instance.state.job_count != 0 else 'idle' + uptime = round(time.time() - instance.state.server_start) + installer.log.debug(f'Server alive={alive} jobs={instance.state.total_jobs} requests={requests} uptime={uptime}s memory {get_memory_stats()} {state}') if not alive: - if instance.wants_restart: + if uv is not None and uv.wants_restart: installer.log.info('Server restarting...') - instance = start_server(immediate=False, server=instance) + uv, instance = start_server(immediate=False, server=instance) else: installer.log.info('Exiting...') break diff --git a/modules/api/api.py b/modules/api/api.py index ce5bda675..cf721d445 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -131,7 +131,7 @@ class Api: self.add_api_route("/sdapi/v1/hypernetworks", self.get_hypernetworks, methods=["GET"], response_model=List[models.HypernetworkItem]) self.add_api_route("/sdapi/v1/face-restorers", self.get_face_restorers, methods=["GET"], response_model=List[models.FaceRestorerItem]) self.add_api_route("/sdapi/v1/realesrgan-models", self.get_realesrgan_models, methods=["GET"], response_model=List[models.RealesrganItem]) - self.add_api_route("/sdapi/v1/prompt-styles", self.get_prompt_styles, methods=["GET"], response_model=List[models.PromptStyleItem]) + self.add_api_route("/sdapi/v1/prompt-styles", self.get_prompt_styles, methods=["GET"], response_model=List[models.StyleItem]) self.add_api_route("/sdapi/v1/embeddings", self.get_embeddings, methods=["GET"], response_model=models.EmbeddingsResponse) self.add_api_route("/sdapi/v1/refresh-checkpoints", self.refresh_checkpoints, methods=["POST"]) self.add_api_route("/sdapi/v1/sd-vae", self.get_sd_vaes, methods=["GET"], response_model=List[models.SDVaeItem]) @@ -263,7 +263,7 @@ class Api: p.scripts = script_runner p.outpath_grids = shared.opts.outdir_grids or shared.opts.outdir_txt2img_grids p.outpath_samples = shared.opts.outdir_samples or shared.opts.outdir_txt2img_samples - shared.state.begin() + shared.state.begin('api-txt2img') script_args = self.init_script_args(p, txt2imgreq, self.default_script_arg_txt2img, selectable_scripts, selectable_script_idx, script_runner) if selectable_scripts is not None: processed = scripts.scripts_txt2img.run(p, *script_args) # Need to pass args as list here @@ -311,7 +311,7 @@ class Api: p.scripts = script_runner p.outpath_grids = shared.opts.outdir_img2img_grids p.outpath_samples = shared.opts.outdir_img2img_samples - shared.state.begin() + shared.state.begin('api-img2img') script_args = self.init_script_args(p, img2imgreq, self.default_script_arg_img2img, selectable_scripts, selectable_script_idx, script_runner) if selectable_scripts is not None: processed = scripts.scripts_img2img.run(p, *script_args) # Need to pass args as list here @@ -478,12 +478,7 @@ class Api: return [{"name":x.name,"path":x.data_path, "scale":x.scale} for x in get_realesrgan_models(None)] def get_prompt_styles(self): - styleList = [] - for k in shared.prompt_styles.styles: - style = shared.prompt_styles.styles[k] - styleList.append({"name":style[0], "prompt": style[1], "negative_prompt": style[2]}) - - return styleList + return [{ 'name': v.name, 'prompt': v.prompt, 'negative_prompt': v.negative_prompt, 'extra': v.extra, 'filename': v.filename, 'preview': v.preview} for v in shared.prompt_styles.styles.values()] def get_embeddings(self): db = sd_hijack.model_hijack.embedding_db @@ -513,7 +508,7 @@ class Api: def create_embedding(self, args: dict): try: - shared.state.begin() + shared.state.begin('api-create-embedding') filename = create_embedding(**args) # create empty embedding sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings() # reload embeddings so new one can be immediately used shared.state.end() @@ -524,7 +519,7 @@ class Api: def create_hypernetwork(self, args: dict): try: - shared.state.begin() + shared.state.begin('api-create-hypernetwork') filename = create_hypernetwork(**args) # create empty embedding # pylint: disable=E1111 shared.state.end() return models.CreateResponse(info = f"create hypernetwork filename: {filename}") @@ -534,7 +529,7 @@ class Api: def preprocess(self, args: dict): try: - shared.state.begin() + shared.state.begin('api-preprocess') preprocess(**args) # quick operation unless blip/booru interrogation is enabled shared.state.end() return models.PreprocessResponse(info = 'preprocess complete') @@ -550,7 +545,7 @@ class Api: def train_embedding(self, args: dict): try: - shared.state.begin() + shared.state.begin('api-train-embedding') apply_optimizations = False error = None filename = '' @@ -571,7 +566,7 @@ class Api: def train_hypernetwork(self, args: dict): try: - shared.state.begin() + shared.state.begin('api-train-hypernetwork') shared.loaded_hypernetworks = [] apply_optimizations = False error = None diff --git a/modules/api/models.py b/modules/api/models.py index 142d4db55..da4158dcd 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -264,10 +264,13 @@ class RealesrganItem(BaseModel): path: Optional[str] = Field(title="Path") scale: Optional[int] = Field(title="Scale") -class PromptStyleItem(BaseModel): +class StyleItem(BaseModel): name: str = Field(title="Name") prompt: Optional[str] = Field(title="Prompt") negative_prompt: Optional[str] = Field(title="Negative Prompt") + extra: Optional[str] = Field(title="Extra") + filename: Optional[str] = Field(title="Filename") + preview: Optional[str] = Field(title="Preview") class ArtistItem(BaseModel): name: str = Field(title="Name") diff --git a/modules/call_queue.py b/modules/call_queue.py index 568d79344..d75ae55cd 100644 --- a/modules/call_queue.py +++ b/modules/call_queue.py @@ -19,6 +19,7 @@ def wrap_queued_call(func): def wrap_gradio_gpu_call(func, extra_outputs=None): + name = func.__name__ def f(*args, **kwargs): # if the first argument is a string that says "task(...)", it is treated as a job id if len(args) > 0 and type(args[0]) == str and args[0][0:5] == "task(" and args[0][-1] == ")": @@ -27,7 +28,6 @@ def wrap_gradio_gpu_call(func, extra_outputs=None): else: id_task = None with queue_lock: - shared.state.begin() progress.start_task(id_task) res = [None, '', '', ''] try: @@ -42,13 +42,15 @@ def wrap_gradio_gpu_call(func, extra_outputs=None): progress.finish_task(id_task) shared.state.end() return res - return wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True) + return wrap_gradio_call(f, extra_outputs=extra_outputs, add_stats=True, name=name) -def wrap_gradio_call(func, extra_outputs=None, add_stats=False): +def wrap_gradio_call(func, extra_outputs=None, add_stats=False, name=None): + job_name = name if name is not None else func.__name__ def f(*args, extra_outputs_array=extra_outputs, **kwargs): t = time.perf_counter() shared.mem_mon.reset() + shared.state.begin(job_name) try: if shared.cmd_opts.profile: pr = cProfile.Profile() @@ -67,15 +69,10 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False): print('Profile Exec:', s.getvalue()) except Exception as e: errors.display(e, 'gradio call') - shared.state.job = "" - shared.state.job_count = 0 if extra_outputs_array is None: extra_outputs_array = [None, ''] res = extra_outputs_array + [f"
{html.escape(type(e).__name__+': '+str(e))}
"] - shared.state.skipped = False - shared.state.interrupted = False - shared.state.paused = False - shared.state.job_count = 0 + shared.state.end() if not add_stats: return tuple(res) elapsed = time.perf_counter() - t diff --git a/modules/cmd_args.py b/modules/cmd_args.py index 41b6340da..23e13c08b 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -36,6 +36,7 @@ group.add_argument("--tls-certfile", type=str, help="Enable TLS and specify cert group.add_argument("--tls-selfsign", action="store_true", help="Enable TLS with self-signed certificates, default: %(default)s", default=None) group.add_argument("--server-name", type=str, help="Sets hostname of server, default: %(default)s", default=None) group.add_argument("--no-hashing", action='store_true', help="Disable hashing of checkpoints, default: %(default)s", default=False) +group.add_argument("--no-metadata", action='store_true', help="Disable reading of metadata from models, default: %(default)s", default=False) group.add_argument("--no-download", action='store_true', help="Disable download of default model, default: %(default)s", default=False) group.add_argument("--profile", action='store_true', help="Run profiler, default: %(default)s") group.add_argument("--disable-queue", action='store_true', help="Disable queues, default: %(default)s") diff --git a/modules/codeformer_model.py b/modules/codeformer_model.py index e6e75b219..34e3a7a5a 100644 --- a/modules/codeformer_model.py +++ b/modules/codeformer_model.py @@ -97,7 +97,7 @@ def setup_model(dirname): cropped_face_t = cropped_face_t.unsqueeze(0).to(devices.device_codeformer) try: - with torch.no_grad(): + with devices.inference_context(): output = self.net(cropped_face_t, w=w if w is not None else shared.opts.code_former_weight, adain=True)[0] restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1)) del output diff --git a/modules/deepbooru.py b/modules/deepbooru.py index 547e1b4c6..de50853d6 100644 --- a/modules/deepbooru.py +++ b/modules/deepbooru.py @@ -56,7 +56,7 @@ class DeepDanbooru: pic = images.resize_image(2, pil_image.convert("RGB"), 512, 512) a = np.expand_dims(np.array(pic, dtype=np.float32), 0) / 255 - with torch.no_grad(), devices.autocast(): + with devices.inference_context(), devices.autocast(): x = torch.from_numpy(a).to(devices.device) y = self.model(x)[0].detach().cpu().numpy() diff --git a/modules/devices.py b/modules/devices.py index 8f306adbe..8a7db4244 100644 --- a/modules/devices.py +++ b/modules/devices.py @@ -17,6 +17,51 @@ def has_mps() -> bool: return mac_specific.has_mps +def get_gpu_info(): + def get_driver(): + import os + import subprocess + if torch.cuda.is_available() and torch.version.cuda: + try: + result = subprocess.run('nvidia-smi --query-gpu=driver_version --format=csv,noheader', shell=True, check=False, env=os.environ, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + version = result.stdout.decode(encoding="utf8", errors="ignore").strip() + return version + except Exception: + return '' + else: + return '' + + if not torch.cuda.is_available(): + return {} + else: + try: + if torch.version.cuda: + return { + 'device': f'{torch.cuda.get_device_name(torch.cuda.current_device())} n={torch.cuda.device_count()} arch={torch.cuda.get_arch_list()[-1]} cap={torch.cuda.get_device_capability(device)}', + 'cuda': torch.version.cuda, + 'cudnn': torch.backends.cudnn.version(), + 'driver': get_driver(), + } + elif torch.version.hip: + return { + 'device': f'{torch.cuda.get_device_name(torch.cuda.current_device())} n={torch.cuda.device_count()}', + 'hip': torch.version.hip, + } + else: + try: + import intel_extension_for_pytorch as ipex# pylint: disable=import-error, unused-import + return { + 'device': f'{torch.xpu.get_device_name(torch.xpu.current_device())} n={torch.xpu.device_count()}', + 'ipex': ipex.__version__, + } + except Exception: + return { + 'device': 'unknown' + } + except Exception as ex: + return { 'error': ex } + + def extract_device_id(args, name): # pylint: disable=redefined-outer-name for x in range(len(args)): if name in args[x]: @@ -93,10 +138,9 @@ def test_fp16(): x = torch.tensor([[1.5,.0,.0,.0]]).to(device).half() layerNorm = torch.nn.LayerNorm(4, eps=0.00001, elementwise_affine=True, dtype=torch.float16, device=device) _y = layerNorm(x) - shared.log.debug('Torch FP16 test passed') return True - except Exception as e: - shared.log.warning(f'Torch FP16 test failed: Forcing FP32 operations: {e}') + except Exception as ex: + shared.log.warning(f'Torch FP16 test failed: Forcing FP32 operations: {ex}') shared.opts.cuda_dtype = 'FP32' shared.opts.no_half = True shared.opts.no_half_vae = True @@ -116,7 +160,7 @@ def test_bf16(): def set_cuda_params(): - shared.log.debug('Verifying Torch settings') + # shared.log.debug('Verifying Torch settings') if cuda_ok: try: torch.backends.cuda.matmul.allow_tf32 = True @@ -133,7 +177,7 @@ def set_cuda_params(): torch.backends.cudnn.allow_tf32 = True except Exception: pass - global dtype, dtype_vae, dtype_unet, unet_needs_upcast # pylint: disable=global-statement + global dtype, dtype_vae, dtype_unet, unet_needs_upcast, inference_context # pylint: disable=global-statement if shared.opts.cuda_dtype == 'FP32': dtype = torch.float32 dtype_vae = torch.float32 @@ -143,13 +187,15 @@ def set_cuda_params(): dtype = torch.bfloat16 if bf16_ok else torch.float16 dtype_vae = torch.bfloat16 if bf16_ok else torch.float16 dtype_unet = torch.bfloat16 if bf16_ok else torch.float16 + else: + bf16_ok = False if shared.opts.cuda_dtype == 'FP16' or dtype == torch.float16: fp16_ok = test_fp16() dtype = torch.float16 if fp16_ok else torch.float32 dtype_vae = torch.float16 if fp16_ok else torch.float32 dtype_unet = torch.float16 if fp16_ok else torch.float32 else: - pass + fp16_ok = False if shared.opts.no_half: shared.log.info('Torch override dtype: no-half set') dtype = torch.float32 @@ -159,12 +205,18 @@ def set_cuda_params(): shared.log.info('Torch override VAE dtype: no-half set') dtype_vae = torch.float32 unet_needs_upcast = shared.opts.upcast_sampling + if shared.opts.inference_mode == 'inference-mode': + inference_context = torch.inference_mode + elif shared.opts.inference_mode == 'none': + inference_context = contextlib.nullcontext + else: + inference_context = torch.no_grad shared.log.debug(f'Desired Torch parameters: dtype={shared.opts.cuda_dtype} no-half={shared.opts.no_half} no-half-vae={shared.opts.no_half_vae} upscast={shared.opts.upcast_sampling}') - shared.log.info(f'Setting Torch parameters: dtype={dtype} vae={dtype_vae} unet={dtype_unet}') - shared.log.debug(f'Torch default device: {torch.device(get_optimal_device_name())}') + shared.log.info(f'Setting Torch parameters: device={torch.device(get_optimal_device_name())} dtype={dtype} vae={dtype_vae} unet={dtype_unet} context={inference_context.__name__} fp16={fp16_ok} bf16={bf16_ok}') args = cmd_args.parser.parse_args() +backend = 'not set' if args.use_ipex or (hasattr(torch, 'xpu') and torch.xpu.is_available()): backend = 'ipex' from modules.intel.ipex import ipex_init @@ -188,6 +240,7 @@ elif sys.platform == 'darwin': else: backend = 'cpu' +inference_context = torch.no_grad cuda_ok = torch.cuda.is_available() cpu = torch.device("cpu") device = device_interrogate = device_gfpgan = device_esrgan = device_codeformer = None diff --git a/modules/dml/hijack/kdiffusion.py b/modules/dml/hijack/kdiffusion.py index 19e16b013..d772dc88f 100644 --- a/modules/dml/hijack/kdiffusion.py +++ b/modules/dml/hijack/kdiffusion.py @@ -1,7 +1,7 @@ import torch from tqdm.auto import tqdm from k_diffusion import sampling -from modules.shared import device +import modules.devices as devices def dpm_solver_adaptive(self, x, t_start, t_end, order=3, rtol=0.05, atol=0.0078, h_init=0.05, pcoeff=0., icoeff=1., dcoeff=0., accept_safety=0.81, eta=0., s_noise=1., noise_sampler=None): @@ -12,8 +12,8 @@ def dpm_solver_adaptive(self, x, t_start, t_end, order=3, rtol=0.05, atol=0.0078 if not forward and eta: raise ValueError('eta must be 0 for reverse sampling') h_init = abs(h_init) * (1 if forward else -1) - atol = torch.tensor(atol, device=device) - rtol = torch.tensor(rtol, device=device) + atol = torch.tensor(atol, device=devices.device) + rtol = torch.tensor(rtol, device=devices.device) s = t_start x_prev = x accept = True @@ -58,7 +58,7 @@ def dpm_solver_adaptive(self, x, t_start, t_end, order=3, rtol=0.05, atol=0.0078 return x, info -@torch.no_grad() +@devices.inference_context() def sample_dpm_fast(model, x, sigma_min, sigma_max, n, extra_args=None, callback=None, disable=None, eta=0., s_noise=1., noise_sampler=None): """DPM-Solver-Fast (fixed step size). See https://arxiv.org/abs/2206.00927.""" if sigma_min <= 0 or sigma_max <= 0: @@ -67,10 +67,10 @@ def sample_dpm_fast(model, x, sigma_min, sigma_max, n, extra_args=None, callback dpm_solver = sampling.DPMSolver(model, extra_args, eps_callback=pbar.update) if callback is not None: dpm_solver.info_callback = lambda info: callback({'sigma': dpm_solver.sigma(info['t']), 'sigma_hat': dpm_solver.sigma(info['t_up']), **info}) - return dpm_solver.dpm_solver_fast(x, dpm_solver.t(torch.tensor(sigma_max, device=device)), dpm_solver.t(torch.tensor(sigma_min, device=device)), n, eta, s_noise, noise_sampler) + return dpm_solver.dpm_solver_fast(x, dpm_solver.t(torch.tensor(sigma_max, device=devices.device)), dpm_solver.t(torch.tensor(sigma_min, device=devices.device)), n, eta, s_noise, noise_sampler) -@torch.no_grad() +@devices.inference_context() def sample_dpm_adaptive(model, x, sigma_min, sigma_max, extra_args=None, callback=None, disable=None, order=3, rtol=0.05, atol=0.0078, h_init=0.05, pcoeff=0., icoeff=1., dcoeff=0., accept_safety=0.81, eta=0., s_noise=1., noise_sampler=None, return_info=False): """DPM-Solver-12 and 23 (adaptive step size). See https://arxiv.org/abs/2206.00927.""" if sigma_min <= 0 or sigma_max <= 0: @@ -79,7 +79,7 @@ def sample_dpm_adaptive(model, x, sigma_min, sigma_max, extra_args=None, callbac dpm_solver = sampling.DPMSolver(model, extra_args, eps_callback=pbar.update) if callback is not None: dpm_solver.info_callback = lambda info: callback({'sigma': dpm_solver.sigma(info['t']), 'sigma_hat': dpm_solver.sigma(info['t_up']), **info}) - x, info = dpm_solver.dpm_solver_adaptive(x, dpm_solver.t(torch.tensor(sigma_max, device=device)), dpm_solver.t(torch.tensor(sigma_min, device=device)), order, rtol, atol, h_init, pcoeff, icoeff, dcoeff, accept_safety, eta, s_noise, noise_sampler) + x, info = dpm_solver.dpm_solver_adaptive(x, dpm_solver.t(torch.tensor(sigma_max, device=devices.device)), dpm_solver.t(torch.tensor(sigma_min, device=devices.device)), order, rtol, atol, h_init, pcoeff, icoeff, dcoeff, accept_safety, eta, s_noise, noise_sampler) if return_info: return x, info return x diff --git a/modules/dml/hijack/plms.py b/modules/dml/hijack/plms.py index 2baef815d..a8afcbd05 100644 --- a/modules/dml/hijack/plms.py +++ b/modules/dml/hijack/plms.py @@ -1,11 +1,10 @@ import torch - from ldm.models.diffusion.ddim import noise_like - import modules.sd_hijack_inpainting as plms_hijack +import modules.devices as devices -@torch.no_grad() +@devices.inference_context() def p_sample_plms(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False, temperature=1., noise_dropout=0., score_corrector=None, corrector_kwargs=None, unconditional_guidance_scale=1., unconditional_conditioning=None, old_eps=None, t_next=None, dynamic_threshold=None): diff --git a/modules/dml/hijack/realesrgan_model.py b/modules/dml/hijack/realesrgan_model.py index 341e9aded..ad3b01cce 100644 --- a/modules/dml/hijack/realesrgan_model.py +++ b/modules/dml/hijack/realesrgan_model.py @@ -1,6 +1,5 @@ import math import torch - from realesrgan import RealESRGANer diff --git a/modules/dml/hijack/stablediffusion.py b/modules/dml/hijack/stablediffusion.py index b14b0ece1..3c634e802 100644 --- a/modules/dml/hijack/stablediffusion.py +++ b/modules/dml/hijack/stablediffusion.py @@ -1,9 +1,10 @@ import torch - from ldm.models.diffusion.ddim import DDIMSampler from ldm.modules.diffusionmodules.util import noise_like +import modules.devices as devices -@torch.no_grad() + +@devices.inference_context() def p_sample_ddim(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False, temperature=1., noise_dropout=0., score_corrector=None, corrector_kwargs=None, unconditional_guidance_scale=1., unconditional_conditioning=None, diff --git a/modules/esrgan_model.py b/modules/esrgan_model.py index e0b79069d..7ed5c33ec 100644 --- a/modules/esrgan_model.py +++ b/modules/esrgan_model.py @@ -199,9 +199,9 @@ def upscale_without_tiling(model, img): img = np.ascontiguousarray(np.transpose(img, (2, 0, 1))) / 255 img = torch.from_numpy(img).float() img = img.unsqueeze(0).to(devices.device_esrgan) - with torch.no_grad(): + with devices.inference_context(): output = model(img) - output = output.squeeze().float().cpu().clamp_(0, 1).numpy() + output = output.squeeze().float().cpu().clamp_(0, 1).detach().numpy() output = 255. * np.moveaxis(output, 0, 2) output = output.astype(np.uint8) output = output[:, :, ::-1] diff --git a/modules/extras.py b/modules/extras.py index f13b61306..6e61b8426 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -54,9 +54,7 @@ def to_half(tensor, enable): def run_modelmerger(id_task, primary_model_name, secondary_model_name, tertiary_model_name, interp_method, multiplier, save_as_half, custom_name, checkpoint_format, config_source, bake_in_vae, discard_weights, save_metadata): # pylint: disable=unused-argument - shared.state.begin() - shared.state.job = 'model-merge' - + shared.state.begin('model-merge') save_as_half = save_as_half == 0 def fail(message): @@ -321,9 +319,7 @@ def run_modelconvert(model, checkpoint_formats, precision, conv_type, custom_nam "vae": vae_conv, "other": others_conv } - shared.state.begin() - shared.state.job = 'model-convert' - + shared.state.begin('model-convert') model_info = sd_models.checkpoints_list[model] shared.state.textinfo = f"Loading {model_info.filename}..." shared.log.info(f"Model convert loading: {model_info.filename}") diff --git a/modules/hashes.py b/modules/hashes.py index ea8b91609..288a175d5 100644 --- a/modules/hashes.py +++ b/modules/hashes.py @@ -21,12 +21,17 @@ def cache(subsection): return s -def calculate_sha256(filename): +def calculate_sha256(filename, quiet=False): hash_sha256 = hashlib.sha256() blksize = 1024 * 1024 - with progress.open(filename, 'rb', description=f'Calculating model hash: [cyan]{filename}', auto_refresh=True) as f: - for chunk in iter(lambda: f.read(blksize), b""): - hash_sha256.update(chunk) + if not quiet: + with progress.open(filename, 'rb', description=f'Calculating model hash: [cyan]{filename}', auto_refresh=True, console=shared.console) as f: + for chunk in iter(lambda: f.read(blksize), b""): + hash_sha256.update(chunk) + else: + with open(filename, 'rb') as f: + for chunk in iter(lambda: f.read(blksize), b""): + hash_sha256.update(chunk) return hash_sha256.hexdigest() @@ -52,7 +57,7 @@ def sha256(filename, title, use_addnet_hash=False): if not os.path.isfile(filename): return None if use_addnet_hash: - with progress.open(filename, 'rb', description=f'Calculating model hash: [cyan]{filename}', auto_refresh=True) as f: + with progress.open(filename, 'rb', description=f'Calculating model hash: [cyan]{filename}', auto_refresh=True, console=shared.console) as f: sha256_value = addnet_hash_safetensors(f) else: sha256_value = calculate_sha256(filename) diff --git a/modules/hypernetworks/hypernetwork.py b/modules/hypernetworks/hypernetwork.py index 85c58cbb4..d9cc95e86 100644 --- a/modules/hypernetworks/hypernetwork.py +++ b/modules/hypernetworks/hypernetwork.py @@ -1,5 +1,4 @@ import datetime -import glob import html import os from collections import deque @@ -35,19 +34,14 @@ class HypernetworkModule(torch.nn.Module): def __init__(self, dim, state_dict=None, layer_structure=None, activation_func=None, weight_init='Normal', add_layer_norm=False, activate_output=False, dropout_structure=None): super().__init__() - self.multiplier = 1.0 - assert layer_structure is not None, "layer_structure must not be None" assert layer_structure[0] == 1, "Multiplier Sequence should start with size 1!" assert layer_structure[-1] == 1, "Multiplier Sequence should end with size 1!" - linears = [] for i in range(len(layer_structure) - 1): - # Add a fully-connected layer linears.append(torch.nn.Linear(int(dim * layer_structure[i]), int(dim * layer_structure[i+1]))) - # Add an activation func except last layer if activation_func == "linear" or activation_func is None or (i >= len(layer_structure) - 2 and not activate_output): pass @@ -55,20 +49,16 @@ class HypernetworkModule(torch.nn.Module): linears.append(self.activation_dict[activation_func]()) else: raise RuntimeError(f'hypernetwork uses an unsupported activation function: {activation_func}') - # Add layer normalization if add_layer_norm: linears.append(torch.nn.LayerNorm(int(dim * layer_structure[i+1]))) - # Everything should be now parsed into dropout structure, and applied here. # Since we only have dropouts after layers, dropout structure should start with 0 and end with 0. if dropout_structure is not None and dropout_structure[i+1] > 0: assert 0 < dropout_structure[i+1] < 1, "Dropout probability should be 0 or float between 0 and 1!" linears.append(torch.nn.Dropout(p=dropout_structure[i+1])) # Code explanation : [1, 2, 1] -> dropout is missing when last_layer_dropout is false. [1, 2, 2, 1] -> [0, 0.3, 0, 0], when its True, [0, 0.3, 0.3, 0]. - self.linear = torch.nn.Sequential(*linears) - if state_dict is not None: self.fix_old_state_dict(state_dict) self.load_state_dict(state_dict) @@ -102,12 +92,10 @@ class HypernetworkModule(torch.nn.Module): 'linear2.bias': 'linear.1.bias', 'linear2.weight': 'linear.1.weight', } - for fr, to in changes.items(): x = state_dict.get(fr, None) if x is None: continue - del state_dict[fr] state_dict[to] = x @@ -162,7 +150,6 @@ class Hypernetwork: self.optimizer_name = None self.optimizer_state_dict = None self.optional_info = None - for size in enable_sizes or []: self.layers[size] = ( HypernetworkModule(size, None, self.layer_structure, self.activation_func, self.weight_init, @@ -210,10 +197,8 @@ class Hypernetwork: def save(self, filename): state_dict = {} optimizer_saved_dict = {} - for k, v in self.layers.items(): state_dict[k] = (v[0].state_dict(), v[1].state_dict()) - state_dict['step'] = self.step state_dict['name'] = self.name state_dict['layer_structure'] = self.layer_structure @@ -227,10 +212,8 @@ class Hypernetwork: state_dict['dropout_structure'] = self.dropout_structure state_dict['last_layer_dropout'] = (self.dropout_structure[-2] != 0) if self.dropout_structure is not None else self.last_layer_dropout state_dict['optional_info'] = self.optional_info if self.optional_info else None - if self.optimizer_name is not None: optimizer_saved_dict['optimizer_name'] = self.optimizer_name - torch.save(state_dict, filename) if shared.opts.save_optimizer_state and self.optimizer_state_dict: optimizer_saved_dict['hash'] = self.shorthash() @@ -241,10 +224,8 @@ class Hypernetwork: self.filename = filename if self.name is None: self.name = os.path.splitext(os.path.basename(filename))[0] - - with progress.open(filename, 'rb', description=f'Loading hypernetwork: [cyan]{filename}', auto_refresh=True) as f: + with progress.open(filename, 'rb', description=f'Loading hypernetwork: [cyan]{filename}', auto_refresh=True, console=shared.console) as f: state_dict = torch.load(f, map_location='cpu') - self.layer_structure = state_dict.get('layer_structure', [1, 2, 1]) self.optional_info = state_dict.get('optional_info', None) self.activation_func = state_dict.get('activation_func', None) @@ -257,11 +238,9 @@ class Hypernetwork: # Dropout structure should have same length as layer structure, Every digits should be in [0,1), and last digit must be 0. if self.dropout_structure is None: self.dropout_structure = parse_dropout_structure(self.layer_structure, self.use_dropout, self.last_layer_dropout) - if shared.opts.print_hypernet_extra: if self.optional_info is not None: print(f" INFO:\n {self.optional_info}\n") - print(f" Layer structure: {self.layer_structure}") print(f" Activation function: {self.activation_func}") print(f" Weight initialization: {self.weight_init}") @@ -269,9 +248,7 @@ class Hypernetwork: print(f" Dropout usage: {self.use_dropout}" ) print(f" Activate last layer: {self.activate_output}") print(f" Dropout structure: {self.dropout_structure}") - optimizer_saved_dict = torch.load(self.filename + '.optim', map_location='cpu') if os.path.exists(self.filename + '.optim') else {} - if self.shorthash() == optimizer_saved_dict.get('hash', None): self.optimizer_state_dict = optimizer_saved_dict.get('optimizer_state_dict', None) else: @@ -285,7 +262,6 @@ class Hypernetwork: self.optimizer_name = "AdamW" if shared.opts.print_hypernet_extra: print("No saved optimizer exists in checkpoint") - for size, sd in state_dict.items(): if type(size) == int: self.layers[size] = ( @@ -294,7 +270,6 @@ class Hypernetwork: HypernetworkModule(size, sd[1], self.layer_structure, self.activation_func, self.weight_init, self.add_layer_norm, self.activate_output, self.dropout_structure), ) - self.name = state_dict.get('name', self.name) self.step = state_dict.get('step', 0) self.sd_checkpoint = state_dict.get('sd_checkpoint', None) @@ -303,54 +278,49 @@ class Hypernetwork: def shorthash(self): sha256 = hashes.sha256(self.filename, f'hypernet/{self.name}') - return sha256[0:10] if sha256 else None def list_hypernetworks(path): res = {} - for filename in sorted(glob.iglob(os.path.join(path, '**/*.pt'), recursive=True), key=str.lower): - name = os.path.splitext(os.path.basename(filename))[0] - # Prevent a hypothetical "None.pt" from being listed. - if name != "None": - res[name] = filename + 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"): + name = os.path.splitext(os.path.basename(fn))[0] + res[name] = filename + elif os.path.isdir(fn) and not fn.startswith('.'): + list_folder(fn) + + list_folder(path) return res def load_hypernetwork(name): path = shared.hypernetworks.get(name, None) - if path is None: return None - hypernetwork = Hypernetwork() - try: hypernetwork.load(path) except Exception as e: errors.display(e, f'hypernetwork load: {path}') return None - return hypernetwork def load_hypernetworks(names, multipliers=None): already_loaded = {} - for hypernetwork in shared.loaded_hypernetworks: if hypernetwork.name in names: already_loaded[hypernetwork.name] = hypernetwork - shared.loaded_hypernetworks.clear() - for i, name in enumerate(names): hypernetwork = already_loaded.get(name, None) if hypernetwork is None: hypernetwork = load_hypernetwork(name) - if hypernetwork is None: continue - hypernetwork.set_multiplier(multipliers[i] if multipliers else 1.0) shared.loaded_hypernetworks.append(hypernetwork) @@ -368,14 +338,11 @@ def find_closest_hypernetwork_name(search: str): def apply_single_hypernetwork(hypernetwork, context_k, context_v, layer=None): hypernetwork_layers = (hypernetwork.layers if hypernetwork is not None else {}).get(context_k.shape[2], None) - if hypernetwork_layers is None: return context_k, context_v - if layer is not None: layer.hyper_k = hypernetwork_layers[0] layer.hyper_v = hypernetwork_layers[1] - context_k = devices.cond_cast_unet(hypernetwork_layers[0](devices.cond_cast_float(context_k))) context_v = devices.cond_cast_unet(hypernetwork_layers[1](devices.cond_cast_float(context_v))) return context_k, context_v @@ -386,33 +353,25 @@ def apply_hypernetworks(hypernetworks, context, layer=None): context_v = context for hypernetwork in hypernetworks: context_k, context_v = apply_single_hypernetwork(hypernetwork, context_k, context_v, layer) - return context_k, context_v def attention_CrossAttention_forward(self, x, context=None, mask=None): h = self.heads - q = self.to_q(x) context = default(context, x) - context_k, context_v = apply_hypernetworks(shared.loaded_hypernetworks, context, self) k = self.to_k(context_k) v = self.to_v(context_v) - q, k, v = (rearrange(t, 'b n (h d) -> (b h) n d', h=h) for t in (q, k, v)) - sim = einsum('b i d, b j d -> b i j', q, k) * self.scale - if mask is not None: mask = rearrange(mask, 'b ... -> b (...)') max_neg_value = -torch.finfo(sim.dtype).max mask = repeat(mask, 'b j -> (b h) () j', h=h) sim.masked_fill_(~mask, max_neg_value) - # attention, what we cannot get enough of attn = sim.softmax(dim=-1) - out = einsum('b i j, b j d -> b i d', attn, v) out = rearrange(out, '(b h) n d -> b n (h d)', h=h) return self.to_out(out) @@ -421,7 +380,6 @@ def attention_CrossAttention_forward(self, x, context=None, mask=None): def stack_conds(conds): if len(conds) == 1: return torch.stack(conds) - # same as in reconstruct_multicond_batch token_count = max([x.shape[0] for x in conds]) for i in range(len(conds)): @@ -429,7 +387,6 @@ def stack_conds(conds): last_vector = conds[i][-1:] last_vector_repeated = last_vector.repeat([token_count - conds[i].shape[0], 1]) conds[i] = torch.vstack([conds[i], last_vector_repeated]) - return torch.stack(conds) @@ -464,19 +421,15 @@ def create_hypernetwork(name, enable_sizes, overwrite_old, layer_structure=None, # Remove illegal characters from name. name = "".join( x for x in name if (x.isalnum() or x in "._- ")) assert name, "Name cannot be empty!" - fn = os.path.join(shared.opts.hypernetwork_dir, f"{name}.pt") if not overwrite_old: assert not os.path.exists(fn), f"file {fn} already exists" - if type(layer_structure) == str: layer_structure = [float(x.strip()) for x in layer_structure.split(",")] - if use_dropout and dropout_structure and type(dropout_structure) == str: dropout_structure = [float(x.strip()) for x in dropout_structure.split(",")] else: dropout_structure = [0] * len(layer_structure) - hypernet = modules.hypernetworks.hypernetwork.Hypernetwork( name=name, enable_sizes=[int(x) for x in enable_sizes], diff --git a/modules/images.py b/modules/images.py index 5d7684285..49f83669f 100644 --- a/modules/images.py +++ b/modules/images.py @@ -44,13 +44,10 @@ def image_grid(imgs, batch_size=1, rows=None): rows = shared.opts.n_rows elif shared.opts.n_rows == 0: rows = batch_size - elif shared.opts.grid_prevent_empty_spots: + else: rows = math.floor(math.sqrt(len(imgs))) while len(imgs) % rows != 0: rows -= 1 - else: - rows = math.sqrt(len(imgs)) - rows = round(rows) if rows > len(imgs): rows = len(imgs) cols = math.ceil(len(imgs) / rows) @@ -204,7 +201,7 @@ def draw_prompt_matrix(im, width, height, all_prompts, margin=0): return draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin) -def resize_image(resize_mode, im, width, height, upscaler_name=None): +def resize_image(resize_mode, im, width, height, upscaler_name=None, output_type='image'): """ Resizes an image with the specified resize_mode, width, and height. Args: @@ -264,6 +261,8 @@ def resize_image(resize_mode, im, width, height, upscaler_name=None): fill_width = width // 2 - src_w // 2 res.paste(resized.resize((fill_width, height), box=(0, 0, 0, height)), box=(0, 0)) res.paste(resized.resize((fill_width, height), box=(resized.width, 0, resized.width, height)), box=(fill_width + src_w, 0)) + if output_type == 'np': + return np.array(res) return res @@ -292,7 +291,7 @@ def sanitize_filename_part(text, replace_spaces=True): class FilenameGenerator: replacements = { - 'batch_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.batch_size == 1 else self.p.batch_index + 1, + 'batch_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p is None or self.p.batch_size == 1 else self.p.batch_index + 1, 'cfg': lambda self: self.p and self.p.cfg_scale, 'clip_skip': lambda self: self.p and self.p.clip_skip, 'date': lambda self: datetime.datetime.now().strftime('%Y-%m-%d'), @@ -385,8 +384,6 @@ class FilenameGenerator: def apply(self, x): res = '' - if self.p is None: - return res for m in re_pattern.finditer(x): text, pattern = m.groups() if pattern is None: @@ -512,7 +509,7 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', i The base filename which will be applied to `filename pattern`. seed, prompt, short_filename, extension (`str`): - Image file extension, default is `png`. + Image file extension, default is `jpg`. pngsectionname (`str`): Specify the name of the section which `info` will be saved in. info (`str` or `PngImagePlugin.iTXt`): @@ -553,7 +550,10 @@ def save_image(image, path, basename, seed=None, prompt=None, extension='jpg', i file_decoration = shared.opts.samples_filename_pattern else: file_decoration = "[seq]-[prompt_words]" - file_decoration = namegen.apply(file_decoration).strip(' ').strip('-') + suffix + file_decoration = namegen.apply(file_decoration).strip(' ').strip('-') + if len(file_decoration) == 0: + file_decoration = namegen.apply('[seq]').strip(' ').strip('-') + file_decoration += suffix if shared.opts.save_images_add_number: if '[seq]' not in file_decoration: file_decoration = f"[seq]-{file_decoration}" diff --git a/modules/img2img.py b/modules/img2img.py index fcfc50d90..01304b509 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -83,7 +83,7 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s shared.log.warning('Model not loaded') return [], '', '', 'Error: model not loaded' - shared.log.debug(f'img2img: id_task={id_task}|mode={mode}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|init_img={init_img}|sketch={sketch}|init_img_with_mask={init_img_with_mask}|inpaint_color_sketch={inpaint_color_sketch}|inpaint_color_sketch_orig={inpaint_color_sketch_orig}|init_img_inpaint={init_img_inpaint}|init_mask_inpaint={init_mask_inpaint}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|mask_blur={mask_blur}|mask_alpha={mask_alpha}|inpainting_fill={inpainting_fill}|full_quality={full_quality}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|image_cfg_scale={image_cfg_scale}|clip_skip={clip_skip}|denoising_strength={denoising_strength}|seed={seed}|subseed{subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|selected_scale_tab={selected_scale_tab}|height={height}|width={width}|scale_by={scale_by}|resize_mode={resize_mode}|inpaint_full_res={inpaint_full_res}|inpaint_full_res_padding={inpaint_full_res_padding}|inpainting_mask_invert={inpainting_mask_invert}|img2img_batch_files={img2img_batch_files}|img2img_batch_input_dir={img2img_batch_input_dir}|img2img_batch_output_dir={img2img_batch_output_dir}|img2img_batch_inpaint_mask_dir={img2img_batch_inpaint_mask_dir}|override_settings_texts={override_settings_texts}|args={args}') + shared.log.debug(f'img2img: id_task={id_task}|mode={mode}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|init_img={init_img}|sketch={sketch}|init_img_with_mask={init_img_with_mask}|inpaint_color_sketch={inpaint_color_sketch}|inpaint_color_sketch_orig={inpaint_color_sketch_orig}|init_img_inpaint={init_img_inpaint}|init_mask_inpaint={init_mask_inpaint}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|mask_blur={mask_blur}|mask_alpha={mask_alpha}|inpainting_fill={inpainting_fill}|full_quality={full_quality}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|image_cfg_scale={image_cfg_scale}|clip_skip={clip_skip}|denoising_strength={denoising_strength}|seed={seed}|subseed{subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}|selected_scale_tab={selected_scale_tab}|height={height}|width={width}|scale_by={scale_by}|resize_mode={resize_mode}|inpaint_full_res={inpaint_full_res}|inpaint_full_res_padding={inpaint_full_res_padding}|inpainting_mask_invert={inpainting_mask_invert}|img2img_batch_files={img2img_batch_files}|img2img_batch_input_dir={img2img_batch_input_dir}|img2img_batch_output_dir={img2img_batch_output_dir}|img2img_batch_inpaint_mask_dir={img2img_batch_inpaint_mask_dir}|override_settings_texts={override_settings_texts}') if init_img is None: shared.log.debug('Init image not set') diff --git a/modules/intel/ipex/__init__.py b/modules/intel/ipex/__init__.py index 9794f98b7..bd2e8e142 100644 --- a/modules/intel/ipex/__init__.py +++ b/modules/intel/ipex/__init__.py @@ -2,22 +2,14 @@ import os import sys import contextlib import torch -import intel_extension_for_pytorch as ipex -from modules import shared -from .diffusers import ipex_diffusers +import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import from .hijacks import ipex_hijacks +from .attention import attention_init +from .diffusers import ipex_diffusers -#ControlNet depth_leres++ -class DummyDataParallel(torch.nn.Module): - def __new__(cls, module, device_ids=None, output_device=None, dim=0): - if type(device_ids) is list and len(device_ids) > 1: - shared.log.warning("IPEX backend doesn't support DataParallel on multiple XPU devices") - return module.to(shared.device) +# pylint: disable=protected-access, missing-function-docstring, line-too-long -def return_null_context(*args, **kwargs): - return contextlib.nullcontext() - -def ipex_init(): +def ipex_init(): # pylint: disable=too-many-statements try: #Replace cuda with xpu: torch.cuda.current_device = torch.xpu.current_device @@ -140,10 +132,13 @@ def ipex_init(): torch.cuda.amp.common.amp_definitely_not_available = lambda: False try: torch.cuda.amp.GradScaler = torch.xpu.amp.GradScaler - except Exception: - from .gradscaler import gradscaler_init - gradscaler_init() - torch.cuda.amp.GradScaler = torch.xpu.amp.GradScaler + except Exception: # pylint: disable=broad-exception-caught + try: + from .gradscaler import gradscaler_init # pylint: disable=import-outside-toplevel, import-error + gradscaler_init() + torch.cuda.amp.GradScaler = torch.xpu.amp.GradScaler + except Exception: # pylint: disable=broad-exception-caught + torch.cuda.amp.GradScaler = ipex.cpu.autocast._grad_scaler.GradScaler #C torch._C._cuda_getCurrentRawStream = ipex._C._getCurrentStream @@ -152,20 +147,20 @@ def ipex_init(): #Fix functions with ipex: torch.cuda.mem_get_info = lambda device=None: [(torch.xpu.get_device_properties(device).total_memory - torch.xpu.memory_allocated(device)), torch.xpu.get_device_properties(device).total_memory] - torch._utils._get_available_device_type = lambda: "xpu" # pylint: disable=protected-access + torch._utils._get_available_device_type = lambda: "xpu" torch.has_cuda = True torch.cuda.has_half = True - torch.cuda.is_bf16_supported = True + torch.cuda.is_bf16_supported = lambda *args, **kwargs: True + torch.cuda.is_fp16_supported = lambda *args, **kwargs: True #torch.version.cuda = "11.7" #Breaks System Info - torch.cuda.get_device_capability = lambda: [11,7] + torch.cuda.get_device_capability = lambda *args, **kwargs: [11,7] torch.cuda.get_device_properties.major = 11 torch.cuda.get_device_properties.minor = 7 - torch.backends.cuda.sdp_kernel = return_null_context - torch.nn.DataParallel = DummyDataParallel - torch.cuda.ipc_collect = lambda: None - torch.cuda.utilization = lambda: 0 + torch.cuda.ipc_collect = lambda *args, **kwargs: None + torch.cuda.utilization = lambda *args, **kwargs: 0 ipex_hijacks() + attention_init() ipex_diffusers() except Exception as e: return False, e diff --git a/modules/intel/ipex/attention.py b/modules/intel/ipex/attention.py new file mode 100644 index 000000000..d7335bfaf --- /dev/null +++ b/modules/intel/ipex/attention.py @@ -0,0 +1,128 @@ +import torch +import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import + +# pylint: disable=protected-access, missing-function-docstring, line-too-long + +original_torch_bmm = torch.bmm +def torch_bmm(input, mat2, *, out=None): + if input.dtype != mat2.dtype: + mat2 = mat2.to(input.dtype) + + #ARC GPUs can't allocate more than 4GB to a single block, Slice it: + batch_size_attention, input_tokens, mat2_shape = input.shape[0], input.shape[1], mat2.shape[2] + block_multiply = 2.4 if input.dtype == torch.float32 else 1.2 + block_size = (batch_size_attention * input_tokens * mat2_shape) / 1024 * block_multiply #MB + split_slice_size = batch_size_attention + if block_size >= 4000: + do_split = True + #Find something divisible with the input_tokens + while ((split_slice_size * input_tokens * mat2_shape) / 1024 * block_multiply) > 4000: + split_slice_size = split_slice_size // 2 + if split_slice_size <= 1: + split_slice_size = 1 + break + else: + do_split = False + + split_block_size = (split_slice_size * input_tokens * mat2_shape) / 1024 * block_multiply #MB + split_2_slice_size = input_tokens + if split_block_size >= 4000: + do_split_2 = True + #Find something divisible with the input_tokens + while ((split_slice_size * split_2_slice_size * mat2_shape) / 1024 * block_multiply) > 4000: + split_2_slice_size = split_2_slice_size // 2 + if split_2_slice_size <= 1: + split_2_slice_size = 1 + break + else: + do_split_2 = False + + if do_split: + hidden_states = torch.zeros(input.shape[0], input.shape[1], mat2.shape[2], device=input.device, dtype=input.dtype) + for i in range(batch_size_attention // split_slice_size): + start_idx = i * split_slice_size + end_idx = (i + 1) * split_slice_size + if do_split_2: + for i2 in range(input_tokens // split_2_slice_size): # pylint: disable=invalid-name + start_idx_2 = i2 * split_2_slice_size + end_idx_2 = (i2 + 1) * split_2_slice_size + hidden_states[start_idx:end_idx, start_idx_2:end_idx_2] = original_torch_bmm( + input[start_idx:end_idx, start_idx_2:end_idx_2], + mat2[start_idx:end_idx, start_idx_2:end_idx_2], + out=out + ) + else: + hidden_states[start_idx:end_idx] = original_torch_bmm( + input[start_idx:end_idx], + mat2[start_idx:end_idx], + out=out + ) + else: + return original_torch_bmm(input, mat2, out=out) + return hidden_states + +original_scaled_dot_product_attention = torch.nn.functional.scaled_dot_product_attention +def scaled_dot_product_attention(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False): + #ARC GPUs can't allocate more than 4GB to a single block, Slice it: + shape_one, batch_size_attention, query_tokens, shape_four = query.shape + block_multiply = 2.4 if query.dtype == torch.float32 else 1.2 + block_size = (shape_one * batch_size_attention * query_tokens * shape_four) / 1024 * block_multiply #MB + split_slice_size = batch_size_attention + if block_size >= 4000: + do_split = True + #Find something divisible with the shape_one + while ((shape_one * split_slice_size * query_tokens * shape_four) / 1024 * block_multiply) > 4000: + split_slice_size = split_slice_size // 2 + if split_slice_size <= 1: + split_slice_size = 1 + break + else: + do_split = False + + split_block_size = (shape_one * split_slice_size * query_tokens * shape_four) / 1024 * block_multiply #MB + split_2_slice_size = query_tokens + if split_block_size >= 4000: + do_split_2 = True + #Find something divisible with the batch_size_attention + while ((shape_one * split_slice_size * split_2_slice_size * shape_four) / 1024 * block_multiply) > 4000: + split_2_slice_size = split_2_slice_size // 2 + if split_2_slice_size <= 1: + split_2_slice_size = 1 + break + else: + do_split_2 = False + + if do_split: + hidden_states = torch.zeros(query.shape, device=query.device, dtype=query.dtype) + for i in range(batch_size_attention // split_slice_size): + start_idx = i * split_slice_size + end_idx = (i + 1) * split_slice_size + if do_split_2: + for i2 in range(query_tokens // split_2_slice_size): # pylint: disable=invalid-name + start_idx_2 = i2 * split_2_slice_size + end_idx_2 = (i2 + 1) * split_2_slice_size + hidden_states[:, start_idx:end_idx, start_idx_2:end_idx_2] = original_scaled_dot_product_attention( + query[:, start_idx:end_idx, start_idx_2:end_idx_2], + key[:, start_idx:end_idx, start_idx_2:end_idx_2], + value[:, start_idx:end_idx, start_idx_2:end_idx_2], + attn_mask=attn_mask[:, start_idx:end_idx, start_idx_2:end_idx_2] if attn_mask is not None else attn_mask, + dropout_p=dropout_p, is_causal=is_causal + ) + else: + hidden_states[:, start_idx:end_idx] = original_scaled_dot_product_attention( + query[:, start_idx:end_idx], + key[:, start_idx:end_idx], + value[:, start_idx:end_idx], + attn_mask=attn_mask[:, start_idx:end_idx] if attn_mask is not None else attn_mask, + dropout_p=dropout_p, is_causal=is_causal + ) + else: + return original_scaled_dot_product_attention( + query, key, value, attn_mask=attn_mask, dropout_p=dropout_p, is_causal=is_causal + ) + return hidden_states + +def attention_init(): + #ARC GPUs can't allocate more than 4GB to a single block: + torch.bmm = torch_bmm + torch.nn.functional.scaled_dot_product_attention = scaled_dot_product_attention diff --git a/modules/intel/ipex/diffusers.py b/modules/intel/ipex/diffusers.py index e03592197..4c39896ed 100644 --- a/modules/intel/ipex/diffusers.py +++ b/modules/intel/ipex/diffusers.py @@ -1,11 +1,11 @@ import torch -import intel_extension_for_pytorch as ipex -import torch.nn.functional as F -import diffusers #0.20.2 +import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import +import diffusers #0.21.1 # pylint: disable=import-error +from diffusers.models.attention_processor import Attention -Attention = diffusers.models.attention_processor.Attention +# pylint: disable=protected-access, missing-function-docstring, line-too-long -class SlicedAttnProcessor: +class SlicedAttnProcessor: # pylint: disable=too-few-public-methods r""" Processor for implementing sliced attention. @@ -18,7 +18,7 @@ class SlicedAttnProcessor: def __init__(self, slice_size): self.slice_size = slice_size - def __call__(self, attn: Attention, hidden_states, encoder_hidden_states=None, attention_mask=None): + def __call__(self, attn: Attention, hidden_states, encoder_hidden_states=None, attention_mask=None): # pylint: disable=too-many-statements, too-many-locals, too-many-branches residual = hidden_states input_ndim = hidden_states.ndim @@ -74,7 +74,7 @@ class SlicedAttnProcessor: end_idx = (i + 1) * self.slice_size if do_split_2: - for i2 in range(query_tokens // split_2_slice_size): + for i2 in range(query_tokens // split_2_slice_size): # pylint: disable=invalid-name start_idx_2 = i2 * split_2_slice_size end_idx_2 = (i2 + 1) * split_2_slice_size @@ -114,147 +114,6 @@ class SlicedAttnProcessor: return hidden_states -class AttnProcessor2_0: - r""" - Processor for implementing scaled dot-product attention (enabled by default if you're using PyTorch 2.0). - """ - - def __init__(self): - if not hasattr(F, "scaled_dot_product_attention"): - raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.") - - def __call__( - self, - attn: Attention, - hidden_states, - encoder_hidden_states=None, - attention_mask=None, - temb=None, - ): - residual = hidden_states - - if attn.spatial_norm is not None: - hidden_states = attn.spatial_norm(hidden_states, temb) - - input_ndim = hidden_states.ndim - - if input_ndim == 4: - batch_size, channel, height, width = hidden_states.shape - hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2) - - batch_size, sequence_length, _ = ( - hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape - ) - - if attention_mask is not None: - attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size) - # scaled_dot_product_attention expects attention_mask shape to be - # (batch, heads, source_length, target_length) - attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1]) - - if attn.group_norm is not None: - hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2) - - query = attn.to_q(hidden_states) - - if encoder_hidden_states is None: - encoder_hidden_states = hidden_states - elif attn.norm_cross: - encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states) - - key = attn.to_k(encoder_hidden_states) - value = attn.to_v(encoder_hidden_states) - - inner_dim = key.shape[-1] - head_dim = inner_dim // attn.heads - - query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) - - key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) - value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2) - - #ARC GPUs can't allocate more than 4GB to a single block, Slice it: - shape_one, batch_size_attention, query_tokens, shape_four = query.shape - block_multiply = 2.4 if query.dtype == torch.float32 else 1.2 - block_size = (shape_one * batch_size_attention * query_tokens * shape_four) / 1024 * block_multiply #MB - split_slice_size = batch_size_attention - if block_size >= 4000: - do_split = True - #Find something divisible with the shape_one - while ((shape_one * split_slice_size * query_tokens * shape_four) / 1024 * block_multiply) > 4000: - split_slice_size = split_slice_size // 2 - if split_slice_size <= 1: - split_slice_size = 1 - break - else: - do_split = False - - split_block_size = (shape_one * split_slice_size * query_tokens * shape_four) / 1024 * block_multiply #MB - split_2_slice_size = query_tokens - if split_block_size >= 4000: - do_split_2 = True - #Find something divisible with the batch_size_attention - while ((shape_one * split_slice_size * split_2_slice_size * shape_four) / 1024 * block_multiply) > 4000: - split_2_slice_size = split_2_slice_size // 2 - if split_2_slice_size <= 1: - split_2_slice_size = 1 - break - else: - do_split_2 = False - - if do_split: - hidden_states = torch.zeros(query.shape, device=query.device, dtype=query.dtype) - for i in range(batch_size_attention // split_slice_size): - start_idx = i * split_slice_size - end_idx = (i + 1) * split_slice_size - if do_split_2: - for i2 in range(query_tokens // split_2_slice_size): - start_idx_2 = i2 * split_2_slice_size - end_idx_2 = (i2 + 1) * split_2_slice_size - - query_slice = query[:, start_idx:end_idx, start_idx_2:end_idx_2] - key_slice = key[:, start_idx:end_idx, start_idx_2:end_idx_2] - attn_mask_slice = attention_mask[:, start_idx:end_idx, start_idx_2:end_idx_2] if attention_mask is not None else None - - attn_slice = F.scaled_dot_product_attention( - query_slice, key_slice, value[:, start_idx:end_idx, start_idx_2:end_idx_2], - attn_mask=attn_mask_slice, dropout_p=0.0, is_causal=False - ) - hidden_states[:, start_idx:end_idx, start_idx_2:end_idx_2] = attn_slice - else: - query_slice = query[:, start_idx:end_idx] - key_slice = key[:, start_idx:end_idx] - attn_mask_slice = attention_mask[:, start_idx:end_idx] if attention_mask is not None else None - - attn_slice = F.scaled_dot_product_attention( - query_slice, key_slice, value[:, start_idx:end_idx], - attn_mask=attn_mask_slice, dropout_p=0.0, is_causal=False - ) - hidden_states[:, start_idx:end_idx] = attn_slice - else: - hidden_states = F.scaled_dot_product_attention( - query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False - ) - - hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim) - hidden_states = hidden_states.to(query.dtype) - - # linear proj - hidden_states = attn.to_out[0](hidden_states) - # dropout - hidden_states = attn.to_out[1](hidden_states) - - if input_ndim == 4: - hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width) - - if attn.residual_connection: - hidden_states = hidden_states + residual - - hidden_states = hidden_states / attn.rescale_output_factor - - return hidden_states - def ipex_diffusers(): #ARC GPUs can't allocate more than 4GB to a single block: diffusers.models.attention_processor.SlicedAttnProcessor = SlicedAttnProcessor - diffusers.models.attention_processor.AttnProcessor2_0 = AttnProcessor2_0 diff --git a/modules/intel/ipex/gradscaler.py b/modules/intel/ipex/gradscaler.py index 217f6cc7d..530212101 100644 --- a/modules/intel/ipex/gradscaler.py +++ b/modules/intel/ipex/gradscaler.py @@ -1,14 +1,15 @@ -import torch from collections import defaultdict -import intel_extension_for_pytorch as ipex -import intel_extension_for_pytorch._C as core -from modules import shared +import torch +import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import +import intel_extension_for_pytorch._C as core # pylint: disable=import-error, unused-import + +# pylint: disable=protected-access, missing-function-docstring, line-too-long OptState = ipex.cpu.autocast._grad_scaler.OptState _MultiDeviceReplicator = ipex.cpu.autocast._grad_scaler._MultiDeviceReplicator _refresh_per_optimizer_state = ipex.cpu.autocast._grad_scaler._refresh_per_optimizer_state -def _unscale_grads_(self, optimizer, inv_scale, found_inf, allow_fp16): +def _unscale_grads_(self, optimizer, inv_scale, found_inf, allow_fp16): # pylint: disable=unused-argument per_device_inv_scale = _MultiDeviceReplicator(inv_scale) per_device_found_inf = _MultiDeviceReplicator(found_inf) @@ -40,7 +41,7 @@ def _unscale_grads_(self, optimizer, inv_scale, found_inf, allow_fp16): else: to_unscale = param.grad - # TODO: is there a way to split by device and dtype without appending in the inner loop? + # -: is there a way to split by device and dtype without appending in the inner loop? to_unscale = to_unscale.to("cpu") per_device_and_dtype_grads[to_unscale.device][ to_unscale.dtype @@ -86,7 +87,7 @@ def unscale_(self, optimizer): optimizer_state = self._per_optimizer_states[id(optimizer)] - if optimizer_state["stage"] is OptState.UNSCALED: + if optimizer_state["stage"] is OptState.UNSCALED: # pylint: disable=no-else-raise raise RuntimeError( "unscale_() has already been called on this optimizer since the last update()." ) @@ -175,5 +176,4 @@ def gradscaler_init(): torch.xpu.amp.GradScaler._unscale_grads_ = _unscale_grads_ torch.xpu.amp.GradScaler.unscale_ = unscale_ torch.xpu.amp.GradScaler.update = update - - + return torch.xpu.amp.GradScaler diff --git a/modules/intel/ipex/hijacks.py b/modules/intel/ipex/hijacks.py index 9e1d9713e..0fd39137d 100644 --- a/modules/intel/ipex/hijacks.py +++ b/modules/intel/ipex/hijacks.py @@ -1,19 +1,63 @@ +import contextlib import torch -import intel_extension_for_pytorch as ipex -from modules import devices +import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import from modules.sd_hijack_utils import CondFunc +from modules import devices + +# pylint: disable=protected-access, missing-function-docstring, line-too-long, unnecessary-lambda, no-else-return + +def _shutdown_workers(self): + if torch.utils.data._utils is None or torch.utils.data._utils.python_exit_status is True or torch.utils.data._utils.python_exit_status is None: + return + if hasattr(self, "_shutdown") and not self._shutdown: + self._shutdown = True + try: + if hasattr(self, '_pin_memory_thread'): + self._pin_memory_thread_done_event.set() + self._worker_result_queue.put((None, None)) + self._pin_memory_thread.join() + self._worker_result_queue.cancel_join_thread() + self._worker_result_queue.close() + self._workers_done_event.set() + for worker_id in range(len(self._workers)): + if self._persistent_workers or self._workers_status[worker_id]: + self._mark_worker_as_unavailable(worker_id, shutdown=True) + for w in self._workers: # pylint: disable=invalid-name + w.join(timeout=torch.utils.data._utils.MP_STATUS_CHECK_INTERVAL) + for q in self._index_queues: # pylint: disable=invalid-name + q.cancel_join_thread() + q.close() + finally: + if self._worker_pids_set: + torch.utils.data._utils.signal_handling._remove_worker_pids(id(self)) + self._worker_pids_set = False + for w in self._workers: # pylint: disable=invalid-name + if w.is_alive(): + w.terminate() + +class DummyDataParallel(torch.nn.Module): # pylint: disable=missing-class-docstring, unused-argument, too-few-public-methods + def __new__(cls, module, device_ids=None, output_device=None, dim=0): # pylint: disable=unused-argument + if isinstance(device_ids, list) and len(device_ids) > 1: + print("IPEX backend doesn't support DataParallel on multiple XPU devices") + return module.to(devices.device) + +def return_null_context(*args, **kwargs): # pylint: disable=unused-argument + return contextlib.nullcontext() def check_device(device): return bool((isinstance(device, torch.device) and device.type == "cuda") or (isinstance(device, str) and "cuda" in device) or isinstance(device, int)) -def ipex_no_cuda(orig_func, *args, **kwargs): # pylint: disable=redefined-outer-name +def return_xpu(device): + return f"xpu:{device.split(':')[-1]}" if isinstance(device, str) and ":" in device else f"xpu:{device}" if isinstance(device, int) else torch.device(devices.device) if isinstance(device, torch.device) else devices.device + +def ipex_no_cuda(orig_func, *args, **kwargs): torch.cuda.is_available = lambda: False orig_func(*args, **kwargs) torch.cuda.is_available = torch.xpu.is_available original_autocast = torch.autocast def ipex_autocast(*args, **kwargs): - if args[0] == "cuda" or args[0] == "xpu": + if len(args) > 0 and args[0] == "cuda" or args[0] == "xpu": if "dtype" in kwargs: return original_autocast("xpu", *args[1:], **kwargs) else: @@ -23,66 +67,75 @@ def ipex_autocast(*args, **kwargs): #Embedding BF16 original_torch_cat = torch.cat -def torch_cat(input, *args, **kwargs): - if len(input) == 3 and (input[0].dtype != input[1].dtype or input[2].dtype != input[1].dtype): - return original_torch_cat([input[0].to(input[1].dtype), input[1], input[2].to(input[1].dtype)], *args, **kwargs) +def torch_cat(tensor, *args, **kwargs): + if len(tensor) == 3 and (tensor[0].dtype != tensor[1].dtype or tensor[2].dtype != tensor[1].dtype): + return original_torch_cat([tensor[0].to(tensor[1].dtype), tensor[1], tensor[2].to(tensor[1].dtype)], *args, **kwargs) else: - return original_torch_cat(input, *args, **kwargs) + return original_torch_cat(tensor, *args, **kwargs) #Latent antialias: original_interpolate = torch.nn.functional.interpolate -def interpolate(input, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None, antialias=False): - if antialias: - return original_interpolate(input.to("cpu", dtype=torch.float32), size=size, scale_factor=scale_factor, mode=mode, - align_corners=align_corners, recompute_scale_factor=recompute_scale_factor, antialias=antialias).to(devices.device, dtype=devices.dtype) +def interpolate(tensor, size=None, scale_factor=None, mode='nearest', align_corners=None, recompute_scale_factor=None, antialias=False): # pylint: disable=too-many-arguments + if antialias or align_corners is not None: + return_device = tensor.device + return_dtype = tensor.dtype + return original_interpolate(tensor.to("cpu", dtype=torch.float32), size=size, scale_factor=scale_factor, mode=mode, + align_corners=align_corners, recompute_scale_factor=recompute_scale_factor, antialias=antialias).to(return_device, dtype=return_dtype) else: - return original_interpolate(input, size=size, scale_factor=scale_factor, mode=mode, + return original_interpolate(tensor, size=size, scale_factor=scale_factor, mode=mode, align_corners=align_corners, recompute_scale_factor=recompute_scale_factor, antialias=antialias) +original_linalg_solve = torch.linalg.solve +def linalg_solve(A, B, *args, **kwargs): # pylint: disable=invalid-name + if A.device != torch.device("cpu") or B.device != torch.device("cpu"): + return_device = A.device + return original_linalg_solve(A.to("cpu"), B.to("cpu"), *args, **kwargs).to(return_device) + else: + return original_linalg_solve(A, B, *args, **kwargs) + def ipex_hijacks(): CondFunc('torch.Tensor.to', - lambda orig_func, self, device=None, *args, **kwargs: orig_func(self, devices.device, *args, **kwargs), + lambda orig_func, self, device=None, *args, **kwargs: orig_func(self, return_xpu(device), *args, **kwargs), lambda orig_func, self, device=None, *args, **kwargs: check_device(device)) CondFunc('torch.Tensor.cuda', - lambda orig_func, self, device=None, *args, **kwargs: orig_func(self, devices.device, *args, **kwargs), + lambda orig_func, self, device=None, *args, **kwargs: orig_func(self, return_xpu(device), *args, **kwargs), lambda orig_func, self, device=None, *args, **kwargs: check_device(device)) CondFunc('torch.empty', - lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=devices.device, **kwargs), + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), lambda orig_func, *args, device=None, **kwargs: check_device(device)) CondFunc('torch.load', - lambda orig_func, *args, map_location=None, **kwargs: orig_func(*args, devices.device, **kwargs), + lambda orig_func, *args, map_location=None, **kwargs: orig_func(*args, return_xpu(map_location), **kwargs), lambda orig_func, *args, map_location=None, **kwargs: map_location is None or check_device(map_location)) CondFunc('torch.randn', - lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=devices.device, **kwargs), + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), lambda orig_func, *args, device=None, **kwargs: check_device(device)) CondFunc('torch.ones', - lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=devices.device, **kwargs), + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), lambda orig_func, *args, device=None, **kwargs: check_device(device)) CondFunc('torch.zeros', - lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=devices.device, **kwargs), + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), lambda orig_func, *args, device=None, **kwargs: check_device(device)) CondFunc('torch.tensor', - lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=devices.device, **kwargs), + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), + lambda orig_func, *args, device=None, **kwargs: check_device(device)) + CondFunc('torch.linspace', + lambda orig_func, *args, device=None, **kwargs: orig_func(*args, device=return_xpu(device), **kwargs), lambda orig_func, *args, device=None, **kwargs: check_device(device)) CondFunc('torch.Generator', - lambda orig_func, device: torch.xpu.Generator(device), - lambda orig_func, device: device != torch.device("cpu") and device != "cpu") - #Crashes the GPU: - CondFunc('torch.linalg.solve', - lambda orig_func, A, B, *args, **kwargs: orig_func(A.to("cpu"), B.to("cpu"), *args, **kwargs).to(devices.device), - lambda orig_func, A, B, *args, **kwargs: A.device != torch.device("cpu") or B.device != torch.device("cpu")) + lambda orig_func, device=None: torch.xpu.Generator(device), + lambda orig_func, device=None: device is not None and device != torch.device("cpu") and device != "cpu") #TiledVAE and ControlNet: CondFunc('torch.batch_norm', lambda orig_func, input, weight, bias, *args, **kwargs: orig_func(input, - weight if weight is not None else torch.ones(input.size()[1], device=devices.device), - bias if bias is not None else torch.zeros(input.size()[1], device=devices.device), *args, **kwargs), + weight if weight is not None else torch.ones(input.size()[1], device=input.device), + bias if bias is not None else torch.zeros(input.size()[1], device=input.device), *args, **kwargs), lambda orig_func, input, *args, **kwargs: input.device != torch.device("cpu")) CondFunc('torch.instance_norm', lambda orig_func, input, weight, bias, *args, **kwargs: orig_func(input, - weight if weight is not None else torch.ones(input.size()[1], device=devices.device), - bias if bias is not None else torch.zeros(input.size()[1], device=devices.device), *args, **kwargs), + weight if weight is not None else torch.ones(input.size()[1], device=input.device), + bias if bias is not None else torch.zeros(input.size()[1], device=input.device), *args, **kwargs), lambda orig_func, input, *args, **kwargs: input.device != torch.device("cpu")) #Functions with dtype errors: @@ -94,10 +147,9 @@ def ipex_hijacks(): CondFunc('torch.nn.modules.linear.Linear.forward', lambda orig_func, self, input: orig_func(self, input.to(self.weight.data.dtype)), lambda orig_func, self, input: input.dtype != self.weight.data.dtype) - #Embedding FP32: - CondFunc('torch.bmm', - lambda orig_func, input, mat2, *args, **kwargs: orig_func(input, mat2.to(input.dtype), *args, **kwargs), - lambda orig_func, input, mat2, *args, **kwargs: input.dtype != mat2.dtype) + CondFunc('torch.nn.modules.conv.Conv2d.forward', + lambda orig_func, self, input: orig_func(self, input.to(self.weight.data.dtype)), + lambda orig_func, self, input: input.dtype != self.weight.data.dtype) #BF16: CondFunc('torch.nn.functional.layer_norm', lambda orig_func, input, normalized_shape=None, weight=None, *args, **kwargs: @@ -118,6 +170,10 @@ def ipex_hijacks(): lambda orig_func, *args, **kwargs: True) #Functions that make compile mad with CondFunc: + torch.utils.data.dataloader._MultiProcessingDataLoaderIter._shutdown_workers = _shutdown_workers + torch.nn.DataParallel = DummyDataParallel torch.autocast = ipex_autocast torch.cat = torch_cat + torch.linalg.solve = linalg_solve torch.nn.functional.interpolate = interpolate + torch.backends.cuda.sdp_kernel = return_null_context diff --git a/modules/intel/openvino/__init__.py b/modules/intel/openvino/__init__.py index b39eb3cdb..b6bcec4ab 100644 --- a/modules/intel/openvino/__init__.py +++ b/modules/intel/openvino/__init__.py @@ -8,7 +8,7 @@ from torch._dynamo.backends.registry import register_backend from torch.fx.experimental.proxy_tensor import make_fx from torch._inductor.compile_fx import compile_fx from hashlib import sha256 -from modules import shared +from modules import shared, devices @register_backend @fake_tensor_unsupported @@ -89,7 +89,7 @@ def openvino_fx(subgraph, example_inputs): else: example_inputs.reverse() model = make_fx(subgraph)(*example_inputs) - with torch.no_grad(): + with devices.inference_context(): model.eval() partitioner = Partitioner() compiled_model = partitioner.make_partitions(model) diff --git a/modules/interrogate.py b/modules/interrogate.py index be465d9b2..d95209792 100644 --- a/modules/interrogate.py +++ b/modules/interrogate.py @@ -174,15 +174,14 @@ class InterrogateModels: transforms.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)) ])(pil_image).unsqueeze(0).type(self.dtype).to(devices.device_interrogate) - with torch.no_grad(): + with devices.inference_context(): caption = self.blip_model.generate(gpu_image, sample=False, num_beams=shared.opts.interrogate_clip_num_beams, min_length=shared.opts.interrogate_clip_min_length, max_length=shared.opts.interrogate_clip_max_length) return caption[0] def interrogate(self, pil_image): res = "" - shared.state.begin() - shared.state.job = 'interrogate' + shared.state.begin('interrogate') try: if shared.cmd_opts.lowvram or shared.cmd_opts.medvram: lowvram.send_everything_to_cpu() @@ -198,7 +197,7 @@ class InterrogateModels: clip_image = self.clip_preprocess(pil_image).unsqueeze(0).type(self.dtype).to(devices.device_interrogate) - with torch.no_grad(), devices.autocast(): + with devices.inference_context(), devices.autocast(): image_features = self.clip_model.encode_image(clip_image).type(self.dtype) image_features /= image_features.norm(dim=-1, keepdim=True) diff --git a/modules/loader.py b/modules/loader.py index f9c68c5d4..54dafa9e4 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -8,7 +8,6 @@ from modules import timer, errors initialized = False logging.getLogger("DeepSpeed").disabled = True import torch # pylint: disable=C0411 -errors.log.debug(f'Loaded Torch=={torch.__version__}') try: import intel_extension_for_pytorch as ipex # pylint: disable=import-error, unused-import errors.log.debug(f'Loaded IPEX=={ipex.__version__}') @@ -29,10 +28,9 @@ timer.startup.record("torch") from fastapi import FastAPI # pylint: disable=W0611,C0411 import gradio # pylint: disable=W0611,C0411 -errors.log.debug(f'Loaded Gradio=={gradio.__version__}') timer.startup.record("gradio") errors.install([gradio]) import diffusers # pylint: disable=W0611,C0411 -errors.log.debug(f'Loaded Diffusers=={diffusers.__version__}') timer.startup.record("diffusers") +errors.log.debug(f'Loaded packages: torch={torch.__version__} diffusers={diffusers.__version__} gradio={gradio.__version__}') diff --git a/modules/modelloader.py b/modules/modelloader.py index bb0639389..7d99f1714 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -4,6 +4,7 @@ import shutil import importlib from typing import Dict from urllib.parse import urlparse +import PIL.Image as Image from modules import shared from modules.upscaler import Upscaler, UpscalerLanczos, UpscalerNearest, UpscalerNone from modules.paths import script_path, models_path @@ -59,17 +60,17 @@ def download_civit_preview(model_path: str, preview_url: str): import rich.progress as p _, ext = os.path.splitext(preview_url) model_name, _ = os.path.splitext(os.path.basename(model_path)) - preview_file = os.path.splitext(model_path)[0] + ext + preview_file = f'{os.path.splitext(model_path)[0]}{ext}' if '.safetensors' in model_path.lower() else f'{model_path}{ext}' res = f'CivitAI download: name={model_name} url={preview_url}' req = requests.get(preview_url, stream=True, timeout=30) total_size = int(req.headers.get('content-length', 0)) block_size = 16384 # 16KB blocks written = 0 - shared.state.begin() - shared.state.job = 'download preview' + img = None + shared.state.begin('civitai-download-preview') try: with open(preview_file, 'wb') as f: - with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn()) as progress: + with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), console=shared.console) as progress: task = progress.add_task(description="Download starting", total=total_size) for data in req.iter_content(block_size): written = written + len(data) @@ -78,13 +79,14 @@ def download_civit_preview(model_path: str, preview_url: str): if written < 1024: # min threshold os.remove(preview_file) raise ValueError(f'removed invalid download: bytes={written}') + img = Image.open(preview_file) except Exception as e: shared.log.error(f'CivitAI download error: name={model_name} url={preview_url} {e}') - if total_size == written: - shared.log.info(f'{res} size={total_size}') - else: - shared.log.error(f'{res} size={total_size} written={written}') shared.state.end() + if img is None: + return res + shared.log.info(f'{res} size={total_size} image={img.size}') + img.close() return res @@ -105,18 +107,17 @@ def download_civit_model(model_url: str, model_name: str, model_path: str, model total_size = int(req.headers.get('content-length', 0)) block_size = 16384 # 16KB blocks written = 0 - shared.state.begin() - shared.state.job = 'download model' + shared.state.begin('civitai-download-model') try: with open(model_file, 'wb') as f: - with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn()) as progress: + with p.Progress(p.TextColumn('[cyan]{task.description}'), p.DownloadColumn(), p.BarColumn(), p.TaskProgressColumn(), p.TimeRemainingColumn(), p.TimeElapsedColumn(), p.TransferSpeedColumn(), console=shared.console) as progress: task = progress.add_task(description="Download starting", total=total_size) # for data in tqdm(req.iter_content(block_size), total=total_size//1024, unit='KB', unit_scale=False): for data in req.iter_content(block_size): written = written + len(data) f.write(data) progress.update(task, advance=block_size, description="Downloading") - if written < 1024 * 1024 * 1024: # min threshold + if written < 1024 * 1024: # min threshold os.remove(model_file) raise ValueError(f'removed invalid download: bytes={written}') if preview is not None: @@ -136,8 +137,7 @@ def download_civit_model(model_url: str, model_name: str, model_path: str, model def download_diffusers_model(hub_id: str, cache_dir: str = None, download_config: Dict[str, str] = None, token = None, variant = None, revision = None, mirror = None): from diffusers import DiffusionPipeline import huggingface_hub as hf - shared.state.begin() - shared.state.job = 'download model' + shared.state.begin('huggingface-download-model') if download_config is None: download_config = { "force_download": False, @@ -157,26 +157,28 @@ def download_diffusers_model(hub_id: str, cache_dir: str = None, download_config if token is not None and len(token) > 2: shared.log.debug(f"Diffusers authentication: {token}") hf.login(token) - pipeline_dir = DiffusionPipeline.download(hub_id, **download_config) + pipeline_dir = None try: - model_info_dict = hf.model_info(hub_id).cardData # pylint: disable=no-member # TODO Diffusers is this real error? + pipeline_dir = DiffusionPipeline.download(hub_id, **download_config) + except Exception as e: + shared.log.error(f"Diffusers download error: {hub_id} {e}") + try: + model_info_dict = hf.model_info(hub_id).cardData if pipeline_dir is not None else None # pylint: disable=no-member # TODO Diffusers is this real error? except Exception: model_info_dict = None - # some checkpoints need to be downloaded as "hidden" as they just serve as pre- or post-pipelines of other pipelines - if model_info_dict is not None and "prior" in model_info_dict: + if model_info_dict is not None and "prior" in model_info_dict: # some checkpoints need to be downloaded as "hidden" as they just serve as pre- or post-pipelines of other pipelines download_dir = DiffusionPipeline.download(model_info_dict["prior"][0], **download_config) model_info_dict["prior"] = download_dir - # mark prior as hidden - with open(os.path.join(download_dir, "hidden"), "w", encoding="utf-8") as f: + with open(os.path.join(download_dir, "hidden"), "w", encoding="utf-8") as f: # mark prior as hidden f.write("True") - shared.writefile(model_info_dict, os.path.join(pipeline_dir, "model_info.json")) + if pipeline_dir is not None: + shared.writefile(model_info_dict, os.path.join(pipeline_dir, "model_info.json")) shared.state.end() return pipeline_dir def load_diffusers_models(model_path: str, command_path: str = None): t0 = time.time() - import huggingface_hub as hf places = [] places.append(model_path) if command_path is not None and command_path != model_path: @@ -187,12 +189,35 @@ def load_diffusers_models(model_path: str, command_path: str = None): if not os.path.isdir(place): continue try: + """ + import huggingface_hub as hf res = hf.scan_cache_dir(cache_dir=place) for r in list(res.repos): cache_path = os.path.join(r.repo_path, "snapshots", list(r.revisions)[-1].commit_hash) diffuser_repos.append({ 'name': r.repo_id, 'filename': r.repo_id, 'path': cache_path, 'size': r.size_on_disk, 'mtime': r.last_modified, 'hash': list(r.revisions)[-1].commit_hash, 'model_info': str(os.path.join(cache_path, "model_info.json")) }) if not os.path.isfile(os.path.join(cache_path, "hidden")): output.append(str(r.repo_id)) + """ + for folder in os.listdir(place): + try: + if "--" not in folder: + continue + _, name = folder.split("--", maxsplit=1) + name = name.replace("--", "/") + snapshots = os.listdir(os.path.join(place, folder, "snapshots")) + if len(snapshots) == 0: + shared.log.warning(f"Diffusers folder has no snapshots: location={place} folder={folder} name={name}") + continue + commit = snapshots[-1] + folder = os.path.join(place, folder, 'snapshots', commit) + mtime = os.path.getmtime(folder) + info = os.path.join(folder, "model_info.json") + diffuser_repos.append({ 'name': name, 'filename': name, 'path': folder, 'hash': commit, 'mtime': mtime, 'model_info': info }) + if os.path.exists(os.path.join(folder, 'hidden')): + continue + output.append(name) + except Exception as e: + shared.log.error(f"Error analyzing diffusers model: {place}/{folder} {e}") except Exception as e: shared.log.error(f"Error listing diffusers: {place} {e}") shared.log.debug(f'Scanning diffusers cache: {model_path} {command_path} items={len(output)} time={time.time()-t0:.2f}s') @@ -300,6 +325,19 @@ def extension_filter(ext_filter=None, ext_blacklist=None): return (not ext_filter or any(fp.upper().endswith(ew) for ew in ext_filter)) and (not ext_blacklist or not any(fp.upper().endswith(ew) for ew in ext_blacklist)) return filter +def load_file_from_url(url: str, *, model_dir: str, progress: bool = True, file_name = None): + """Download a file from url into model_dir, using the file present if possible. Returns the path to the downloaded file.""" + os.makedirs(model_dir, exist_ok=True) + if not file_name: + parts = urlparse(url) + file_name = os.path.basename(parts.path) + cached_file = os.path.abspath(os.path.join(model_dir, file_name)) + if not os.path.exists(cached_file): + shared.log.info(f'Downloading: url="{url}" file={cached_file}') + from torch.hub import download_url_to_file + download_url_to_file(url, cached_file, progress=progress) + return cached_file + def load_models(model_path: str, model_url: str = None, command_path: str = None, ext_filter=None, download_name=None, ext_blacklist=None) -> list: """ @@ -317,8 +355,7 @@ def load_models(model_path: str, model_url: str = None, command_path: str = None output:list = [*filter(extension_filter(ext_filter, ext_blacklist), directory_files(*places))] if model_url is not None and len(output) == 0: if download_name is not None: - from basicsr.utils.download_util import load_file_from_url - dl = load_file_from_url(model_url, places[0], True, download_name) + dl = load_file_from_url(model_url, model_dir=places[0], progress=True, file_name=download_name) output.append(dl) else: output.append(model_url) @@ -388,7 +425,6 @@ def move_files(src_path: str, dest_path: str, ext_filter: str = None): pass - def load_upscalers(): # We can only do this 'magic' method to dynamically load upscalers if they are referenced, so we'll try to import any _model.py files before looking in __subclasses__ modules_dir = os.path.join(shared.script_path, "modules") @@ -418,6 +454,6 @@ def load_upscalers(): datas += scaler.scalers shared.sd_upscalers = sorted( datas, - # Special case for UpscalerNone keeps it at the beginning of the list. - key=lambda x: x.name.lower() if not isinstance(x.scaler, (UpscalerNone, UpscalerLanczos, UpscalerNearest)) else "" + key=lambda x: x.name.lower() if not isinstance(x.scaler, (UpscalerNone, UpscalerLanczos, UpscalerNearest)) else "" # Special case for UpscalerNone keeps it at the beginning of the list. ) + shared.log.debug(f"Loaded upscalers: items={len(shared.sd_upscalers)}") diff --git a/modules/models/diffusion/uni_pc/sampler.py b/modules/models/diffusion/uni_pc/sampler.py index 21991d2b9..1800b9c2a 100644 --- a/modules/models/diffusion/uni_pc/sampler.py +++ b/modules/models/diffusion/uni_pc/sampler.py @@ -21,7 +21,7 @@ class UniPCSampler(object): # persist steps so we can eventually find denoising strength self.inflated_steps = ddim_num_steps - @torch.no_grad() + @devices.inference_context() def stochastic_encode(self, x0, t, use_original_steps=False, noise=None): if noise is None: noise = torch.randn_like(x0) @@ -119,7 +119,7 @@ class UniPCSampler(object): self.after_sample = after_sample self.after_update = after_update - @torch.no_grad() + @devices.inference_context() def sample(self, S, batch_size, diff --git a/modules/models/diffusion/uni_pc/uni_pc.py b/modules/models/diffusion/uni_pc/uni_pc.py index 6dddbcfbd..fa16a0b48 100644 --- a/modules/models/diffusion/uni_pc/uni_pc.py +++ b/modules/models/diffusion/uni_pc/uni_pc.py @@ -3,7 +3,7 @@ import torch.nn.functional as F import math import time from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeRemainingColumn, TimeElapsedColumn -from modules import shared +from modules import shared, devices class NoiseScheduleVP: @@ -757,10 +757,10 @@ class UniPC: #print(f"Running UniPC Sampling with {timesteps.shape[0]} timesteps, order {order}") assert steps >= order, "UniPC order must be < sampling steps" assert timesteps.shape[0] - 1 == steps - with Progress(TextColumn('[cyan]{task.description}'), BarColumn(), TaskProgressColumn(), TimeRemainingColumn(), TimeElapsedColumn()) as progress: + with Progress(TextColumn('[cyan]{task.description}'), BarColumn(), TaskProgressColumn(), TimeRemainingColumn(), TimeElapsedColumn(), console=shared.console) as progress: task = progress.add_task(description="Initializing", total=steps) t = time.time() - with torch.no_grad(): + with devices.inference_context(): vec_t = timesteps[0].expand((x.shape[0])) model_prev_list = [self.model_fn(x, vec_t)] t_prev_list = [vec_t] diff --git a/modules/paths.py b/modules/paths.py index f4b991f5c..a502be0cf 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -62,13 +62,18 @@ def create_paths(opts, log=None): def fix_path(folder): tgt = opts.data.get(folder, None) or opts.data_labels[folder].default if tgt is None or tgt == '': - return - if os.path.isabs(tgt) or (len(data_path) > 0 and tgt.startswith(data_path)) and not tgt.startswith(script_path): - return + return tgt + if len(data_path) > 0 and tgt.startswith(data_path): # path is already relative to data_path + return tgt fullpath = os.path.join(data_path, tgt) - relpath = os.path.relpath(fullpath, script_path) - opts.data[folder] = relpath - return + if len(data_path) > 0 and os.path.isabs(data_path): + return fullpath + try: + relpath = os.path.relpath(fullpath, script_path) + opts.data[folder] = relpath + except: + opts.data[folder] = fullpath + return opts.data[folder] create_path(data_path) create_path(script_path) diff --git a/modules/paths_internal.py b/modules/paths_internal.py index 44afc4ad3..83c097e46 100644 --- a/modules/paths_internal.py +++ b/modules/paths_internal.py @@ -18,4 +18,4 @@ cmd_opts_pre = parser_pre.parse_known_args()[0] data_path = cmd_opts_pre.data_dir models_path = cmd_opts_pre.models_dir if os.path.isabs(cmd_opts_pre.models_dir) else os.path.join(data_path, cmd_opts_pre.models_dir) extensions_dir = os.path.join(data_path, "extensions") -extensions_builtin_dir = os.path.join(script_path, "extensions-builtin") +extensions_builtin_dir = "extensions-builtin" diff --git a/modules/postprocessing.py b/modules/postprocessing.py index 4ff648461..c82aac540 100644 --- a/modules/postprocessing.py +++ b/modules/postprocessing.py @@ -10,8 +10,7 @@ from modules.shared import opts def run_postprocessing(extras_mode, image, image_folder: List[tempfile.NamedTemporaryFile], input_dir, output_dir, show_extras_results, *args, save_output: bool = True): devices.torch_gc() - shared.state.begin() - shared.state.job = 'extras' + shared.state.begin('extras') image_data = [] image_names = [] image_ext = [] diff --git a/modules/processing.py b/modules/processing.py index 50870d405..b5e437097 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -158,6 +158,7 @@ class StableDiffusionProcessing: self.clip_skip = clip_skip self.iteration = 0 self.is_hr_pass = False + self.hr_force = False self.enable_hr = None self.refiner_steps = 5 self.refiner_start = 0 @@ -211,7 +212,6 @@ class StableDiffusionProcessing: conditioning_mask = np.array(image_mask.convert("L")) conditioning_mask = conditioning_mask.astype(np.float32) / 255.0 conditioning_mask = torch.from_numpy(conditioning_mask[None, None]) - # Inpainting model uses a discretized mask as input, so we round to either 1.0 or 0.0 conditioning_mask = torch.round(conditioning_mask) else: @@ -488,11 +488,11 @@ def create_infotext(p: StableDiffusionProcessing, all_prompts, all_seeds, all_su "Backend": 'Diffusers' if shared.backend == shared.Backend.DIFFUSERS else 'Original', "Version": git_commit, "Comment": comment, - "Operations": ', '.join(list(set(p.ops))).replace('"', '') if len(p.ops) > 0 else None, + "Operations": '; '.join(p.ops).replace('"', '') if len(p.ops) > 0 else 'none', } if 'txt2img' in p.ops: pass - if 'hires' in p.ops: + if 'hires' in p.ops or 'upscale' in p.ops: args["Hires steps"] = p.hr_second_pass_steps args["Hires upscaler"] = p.hr_upscaler args["Hires upscale"] = p.hr_scale @@ -707,7 +707,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: return '' ema_scope_context = p.sd_model.ema_scope if shared.backend == shared.Backend.ORIGINAL else nullcontext - with torch.no_grad(), ema_scope_context(): + with devices.inference_context(), ema_scope_context(): t0 = time.time() with devices.autocast(): p.init(p.all_prompts, p.all_seeds, p.all_subseeds) @@ -800,8 +800,13 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: for i, x_sample in enumerate(x_samples_ddim): p.batch_index = i - 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) + if type(x_sample) == Image.Image: + 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: if shared.opts.save and not p.do_not_save_samples and shared.opts.save_images_before_face_restoration: orig = p.restore_faces @@ -811,7 +816,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: images.save_image(Image.fromarray(x_sample), path=p.outpath_samples, basename="", seed=p.seeds[i], prompt=p.prompts[i], extension=shared.opts.samples_format, info=info, p=p, suffix="-before-face-restoration") p.ops.append('face') x_sample = modules.face_restoration.restore_faces(x_sample) - image = Image.fromarray(x_sample) + image = Image.fromarray(x_sample) if p.scripts is not None: pp = modules.scripts.PostprocessImageArgs(image) p.scripts.postprocess_image(p, pp) @@ -853,8 +858,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.color_corrections = None index_of_first_image = 0 - unwanted_grid_because_of_img_count = len(output_images) < 2 and shared.opts.grid_only_if_multiple - if (shared.opts.return_grid or shared.opts.grid_save) and not p.do_not_save_grid and not unwanted_grid_because_of_img_count: + if (shared.opts.return_grid or shared.opts.grid_save) and not p.do_not_save_grid and len(output_images) > 1: if images.check_grid_size(output_images): grid = images.image_grid(output_images, p.batch_size) if shared.opts.return_grid: @@ -895,15 +899,15 @@ def old_hires_fix_first_pass_dimensions(width, height): class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): - sampler = None - def __init__(self, enable_hr: bool = False, denoising_strength: float = 0.75, firstphase_width: int = 0, firstphase_height: int = 0, hr_scale: float = 2.0, hr_upscaler: str = None, hr_second_pass_steps: int = 0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_steps: int = 5, refiner_start: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): + def __init__(self, enable_hr: bool = False, denoising_strength: float = 0.75, firstphase_width: int = 0, firstphase_height: int = 0, hr_scale: float = 2.0, hr_force: bool = False, hr_upscaler: str = None, hr_second_pass_steps: int = 0, hr_resize_x: int = 0, hr_resize_y: int = 0, refiner_steps: int = 5, refiner_start: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): super().__init__(**kwargs) self.enable_hr = enable_hr self.denoising_strength = denoising_strength self.hr_scale = hr_scale self.hr_upscaler = hr_upscaler + self.hr_force = hr_force self.hr_second_pass_steps = hr_second_pass_steps self.hr_resize_x = hr_resize_x self.hr_resize_y = hr_resize_y @@ -921,11 +925,11 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): self.refiner_start = refiner_start self.refiner_prompt = refiner_prompt self.refiner_negative = refiner_negative + self.sampler = None def init(self, all_prompts, all_seeds, all_subseeds): if shared.backend == shared.Backend.DIFFUSERS: modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.TEXT_2_IMAGE) - self.width = self.width or 512 self.height = self.height or 512 @@ -985,13 +989,14 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): if shared.backend == shared.Backend.DIFFUSERS: modules.sd_models.set_diffuser_pipe(self.sd_model, modules.sd_models.DiffusersTaskType.TEXT_2_IMAGE) + latent_scale_mode = shared.latent_upscale_modes.get(self.hr_upscaler, None) if self.hr_upscaler is not None else shared.latent_upscale_modes.get(shared.latent_upscale_default_mode, "None") + if self.enable_hr and (latent_scale_mode is None or self.hr_force): + if len([x for x in shared.sd_upscalers if x.name == self.hr_upscaler]) == 0: + shared.log.warning(f"Cannot find upscaler for hires: {self.hr_upscaler}") + self.enable_hr = False + self.ops.append('txt2img') self.sampler = modules.sd_samplers.create_sampler(self.sampler_name, self.sd_model) - latent_scale_mode = shared.latent_upscale_modes.get(self.hr_upscaler, None) if self.hr_upscaler is not None else shared.latent_upscale_modes.get(shared.latent_upscale_default_mode, "None") - if self.enable_hr and latent_scale_mode is None: - if len([x for x in shared.sd_upscalers if x.name == self.hr_upscaler]) == 0: - shared.log.warning("Could not find upscaler to use with hrfix") - self.enable_hr = False x = create_random_tensors([4, self.height // 8, self.width // 8], seeds=seeds, subseeds=subseeds, subseed_strength=self.subseed_strength, seed_resize_from_h=self.seed_resize_from_h, seed_resize_from_w=self.seed_resize_from_w, p=self) samples = self.sampler.sample(self, x, conditioning, unconditional_conditioning, image_conditioning=self.txt2img_image_conditioning(x)) if not self.enable_hr or shared.state.interrupted or shared.state.skipped: @@ -1002,16 +1007,9 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): self.ops.append('hires') target_width = self.hr_upscale_to_x target_height = self.hr_upscale_to_y - - if latent_scale_mode is not None: - for i in range(samples.shape[0]): - save_intermediate(samples, i) - samples = torch.nn.functional.interpolate(samples, size=(target_height // 8, target_width // 8), mode=latent_scale_mode["mode"], antialias=latent_scale_mode["antialias"]) - if getattr(self, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) < 1.0: - image_conditioning = self.img2img_image_conditioning(decode_first_stage(self.sd_model, samples.to(dtype=devices.dtype_vae)), samples) - else: - image_conditioning = self.txt2img_image_conditioning(samples.to(dtype=devices.dtype_vae)) - else: + for i in range(samples.shape[0]): + save_intermediate(samples, i) + if latent_scale_mode is None or self.hr_force: # non-latent upscaling decoded_samples = decode_first_stage(self.sd_model, samples.to(dtype=devices.dtype_vae)) lowres_samples = torch.clamp((decoded_samples + 1.0) / 2.0, min=0.0, max=1.0) batch_images = [] @@ -1036,24 +1034,30 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing): else: samples = self.sd_model.get_first_stage_encoding(self.sd_model.encode_first_stage(decoded_samples)) image_conditioning = self.img2img_image_conditioning(decoded_samples, samples) - shared.state.nextjob() - if self.latent_sampler == "PLMS": - self.latent_sampler = 'UniPC' - self.sampler = modules.sd_samplers.create_sampler(self.latent_sampler or self.sampler_name, self.sd_model) - samples = samples[:, :, self.truncate_y//2:samples.shape[2]-(self.truncate_y+1)//2, self.truncate_x//2:samples.shape[3]-(self.truncate_x+1)//2] - noise = create_random_tensors(samples.shape[1:], seeds=seeds, subseeds=subseeds, subseed_strength=subseed_strength, p=self) + else: + samples = torch.nn.functional.interpolate(samples, size=(target_height // 8, target_width // 8), mode=latent_scale_mode["mode"], antialias=latent_scale_mode["antialias"]) + if getattr(self, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) < 1.0: + image_conditioning = self.img2img_image_conditioning(decode_first_stage(self.sd_model, samples.to(dtype=devices.dtype_vae)), samples) + else: + image_conditioning = self.txt2img_image_conditioning(samples.to(dtype=devices.dtype_vae)) + if self.latent_sampler == "PLMS": + self.latent_sampler = 'UniPC' + if self.hr_force or latent_scale_mode is not None: + devices.torch_gc() # GC now before running the next img2img to prevent running out of memory + self.sampler = modules.sd_samplers.create_sampler(self.latent_sampler or self.sampler_name, self.sd_model) + samples = samples[:, :, self.truncate_y//2:samples.shape[2]-(self.truncate_y+1)//2, self.truncate_x//2:samples.shape[3]-(self.truncate_x+1)//2] + noise = create_random_tensors(samples.shape[1:], seeds=seeds, subseeds=subseeds, subseed_strength=subseed_strength, p=self) + modules.sd_models.apply_token_merging(self.sd_model, self.get_token_merging_ratio(for_hr=True)) + samples = self.sampler.sample_img2img(self, samples, noise, conditioning, unconditional_conditioning, steps=self.hr_second_pass_steps or self.steps, image_conditioning=image_conditioning) + modules.sd_models.apply_token_merging(self.sd_model, self.get_token_merging_ratio()) x = None - devices.torch_gc() # GC now before running the next img2img to prevent running out of memory - modules.sd_models.apply_token_merging(self.sd_model, self.get_token_merging_ratio(for_hr=True)) - samples = self.sampler.sample_img2img(self, samples, noise, conditioning, unconditional_conditioning, steps=self.hr_second_pass_steps or self.steps, image_conditioning=image_conditioning) - modules.sd_models.apply_token_merging(self.sd_model, self.get_token_merging_ratio()) + shared.state.nextjob() self.is_hr_pass = False return samples class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): - sampler = None def __init__(self, init_images: list = None, resize_mode: int = 0, denoising_strength: float = 0.3, image_cfg_scale: float = None, mask: Any = None, mask_blur: int = 4, inpainting_fill: int = 0, inpaint_full_res: bool = True, inpaint_full_res_padding: int = 0, inpainting_mask_invert: int = 0, initial_noise_multiplier: float = None, refiner_steps: int = 5, refiner_start: float = 0, refiner_prompt: str = '', refiner_negative: str = '', **kwargs): super().__init__(**kwargs) @@ -1081,6 +1085,7 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing): self.enable_hr = None self.is_batch = False self.scale_by = 1.0 + self.sampler = None def init(self, all_prompts, all_seeds, all_subseeds): if shared.backend == shared.Backend.DIFFUSERS and self.image_mask is None: diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 02291c986..d22c73a13 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -2,6 +2,7 @@ import time import inspect import typing import torch +import torchvision.transforms.functional as TF import modules.devices as devices import modules.shared as shared import modules.sd_samplers as sd_samplers @@ -24,20 +25,24 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro results = [] if p.enable_hr and p.hr_upscaler != 'None' and p.denoising_strength > 0 and len(getattr(p, 'init_images', [])) == 0: p.is_hr_pass = True - is_refiner_enabled = p.enable_hr and p.refiner_steps > 0 and shared.sd_refiner is not None + is_refiner_enabled = p.enable_hr and p.refiner_steps > 0 and p.refiner_start > 0 and p.refiner_start < 1 and shared.sd_refiner is not None def hires_resize(latents): # input=latents output=pil latent_upscaler = shared.latent_upscale_modes.get(p.hr_upscaler, None) shared.log.info(f'Hires: upscaler={p.hr_upscaler} width={p.hr_upscale_to_x} height={p.hr_upscale_to_y} images={latents.shape[0]}') if latent_upscaler is not None: latents = torch.nn.functional.interpolate(latents, size=(p.hr_upscale_to_y // 8, p.hr_upscale_to_x // 8), mode=latent_upscaler["mode"], antialias=latent_upscaler["antialias"]) - first_pass_images = vae_decode(latents=latents, model=shared.sd_model, full_quality=True, output_type='pil') + first_pass_images = vae_decode(latents=latents, model=shared.sd_model, full_quality=p.full_quality, output_type='pil') p.init_images = [] for first_pass_image in first_pass_images: - init_image = images.resize_image(1, first_pass_image, p.hr_upscale_to_x, p.hr_upscale_to_y, upscaler_name=p.hr_upscaler) if latent_upscaler is None else first_pass_image + if latent_upscaler is None: + init_image = images.resize_image(1, first_pass_image, p.hr_upscale_to_x, p.hr_upscale_to_y, upscaler_name=p.hr_upscaler) + else: + init_image = first_pass_image + # if is_refiner_enabled: + # init_image = vae_encode(init_image, model=shared.sd_model, full_quality=p.full_quality) p.init_images.append(init_image) - p.width = p.hr_upscale_to_x - p.height = p.hr_upscale_to_y + return p.init_images def save_intermediate(latents, suffix): for i in range(len(latents)): @@ -63,7 +68,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro time.sleep(0.1) def full_vae_decode(latents, model): - 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={model.vae.config.get("force_upcast", None)} images={latents.shape[0]}') + t0 = time.time() if shared.opts.diffusers_move_unet and not model.has_accelerate: shared.log.debug('Moving to CPU: model=UNet') unet_device = model.unet.device @@ -72,18 +77,47 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro if not shared.cmd_opts.lowvram and not shared.opts.diffusers_seq_cpu_offload: model.vae.to(devices.device) latents.to(model.vae.device) + + needs_upcasting = model.vae.dtype == torch.float16 and model.vae.config.force_upcast + if needs_upcasting: # this is done by diffusers automatically if output_type != 'latent' + model.upcast_vae() + 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 model.has_accelerate: 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={model.vae.config.get("force_upcast", None)} images={latents.shape[0]} latents={latents.shape} time={round(t1-t0, 3)}s') return decoded + 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 model.has_accelerate: + 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: + model.vae.to(devices.device) + encoded = model.vae.encode(image.to(model.vae.device, model.vae.dtype)) + if shared.opts.diffusers_move_unet and not model.has_accelerate: + model.unet.to(unet_device) + return encoded + def taesd_vae_decode(latents): - shared.log.debug(f'VAE decode: name=TAESD images={latents.shape[0]}') - decoded = torch.zeros((len(latents), 3, p.height, p.width), dtype=devices.dtype_vae, device=devices.device) + shared.log.debug(f'VAE decode: name=TAESD images={len(latents)} latents={latents.shape}') + if len(latents) == 0: + return [] + decoded = torch.zeros((len(latents), 3, latents.shape[2] * 8, latents.shape[3] * 8), dtype=devices.dtype_vae, device=devices.device) for i in range(len(output.images)): decoded[i] = (sd_vae_taesd.decode(latents[i]) * 2.0) - 1.0 return decoded + def taesd_vae_encode(image): + shared.log.debug(f'VAE encode: name=TAESD image={image.shape}') + encoded = sd_vae_taesd.encode(image) + return encoded + def vae_decode(latents, model, output_type='np', full_quality=True): if not torch.is_tensor(latents): # already decoded return latents @@ -104,6 +138,19 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro imgs = model.image_processor.postprocess(decoded, output_type=output_type) return imgs + def vae_encode(image, model, full_quality=True): # pylint: disable=unused-variable + if shared.state.interrupted or shared.state.skipped: + return [] + if not hasattr(model, 'vae'): + shared.log.error('VAE not found in model') + return [] + tensor = TF.to_tensor(image.convert("RGB")).unsqueeze(0).to(devices.device, devices.dtype_vae) + if full_quality: + latents = full_vae_encode(image=tensor, model=shared.sd_model) + else: + latents = taesd_vae_encode(image=tensor) + return latents + def fix_prompts(prompts, negative_prompts, prompts_2, negative_prompts_2): if type(prompts) is str: prompts = [prompts] @@ -129,7 +176,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro except Exception: is_refiner = False if hasattr(model, "set_progress_bar_config"): - model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} '+desc, ncols=80, colour='#327fba') + model.set_progress_bar_config(bar_format='Progress {rate_fmt}{postfix} {bar} {percentage:3.0f}% {n_fmt}/{total_fmt} {elapsed} {remaining} ' + '\x1b[38;5;71m' + desc, ncols=80, colour='#327fba') args = {} signature = inspect.signature(type(model).__call__) possible = signature.parameters.keys() @@ -140,7 +187,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro negative_embed = None negative_pooled = None prompts, negative_prompts, prompts_2, negative_prompts_2 = fix_prompts(prompts, negative_prompts, prompts_2, negative_prompts_2) - if shared.opts.prompt_attention in {'Compel parser', 'Full parser'}: + if shared.opts.prompt_attention in {'Compel parser', 'Full parser'} and 'StableDiffusion' in model.__class__.__name__: prompt_embed, pooled, negative_embed, negative_pooled = prompt_parser_diffusers.compel_encode_prompts(model, prompts, negative_prompts, prompts_2, negative_prompts_2, is_refiner, kwargs.pop("clip_skip", None)) if 'prompt' in possible: if hasattr(model, 'text_encoder') and 'prompt_embeds' in possible and prompt_embed is not None: @@ -315,35 +362,38 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro return results # optional hires pass + latent_scale_mode = shared.latent_upscale_modes.get(p.hr_upscaler, None) if (hasattr(p, "hr_upscaler") and p.hr_upscaler is not None) else shared.latent_upscale_modes.get(shared.latent_upscale_default_mode, "None") if p.is_hr_pass: p.init_hr() - recompile_model(hires=True) if p.width != p.hr_upscale_to_x or p.height != p.hr_upscale_to_y: + p.ops.append('upscale') if shared.opts.save and not p.do_not_save_samples and shared.opts.save_images_before_highres_fix and hasattr(shared.sd_model, 'vae'): save_intermediate(latents=output.images, suffix="-before-hires") - hires_resize(latents=output.images) - sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) - p.ops.append('hires') - hires_args = set_pipeline_args( - model=shared.sd_model, - prompts=prompts, - negative_prompts=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), - eta=shared.opts.eta_ddim, - guidance_scale=p.image_cfg_scale if p.image_cfg_scale is not None else p.cfg_scale, - guidance_rescale=p.diffusers_guidance_rescale, - output_type='latent' if hasattr(shared.sd_model, 'vae') else 'np', - clip_skip=p.clip_skip, - image=p.init_images, - strength=p.denoising_strength, - desc='Hires', - ) - try: - output = shared.sd_model(**hires_args) # pylint: disable=not-callable - except AssertionError as e: - shared.log.info(e) + output.images = hires_resize(latents=output.images) + if latent_scale_mode is not None or p.hr_force: + p.ops.append('hires') + recompile_model(hires=True) + sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.IMAGE_2_IMAGE) + hires_args = set_pipeline_args( + model=shared.sd_model, + prompts=prompts, + negative_prompts=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), + eta=shared.opts.eta_ddim, + guidance_scale=p.image_cfg_scale if p.image_cfg_scale is not None else p.cfg_scale, + guidance_rescale=p.diffusers_guidance_rescale, + output_type='latent' if hasattr(shared.sd_model, 'vae') else 'np', + clip_skip=p.clip_skip, + image=p.init_images, + strength=p.denoising_strength, + desc='Hires', + ) + try: + output = shared.sd_model(**hires_args) # pylint: disable=not-callable + except AssertionError as e: + shared.log.info(e) # optional refiner pass or decode if is_refiner_enabled: @@ -368,6 +418,11 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro refiner_is_sdxl = bool("StableDiffusionXL" in shared.sd_refiner.__class__.__name__) p.ops.append('refine') for i in range(len(output.images)): + image = output.images[i] + # if (image.shape[2] == 3) and (image.shape[0] % 8 != 0 or image.shape[1] % 8 != 0): + # shared.log.warning(f'Refiner requires image size to be divisible by 8: {image.shape}') + # results.append(image) + # return results refiner_args = set_pipeline_args( model=shared.sd_refiner, prompts=[p.refiner_prompt] if len(p.refiner_prompt) > 0 else prompts[i], @@ -379,7 +434,7 @@ def process_diffusers(p: StableDiffusionProcessing, seeds, prompts, negative_pro guidance_rescale=p.diffusers_guidance_rescale, denoising_start=p.refiner_start if p.refiner_start > 0 and p.refiner_start < 1 else None, denoising_end=1 if p.refiner_start > 0 and p.refiner_start < 1 else None, - image=output.images[i], + image=image, output_type='latent' if hasattr(shared.sd_refiner, 'vae') else 'np', clip_skip=p.clip_skip, desc='Refiner', diff --git a/modules/prompt_parser_diffusers.py b/modules/prompt_parser_diffusers.py index 6a8b1bef0..57cbf7f17 100644 --- a/modules/prompt_parser_diffusers.py +++ b/modules/prompt_parser_diffusers.py @@ -62,7 +62,8 @@ class DiffusersTextualInversionManager(BaseTextualInversionManager): replacement += f" {token}_{i}" i += 1 prompt = prompt.replace(token, replacement) - self.pipe.embedding_db.embeddings_used = list(set(self.pipe.embedding_db.embeddings_used)) + if hasattr(self.pipe, 'embedding_db'): + self.pipe.embedding_db.embeddings_used = list(set(self.pipe.embedding_db.embeddings_used)) return prompt def expand_textual_inversion_token_ids_if_necessary(self, token_ids: typing.List[int]) -> typing.List[int]: diff --git a/modules/script_callbacks.py b/modules/script_callbacks.py index 29cd9470a..28b906aaf 100644 --- a/modules/script_callbacks.py +++ b/modules/script_callbacks.py @@ -98,6 +98,7 @@ callback_map = dict( callbacks_ui_settings=[], callbacks_before_image_saved=[], callbacks_image_saved=[], + callbacks_image_save_btn=[], callbacks_cfg_denoiser=[], callbacks_cfg_denoised=[], callbacks_cfg_after_cfg=[], @@ -205,6 +206,16 @@ def image_saved_callback(params: ImageSaveParams): report_exception(e, c, 'image_saved_callback') +def image_save_btn_callback(filename: str): + for c in callback_map['callbacks_image_save_btn']: + try: + t0 = time.time() + c.callback(filename) + timer(t0, c.script, 'image_save_btn') + except Exception as e: + report_exception(e, c, 'image_save_btn_callback') + + def cfg_denoiser_callback(params: CFGDenoiserParams): for c in callback_map['callbacks_cfg_denoiser']: try: @@ -376,6 +387,14 @@ def on_image_saved(callback): add_callback(callback_map['callbacks_image_saved'], callback) +def on_image_save_btn(callback): + """register a function to be called after an image save button is pressed. + The callback is called with one argument: + - params: ImageSaveParams - parameters the image was saved with. Changing fields in this object does nothing. + """ + add_callback(callback_map['callbacks_image_save_btn'], callback) + + def on_cfg_denoiser(callback): """register a function to be called in the kdiffussion cfg_denoiser method after building the inner model inputs. The callback is called with one argument: diff --git a/modules/script_loading.py b/modules/script_loading.py index b28f6b65d..61f08527a 100644 --- a/modules/script_loading.py +++ b/modules/script_loading.py @@ -1,6 +1,9 @@ +import io import os +import contextlib import importlib.util import modules.errors as errors +from installer import setup_logging preloaded = [] @@ -10,13 +13,18 @@ def load_module(path): module_spec = importlib.util.spec_from_file_location(os.path.basename(path), path) module = importlib.util.module_from_spec(module_spec) try: - module_spec.loader.exec_module(module) + # stdout = io.StringIO() + with contextlib.redirect_stdout(io.StringIO()) as stdout: + module_spec.loader.exec_module(module) + setup_logging() # reset since scripts can hijaack logging + for line in stdout.getvalue().splitlines(): + if len(line) > 0: + errors.log.info(f"Extension: script='{os.path.relpath(path)}' {line.strip()}") except Exception as e: errors.display(e, f'Module load: {path}') return module - def preload_extensions(extensions_dir, parser): if not os.path.isdir(extensions_dir): return diff --git a/modules/scripts.py b/modules/scripts.py index 33111a3d3..b81ce2aa1 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -343,7 +343,7 @@ class ScriptRunner: def setup_ui(self): import modules.api.models as api_models self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts] - inputs = [None] + inputs = [] inputs_alwayson = [True] def create_script_ui(script, inputs, inputs_alwayson): @@ -377,38 +377,28 @@ class ScriptRunner: inputs_alwayson += [script.alwayson for _ in controls] script.args_to = len(inputs) - with gr.Group(elem_id='scripts_alwayson_img2img' if self.is_img2img else 'scripts_alwayson_txt2img'): - for script in self.alwayson_scripts: - t0 = time.time() - elem_id = f'script_{"txt2img" if script.is_txt2img else "img2img"}_{script.title().lower().replace(" ", "_")}' - with gr.Group(elem_id=elem_id) as group: - create_script_ui(script, inputs, inputs_alwayson) - script.group = group - time_setup[script.title()] = time_setup.get(script.title(), 0) + (time.time()-t0) - dropdown = gr.Dropdown(label="Script", elem_id="script_list", choices=["None"] + self.titles, value="None", type="index") - inputs[0] = dropdown + inputs.insert(0, dropdown) for script in self.selectable_scripts: with gr.Group(visible=False) as group: t0 = time.time() create_script_ui(script, inputs, inputs_alwayson) time_setup[script.title()] = time_setup.get(script.title(), 0) + (time.time()-t0) - script.group = group + script.group = group def select_script(script_index): selected_script = self.selectable_scripts[script_index - 1] if script_index > 0 else None return [gr.update(visible=selected_script == s) for s in self.selectable_scripts] def init_field(title): - """called when an initial value is set from ui-config.json to show script's UI components""" - if title == 'None': + if title == 'None': # called when an initial value is set from ui-config.json to show script's UI components return script_index = self.titles.index(title) self.selectable_scripts[script_index].group.visible = True dropdown.init_field = init_field dropdown.change(fn=select_script, inputs=[dropdown], outputs=[script.group for script in self.selectable_scripts]) - + def onload_script_visibility(params): title = params.get('Script', None) if title: @@ -419,6 +409,16 @@ class ScriptRunner: else: return gr.update(visible=False) + # with gr.Group(elem_id='scripts_alwayson_img2img' if self.is_img2img else 'scripts_alwayson_txt2img'): + with gr.Accordion(label="Extensions", elem_id='scripts_alwayson_img2img' if self.is_img2img else 'scripts_alwayson_txt2img'): + for script in self.alwayson_scripts: + t0 = time.time() + elem_id = f'script_{"txt2img" if script.is_txt2img else "img2img"}_{script.title().lower().replace(" ", "_")}' + with gr.Group(elem_id=elem_id) as group: + create_script_ui(script, inputs, inputs_alwayson) + script.group = group + time_setup[script.title()] = time_setup.get(script.title(), 0) + (time.time()-t0) + self.infotext_fields.append( (dropdown, lambda x: gr.update(value=x.get('Script', 'None'))) ) self.infotext_fields.extend( [(script.group, onload_script_visibility) for script in self.selectable_scripts] ) return inputs diff --git a/modules/sd_hijack_clip.py b/modules/sd_hijack_clip.py index 73fc00c9c..14510a689 100644 --- a/modules/sd_hijack_clip.py +++ b/modules/sd_hijack_clip.py @@ -179,7 +179,7 @@ class FrozenCLIPEmbedderWithCustomWordsBase(torch.nn.Module): used_embeddings[embedding.name] = embedding z = self.process_tokens(tokens, multipliers) zs.append(z) - self.hijack.embedding_db.embeddings_used = [name for name, embedding in used_embeddings.items()] + self.hijack.embedding_db.embeddings_used = list(used_embeddings) return torch.hstack(zs) def process_tokens(self, remade_batch_tokens, batch_multipliers): diff --git a/modules/sd_hijack_inpainting.py b/modules/sd_hijack_inpainting.py index 7b392b4f4..882560a1c 100644 --- a/modules/sd_hijack_inpainting.py +++ b/modules/sd_hijack_inpainting.py @@ -1,8 +1,11 @@ +import io +import contextlib import torch - -import ldm.models.diffusion.ddpm -import ldm.models.diffusion.ddim -import ldm.models.diffusion.plms +stdout = io.StringIO() +with contextlib.redirect_stdout(stdout): + import ldm.models.diffusion.ddpm + import ldm.models.diffusion.ddim + import ldm.models.diffusion.plms from ldm.models.diffusion.ddpm import LatentDiffusion # pylint: disable=unused-import from ldm.models.diffusion.plms import PLMSSampler # pylint: disable=unused-import diff --git a/modules/sd_models.py b/modules/sd_models.py index 3243d4951..318d2915e 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1,5 +1,3 @@ -import collections -import os.path import re import io import sys @@ -7,6 +5,9 @@ import json import time import logging import threading +import contextlib +import collections +import os.path from os import mkdir from urllib import request from enum import Enum @@ -130,7 +131,7 @@ class NoWatermark: def setup_model(): if not os.path.exists(model_path): - os.makedirs(model_path) + os.makedirs(model_path, exist_ok=True) list_models() enable_midas_autodownload() @@ -256,7 +257,7 @@ def select_checkpoint(op='model'): if checkpoint_info is not None: shared.log.debug(f'Select checkpoint: {op} {checkpoint_info.title if checkpoint_info is not None else None}') return checkpoint_info - if len(checkpoints_list) == 0: + if len(checkpoints_list) == 0 and not shared.cmd_opts.no_download: shared.log.error("Cannot run without a checkpoint") shared.log.error("Use --ckpt to force using existing checkpoint") return None @@ -309,6 +310,20 @@ def write_metadata(): sd_metadata_pending = 0 +def scrub_dict(dict_obj, keys): + for key in list(dict_obj.keys()): + if not isinstance(dict_obj, dict): + continue + elif key in keys: + dict_obj.pop(key, None) + elif isinstance(dict_obj[key], dict): + scrub_dict(dict_obj[key], keys) + elif isinstance(dict_obj[key], list): + for item in dict_obj[key]: + scrub_dict(item, keys) + return + + def read_metadata_from_safetensors(filename): global sd_metadata # pylint: disable=global-statement if sd_metadata is None: @@ -320,6 +335,8 @@ def read_metadata_from_safetensors(filename): if res is not None: return res res = {} + if shared.cmd_opts.no_metadata: + return {} try: t0 = time.time() with open(filename, mode="rb") as file: @@ -331,15 +348,29 @@ def read_metadata_from_safetensors(filename): json_data = json_start + file.read(metadata_len-2) json_obj = json.loads(json_data) for k, v in json_obj.get("__metadata__", {}).items(): + if v.startswith("data:"): + v = 'data' if k == 'format' and v == 'pt': continue - if isinstance(v, str) and v[0:1] == '{': + large = True if len(v) > 2048 else False + if large and k == 'ss_datasets': + continue + if large and k == 'workflow': + continue + if large and k == 'prompt': + continue + if large and k == 'ss_bucket_info': + continue + if v[0:1] == '{': try: - res[k] = json.loads(v) + v = json.loads(v) + if large and k == 'ss_tag_frequency': + v = { i: len(j) for i, j in v.items() } + if large and k == 'sd_merge_models': + scrub_dict(v, ['sd_merge_recipe']) except Exception: pass - else: - res[k] = v + res[k] = v sd_metadata[filename] = res global sd_metadata_pending # pylint: disable=global-statement sd_metadata_pending += 1 @@ -356,7 +387,7 @@ def read_state_dict(checkpoint_file, map_location=None): # pylint: disable=unuse return None try: pl_sd = None - with progress.open(checkpoint_file, 'rb', description=f'[cyan]Loading weights: [yellow]{checkpoint_file}', auto_refresh=True) as f: + with progress.open(checkpoint_file, 'rb', description=f'[cyan]Loading weights: [yellow]{checkpoint_file}', auto_refresh=True, console=shared.console) as f: _, extension = os.path.splitext(checkpoint_file) if extension.lower() == ".ckpt" and shared.opts.sd_disable_ckpt: shared.log.warning(f"Checkpoint loading disabled: {checkpoint_file}") @@ -688,13 +719,19 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No if vae is not None: diffusers_load_config["vae"] = vae - shared.log.info(f'Loading diffuser {op}: {checkpoint_info.filename}') + # shared.log.info(f'Loading diffuser {op}: {checkpoint_info.filename}') if not os.path.isfile(checkpoint_info.path): try: + # os.environ.setdefault('HUGGINGFACE_HUB_CACHE', shared.opts.diffusers_dir) # evalulated only on initial diffusers load + # diffusers_load_config["cache_dir "] = shared.opts.diffusers_dir # ignored for connected pipelines such as kandinsky-prior + # diffusers.utils.constants.DIFFUSERS_CACHE = shared.opts.diffusers_dir # shared.log.debug(f'Diffusers load {op} config: {diffusers_load_config}') - sd_model = diffusers.DiffusionPipeline.from_pretrained(checkpoint_info.path, **diffusers_load_config) + # sd_model = diffusers.DiffusionPipeline.from_pretrained(checkpoint_info.path, **diffusers_load_config) + sd_model = diffusers.AutoPipelineForText2Image.from_pretrained(checkpoint_info.path, cache_dir=shared.opts.diffusers_dir, **diffusers_load_config) + sd_model.model_type = sd_model.__class__.__name__ except Exception as e: - shared.log.error(f'Failed loading model {op}: {checkpoint_info.path} {e}') + shared.log.error(f'Failed loading {op}: {checkpoint_info.path} {e}') + return else: diffusers_load_config["local_files_only "] = True diffusers_load_config["extract_ema"] = shared.opts.diffusers_extract_ema @@ -705,7 +742,13 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No try: if model_type.startswith('Stable Diffusion'): diffusers_load_config['force_zeros_for_empty_prompt '] = shared.opts.diffusers_force_zeros - diffusers_load_config['requires_aesthetics_score '] = shared.opts.diffusers_aesthetics_score + diffusers_load_config['requires_aesthetics_score'] = shared.opts.diffusers_aesthetics_score + diffusers_load_config['config_files'] = { + 'v1': 'configs/v1-inference.yaml', + 'v2': 'configs/v2-inference-768-v.yaml', + 'xl': 'configs/sd_xl_base.yaml', + 'xl_refiner': 'configs/sd_xl_refiner.yaml', + } if hasattr(pipeline, 'from_single_file'): diffusers_load_config['use_safetensors'] = True sd_model = pipeline.from_single_file(checkpoint_info.path, **diffusers_load_config) @@ -786,6 +829,9 @@ def load_diffuser(checkpoint_info=None, already_loaded_state_dict=None, timer=No else: sd_model.vae.config["force_upcast"] = False sd_model.vae.config.force_upcast = False + if shared.opts.no_half_vae: + devices.dtype_vae = torch.float32 + sd_model.vae.to(devices.dtype_vae) shared.log.debug(f'Model {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() @@ -990,13 +1036,17 @@ def load_model(checkpoint_info=None, already_loaded_state_dict=None, timer=None, timer.record("config") shared.log.debug(f'Model config loaded: {memory_stats()}') sd_model = None - # shared.log.debug(f'Model config: {sd_config.model.get("params", dict())}') - try: - clip_is_included_into_sd = sd1_clip_weight in state_dict or sd2_clip_weight in state_dict - with sd_disable_initialization.DisableInitialization(disable_clip=clip_is_included_into_sd): + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + try: + clip_is_included_into_sd = sd1_clip_weight in state_dict or sd2_clip_weight in state_dict + with sd_disable_initialization.DisableInitialization(disable_clip=clip_is_included_into_sd): + sd_model = instantiate_from_config(sd_config.model) + except Exception: sd_model = instantiate_from_config(sd_config.model) - except Exception: - sd_model = instantiate_from_config(sd_config.model) + for line in stdout.getvalue().splitlines(): + if len(line) > 0: + shared.log.info(f'LDM: {line.strip()}') shared.log.debug(f"Model created from config: {checkpoint_config}") sd_model.used_config = checkpoint_config timer.record("create") @@ -1141,16 +1191,22 @@ def apply_token_merging(sd_model, token_merging_ratio=0): current_token_merging_ratio = getattr(sd_model, 'applied_token_merged_ratio', 0) if token_merging_ratio is None or current_token_merging_ratio is None or current_token_merging_ratio == token_merging_ratio: return - if current_token_merging_ratio > 0: - tomesd.remove_patch(sd_model) + try: + if current_token_merging_ratio > 0: + tomesd.remove_patch(sd_model) + except Exception: + pass if token_merging_ratio > 0: - shared.log.debug(f'Applying token merging: ratio={token_merging_ratio}') - tomesd.apply_patch( - sd_model, - ratio=token_merging_ratio, - use_rand=False, # can cause issues with some samplers - merge_attn=True, - merge_crossattn=False, - merge_mlp=False - ) - sd_model.applied_token_merged_ratio = token_merging_ratio + try: + tomesd.apply_patch( + sd_model, + ratio=token_merging_ratio, + use_rand=False, # can cause issues with some samplers + merge_attn=True, + merge_crossattn=False, + merge_mlp=False + ) + shared.log.debug(f'Applying token merging: ratio={token_merging_ratio}') + sd_model.applied_token_merged_ratio = token_merging_ratio + except: + shared.log.warning(f'Token merging not supported: pipeline={sd_model.__class__.__name__}') diff --git a/modules/sd_models_config.py b/modules/sd_models_config.py index 819bebd34..163390ca6 100644 --- a/modules/sd_models_config.py +++ b/modules/sd_models_config.py @@ -2,7 +2,7 @@ import os import torch -from modules import paths, sd_disable_initialization +from modules import paths, sd_disable_initialization, devices sd_repo_configs_path = os.path.join(paths.paths['Stable Diffusion'], "configs", "stable-diffusion") config_default = paths.sd_default_config @@ -21,12 +21,9 @@ def is_using_v_parameterization_for_sd2(state_dict): """ Detects whether unet in state_dict is using v-parameterization. Returns True if it is. You're welcome. """ - import ldm.modules.diffusionmodules.openaimodel - from modules import devices device = devices.cpu - with sd_disable_initialization.DisableInitialization(): unet = ldm.modules.diffusionmodules.openaimodel.UNetModel( use_checkpoint=True, @@ -47,7 +44,7 @@ def is_using_v_parameterization_for_sd2(state_dict): ) unet.eval() - with torch.no_grad(): + with devices.inference_context(): unet_sd = {k.replace("model.diffusion_model.", ""): v for k, v in state_dict.items() if "model.diffusion_model." in k} unet.load_state_dict(unet_sd, strict=True) unet.to(device=device, dtype=torch.float) diff --git a/modules/sd_samplers.py b/modules/sd_samplers.py index e0f6493c8..61a56c82a 100644 --- a/modules/sd_samplers.py +++ b/modules/sd_samplers.py @@ -23,7 +23,7 @@ def list_samplers(backend_name = shared.backend): samplers = all_samplers samplers_for_img2img = all_samplers samplers_map = {} - shared.log.debug(f'Available samplers: {[x.name for x in all_samplers]}') + # shared.log.debug(f'Available samplers: {[x.name for x in all_samplers]}') def find_sampler_config(name): diff --git a/modules/sd_samplers_kdiffusion.py b/modules/sd_samplers_kdiffusion.py index 2134ed0d7..e39f6ab19 100644 --- a/modules/sd_samplers_kdiffusion.py +++ b/modules/sd_samplers_kdiffusion.py @@ -142,6 +142,16 @@ class CFGDenoiser(torch.nn.Module): else: cond_in = torch.cat([tensor, uncond]) + """ + adjusted_cond_scale = cond_scale # Adjusted cond_scale for uncond + last_uncond_steps = max(0, state.sampling_steps - 2) # Determine the last two steps before uncond stops + if self.step >= last_uncond_steps: # Check if we're in the last two steps before uncond stops + adjusted_cond_scale *= 1.5 # Apply uncond with 150% cond_scale + else: + if (self.step - last_uncond_steps) % 3 == 0: # Check if it's one of every three steps after uncond stops + adjusted_cond_scale *= 1.5 # Apply uncond with 150% cond_scale + """ + if shared.batch_cond_uncond: x_out = self.inner_model(x_in, sigma_in, cond=make_condition_dict([cond_in], image_cond_in)) else: diff --git a/modules/sd_vae.py b/modules/sd_vae.py index d632618a1..0360d83b3 100644 --- a/modules/sd_vae.py +++ b/modules/sd_vae.py @@ -232,6 +232,8 @@ def reload_vae_weights(sd_model=None, vae_file=unspecified): from modules import lowvram, sd_hijack if not sd_model: sd_model = shared.sd_model + if sd_model is None: + return global checkpoint_info # pylint: disable=global-statement checkpoint_info = sd_model.sd_checkpoint_info checkpoint_file = checkpoint_info.filename diff --git a/modules/shared.py b/modules/shared.py index ebc142a63..01f39e13c 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -1,14 +1,17 @@ +import io import os import sys import time import json import datetime +import contextlib import urllib.request from urllib.parse import urlparse from enum import Enum import gradio as gr import tqdm import fasteners +from rich.console import Console from modules import errors, ui_components, shared_items, cmd_args from modules.paths_internal import models_path, script_path, data_path, sd_configs_path, sd_default_config, sd_model_file, default_sd_model_file, extensions_dir, extensions_builtin_dir # pylint: disable=W0611 from modules.dml import memory_providers, default_memory_provider, directml_do_hijack @@ -17,6 +20,7 @@ import modules.memmon import modules.styles import modules.devices as devices # pylint: disable=R0402 import modules.paths_internal as paths +from installer import print_dict from installer import log as central_logger # pylint: disable=E0611 @@ -66,7 +70,7 @@ restricted_opts = { "outdir_init_images" } compatibility_opts = ['clip_skip', 'uni_pc_lower_order_final', 'uni_pc_order'] - +console = Console(log_time=True, log_time_format='%H:%M:%S-%f') def is_url(string): parsed_url = urlparse(string) @@ -91,6 +95,7 @@ class State: job = "" job_no = 0 job_count = 0 + total_jobs = 0 processing_has_refined_job_count = False job_timestamp = '0' sampling_step = 0 @@ -137,19 +142,21 @@ class State: } return obj - def begin(self): - self.sampling_step = 0 - self.job_count = -1 - self.processing_has_refined_job_count = False - self.job_no = 0 - self.job_timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - self.current_latent = None + def begin(self, title=""): + self.total_jobs += 1 self.current_image = None self.current_image_sampling_step = 0 + self.current_latent = None self.id_live_preview = 0 - self.skipped = False self.interrupted = False + self.job = title + self.job_count = -1 + self.job_no = 0 + self.job_timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S") self.paused = False + self.processing_has_refined_job_count = False + self.sampling_step = 0 + self.skipped = False self.textinfo = None self.time_start = time.time() devices.torch_gc() @@ -157,7 +164,10 @@ class State: def end(self): self.job = "" self.job_count = 0 + self.job_no = 0 self.paused = False + self.interrupted = False + self.skipped = False devices.torch_gc() def set_current_image(self): @@ -175,8 +185,9 @@ class State: image = modules.sd_samplers.samples_to_image_grid(self.current_latent) if opts.show_progress_grid else modules.sd_samplers.sample_to_image(self.current_latent) self.assign_current_image(image) self.current_image_sampling_step = self.sampling_step - except Exception as e: - log.error(f'Error setting current image: step={self.sampling_step} {e}') + except Exception: + # log.error(f'Error setting current image: step={self.sampling_step} {e}') + pass def assign_current_image(self, image): self.current_image = image @@ -194,7 +205,7 @@ else: class OptionInfo: - def __init__(self, default=None, label="", component=None, component_args=None, onchange=None, section=None, refresh=None, submit=None, comment_before='', comment_after=''): + def __init__(self, default=None, label="", component=None, component_args=None, onchange=None, section=None, refresh=None, folder=None, submit=None, comment_before='', comment_after=''): self.default = default self.label = label self.component = component @@ -202,6 +213,7 @@ class OptionInfo: self.onchange = onchange self.section = section self.refresh = refresh + self.folder = folder self.comment_before = comment_before # HTML text that will be added after label in UI self.comment_after = comment_after # HTML text that will be added before label in UI self.submit = submit @@ -244,19 +256,38 @@ def refresh_checkpoints(): import modules.sd_models # pylint: disable=W0621 return modules.sd_models.list_models() + def refresh_vaes(): import modules.sd_vae # pylint: disable=W0621 modules.sd_vae.refresh_vae_list() + def list_samplers(): import modules.sd_samplers # pylint: disable=W0621 modules.sd_samplers.set_samplers() return modules.sd_samplers.all_samplers + +def temp_disable_extensions(): + disabled = [] + if backend == Backend.DIFFUSERS: + for ext in ['sd-webui-controlnet', 'multidiffusion-upscaler-for-automatic1111', 'a1111-sd-webui-lycoris']: + if ext not in opts.disabled_extensions: + disabled.append(ext) + log.info(f'Diffusers disabling uncompatible extensions: {disabled}') + if opts.lyco_patch_lora and backend != Backend.DIFFUSERS: + cmd_opts.lyco_dir = opts.lora_dir + if 'Lora' not in opts.disabled_extensions: + disabled.append('Lora') + cmd_opts.controlnet_loglevel = 'WARNING' + return disabled + + def list_builtin_themes(): files = [os.path.splitext(f)[0] for f in os.listdir('javascript') if f.endswith('.css')] return files + def list_themes(): fn = os.path.join('html', 'themes.json') if not os.path.exists(fn): @@ -266,25 +297,14 @@ def list_themes(): res = json.loads(f.read()) else: res = [] - list_builtin_themes() - builtin = list_builtin_themes() + ["gradio/default", "gradio/base", "gradio/glass", "gradio/monochrome", "gradio/soft"] - themes = sorted(builtin) + sorted({x['id'] for x in res if x['status'] == 'RUNNING' and 'test' not in x['id'].lower()}, key=str.casefold) + builtin = list_builtin_themes() + default = ["gradio/default", "gradio/base", "gradio/glass", "gradio/monochrome", "gradio/soft"] + external = {x['id'] for x in res if x['status'] == 'RUNNING' and 'test' not in x['id'].lower()} + log.info(f'Themes: builtin={len(builtin)} default={len(default)} external={len(external)}') + themes = sorted(builtin) + sorted(default) + sorted(external, key=str.casefold) return themes -def temp_disable_extensions(): - disabled = [] - if backend == Backend.DIFFUSERS: - for ext in ['sd-webui-controlnet', 'multidiffusion-upscaler-for-automatic1111', 'a1111-sd-webui-lycoris']: - if ext not in opts.disabled_extensions: - disabled.append(ext) - log.warning(f'Diffusers disabling uncompatible extensions: {disabled}') - if opts.lyco_patch_lora and backend != Backend.DIFFUSERS: - if 'Lora' not in opts.disabled_extensions: - disabled.append('Lora') - return disabled - - def refresh_themes(): import requests try: @@ -292,8 +312,8 @@ def refresh_themes(): if req.status_code == 200: res = req.json() fn = os.path.join('html', 'themes.json') - with open(fn, mode='w', encoding='utf=8') as f: - f.write(json.dumps(res)) + writefile(res, fn) + list_themes() else: log.error('Error refreshing UI themes') except Exception: @@ -339,7 +359,7 @@ if devices.backend == "cpu": elif devices.backend == "mps": cross_attention_optimization_default = "Doggettx's" elif devices.backend == "ipex": - cross_attention_optimization_default = "Sub-quadratic" + cross_attention_optimization_default = "Scaled-Dot-Product" elif devices.backend == "directml": cross_attention_optimization_default = "Sub-quadratic" elif devices.backend == "rocm": @@ -347,6 +367,7 @@ elif devices.backend == "rocm": else: # cuda cross_attention_optimization_default ="Scaled-Dot-Product" + options_templates.update(options_section(('sd', "Execution & Models"), { "sd_backend": OptionInfo("diffusers" if cmd_opts.use_openvino else "original", "Execution backend", gr.Radio, lambda: {"choices": ["original", "diffusers"] }), "sd_checkpoint_autoload": OptionInfo(True, "Model autoload on server start"), @@ -355,7 +376,7 @@ options_templates.update(options_section(('sd', "Execution & Models"), { "sd_checkpoint_cache": OptionInfo(0, "Number of cached models", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), "sd_vae_checkpoint_cache": OptionInfo(0, "Number of cached VAEs", gr.Slider, {"minimum": 0, "maximum": 10, "step": 1}), "sd_vae": OptionInfo("Automatic", "VAE model", gr.Dropdown, lambda: {"choices": shared_items.sd_vae_items()}, refresh=shared_items.refresh_vae_list), - "sd_model_dict": OptionInfo('None', "Use dict from model", gr.Dropdown, lambda: {"choices": ['None'] + list_checkpoint_tiles()}, refresh=refresh_checkpoints), + "sd_model_dict": OptionInfo('None', "Use baseline data from a different model", gr.Dropdown, lambda: {"choices": ['None'] + list_checkpoint_tiles()}, refresh=refresh_checkpoints), "stream_load": OptionInfo(False, "Load models using stream loading method"), "model_reuse_dict": OptionInfo(False, "When loading models attempt to reuse previous model dictionary"), "prompt_attention": OptionInfo("Full parser", "Prompt attention parser", gr.Radio, lambda: {"choices": ["Full parser", "Compel parser", "A1111 parser", "Fixed attention"] }), @@ -373,6 +394,7 @@ options_templates.update(options_section(('optimizations', "Optimizations"), { "token_merging_ratio": OptionInfo(0.0, "Token merging ratio", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}), "token_merging_ratio_img2img": OptionInfo(0.0, "Token merging ratio for img2img", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}), "token_merging_ratio_hr": OptionInfo(0.0, "Token merging ratio for hires pass", gr.Slider, {"minimum": 0.0, "maximum": 0.9, "step": 0.1}), + "inference_mode": OptionInfo("no-grad", "Torch inference mode", gr.Radio, lambda: {"choices": ["no-grad", "inference-mode", "none"]}), "sd_vae_sliced_encode": OptionInfo(False, "VAE Slicing (original)"), })) @@ -389,8 +411,9 @@ options_templates.update(options_section(('cuda', "Compute Settings"), { "rollback_vae": OptionInfo(False, "Attempt VAE roll back when produced NaN values (experimental)"), "opt_channelslast": OptionInfo(False, "Use channels last as torch memory format "), "cudnn_benchmark": OptionInfo(False, "Enable full-depth cuDNN benchmark feature"), - # "cuda_allow_tf32": OptionInfo(True, "Allow TF32 math ops"), - # "cuda_allow_tf16_reduced": OptionInfo(True, "Allow TF16 reduced precision math ops"), + "ipex_optimize": OptionInfo(True if devices.backend == "ipex" else False, "Enable IPEX Optimize for Intel GPUs"), + "directml_memory_provider": OptionInfo(default_memory_provider, 'DirectML memory stats provider', gr.Radio, lambda: {"choices": memory_providers}), + "cuda_compile_sep": OptionInfo("

Model Compile

", "", gr.HTML), "cuda_compile": OptionInfo(True if cmd_opts.use_openvino else False, "Enable model compile"), "cuda_compile_backend": OptionInfo("openvino_fx" if cmd_opts.use_openvino else "none", "Model compile backend", gr.Radio, lambda: {"choices": ['none', 'inductor', 'cudagraphs', 'aot_ts_nvfuser', 'hidet', 'ipex', 'openvino_fx']}), "cuda_compile_mode": OptionInfo("default", "Model compile mode", gr.Radio, lambda: {"choices": ['default', 'reduce-overhead', 'max-autotune']}), @@ -398,8 +421,6 @@ options_templates.update(options_section(('cuda', "Compute Settings"), { "cuda_compile_precompile": OptionInfo(False, "Model compile precompile"), "cuda_compile_verbose": OptionInfo(False, "Model compile verbose mode"), "cuda_compile_errors": OptionInfo(True, "Model compile suppress errors"), - "ipex_optimize": OptionInfo(True if devices.backend == "ipex" else False, "Enable IPEX Optimize for Intel GPUs"), - "directml_memory_provider": OptionInfo(default_memory_provider, 'DirectML memory stats provider', gr.Dropdown, lambda: {"choices": memory_providers}), })) options_templates.update(options_section(('diffusers', "Diffusers Settings"), { @@ -423,26 +444,26 @@ options_templates.update(options_section(('diffusers', "Diffusers Settings"), { })) options_templates.update(options_section(('system-paths', "System Paths"), { - "temp_dir": OptionInfo("", "Directory for temporary images; leave empty for default"), + "temp_dir": OptionInfo("", "Directory for temporary images; leave empty for default", folder=True), "clean_temp_dir_at_start": OptionInfo(True, "Cleanup non-default temporary directory when starting webui"), - "ckpt_dir": OptionInfo(os.path.join(paths.models_path, 'Stable-diffusion'), "Path to directory with stable diffusion checkpoints"), - "diffusers_dir": OptionInfo(os.path.join(paths.models_path, 'Diffusers'), "Path to directory with stable diffusion diffusers"), - "vae_dir": OptionInfo(os.path.join(paths.models_path, 'VAE'), "Path to directory with VAE files"), - "sd_lora": OptionInfo("", "Add LoRA to prompt", gr.CheckboxGroup, {"choices": [], "visible": False}), - "lora_dir": OptionInfo(os.path.join(paths.models_path, 'Lora'), "Path to directory with LoRA network(s)"), - "lyco_dir": OptionInfo(os.path.join(paths.models_path, 'LyCORIS'), "Path to directory with LyCORIS network(s)"), - "styles_dir": OptionInfo(os.path.join(paths.data_path, 'styles.csv'), "Path to user-defined styles file"), - "embeddings_dir": OptionInfo(os.path.join(paths.models_path, 'embeddings'), "Embeddings directory for textual inversion"), - "hypernetwork_dir": OptionInfo(os.path.join(paths.models_path, 'hypernetworks'), "Hypernetwork directory"), - "codeformer_models_path": OptionInfo(os.path.join(paths.models_path, 'Codeformer'), "Path to directory with codeformer model file(s)"), - "gfpgan_models_path": OptionInfo(os.path.join(paths.models_path, 'GFPGAN'), "Path to directory with GFPGAN model file(s)"), - "esrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'ESRGAN'), "Path to directory with ESRGAN model file(s)"), - "bsrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'BSRGAN'), "Path to directory with BSRGAN model file(s)"), - "realesrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'RealESRGAN'), "Path to directory with RealESRGAN model file(s)"), - "scunet_models_path": OptionInfo(os.path.join(paths.models_path, 'ScuNET'), "Path to directory with ScuNET model file(s)"), - "swinir_models_path": OptionInfo(os.path.join(paths.models_path, 'SwinIR'), "Path to directory with SwinIR model file(s)"), - "ldsr_models_path": OptionInfo(os.path.join(paths.models_path, 'LDSR'), "Path to directory with LDSR model file(s)"), - "clip_models_path": OptionInfo(os.path.join(paths.models_path, 'CLIP'), "Path to directory with CLIP model file(s)"), + "ckpt_dir": OptionInfo(os.path.join(paths.models_path, 'Stable-diffusion'), "Folder with stable diffusion models", folder=True), + "diffusers_dir": OptionInfo(os.path.join(paths.models_path, 'Diffusers'), "Folder with Hugggingface models", folder=True), + "vae_dir": OptionInfo(os.path.join(paths.models_path, 'VAE'), "Folder with VAE files", folder=True), + "sd_lora": OptionInfo("", "Add LoRA to prompt", gr.Textbox, {"visible": False}), + "lora_dir": OptionInfo(os.path.join(paths.models_path, 'Lora'), "Folder with LoRA network(s)", folder=True), + "lyco_dir": OptionInfo(os.path.join(paths.models_path, 'LyCORIS'), "Folder with LyCORIS network(s)", folder=True), + "styles_dir": OptionInfo(os.path.join(paths.data_path, 'styles.csv'), "File or Folder with user-defined styles", folder=True), + "embeddings_dir": OptionInfo(os.path.join(paths.models_path, 'embeddings'), "Folder with textual inversion embeddings", folder=True), + "hypernetwork_dir": OptionInfo(os.path.join(paths.models_path, 'hypernetworks'), "Folder with Hypernetwork models", folder=True), + "codeformer_models_path": OptionInfo(os.path.join(paths.models_path, 'Codeformer'), "Folder with codeformer models", folder=True), + "gfpgan_models_path": OptionInfo(os.path.join(paths.models_path, 'GFPGAN'), "Folder with GFPGAN models", folder=True), + "esrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'ESRGAN'), "Folder with ESRGAN models", folder=True), + "bsrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'BSRGAN'), "Folder with BSRGAN models", folder=True), + "realesrgan_models_path": OptionInfo(os.path.join(paths.models_path, 'RealESRGAN'), "Folder with RealESRGAN models", folder=True), + "scunet_models_path": OptionInfo(os.path.join(paths.models_path, 'ScuNET'), "Folder with ScuNET models", folder=True), + "swinir_models_path": OptionInfo(os.path.join(paths.models_path, 'SwinIR'), "Folder with SwinIR models", folder=True), + "ldsr_models_path": OptionInfo(os.path.join(paths.models_path, 'LDSR'), "Folder with LDSR models", folder=True), + "clip_models_path": OptionInfo(os.path.join(paths.models_path, 'CLIP'), "Folder with CLIP models", folder=True), })) options_templates.update(options_section(('saving-images', "Image Options"), { @@ -464,11 +485,9 @@ options_templates.update(options_section(('saving-images', "Image Options"), { "grid_save": OptionInfo(True, "Always save all generated image grids"), "grid_format": OptionInfo('jpg', 'File format for grids', gr.Dropdown, lambda: {"choices": ["jpg", "png", "webp", "tiff", "jp2"]}), "n_rows": OptionInfo(-1, "Grid row count", gr.Slider, {"minimum": -1, "maximum": 16, "step": 1}), - "grid_only_if_multiple": OptionInfo(True, "Do not save grids consisting of one picture"), - "grid_prevent_empty_spots": OptionInfo(True, "Prevent empty spots in grid (when set to autodetect)"), "save_sep_options": OptionInfo("

Intermediate Image Saving

", "", gr.HTML), - "save_init_img": OptionInfo(True, "Save copy of img2img init images (helps track workflow)"), + "save_init_img": OptionInfo(True, "Save copy of img2img init images"), "save_images_before_highres_fix": OptionInfo(False, "Save copy of image before applying highres fix"), "save_images_before_refiner": OptionInfo(False, "Save copy of image before running refiner"), "save_images_before_face_restoration": OptionInfo(False, "Save copy of image before doing face restoration"), @@ -490,19 +509,19 @@ options_templates.update(options_section(('saving-paths', "Image Naming & Paths" "use_save_to_dirs_for_ui": OptionInfo(False, "Save images to a subdirectory when using Save button"), "directories_filename_pattern": OptionInfo("[date]", "Directory name pattern", component_args=hide_dirs), "directories_max_prompt_words": OptionInfo(8, "Max prompt words for [prompt_words] pattern", gr.Slider, {"minimum": 1, "maximum": 99, "step": 1, **hide_dirs}), - "outdir_samples": OptionInfo("", "Output directory for images", component_args=hide_dirs), - "outdir_txt2img_samples": OptionInfo("outputs/text", 'Output directory for txt2img images', component_args=hide_dirs), - "outdir_img2img_samples": OptionInfo("outputs/image", 'Output directory for img2img images', component_args=hide_dirs), - "outdir_extras_samples": OptionInfo("outputs/extras", 'Output directory for images from extras tab', component_args=hide_dirs), - "outdir_save": OptionInfo("outputs/save", "Directory for saving images using the Save button", component_args=hide_dirs), - "outdir_init_images": OptionInfo("outputs/init-images", "Directory for saving init images when using img2img", component_args=hide_dirs), + "outdir_samples": OptionInfo("", "Output directory for images", component_args=hide_dirs, folder=True), + "outdir_txt2img_samples": OptionInfo("outputs/text", 'Output directory for txt2img images', component_args=hide_dirs, folder=True), + "outdir_img2img_samples": OptionInfo("outputs/image", 'Output directory for img2img images', component_args=hide_dirs, folder=True), + "outdir_extras_samples": OptionInfo("outputs/extras", 'Output directory for images from extras tab', component_args=hide_dirs, folder=True), + "outdir_save": OptionInfo("outputs/save", "Directory for saving images using the Save button", component_args=hide_dirs, folder=True), + "outdir_init_images": OptionInfo("outputs/init-images", "Directory for saving init images when using img2img", component_args=hide_dirs, folder=True), "outdir_sep_grids": OptionInfo("

Grids

", "", gr.HTML), "grid_extended_filename": OptionInfo(True, "Add extended info (seed, prompt) to filename when saving grid"), "grid_save_to_dirs": OptionInfo(False, "Save grids to a subdirectory"), - "outdir_grids": OptionInfo("", "Output directory for grids", component_args=hide_dirs), - "outdir_txt2img_grids": OptionInfo("outputs/grids", 'Output directory for txt2img grids', component_args=hide_dirs), - "outdir_img2img_grids": OptionInfo("outputs/grids", 'Output directory for img2img grids', component_args=hide_dirs), + "outdir_grids": OptionInfo("", "Output directory for grids", component_args=hide_dirs, folder=True), + "outdir_txt2img_grids": OptionInfo("outputs/grids", 'Output directory for txt2img grids', component_args=hide_dirs, folder=True), + "outdir_img2img_grids": OptionInfo("outputs/grids", 'Output directory for img2img grids', component_args=hide_dirs, folder=True), })) @@ -529,13 +548,13 @@ options_templates.update(options_section(('live-preview', "Live Previews"), { "live_previews_enable": OptionInfo(True, "Show live previews of the created image"), "show_progress_grid": OptionInfo(True, "Show previews of all images generated in a batch as a grid"), "notification_audio_enable": OptionInfo(False, "Play a sound when images are finished generating"), - "notification_audio_path": OptionInfo("html/notification.mp3","Path to notification sound", component_args=hide_dirs), + "notification_audio_path": OptionInfo("html/notification.mp3","Path to notification sound", component_args=hide_dirs, folder=True), "show_progress_every_n_steps": OptionInfo(1, "Live preview display period", gr.Slider, {"minimum": -1, "maximum": 32, "step": 1}), "show_progress_type": OptionInfo("Approximate NN", "Live preview method", gr.Radio, {"choices": ["Full VAE", "Approximate NN", "Approximate simple", "TAESD"]}), "live_preview_content": OptionInfo("Combined", "Live preview subject", gr.Radio, {"choices": ["Combined", "Prompt", "Negative prompt"]}), - "live_preview_refresh_period": OptionInfo(500, "Progressbar/preview update period, in milliseconds", gr.Slider, {"minimum": 0, "maximum": 5000, "step": 25}), + "live_preview_refresh_period": OptionInfo(500, "Progress update period", gr.Slider, {"minimum": 0, "maximum": 5000, "step": 25}), "logmonitor_show": OptionInfo(True, "Show log view"), - "logmonitor_refresh_period": OptionInfo(5000, "Log view update period, in milliseconds", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), + "logmonitor_refresh_period": OptionInfo(5000, "Log view update period", gr.Slider, {"minimum": 0, "maximum": 30000, "step": 25}), })) options_templates.update(options_section(('sampler-params', "Sampler Settings"), { @@ -548,9 +567,9 @@ options_templates.update(options_section(('sampler-params', "Sampler Settings"), "schedulers_sep_diffusers": OptionInfo("

Diffusers specific config

", "", gr.HTML), "schedulers_prediction_type": OptionInfo("default", "Samplers override model prediction type", gr.Radio, lambda: {"choices": ['default', 'epsilon', 'sample', 'v-prediction']}), - "schedulers_use_karras": OptionInfo(True, "Samplers should use Karras sigmas where applicable"), - "schedulers_use_loworder": OptionInfo(True, "Samplers should use use lower-order solvers in the final steps where applicable"), - "schedulers_use_thresholding": OptionInfo(False, "Samplers should use dynamic thresholding where applicable"), + "schedulers_use_karras": OptionInfo(True, "Samplers use Karras sigmas where applicable"), + "schedulers_use_loworder": OptionInfo(True, "Samplers use simplified solvers in final steps where applicable"), + "schedulers_use_thresholding": OptionInfo(False, "Samplers use dynamic thresholding where applicable"), "schedulers_dpm_solver": OptionInfo("sde-dpmsolver++", "Samplers DPM solver algorithm", gr.Radio, lambda: {"choices": ['dpmsolver', 'dpmsolver++', 'sde-dpmsolver++']}), "schedulers_beta_schedule": OptionInfo("default", "Samplers override beta schedule", gr.Radio, lambda: {"choices": ['default', 'linear', 'scaled_linear', 'squaredcos_cap_v2']}), 'schedulers_beta_start': OptionInfo(0, "Samplers override beta start", gr.Number, {}), @@ -572,11 +591,8 @@ options_templates.update(options_section(('sampler-params', "Sampler Settings"), })) options_templates.update(options_section(('postprocessing', "Postprocessing"), { - 'postprocessing_enable_in_main_ui': OptionInfo([], "Enable addtional postprocessing operations", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), + 'postprocessing_enable_in_main_ui': OptionInfo([], "Enable additional postprocessing operations", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), 'postprocessing_operation_order': OptionInfo([], "Postprocessing operation order", ui_components.DropdownMulti, lambda: {"choices": [x.name for x in shared_items.postprocessing_scripts()]}), - # "use_old_hires_fix_width_height": OptionInfo(False, "Hires fix uses width & height to set final resolution"), - # "dont_fix_second_order_samplers_schedule": OptionInfo(False, "Do not fix prompt schedule for second order samplers"), - "postprocessing_sep_img2img": OptionInfo("

Img2Img & Inpainting

", "", gr.HTML), "img2img_color_correction": OptionInfo(False, "Apply color correction to match original colors"), "img2img_fix_steps": OptionInfo(False, "For image processing do exact number of steps as specified"), @@ -607,7 +623,7 @@ options_templates.update(options_section(('training', "Training"), { "save_training_settings_to_txt": OptionInfo(True, "Save training settings to a text file on training start"), "dataset_filename_word_regex": OptionInfo("", "Filename word regex"), "dataset_filename_join_string": OptionInfo(" ", "Filename join string"), - "embeddings_templates_dir": OptionInfo(os.path.join(paths.script_path, 'train', 'templates'), "Embeddings train templates directory"), + "embeddings_templates_dir": OptionInfo(os.path.join(paths.script_path, 'train', 'templates'), "Embeddings train templates directory", folder=True), "training_image_repeats_per_epoch": OptionInfo(1, "Number of repeats for a single input image per epoch", gr.Number, {"precision": 0}), "training_write_csv_every": OptionInfo(0, "Save CSV file containing the loss to log directory"), "training_enable_tensorboard": OptionInfo(False, "Enable tensorboard logging"), @@ -622,7 +638,7 @@ options_templates.update(options_section(('interrogate', "Interrogate"), { "interrogate_clip_min_length": OptionInfo(32, "Interrogate: minimum description length", gr.Slider, {"minimum": 1, "maximum": 128, "step": 1}), "interrogate_clip_max_length": OptionInfo(192, "Interrogate: maximum description length", gr.Slider, {"minimum": 1, "maximum": 256, "step": 1}), "interrogate_clip_dict_limit": OptionInfo(2048, "CLIP: maximum number of lines in text file"), - "interrogate_clip_skip_categories": OptionInfo(["artists", "movements", "flavors"], "CLIP: skip inquire categories", gr.CheckboxGroup, lambda: {"choices": modules.interrogate.category_types()}, refresh=modules.interrogate.category_types), + "interrogate_clip_skip_categories": OptionInfo(["artists", "movements", "flavors"], "Interrogate: skip categories", gr.CheckboxGroup, lambda: {"choices": modules.interrogate.category_types()}, refresh=modules.interrogate.category_types), "interrogate_deepbooru_score_threshold": OptionInfo(0.65, "Interrogate: deepbooru score threshold", gr.Slider, {"minimum": 0, "maximum": 1, "step": 0.01}), "deepbooru_sort_alpha": OptionInfo(False, "Interrogate: deepbooru sort alphabetically"), "deepbooru_use_spaces": OptionInfo(False, "Use spaces for tags in deepbooru"), @@ -805,7 +821,6 @@ class Options: value = expected_type(value) return value - opts = Options() config_filename = cmd_opts.config opts.load(config_filename) @@ -817,7 +832,8 @@ else: opts.data['sd_backend'] = 'diffusers' if backend == Backend.DIFFUSERS else 'original' opts.data['uni_pc_lower_order_final'] = opts.schedulers_use_loworder opts.data['uni_pc_order'] = opts.schedulers_solver_order -log.info(f'Engine: backend={backend}') +log.info(f'Engine: backend={backend} compute={devices.backend} mode={devices.inference_context.__name__} device={devices.get_optimal_device_name()}') +log.info(f'Device: {print_dict(devices.get_gpu_info())}') prompt_styles = modules.styles.StyleDatabase(opts) cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or (cmd_opts.server_name or False)) and not cmd_opts.insecure @@ -906,14 +922,20 @@ total_tqdm = TotalTQDM() def restart_server(restart=True): if demo is None: return - log.info('Server shutdown requested') + log.warning('Server shutdown requested') try: - demo.server.wants_restart = restart - demo.server.should_exit = True - demo.server.force_exit = True - demo.close(verbose=False) - demo.server.close() - demo.fns = [] + sys.tracebacklimit = 0 + stdout = io.StringIO() + stderr = io.StringIO() + with contextlib.redirect_stdout(stdout), contextlib.redirect_stdout(stderr): + demo.server.wants_restart = restart + demo.server.should_exit = True + demo.server.force_exit = True + demo.close(verbose=False) + demo.server.close() + demo.fns = [] + time.sleep(1) + sys.tracebacklimit = 100 # os._exit(0) except (Exception, BaseException) as e: log.error(f'Server shutdown error: {e}') diff --git a/modules/styles.py b/modules/styles.py index a13da7bcd..abeeb5e08 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -3,22 +3,18 @@ from __future__ import annotations import csv import os import json -import shutil -import typing from installer import log from modules import paths -if typing.TYPE_CHECKING: - # Only import this when code is being type-checked, it doesn't have any effect at runtime - from .processing import StableDiffusionProcessing - - -class PromptStyle(typing.NamedTuple): - name: str - prompt: str - negative_prompt: str - extra: str = "" +class Style(): + def __init__(self, name: str, prompt: str = "", negative_prompt: str = "", extra: str = "", filename: str = "", preview: str = ""): + self.name = name + self.prompt = prompt + self.negative_prompt = negative_prompt + self.extra = extra + self.filename = filename + self.preview = preview def merge_prompts(style_prompt: str, prompt: str) -> str: @@ -43,36 +39,40 @@ def apply_styles_to_prompt(prompt, styles): class StyleDatabase: def __init__(self, opts): - self.no_style = PromptStyle("None", "", "") + self.no_style = Style("None") self.styles = {} self.path = opts.styles_dir - if os.path.isfile(opts.styles_dir): + if os.path.isfile(opts.styles_dir) or opts.styles_dir.endswith(".csv"): legacy_file = opts.styles_dir self.load_csv(legacy_file) opts.styles_dir = os.path.join(paths.models_path, "styles") self.path = opts.styles_dir - self.mkdir() + os.makedirs(opts.styles_dir, exist_ok=True) self.save_styles(opts.styles_dir, verbose=True) - log.debug(f'Migrated styles: file={legacy_file} folder={self.path}') - self.mkdir() - self.reload() - - def mkdir(self): - if not os.path.isdir(self.path): - os.makedirs(self.path, exist_ok=True) - log.debug(f'Created styles: folder={self.path}') + log.debug(f'Migrated styles: file={legacy_file} folder={opts.styles_dir}') + self.reload() + if not os.path.isdir(opts.styles_dir): + opts.styles_dir = os.path.join(paths.models_path, "styles") + self.path = opts.styles_dir + os.makedirs(opts.styles_dir, exist_ok=True) def reload(self): self.styles.clear() - for fn in os.listdir(self.path): - if not fn.endswith(".json"): - continue - with open(os.path.join(self.path, fn), 'r', encoding='utf-8') as f: - try: - style = json.load(f) - self.styles[style["name"]] = PromptStyle(style["name"], style["prompt"], style["negative"], style["extra"]) - except Exception as e: - log.error(f'Failed to load style: file={fn} error={e}') + 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(".json"): + with open(fn, 'r', encoding='utf-8') as f: + try: + style = json.load(f) + fn = os.path.splitext(os.path.relpath(fn, self.path))[0] + self.styles[style["name"]] = Style(style["name"], style.get("prompt", ""), style.get("negative", ""), style.get("extra", ""), fn, style.get("preview", "")) + except Exception as e: + log.error(f'Failed to load style: file={fn} error={e}') + elif os.path.isdir(fn) and not fn.startswith('.'): + list_folder(fn) + + list_folder(self.path) log.debug(f'Loaded styles: folder={self.path} items={len(self.styles.keys())}') def get_style_prompts(self, styles): @@ -94,8 +94,11 @@ class StyleDatabase: "prompt": self.styles[name].prompt, "negative": self.styles[name].negative_prompt, "extra": "", + "preview": "", } - fn = os.path.join(path, name + ".json") + keepcharacters = (' ','.','_') + fn = "".join(c for c in name if c.isalnum() or c in keepcharacters).rstrip() + fn = os.path.join(path, fn + ".json") try: with open(fn, 'w', encoding='utf-8') as f: json.dump(style, f, indent=2) @@ -103,20 +106,23 @@ class StyleDatabase: log.debug(f'Saved style: name={name} file={fn}') except Exception as e: log.error(f'Failed to save style: name={name} file={path} error={e}') - log.debug(f'Saved styles: {path} {len(self.styles.keys())}') + count = len(list(self.styles)) + if count > 0: + log.debug(f'Saved styles: {path} {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) for row in reader: try: - prompt = row["prompt"] if "prompt" in row else row["text"] - negative_prompt = row.get("negative_prompt", "") - self.styles[row["name"]] = PromptStyle(row["name"], prompt, negative_prompt) + self.styles[row["name"]] = Style(row["name"], row["prompt"] if "prompt" in row else row["text"], row.get("negative_prompt", "")) except Exception: log.error(f'Styles error: file={legacy_file} row={row}') log.debug(f'Loaded legacy styles: file={legacy_file} items={len(self.styles.keys())}') + """ def save_csv(self, path: str) -> None: import tempfile basedir = os.path.dirname(path) @@ -124,8 +130,9 @@ class StyleDatabase: os.makedirs(basedir, exist_ok=True) fd, temp_path = tempfile.mkstemp(".csv") with os.fdopen(fd, "w", encoding="utf-8-sig", newline='') as file: - writer = csv.DictWriter(file, fieldnames=PromptStyle._fields) + writer = csv.DictWriter(file, fieldnames=Style._fields) writer.writeheader() writer.writerows(style._asdict() for k, style in self.styles.items()) log.debug(f'Saved legacy styles: {path} {len(self.styles.keys())}') shutil.move(temp_path, path) + """ diff --git a/modules/taesd/sd_vae_taesd.py b/modules/taesd/sd_vae_taesd.py index 179416624..d50f533c3 100644 --- a/modules/taesd/sd_vae_taesd.py +++ b/modules/taesd/sd_vae_taesd.py @@ -61,3 +61,22 @@ def decode(latents): enc = latents.unsqueeze(0).to(devices.device, devices.dtype_vae) image = vae.decoder(enc).clamp(0, 1).detach() return image[0] + +def encode(image): + from modules import shared + model_class = shared.sd_model_type + if model_class == 'ldm': + model_class = 'sd' + if 'sd' not in model_class: + shared.log.warning(f'TAESD unsupported model type: {model_class}') + return Image.new('RGB', (8, 8), color = (0, 0, 0)) + vae = taesd_models[f'{model_class}-encoder'] + if vae is None: + model_path = os.path.join(paths_internal.models_path, "TAESD", f"tae{model_class}_encoder.pth") + download_model(model_path) + if os.path.exists(model_path): + taesd_models[f'{model_class}-encoder'] = TAESD(encoder_path=model_path, decoder_path=None) + vae = taesd_models[f'{model_class}-encoder'] + vae.to(devices.device, devices.dtype_vae) + latents = vae.encoder(image).detach() + return latents diff --git a/modules/taesd/taesd.py b/modules/taesd/taesd.py index 0355a81ff..4900a5ab0 100644 --- a/modules/taesd/taesd.py +++ b/modules/taesd/taesd.py @@ -5,6 +5,8 @@ Tiny AutoEncoder for Stable Diffusion """ import torch import torch.nn as nn +from modules import devices + def conv(n_in, n_out, **kwargs): return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs) @@ -65,7 +67,7 @@ class TAESD(nn.Module): return x.sub(TAESD.latent_shift).mul(2 * TAESD.latent_magnitude) -@torch.no_grad() +@devices.inference_context() def main(): from PIL import Image import sys diff --git a/modules/textual_inversion/autocrop.py b/modules/textual_inversion/autocrop.py index 31abd782d..bac4e618e 100644 --- a/modules/textual_inversion/autocrop.py +++ b/modules/textual_inversion/autocrop.py @@ -295,10 +295,8 @@ def is_square(w, h): def download_and_cache_models(dirname): download_url = 'https://github.com/opencv/opencv_zoo/blob/91fb0290f50896f38a0ab1e558b74b16bc009428/models/face_detection_yunet/face_detection_yunet_2022mar.onnx?raw=true' model_file_name = 'face_detection_yunet.onnx' - if not os.path.exists(dirname): - os.makedirs(dirname) - + os.makedirs(dirname, exist_ok=True) cache_file = os.path.join(dirname, model_file_name) if not os.path.exists(cache_file): print(f"downloading face detection model from '{download_url}' to '{cache_file}'") diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index a0090cdd3..a9e200822 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -21,13 +21,10 @@ textual_inversion_templates = {} def list_textual_inversion_templates(): textual_inversion_templates.clear() - for root, _dirs, fns in os.walk(shared.opts.embeddings_templates_dir): for fn in fns: path = os.path.join(root, fn) - textual_inversion_templates[fn] = TextualInversionTemplate(fn, path) - return textual_inversion_templates @@ -35,6 +32,7 @@ class Embedding: def __init__(self, vec, name, step=None): self.vec = vec self.name = name + self.tag = name self.step = step self.shape = None self.vectors = 0 @@ -81,13 +79,11 @@ class DirWithTextualInversionEmbeddings: def has_changed(self): if not os.path.isdir(self.path): return False - return directory_mtime(self.path) != self.mtime def update(self): if not os.path.isdir(self.path): return - self.mtime = directory_mtime(self.path) @@ -148,9 +144,9 @@ class EmbeddingDatabase: embeddings_dict[k] = f.get_tensor(k) for i in range(len(embeddings_dict["clip_l"])): if i == 0: - token = name + token = name.lower() else: - token = f"{name}_{i}" + token = f"{name.lower()}_{i}" pipe.tokenizer.add_tokens(token) token_id = pipe.tokenizer.convert_tokens_to_ids(token) pipe.text_encoder.resize_token_embeddings(len(pipe.tokenizer)) @@ -177,19 +173,14 @@ class EmbeddingDatabase: return if ext in ['.PNG', '.WEBP', '.JXL', '.AVIF']: - _, second_ext = os.path.splitext(name) - if second_ext.upper() == '.PREVIEW': + if '.preview' in filename.lower(): return embed_image = Image.open(path) if hasattr(embed_image, 'text') and 'sd-ti-embedding' in embed_image.text: data = embedding_from_b64(embed_image.text['sd-ti-embedding']) - name = data.get('name', name) else: data = extract_image_data_embed(embed_image) - if data: - name = data.get('name', name) - else: - # if data is None, means this is not an embeding, just a preview image + if not data: # if data is None, means this is not an embeding, just a preview image return elif ext in ['.BIN', '.PT']: data = torch.load(path, map_location="cpu") @@ -207,17 +198,18 @@ class EmbeddingDatabase: # diffuser concepts elif type(data) == dict and type(next(iter(data.values()))) == torch.Tensor: if len(data.keys()) != 1: - # shared.log.warning(f"Skipping embedding: {filename} multiple keys found") self.skipped_embeddings[name] = Embedding(None, name) return emb = next(iter(data.values())) if len(emb.shape) == 1: emb = emb.unsqueeze(0) else: - raise RuntimeError(f"Couldn't identify {filename} as neither textual inversion embedding nor diffuser concept.") + raise RuntimeError(f"Couldn't identify {filename} as textual inversion embedding") vec = emb.detach().to(devices.device, dtype=torch.float32) + # name = data.get('name', name) embedding = Embedding(vec, name) + embedding.tag = data.get('name', None) embedding.step = data.get('step', None) embedding.sd_checkpoint = data.get('sd_checkpoint', None) embedding.sd_checkpoint_name = data.get('sd_checkpoint_name', None) diff --git a/modules/txt2img.py b/modules/txt2img.py index 648eb836a..899af07db 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -4,9 +4,9 @@ from modules.generation_parameters_copypaste import create_override_settings_dic from modules.ui import plaintext_to_html -def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, latent_index: int, full_quality: bool, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, diffusers_guidance_rescale: float, clip_skip: int, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, refiner_steps: int, refiner_start: int, refiner_prompt: str, refiner_negative: str, override_settings_texts, *args): # pylint: disable=unused-argument +def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, latent_index: int, full_quality: bool, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, diffusers_guidance_rescale: float, clip_skip: int, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_force: bool, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, refiner_steps: int, refiner_start: int, refiner_prompt: str, refiner_negative: str, override_settings_texts, *args): # pylint: disable=unused-argument - shared.log.debug(f'txt2img: id_task={id_task}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|full_quality={full_quality}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|clip_skip={clip_skip}|seed={seed}|subseed={subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}||height={height}|width={width}|enable_hr={enable_hr}|denoising_strength={denoising_strength}|hr_scale={hr_scale}|hr_upscaler={hr_upscaler}|hr_second_pass_steps={hr_second_pass_steps}|hr_resize_x={hr_resize_x}|hr_resize_y={hr_resize_y}|image_cfg_scale={image_cfg_scale}|diffusers_guidance_rescale={diffusers_guidance_rescale}|refiner_steps={refiner_steps}|refiner_start={refiner_start}||refiner_prompt={refiner_prompt}|refiner_negative={refiner_negative}|override_settings_texts={override_settings_texts}|args={args}') + shared.log.debug(f'txt2img: id_task={id_task}|prompt={prompt}|negative_prompt={negative_prompt}|prompt_styles={prompt_styles}|steps={steps}|sampler_index={sampler_index}|latent_index={latent_index}|full_quality={full_quality}|restore_faces={restore_faces}|tiling={tiling}|n_iter={n_iter}|batch_size={batch_size}|cfg_scale={cfg_scale}|clip_skip={clip_skip}|seed={seed}|subseed={subseed}|subseed_strength={subseed_strength}|seed_resize_from_h={seed_resize_from_h}|seed_resize_from_w={seed_resize_from_w}||height={height}|width={width}|enable_hr={enable_hr}|denoising_strength={denoising_strength}|hr_scale={hr_scale}|hr_upscaler={hr_upscaler}|hr_force={hr_force}|hr_second_pass_steps={hr_second_pass_steps}|hr_resize_x={hr_resize_x}|hr_resize_y={hr_resize_y}|image_cfg_scale={image_cfg_scale}|diffusers_guidance_rescale={diffusers_guidance_rescale}|refiner_steps={refiner_steps}|refiner_start={refiner_start}|refiner_prompt={refiner_prompt}|refiner_negative={refiner_negative}|override_settings_texts={override_settings_texts}') if shared.sd_model is None: shared.log.warning('Model not loaded') @@ -49,6 +49,7 @@ def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, step denoising_strength=denoising_strength, hr_scale=hr_scale, hr_upscaler=hr_upscaler, + hr_force=hr_force, hr_second_pass_steps=hr_second_pass_steps, hr_resize_x=hr_resize_x, hr_resize_y=hr_resize_y, diff --git a/modules/ui.py b/modules/ui.py index 87c9cbb1b..c1154d9d9 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -73,7 +73,7 @@ def send_gradio_gallery_to_image(x): def add_style(name: str, prompt: str, negative_prompt: str): if name is None: return [gr_show() for x in range(4)] - style = modules.styles.PromptStyle(name, prompt, negative_prompt) + style = modules.styles.Style(name, prompt, negative_prompt) modules.shared.prompt_styles.styles[style.name] = style modules.shared.prompt_styles.save_styles(modules.shared.opts.styles_dir) return [gr.Dropdown.update(visible=True, choices=list(modules.shared.prompt_styles.styles)) for _ in range(2)] @@ -235,11 +235,11 @@ def create_toprow(is_img2img): with gr.Row(): with gr.Column(scale=80): with gr.Row(): - prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) + prompt = gr.Textbox(elem_id=f"{id_part}_prompt", label="Prompt", show_label=False, lines=3, placeholder="Prompt", elem_classes=["prompt"]) with gr.Row(): with gr.Column(scale=80): with gr.Row(): - negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) + negative_prompt = gr.Textbox(elem_id=f"{id_part}_neg_prompt", label="Negative prompt", show_label=False, lines=3, placeholder="Negative prompt", elem_classes=["prompt"]) button_interrogate = None button_deepbooru = None if is_img2img: @@ -270,7 +270,7 @@ def create_toprow(is_img2img): negative_token_button = gr.Button(visible=False, elem_id=f"{id_part}_negative_token_button") with gr.Row(elem_id=f"{id_part}_styles_row"): prompt_styles = gr.Dropdown(label="Styles", elem_id=f"{id_part}_styles", choices=[k for k, v in modules.shared.prompt_styles.styles.items()], value=[], multiselect=True) - create_refresh_button(prompt_styles, modules.shared.prompt_styles.reload, lambda: {"choices": [k for k, v in modules.shared.prompt_styles.styles.items()]}, f"refresh_{id_part}_styles") + # create_refresh_button(prompt_styles, modules.shared.prompt_styles.reload, lambda: {"choices": [k for k, v in modules.shared.prompt_styles.styles.items()]}, f"refresh_{id_part}_styles") prompt_styles_btn = gr.Button('Apply', elem_id=f"{id_part}_styles_select", visible=False) prompt_styles_btn.click(_js="applyStyles", fn=parse_style, inputs=[prompt_styles], outputs=[prompt_styles]) return prompt, prompt_styles, negative_prompt, submit, button_interrogate, button_deepbooru, prompt_style_apply, save_style, paste, extra_networks_button, token_counter, token_button, negative_token_counter, negative_token_button @@ -389,35 +389,36 @@ def create_ui(startup_timer = None): tiling = gr.Checkbox(label='Tiling', value=False, elem_id="txt2img_tiling") with FormGroup(visible=show_second_pass.value, elem_id="txt2img_second_pass") as second_pass_group: - with FormRow(elem_id="sampler_selection_txt2img_alt_row1"): - latent_index = gr.Dropdown(label='Secondary sampler', elem_id="txt2img_sampling_alt", choices=[x.name for x in modules.sd_samplers.samplers], value='Default', type="index") - denoising_strength = gr.Slider(minimum=0.05, maximum=1.0, step=0.01, label='Denoising strength', value=0.3, elem_id="txt2img_denoising_strength") - with FormRow(elem_id="txt2img_hires_finalres", variant="compact"): - hr_final_resolution = FormHTML(value="", elem_id="txtimg_hr_finalres", label="Upscaled resolution", interactive=False) - with FormRow(elem_id="txt2img_hires_fix_row1", variant="compact"): - hr_upscaler = gr.Dropdown(label="Upscaler", elem_id="txt2img_hr_upscaler", choices=[*modules.shared.latent_upscale_modes, *[x.name for x in modules.shared.sd_upscalers]], value=modules.shared.latent_upscale_default_mode) - hr_second_pass_steps = gr.Slider(minimum=0, maximum=99, step=1, label='Hires steps', elem_id="txt2img_steps_alt", value=20) - with FormRow(elem_id="txt2img_hires_fix_row2", variant="compact"): - hr_scale = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label="Upscale by", value=2.0, elem_id="txt2img_hr_scale") - with FormRow(elem_id="txt2img_hires_fix_row3", variant="compact"): - hr_resize_x = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize width to", value=0, elem_id="txt2img_hr_resize_x") - hr_resize_y = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize height to", value=0, elem_id="txt2img_hr_resize_y") - - with FormRow(): - hr_refiner = FormHTML(value="Refiner", elem_id="txtimg_hr_refiner", interactive=False) - with FormRow(elem_id="txt2img_refiner_row1", variant="compact"): - refiner_start = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Refiner start', value=0.8, elem_id="txt2img_refiner_start") - refiner_steps = gr.Slider(minimum=0, maximum=99, step=1, label="Refiner steps", elem_id="txt2img_refiner_steps", value=5) - with FormRow(elem_id="txt2img_refiner_row3", variant="compact"): - refiner_prompt = gr.Textbox(value='', label='Secondary Prompt') - with FormRow(elem_id="txt2img_refiner_row4", variant="compact"): - refiner_negative = gr.Textbox(value='', label='Secondary negative prompt') + with FormGroup(): + with FormRow(elem_id="sampler_selection_txt2img_alt_row1"): + latent_index = gr.Dropdown(label='Secondary sampler', elem_id="txt2img_sampling_alt", choices=[x.name for x in modules.sd_samplers.samplers], value='Default', type="index") + denoising_strength = gr.Slider(minimum=0.05, maximum=1.0, step=0.01, label='Denoising strength', value=0.5, elem_id="txt2img_denoising_strength") + with FormRow(elem_id="txt2img_hires_finalres", variant="compact"): + hr_final_resolution = FormHTML(value="", elem_id="txtimg_hr_finalres", label="Upscaled resolution", interactive=False) + with FormRow(elem_id="txt2img_hires_fix_row1", variant="compact"): + hr_upscaler = gr.Dropdown(label="Upscaler", elem_id="txt2img_hr_upscaler", choices=[*modules.shared.latent_upscale_modes, *[x.name for x in modules.shared.sd_upscalers]], value=modules.shared.latent_upscale_default_mode) + hr_force = gr.Checkbox(label='Force Hires', value=False, elem_id="txt2img_hr_force") + with FormRow(elem_id="txt2img_hires_fix_row2", variant="compact"): + hr_second_pass_steps = gr.Slider(minimum=0, maximum=99, step=1, label='Hires steps', elem_id="txt2img_steps_alt", value=20) + hr_scale = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label="Upscale by", value=2.0, elem_id="txt2img_hr_scale") + with FormRow(elem_id="txt2img_hires_fix_row3", variant="compact"): + hr_resize_x = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize width to", value=0, elem_id="txt2img_hr_resize_x") + hr_resize_y = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize height to", value=0, elem_id="txt2img_hr_resize_y") + with FormGroup(visible=modules.shared.backend == modules.shared.Backend.DIFFUSERS): + with FormRow(): + hr_refiner = FormHTML(value="Refiner", elem_id="txtimg_hr_refiner", interactive=False) + with FormRow(elem_id="txt2img_refiner_row1", variant="compact"): + refiner_start = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label='Refiner start', value=0.8, elem_id="txt2img_refiner_start") + refiner_steps = gr.Slider(minimum=0, maximum=99, step=1, label="Refiner steps", elem_id="txt2img_refiner_steps", value=5) + with FormRow(elem_id="txt2img_refiner_row3", variant="compact"): + refiner_prompt = gr.Textbox(value='', label='Secondary Prompt') + with FormRow(elem_id="txt2img_refiner_row4", variant="compact"): + refiner_negative = gr.Textbox(value='', label='Secondary negative prompt') with FormRow(elem_id="txt2img_override_settings_row") as row: override_settings = create_override_settings_dropdown('txt2img', row) - with FormGroup(elem_id="txt2img_script_container"): - custom_inputs = modules.scripts.scripts_txt2img.setup_ui() + custom_inputs = modules.scripts.scripts_txt2img.setup_ui() hr_resolution_preview_inputs = [show_second_pass, width, height, hr_scale, hr_resize_x, hr_resize_y, hr_upscaler] for preview_input in hr_resolution_preview_inputs: @@ -450,7 +451,7 @@ def create_ui(startup_timer = None): seed, subseed, subseed_strength, seed_resize_from_h, seed_resize_from_w, height, width, show_second_pass, denoising_strength, - hr_scale, hr_upscaler, hr_second_pass_steps, hr_resize_x, hr_resize_y, + hr_scale, hr_upscaler, hr_force, hr_second_pass_steps, hr_resize_x, hr_resize_y, refiner_steps, refiner_start, refiner_prompt, refiner_negative, override_settings, ] + custom_inputs, @@ -465,13 +466,14 @@ def create_ui(startup_timer = None): txt2img_prompt.submit(**txt2img_args) submit.click(**txt2img_args) - def enable_hr_change(visible: bool): - return {"visible": visible, "__type__": "update"}, f'Refiner: {"disabled" if modules.shared.opts.sd_model_refiner == "None" else "enabled"}' + def enable_hr_change(visible: bool, refiner_start): + enabled = modules.shared.opts.sd_model_refiner != "None" and refiner_start > 0 and refiner_start < 1 + return {"visible": visible, "__type__": "update"}, f'Refiner: {"enabled" if enabled else "disabled"}' res_switch_btn.click(lambda w, h: (h, w), inputs=[width, height], outputs=[width, height], show_progress=False) batch_switch_btn.click(lambda w, h: (h, w), inputs=[batch_count, batch_size], outputs=[batch_count, batch_size], show_progress=False) txt_prompt_img.change(fn=modules.images.image_data, inputs=[txt_prompt_img], outputs=[txt2img_prompt, txt_prompt_img]) - show_second_pass.change(enable_hr_change, inputs=[show_second_pass], outputs=[second_pass_group, hr_refiner], show_progress = False) + show_second_pass.change(enable_hr_change, inputs=[show_second_pass, refiner_start], outputs=[second_pass_group, hr_refiner], show_progress = False) show_seed.change(gr_show, inputs=[show_seed], outputs=[seed_group], show_progress = False) show_batch.change(gr_show, inputs=[show_batch], outputs=[batch_group], show_progress = False) show_advanced.change(gr_show, inputs=[show_advanced], outputs=[advanced_group], show_progress = False) @@ -522,7 +524,7 @@ def create_ui(startup_timer = None): negative_token_button.click(fn=wrap_queued_call(update_token_counter), inputs=[txt2img_negative_prompt, steps], outputs=[negative_token_counter]) ui_extra_networks.setup_ui(extra_networks_ui, txt2img_gallery) - log.debug(f'UI interface: tab=txt2img batch={show_batch.value} seed={show_seed.value} advanced={show_advanced.value} second_pass={show_second_pass.value}') + # log.debug(f'UI interface: tab=txt2img batch={show_batch.value} seed={show_seed.value} advanced={show_advanced.value} second_pass={show_second_pass.value}') timer.startup.record("ui-txt2img") @@ -882,8 +884,7 @@ def create_ui(startup_timer = None): parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding( paste_button=img2img_paste, tabname="img2img", source_text_component=img2img_prompt, source_image_component=None, )) - - log.debug(f'UI interface: tab=img2img seed={show_seed.value} resize={show_resize.value} batch={show_batch.value} denoise={show_denoise.value} advanced={show_advanced.value}') + # log.debug(f'UI interface: tab=img2img seed={show_seed.value} resize={show_resize.value} batch={show_batch.value} denoise={show_denoise.value} advanced={show_advanced.value}') timer.startup.record("ui-img2img") @@ -928,11 +929,15 @@ def create_ui(startup_timer = None): if info.refresh is not None: if is_quicksettings: res = comp(label=info.label, value=fun(), elem_id=elem_id, **args) - create_refresh_button(res, info.refresh, args, f"refresh_{key}") + ui_common.create_refresh_button(res, info.refresh, info.component_args, f"refresh_{key}") else: with FormRow(): res = comp(label=info.label, value=fun(), elem_id=elem_id, **args) - create_refresh_button(res, info.refresh, args, f"refresh_{key}") + ui_common.create_refresh_button(res, info.refresh, info.component_args, f"refresh_{key}") + elif info.folder is not None: + with FormRow(): + res = comp(label=info.label, value=fun(), elem_id=elem_id, elem_classes="folder-selector", **args) + ui_common.create_browse_button(res, f"folder_{key}") else: try: res = comp(label=info.label, value=fun(), elem_id=elem_id, **args) @@ -1067,7 +1072,7 @@ def create_ui(startup_timer = None): with gr.TabItem("Show all pages", variant='primary', elem_id="settings_show_all_pages"): create_dirty_indicator("show_all_pages", [], interactive=False) - with gr.TabItem("UI Config", id="system_config", elem_id="system_config_tab"): + with gr.TabItem("UI Config", id="system_config", elem_id="tab_config"): loadsave.create_ui() create_dirty_indicator("tab_defaults", [], interactive=False) @@ -1085,7 +1090,7 @@ def create_ui(startup_timer = None): unload_sd_model.click(fn=unload_sd_weights, inputs=[], outputs=[]) reload_sd_model.click(fn=reload_sd_weights, inputs=[], outputs=[]) request_notifications.click(fn=lambda: None, inputs=[], outputs=[], _js='function(){}') - preview_theme.click(fn=None, _js='preview_theme', inputs=[dummy_component], outputs=[dummy_component]) + preview_theme.click(fn=None, _js='previewTheme', inputs=[], outputs=[]) timer.startup.record("ui-settings") @@ -1154,7 +1159,7 @@ def create_ui(startup_timer = None): show_progress=info.refresh is not None, ) - button_set_checkpoint = gr.Button('Change checkpoint', elem_id='change_checkpoint', visible=False) + button_set_checkpoint = gr.Button('Change model', elem_id='change_checkpoint', visible=False) button_set_checkpoint.click( fn=lambda value, _: run_settings_single(value, key='sd_model_checkpoint'), _js="function(v){ var res = desiredCheckpointName; desiredCheckpointName = ''; return [res || v, null]; }", diff --git a/modules/ui_common.py b/modules/ui_common.py index 58ea9f8f8..b4f31734c 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -9,6 +9,7 @@ from modules import call_queue, shared from modules.generation_parameters_copypaste import image_from_url_text import modules.ui_symbols as symbols import modules.images +import modules.script_callbacks def update_generation_info(generation_info, html_info, img_index): @@ -115,6 +116,8 @@ def save_files(js_data, images, html_info, index): os.makedirs(destination, exist_ok = True) shutil.copy(fullfn, destination) shared.log.info(f"Copying image: {fullfn} -> {destination}") + tgt_filename = os.path.join(destination, os.path.basename(fullfn)) + modules.script_callbacks.image_save_btn_callback(tgt_filename) else: image = image_from_url_text(filedata) info = p.infotexts[i + 1] if len(p.infotexts) > len(p.all_seeds) else p.infotexts[i] # infotexts may be offset by 1 because the first image is the grid @@ -127,6 +130,7 @@ def save_files(js_data, images, html_info, index): if txt_fullfn: filenames.append(os.path.basename(txt_fullfn)) fullfns.append(txt_fullfn) + modules.script_callbacks.image_save_btn_callback(filename) if shared.opts.samples_save_zip and len(fullfns) > 1: zip_filepath = os.path.join(shared.opts.outdir_save, "images.zip") from zipfile import ZipFile @@ -225,3 +229,17 @@ def create_refresh_button(refresh_component, refresh_method, refreshed_args, ele refresh_button = ToolButton(value=symbols.refresh, elem_id=elem_id) refresh_button.click(fn=refresh, inputs=[], outputs=[refresh_component]) return refresh_button + +def create_browse_button(browse_component, elem_id): + + def browse(folder): + # import subprocess + if folder is not None: + return gr.update(value = folder) + return gr.update() + + from modules.ui_components import ToolButton + browse_button = ToolButton(value=symbols.folder, elem_id=elem_id) + browse_button.click(fn=browse, _js="async () => await browseFolder()", inputs=[browse_component], outputs=[browse_component]) + # browse_button.click(fn=browse, inputs=[browse_component], outputs=[browse_component]) + return browse_button diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py index aa7c60ef5..c789391a1 100644 --- a/modules/ui_extensions.py +++ b/modules/ui_extensions.py @@ -34,7 +34,7 @@ def update_extension_list(): try: with open(os.path.join(paths.script_path, "html", "extensions.json"), "r", encoding="utf-8") as f: extensions_list = json.loads(f.read()) - shared.log.debug(f'Extensions list loaded: {os.path.join(paths.script_path, "html", "extensions.json")}') + # shared.log.debug(f'Extensions list loaded: {os.path.join(paths.script_path, "html", "extensions.json")}') except Exception: shared.log.debug(f'Extensions list failed to load: {os.path.join(paths.script_path, "html", "extensions.json")}') found = [] @@ -290,7 +290,7 @@ def refresh_extensions_list_from_data(search_text, sort_column): Current version - + """ if len(extensions_list) == 0: update_extension_list() diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 17cf429b9..bc33ba7a4 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -64,7 +64,7 @@ def get_metadata(page: str = "", item: str = ""): metadata = page.metadata.get(item, 'none') if metadata is None: metadata = '' - shared.log.debug(f'Extra networks metadata: page={page} item={item} len={len(metadata)}') + shared.log.debug(f"Extra networks metadata: page='{page}' item={item} len={len(metadata)}") return JSONResponse({"metadata": metadata}) @@ -75,7 +75,7 @@ def get_info(page: str = "", item: str = ""): info = page.info.get(item, 'none') if info is None: info = '' - shared.log.debug(f'Extra networks info: page={page} item={item} len={len(info)}') + shared.log.debug(f"Extra networks info: page='{page}' item={item} len={len(info)}") return JSONResponse({"info": info}) @@ -150,7 +150,7 @@ class ExtraNetworksPage: def is_empty(self, folder): for f in listdir(folder): _fn, ext = os.path.splitext(f) - if ext.lower() in ['.ckpt', '.safetensors', '.pt'] or os.path.isdir(os.path.join(folder, f)): + if ext.lower() in ['.ckpt', '.safetensors', '.pt', '.json'] or os.path.isdir(os.path.join(folder, f)): return False return True @@ -164,21 +164,20 @@ class ExtraNetworksPage: continue try: img = Image.open(f) - if img.width > 1024 or img.height > 1024 or os.path.getsize(f) > 70000: + if img.width > 1024 or img.height > 1024 or os.path.getsize(f) > 65536: img = img.convert('RGB') img.thumbnail((512, 512), Image.HAMMING) - img.save(fn) + img.save(fn, quality=50) img.close() created += 1 except Exception as e: shared.log.error(f'Extra network error creating thumbnail: {f} {e}') if created > 0: - shared.log.info(f"Extra network created thumbnails: {self.name} {created}") + shared.log.info(f"Extra network thumbnails: {self.name} created={created}") self.missing_thumbs.clear() def create_page(self, tabname, skip = False): - if self.refresh_time is not None and self.refresh_time > refresh_time: - # shared.log.debug(f'Extra networks: {self.name} items={len(self.items)} tab={tabname} cached') + if self.refresh_time is not None and self.refresh_time > refresh_time: # cached page return self.html t0 = time.time() self_name_id = self.name.replace(" ", "_") @@ -219,7 +218,7 @@ class ExtraNetworksPage: else: return '' t1 = time.time() - shared.log.debug(f'Extra networks: {self.name} items={len(self.items)} subdirs={len(subdirs)} tab={tabname} time={round(t1-t0, 2)}') + shared.log.debug(f"Extra networks: page='{self.name}' items={len(self.items)} subdirs={len(subdirs)} tab={tabname} dirs={self.allowed_directories_for_previews()} time={round(t1-t0, 2)}") threading.Thread(target=self.create_thumb).start() def list_items(self): @@ -302,12 +301,48 @@ class ExtraNetworksPage: pass return '' + def save_preview(self, index, images, filename): + try: + image = image_from_url_text(images[int(index)]) + except Exception as e: + shared.log.error(f'Extra network save preview: {filename} {e}') + return + is_allowed = False + for page in extra_pages: + if any(path_is_parent(x, filename) for x in page.allowed_directories_for_previews()): + is_allowed = True + break + if not is_allowed: + shared.log.error(f'Extra network save preview: {filename} not allowed') + return + if image.width > 512 or image.height > 512: + image = image.convert('RGB') + image.thumbnail((512, 512), Image.HAMMING) + image.save(filename, quality=50) + fn, _ext = os.path.splitext(filename) + thumb = fn + '.thumb.jpg' + if os.path.exists(thumb): + shared.log.debug(f'Extra network delete thumbnail: {thumb}') + os.remove(thumb) + shared.log.info(f'Extra network save preview: {filename}') + + def save_description(self, filename, desc): + lastDotIndex = filename.rindex('.') + filename = filename[0:lastDotIndex]+".txt" + if desc != "": + try: + with open(filename, 'w', encoding='utf-8') as f: + f.write(desc) + shared.log.info(f'Extra network save description: {filename} {desc}') + except Exception as e: + shared.log.error(f'Extra network save description: {filename} {e}') + def initialize(): extra_pages.clear() -def register_default_pages(): +def register_pages(): from modules.ui_extra_networks_textual_inversion import ExtraNetworksPageTextualInversion from modules.ui_extra_networks_hypernets import ExtraNetworksPageHypernetworks from modules.ui_extra_networks_checkpoints import ExtraNetworksPageCheckpoints @@ -360,22 +395,22 @@ def create_ui(container, button, tabname, skip_indexing = False): is_visible = not is_visible return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary")) - state_visible = gr.State(value=False) # pylint: disable=abstract-class-instantiated - button.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container, button]) - button_close.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container]) - - def refresh(title): + def en_refresh(title): res = [] for page in extra_pages: - if title == '' or title == page.title or len(page.html) == 0: + if title is None or title == '' or title == page.title or len(page.html) == 0: page.refresh() + page.refresh_time = None page.create_page(ui.tabname) - shared.log.debug(f"Refreshing Extra networks: page={page.title} items={len(page.items)} tab={ui.tabname}") + shared.log.debug(f"Refreshing Extra networks: page='{page.title}' items={len(page.items)} tab={ui.tabname}") res.append(page.html) ui.search.update(value = ui.search.value) return res - button_refresh.click(_js='extraNetworksRefreshButton', fn=refresh, inputs=[ui.search], outputs=ui.pages) + state_visible = gr.State(value=False) # pylint: disable=abstract-class-instantiated + button.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container, button]) + button_close.click(fn=toggle_visibility, inputs=[state_visible], outputs=[state_visible, container]) + button_refresh.click(_js='getENActivePage', fn=en_refresh, inputs=[ui.search], outputs=ui.pages) return ui @@ -387,54 +422,37 @@ def path_is_parent(parent_path, child_path): def setup_ui(ui, gallery): - def save_preview(index, images, filename): - if len(images) == 0: - for page in extra_pages: - page.create_page(ui.tabname) - return [page.html for page in extra_pages] - index = int(index) - index = 0 if index < 0 else index - index = len(images) - 1 if index >= len(images) else index - img_info = images[index if index >= 0 else 0] - image = image_from_url_text(img_info) - is_allowed = False - for extra_page in extra_pages: - if any(path_is_parent(x, filename) for x in extra_page.allowed_directories_for_previews()): - is_allowed = True - break - assert is_allowed, f'writing to {filename} is not allowed' - image.save(filename) - fn, _ext = os.path.splitext(filename) - thumb = fn + '.thumb.jpg' - if os.path.exists(thumb): - shared.log.debug(f'Extra network delete thumbnail: {thumb}') - os.remove(thumb) - shared.log.info(f'Extra network save preview: {filename}') - return [page.create_page(ui.tabname) for page in extra_pages] + def save_preview(pagename, index, images, filename): + res = [] + for page in extra_pages: + if pagename is None or pagename == '' or pagename == page.title or len(page.html) == 0: + page.save_preview(index, images, filename) + res.append(page.create_page(ui.tabname)) + else: + res.append(page.html) + return res + ui.button_save_preview.click( fn=save_preview, - _js="function(x, y, z) {return [selected_gallery_index(), y, z]}", - inputs=[ui.preview_target_filename, gallery, ui.preview_target_filename], - outputs=[*ui.pages] + _js="function(t, i, y, z) {return [getENActivePage(), selected_gallery_index(), y, z]}", + inputs=[ui.search, ui.preview_target_filename, gallery, ui.preview_target_filename], + outputs=ui.pages ) - # write description to a file - def save_description(filename, desc): - lastDotIndex = filename.rindex('.') - filename = filename[0:lastDotIndex]+".txt" - if desc != "": - try: - with open(filename,'w', encoding='utf-8') as f: - f.write(desc) - shared.log.info(f'Extra network save description: {filename} {desc}') - except Exception as e: - shared.log.error(f'Extra network save description: {filename} {e}') - return [page.create_page(ui.tabname) for page in extra_pages] + def save_description(pagename, filename, desc): + res = [] + for page in extra_pages: + if pagename is None or pagename == '' or pagename == page.title or len(page.html) == 0: + page.save_description(filename, desc) + res.append(page.create_page(ui.tabname)) + else: + res.append(page.html) + return res ui.button_save_description.click( fn=save_description, - _js="function(x, y) { return [x, y] }", - inputs=[ui.description_target_filename, ui.description], - outputs=[*ui.pages] + _js="function(t, x, y) { return [getENActivePage(), x, y] }", + inputs=[ui.search, ui.description_target_filename, ui.description], + outputs=ui.pages ) diff --git a/modules/ui_extra_networks_styles.py b/modules/ui_extra_networks_styles.py index 2ccbed93b..c3e402d70 100644 --- a/modules/ui_extra_networks_styles.py +++ b/modules/ui_extra_networks_styles.py @@ -1,7 +1,6 @@ import os import html import json - from modules import shared, ui_extra_networks @@ -12,22 +11,53 @@ class ExtraNetworksPageStyles(ui_extra_networks.ExtraNetworksPage): def refresh(self): shared.prompt_styles.reload() + """ + import io + import base64 + from PIL import Image + + def image2str(image): + buff = io.BytesIO() + image.save(buff, format="JPEG", quality=80) + encoded = base64.b64encode(buff.getvalue()) + return encoded + + def str2image(data): + buff = io.BytesIO(base64.b64decode(data)) + return Image.open(buff) + + def save_preview(self, index, images, filename): + from modules.generation_parameters_copypaste import image_from_url_text + try: + image = image_from_url_text(images[int(index)]) + except Exception: + shared.log.error(f'Extra network save preview: {filename} no image') + return + if image.width > 512 or image.height > 512: + image = image.convert('RGB').thumbnail((512, 512), Image.HAMMING) + for k in shared.prompt_styles.styles.keys(): + if k == filename: + shared.prompt_styles.styles[k].preview = image2str(image) + break + + def save_description(self, filename, desc): + pass + """ + def list_items(self): - styles = list(shared.prompt_styles.styles) - for style in styles: - path = os.path.join(shared.opts.styles_dir, style) - txt = f'Prompt: {shared.prompt_styles.styles[style].prompt}' - negative = shared.prompt_styles.styles[style].negative_prompt - if negative is not None and len(negative) > 0: - txt += f'\nNegative: {negative}' + for k, v in shared.prompt_styles.styles.items(): + fn = os.path.join(shared.opts.styles_dir, v.filename) + txt = f'Prompt: {v.prompt}' + if len(v.negative_prompt) > 0: + txt += f'\nNegative: {v.negative_prompt}' yield { - "name": style, - "search_term": path, - "filename": path, - "preview": self.find_preview(path), + "name": v.name, + "search_term": f'{txt} /{v.filename}', + "filename": v.filename, + "preview": self.find_preview(fn), "description": txt, - "onclick": '"' + html.escape(f"""return selectStyle({json.dumps(style)})""") + '"', - "local_preview": f"{path}.{shared.opts.samples_format}", + "onclick": '"' + html.escape(f"""return selectStyle({json.dumps(k)})""") + '"', + "local_preview": f"{fn}.{shared.opts.samples_format}", } def allowed_directories_for_previews(self): diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index 4ad9ac10a..520c630d1 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -21,12 +21,18 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): def list_items(self): if sd_models.model_data.sd_model is None: embeddings = [] - for root, _dirs, fns in os.walk(shared.opts.embeddings_dir, followlinks=True): - for fn in fns: - if fn.lower().endswith(".pt") or fn.lower().endswith(".safetensors"): - embedding = Embedding(0, fn) - embedding.filename = os.path.join(root, fn) + + 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(0, os.path.basename(fn)) + embedding.filename = fn embeddings.append(embedding) + elif os.path.isdir(fn) and not fn.startswith('.'): + list_folder(fn) + + list_folder(shared.opts.embeddings_dir) elif shared.backend == shared.Backend.ORIGINAL: embeddings = list(sd_hijack.model_hijack.embedding_db.word_embeddings.values()) elif hasattr(sd_models.model_data.sd_model, 'embedding_db'): @@ -35,6 +41,9 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): embeddings = [] for embedding in embeddings: path, _ext = os.path.splitext(embedding.filename) + tags = {} + if embedding.tag is not None: + tags[embedding.tag]=1 yield { "name": os.path.splitext(embedding.name)[0], "filename": embedding.filename, @@ -44,6 +53,7 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): "search_term": self.search_terms_from_path(embedding.filename), "prompt": json.dumps(os.path.splitext(embedding.name)[0]), "local_preview": f"{path}.preview.{shared.opts.samples_format}", + "tags": tags, } def allowed_directories_for_previews(self): diff --git a/modules/ui_loadsave.py b/modules/ui_loadsave.py index b0c647e05..0bd24ff74 100644 --- a/modules/ui_loadsave.py +++ b/modules/ui_loadsave.py @@ -112,7 +112,6 @@ class UiLoadsave: self.write_to_file(self.ui_settings) def iter_changes(self, values): - from modules.shared import log """ given a dictionary with defaults from a file and current values from gradio elements, returns an iterator over tuples of values that are not the same between the file and the current; @@ -138,27 +137,49 @@ class UiLoadsave: continue if (new_value == default_value) and (old_value is None): continue - log.debug(f'Settings: name={name} component={component} old={old_value} default={default_value} new={new_value}') yield name, old_value, new_value, default_value return [] def ui_view(self, *values): - text = [''] - for path, old_value, new_value, default_value in self.iter_changes(values): + text = """ +
VariableUser valueNew valueDefault value
+ + + + + + + + + + + + + + + """ + changed = 0 + for name, old_value, new_value, default_value in self.iter_changes(values): + changed += 1 if old_value is None: - old_value = "None" - text.append(f"") - if len(text) == 1: - text.append("") - text.append("") - return "".join(text) + old_value = "None" + text += f"" + text += "
NameSaved valueNew valueDefault value
{path}{old_value}{new_value}{default_value}
No changes
{name}{old_value}{new_value}{default_value}
" + if changed == 0: + text = '

No changes

' + else: + text = f'

Changed values: {changed}

' + text + return text def ui_apply(self, *values): + from modules.shared import log num_changed = 0 current_ui_settings = self.read_from_file() - for path, _, new_value, _ in self.iter_changes(values): + for name, old_value, new_value, default_value in self.iter_changes(values): + component = self.component_mapping[name] + log.debug(f'Settings: name={name} component={component} old={old_value} default={default_value} new={new_value}') num_changed += 1 - current_ui_settings[path] = new_value + current_ui_settings[name] = new_value if num_changed == 0: return "No changes" self.write_to_file(current_ui_settings) @@ -173,12 +194,12 @@ class UiLoadsave: def create_ui(self): """creates ui elements for editing defaults UI, without adding any logic to them""" - gr.HTML(f"Review changed values and apply them as new user interface defaults
Config file: {self.filename}") with gr.Row(elem_id="config_row"): self.ui_defaults_view = gr.Button(value='View changes', elem_id="ui_defaults_view", variant="secondary") self.ui_defaults_apply = gr.Button(value='Set new defaults', elem_id="ui_defaults_apply", variant="primary") self.ui_defaults_restore = gr.Button(value='Restore system defaults', elem_id="ui_defaults_restore", variant="primary") self.ui_defaults_review = gr.HTML("") + gr.HTML(f"Review changed values and apply them as new user interface defaults

Config file: {self.filename}") def setup_ui(self): """adds logic to elements created with create_ui; all add_block class must be made before this""" diff --git a/modules/ui_models.py b/modules/ui_models.py index 33cdf8b06..bb90ef822 100644 --- a/modules/ui_models.py +++ b/modules/ui_models.py @@ -8,6 +8,7 @@ from modules.ui_common import create_refresh_button from modules.call_queue import wrap_gradio_gpu_call from modules.shared import opts, log import modules.errors +import modules.hashes def create_ui(): @@ -201,19 +202,15 @@ def create_ui(): def hf_download_model(hub_id: str, token, variant, revision, mirror): from modules.modelloader import download_diffusers_model - try: - download_diffusers_model(hub_id, cache_dir=opts.diffusers_dir, token=token, variant=variant, revision=revision, mirror=mirror) - except Exception as e: - log.error(f"Diffuser model downloaded error: model={hub_id} {e}") - return f"Diffuser model downloaded error: model={hub_id} {e}" + download_diffusers_model(hub_id, cache_dir=opts.diffusers_dir, token=token, variant=variant, revision=revision, mirror=mirror) from modules.sd_models import list_models # pylint: disable=W0621 list_models() - log.info(f"Diffuser model downloaded: model={hub_id}") - return f'Diffuser model downloaded: model={hub_id}' + log.info(f'Diffuser model downloaded: model="{hub_id}"') + return f'Diffuser model downloaded: model="{hub_id}"' with gr.Column(scale=6): with gr.Row(): - hf_search_text = gr.Textbox('', label = 'Seach models', placeholder='search huggingface models') + hf_search_text = gr.Textbox('', label = 'Search models', placeholder='search huggingface models') hf_search_btn = ToolButton(value="🔍", label="Search") with gr.Row(): with gr.Column(scale=2): @@ -252,7 +249,7 @@ def create_ui(): if tag is not None and len(tag) > 0: url += f'&tag={tag}' r = requests.get(url, timeout=60, headers=headers) - log.debug(f'CivitAI search: name={name} tag={tag} status={r.status_code}') + log.debug(f'CivitAI search: name="{name}" tag={tag or "none"} status={r.status_code}') if r.status_code != 200: return [], [], [] body = r.json() @@ -261,6 +258,8 @@ def create_ui(): data1 = [] for model in data: found = 0 + if model_type == 'LoRA' and model['type'] == 'LORA': + found += 1 for variant in model['modelVersions']: if model_type == 'SD 1.5': if 'SD 1.' in variant['baseModel']: @@ -297,7 +296,7 @@ def create_ui(): d['baseModel'], d['createdAt'], ]) - log.debug(f'CivitAI select: model={in_data[evt.index[0]]} versions={len(data2)}') + log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" versions={len(data2)}') return data2, preview_img def civit_select2(evt: gr.SelectData, in_data): @@ -315,7 +314,7 @@ def create_ui(): json.dumps(f['metadata']), f['downloadUrl'], ]) - log.debug(f'CivitAI select: model={in_data[evt.index[0]]} files={len(data3)}') + log.debug(f'CivitAI select: model="{in_data[evt.index[0]]}" files={len(data3)}') return data3 def civit_select3(evt: gr.SelectData, in_data): @@ -336,7 +335,7 @@ def create_ui(): list_models() return res - def civit_download_previews(): + def civit_download_previews(civit_previews_rehash): import requests from modules.ui_extra_networks import extra_pages from modules.modelloader import download_civit_preview @@ -347,17 +346,26 @@ def create_ui(): if item.get('fullname', None) is None: continue if 'card-no-preview.png' in item['preview'] and os.path.isfile(item['fullname']): + sha = item.get('hash', None) if item.get('hash', None) is None: - log.debug(f'CivitAI skipping item without hash: name={item["name"]}') + log.debug(f'CivitAI skipping item without hash: name="{item["name"]}"') continue - url = f'https://civitai.com/api/v1/model-versions/by-hash/{item["hash"]}' - r = requests.get(url, timeout=5, headers=headers) - log.debug(f'CivitAI search: name={item["name"]} hash={item["hash"]} status={r.status_code}') + r = requests.get(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}', timeout=5, headers=headers) + log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') if r.status_code == 200: d = r.json() if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: preview_url = d['images'][0]['url'] res += download_civit_preview(item['filename'], preview_url) + '
' + elif civit_previews_rehash and os.stat(item['fullname']).st_size < (1024 * 1024 * 1024): + sha = modules.hashes.calculate_sha256(item['fullname'], quiet=True) + r = requests.get(f'https://civitai.com/api/v1/model-versions/by-hash/{sha}', timeout=5, headers=headers) + log.debug(f'CivitAI search: name="{item["name"]}" hash={sha} status={r.status_code}') + if r.status_code == 200: + d = r.json() + if d.get('images') is not None and len(d['images']) > 0 and len(d['images'][0]['url']) > 0: + preview_url = d['images'][0]['url'] + res += download_civit_preview(item['filename'], preview_url) + '
' return res with gr.Row(): @@ -365,7 +373,7 @@ def create_ui(): civit_model_type = gr.Dropdown(label='Model type', choices=['SD 1.5', 'SD XL', 'LoRA', 'Other'], value='LoRA') with gr.Column(scale=15): with gr.Row(): - civit_search_text = gr.Textbox('', label = 'Seach models', placeholder='keyword') + civit_search_text = gr.Textbox('', label = 'Search models', placeholder='keyword') civit_search_tag = gr.Textbox('', label = '', placeholder='tags') civit_search_btn = ToolButton(value="🔍", label="Search", interactive=False) with gr.Row(): @@ -389,6 +397,7 @@ def create_ui(): civit_results1 = gr.DataFrame(value = None, label = 'Search results', show_label = True, interactive = False, wrap = True, overflow_row_behaviour = 'paginate', max_rows = 10, headers = civit_headers1, datatype = civit_types1, type='array') with gr.Row(): civit_previews_btn = gr.Button(value="Fetch previews for existing models", variant='primary') + civit_previews_rehash = gr.Checkbox(value=False, label="Check alternative hash") civit_search_text.submit(fn=civit_search, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_results1, civit_results2, civit_results3]) civit_search_tag.submit(fn=civit_search, inputs=[civit_search_text, civit_search_tag, civit_model_type], outputs=[civit_results1, civit_results2, civit_results3]) @@ -397,4 +406,4 @@ def create_ui(): civit_results2.select(fn=civit_select2, inputs=[civit_results2], outputs=[civit_results3]) civit_results3.select(fn=civit_select3, inputs=[civit_results3], outputs=[civit_selected, civit_name, civit_search_btn]) civit_download_model_btn.click(fn=civit_download_model, inputs=[civit_selected, civit_name, civit_path, civit_model_type, models_image], outputs=[models_outcome]) - civit_previews_btn.click(fn=civit_download_previews, inputs=[], outputs=[models_outcome]) + civit_previews_btn.click(fn=civit_download_previews, inputs=[civit_previews_rehash], outputs=[models_outcome]) diff --git a/modules/ui_symbols.py b/modules/ui_symbols.py index 86b9a5e64..68fc60aff 100644 --- a/modules/ui_symbols.py +++ b/modules/ui_symbols.py @@ -1,3 +1,14 @@ +refresh = '⟲' +close = '🗙' +load = '⇧' +save = '⇩' +apply = '⇰' +clear = '⊗' +fill = '⊜' +networks = '🗁' +paste = '⇦' + +""" refresh = '🔄' close = '🛗' load = '⬆️' @@ -6,9 +17,10 @@ apply = '⏩' clear = '🚮' fill = '⏫' networks = '🌐' +paste = '📘' +""" switch = '⇅' detect = '📐' folder = '📂' random = '🎲️' reuse = '♻️' -paste = '📘' diff --git a/requirements.txt b/requirements.txt index ccc093b77..67aad81ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,13 +44,13 @@ fasteners typing-extensions==4.7.1 antlr4-python3-runtime==4.9.3 requests==2.31.0 -tqdm==4.65.0 +tqdm==4.66.1 accelerate==0.20.3 opencv-python-headless==4.7.0.72 -diffusers==0.20.2 +diffusers==0.21.1 einops==0.4.1 -gradio==3.41.2 -huggingface_hub==0.16.4 +gradio==3.43.2 +huggingface_hub==0.17.1 numexpr==2.8.4 numpy==1.24.4 numba==0.57.1 diff --git a/scripts/outpainting_mk_2.py b/scripts/outpainting_mk_2.py index 4b0af3053..bbf7aa537 100644 --- a/scripts/outpainting_mk_2.py +++ b/scripts/outpainting_mk_2.py @@ -264,8 +264,7 @@ class Script(scripts.Script): all_images = all_processed_images combined_grid_image = images.image_grid(all_processed_images) - unwanted_grid_because_of_img_count = len(all_processed_images) < 2 and opts.grid_only_if_multiple - if opts.return_grid and not unwanted_grid_because_of_img_count: + if opts.return_grid and len(all_processed_images) > 1: all_images = [combined_grid_image] + all_processed_images res = Processed(p, all_images, initial_seed_and_info[0], initial_seed_and_info[1]) @@ -274,7 +273,7 @@ class Script(scripts.Script): for img in all_processed_images: images.save_image(img, p.outpath_samples, "", res.seed, p.prompt, opts.samples_format, info=res.info, p=p) - if opts.grid_save and not unwanted_grid_because_of_img_count: + if opts.grid_save and len(all_processed_images) > 1: images.save_image(combined_grid_image, p.outpath_grids, "grid", res.seed, p.prompt, opts.samples_format, info=res.info, short_filename=not opts.grid_extended_filename, grid=True, p=p) return res diff --git a/scripts/prompt_matrix.py b/scripts/prompt_matrix.py index 3d8d682db..77ce207a7 100644 --- a/scripts/prompt_matrix.py +++ b/scripts/prompt_matrix.py @@ -39,7 +39,7 @@ def draw_xy_grid(xs, ys, x_label, y_label, cell): class Script(scripts.Script): def title(self): - return "Prompt matrix" + return "Prompt Matrix" def ui(self, is_img2img): gr.HTML('
') @@ -92,7 +92,7 @@ class Script(scripts.Script): p.prompt = all_prompts else: p.negative_prompt = all_prompts - p.seed = [p.seed + (i if different_seeds else 0) for i in range(len(all_prompts))] + p.seed = [int(p.seed + (i if different_seeds else 0)) for i in range(len(all_prompts))] p.prompt_for_display = positive_prompt processed = process_images(p) diff --git a/scripts/prompts_from_file.py b/scripts/prompts_from_file.py index 6c2008043..f53186f3f 100644 --- a/scripts/prompts_from_file.py +++ b/scripts/prompts_from_file.py @@ -101,7 +101,7 @@ def load_prompt_file(file): class Script(scripts.Script): def title(self): - return "Prompts from file" + return "Prompts from File" def ui(self, is_img2img): checkbox_iterate = gr.Checkbox(label="Iterate seed every line", value=False, elem_id=self.elem_id("checkbox_iterate")) diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py index 31307d431..28c46a999 100644 --- a/scripts/xyz_grid.py +++ b/scripts/xyz_grid.py @@ -375,7 +375,7 @@ re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+ class Script(scripts.Script): def title(self): - return "X/Y/Z grid" + return "X/Y/Z Grid" def ui(self, is_img2img): self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] @@ -400,10 +400,10 @@ class Script(scripts.Script): fill_z_button = ToolButton(value=symbols.fill, elem_id="xyz_grid_fill_z_tool_button", visible=False) with gr.Row(variant="compact", elem_id="axis_options"): draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) - no_fixed_seeds = gr.Checkbox(label='Keep random for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) - no_grid = gr.Checkbox(label='Do not create grid', value=False, elem_id=self.elem_id("no_xyz_grid")) - include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) - include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + no_fixed_seeds = gr.Checkbox(label='Keep random seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + no_grid = gr.Checkbox(label='Skip grid', value=False, elem_id=self.elem_id("no_xyz_grid")) + include_lone_images = gr.Checkbox(label='Include sub images', value=False, elem_id=self.elem_id("include_lone_images")) + include_sub_grids = gr.Checkbox(label='Include sub grids', value=False, elem_id=self.elem_id("include_sub_grids")) with gr.Row(variant="compact", elem_id="axis_options"): margin_size = gr.Slider(label="Grid margins", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) with gr.Row(variant="compact", elem_id="swap_axes"): diff --git a/webui.bat b/webui.bat index 6a6edcf10..022be1367 100755 --- a/webui.bat +++ b/webui.bat @@ -7,7 +7,7 @@ mkdir tmp 2>NUL %PYTHON% -c "" >tmp/stdout.txt 2>tmp/stderr.txt if %ERRORLEVEL% == 0 goto :check_pip -echo Couldn't launch python +echo Cannot launch python goto :show_stdout_stderr :check_pip @@ -16,7 +16,7 @@ if %ERRORLEVEL% == 0 goto :start_venv if "%PIP_INSTALLER_LOCATION%" == "" goto :show_stdout_stderr %PYTHON% "%PIP_INSTALLER_LOCATION%" >tmp/stdout.txt 2>tmp/stderr.txt if %ERRORLEVEL% == 0 goto :start_venv -echo Couldn't install pip +echo Cannot install pip goto :show_stdout_stderr :start_venv @@ -27,10 +27,11 @@ dir "%VENV_DIR%\Scripts\Python.exe" >tmp/stdout.txt 2>tmp/stderr.txt if %ERRORLEVEL% == 0 goto :activate_venv for /f "delims=" %%i in ('CALL %PYTHON% -c "import sys; print(sys.executable)"') do set PYTHON_FULLNAME="%%i" -echo Creating venv in directory %VENV_DIR% using python %PYTHON_FULLNAME% +echo Using python: %PYTHON_FULLNAME% +echo Creating VENV: %VENV_DIR% %PYTHON_FULLNAME% -m venv "%VENV_DIR%" >tmp/stdout.txt 2>tmp/stderr.txt if %ERRORLEVEL% == 0 goto :activate_venv -echo Unable to create venv in directory "%VENV_DIR%" +echo Failed creating VENV: "%VENV_DIR%" goto :show_stdout_stderr :activate_venv @@ -42,7 +43,6 @@ if [%ACCELERATE%] == ["True"] goto :accelerate goto :launch :accelerate -echo Checking for accelerate: %ACCELERATE% set ACCELERATE="%VENV_DIR%\Scripts\accelerate.exe" if EXIST %ACCELERATE% goto :accelerate_launch @@ -52,7 +52,7 @@ pause exit /b :accelerate_launch -echo Accelerating +echo Using accelerate %ACCELERATE% launch --num_cpu_threads_per_process=6 launch.py %* pause exit /b @@ -78,5 +78,5 @@ type tmp\stderr.txt :endofscript echo. -echo Launch unsuccessful. Exiting. +echo Launch Failed pause diff --git a/webui.py b/webui.py index 6624934cd..5a1699abe 100644 --- a/webui.py +++ b/webui.py @@ -1,3 +1,4 @@ +import io import os import sys import glob @@ -5,19 +6,17 @@ import signal import asyncio import logging import importlib +import contextlib from threading import Thread import modules.loader import torch # pylint: disable=wrong-import-order from modules import timer, errors, paths # pylint: disable=unused-import - local_url = None -if not modules.loader.initialized: - errors.log.debug('Loading modules') -from installer import log, setup_logging, git_commit +from installer import log, git_commit, custom_excepthook import ldm.modules.encoders.modules # pylint: disable=W0611,C0411,E0401 -from modules.call_queue import queue_lock, wrap_queued_call, wrap_gradio_gpu_call # pylint: disable=W0611,C0411,C0412 +from modules import shared, extensions, extra_networks, ui_tempdir, ui_extra_networks, modelloader # pylint: disable=ungrouped-imports from modules.paths import create_paths -from modules import shared, extensions, extra_networks, ui_tempdir, ui_extra_networks, modelloader +from modules.call_queue import queue_lock, wrap_queued_call, wrap_gradio_gpu_call # pylint: disable=W0611,C0411,C0412 import modules.devices import modules.sd_samplers import modules.upscaler @@ -39,9 +38,11 @@ from modules.shared import cmd_opts, opts import modules.hypernetworks.hypernetwork from modules.middleware import setup_middleware + +sys.excepthook = custom_excepthook +state = shared.state if not modules.loader.initialized: timer.startup.record("libraries") - log.info('Loaded librareis') log.setLevel(logging.DEBUG if cmd_opts.debug else logging.INFO) logging.disable(logging.NOTSET if cmd_opts.debug else logging.DEBUG) if cmd_opts.server_name: @@ -63,6 +64,7 @@ fastapi_args = { } modules.loader.initialized = True + def check_rollback_vae(): if shared.cmd_opts.rollback_vae: if not torch.cuda.is_available(): @@ -77,7 +79,7 @@ def check_rollback_vae(): def initialize(): - log.debug('Entering initialize') + log.debug('Initializing') check_rollback_vae() @@ -104,7 +106,6 @@ def initialize(): t_timer, t_total = modules.scripts.load_scripts() timer.startup.record("extensions") timer.startup.records["extensions"] = t_total # scripts can reset the time - setup_logging() # reset since scripts can hijaack logging log.info(f'Extensions time: {t_timer.summary()}') modelloader.load_upscalers() @@ -117,9 +118,10 @@ def initialize(): modules.textual_inversion.textual_inversion.list_textual_inversion_templates() shared.reload_hypernetworks() + shared.prompt_styles.reload() ui_extra_networks.initialize() - ui_extra_networks.register_default_pages() + ui_extra_networks.register_pages() extra_networks.initialize() extra_networks.register_default_extra_networks() timer.startup.record("extra-networks") @@ -152,8 +154,7 @@ def initialize(): def load_model(): if opts.sd_checkpoint_autoload: - shared.state.begin() - shared.state.job = 'load model' + shared.state.begin('load model') thread_model = Thread(target=lambda: shared.sd_model) thread_model.start() thread_refiner = Thread(target=lambda: shared.sd_refiner) @@ -198,20 +199,18 @@ def async_policy(): super().__init__() self.loop = self.get_event_loop() self.loop.set_exception_handler(self.handle_exception) - log.debug(f"Event loop: {self.loop}") + # log.debug(f"Event loop: {self.loop}") asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy()) def start_common(): log.debug('Entering start sequence') - if cmd_opts.debug and hasattr(shared, 'get_version'): - log.debug(f'Version: {shared.get_version()}') logging.disable(logging.NOTSET if cmd_opts.debug else logging.DEBUG) if shared.cmd_opts.data_dir is not None and len(shared.cmd_opts.data_dir) > 0: log.info(f'Using data path: {shared.cmd_opts.data_dir}') - if shared.cmd_opts.models_dir is not None and len(shared.cmd_opts.models_dir) > 0: - log.info(f'Using models path: {shared.cmd_opts.data_dir}') + if shared.cmd_opts.models_dir is not None and len(shared.cmd_opts.models_dir) > 0 and shared.cmd_opts.models_dir != 'models': + log.info(f'Using models path: {shared.cmd_opts.models_dir}') create_paths(opts, log) async_policy() initialize() @@ -244,28 +243,30 @@ def start_ui(): gradio_auth_creds += [x.strip() for x in line.split(',') if x.strip()] global local_url # pylint: disable=global-statement - app, local_url, share_url = shared.demo.launch( # app is FastAPI(Starlette) instance - share=cmd_opts.share, - server_name=server_name, - server_port=cmd_opts.port if cmd_opts.port != 7860 else None, - ssl_keyfile=cmd_opts.tls_keyfile, - ssl_certfile=cmd_opts.tls_certfile, - ssl_verify=not cmd_opts.tls_selfsign, - debug=False, - auth=[tuple(cred.split(':')) for cred in gradio_auth_creds] if gradio_auth_creds else None, - prevent_thread_lock=True, - max_threads=64, - show_api=False, - quiet=True, - favicon_path='html/logo.ico', - allowed_paths=[os.path.dirname(__file__), cmd_opts.data_dir], - app_kwargs=fastapi_args, - ) + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + app, local_url, share_url = shared.demo.launch( # app is FastAPI(Starlette) instance + share=cmd_opts.share, + server_name=server_name, + server_port=cmd_opts.port if cmd_opts.port != 7860 else None, + ssl_keyfile=cmd_opts.tls_keyfile, + ssl_certfile=cmd_opts.tls_certfile, + ssl_verify=not cmd_opts.tls_selfsign, + debug=False, + auth=[tuple(cred.split(':')) for cred in gradio_auth_creds] if gradio_auth_creds else None, + prevent_thread_lock=True, + max_threads=64, + show_api=False, + quiet=True, + favicon_path='html/logo.ico', + allowed_paths=[os.path.dirname(__file__), cmd_opts.data_dir], + app_kwargs=fastapi_args, + ) if cmd_opts.data_dir is not None: ui_tempdir.register_tmp_file(shared.demo, os.path.join(cmd_opts.data_dir, 'x')) shared.log.info(f'Local URL: {local_url}') if cmd_opts.docs: - shared.log.info(f'API Docs: {local_url[:-1]}/docs') # {local_url[:-1]}?view=api + shared.log.info(f'API Docs: {local_url[:-1]}/docs') # pylint: disable=unsubscriptable-object if share_url is not None: shared.log.info(f'Share URL: {share_url}') shared.log.debug(f'Gradio registered functions: {len(shared.demo.fns)}') @@ -291,7 +292,8 @@ def start_ui(): time_setup = [f'{k}:{round(v,3)}s' for (k,v) in modules.scripts.time_setup.items() if v > 0.005] shared.log.debug(f'Scripts setup: {time_setup}') time_component = [f'{k}:{round(v,3)}s' for (k,v) in modules.scripts.time_component.items() if v > 0.005] - shared.log.debug(f'Scripts components: {time_component}') + if len(time_component) > 0: + shared.log.debug(f'Scripts components: {time_component}') def webui(restart=False): diff --git a/wiki b/wiki index de31c082f..fea51bf38 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit de31c082f8bc04ebf33667587320bbafa469f4c1 +Subproject commit fea51bf38c010520dbf30fb8cb58043f94fb2e8e