Forráskód Böngészése

Merge branch 'dev' into patch-1

AUTOMATIC1111 2 éve
szülő
commit
ce64cab397
48 módosított fájl, 623 hozzáadás és 278 törlés
  1. 1 1
      .gitignore
  2. 1 0
      README.md
  3. 5 5
      environment-wsl2.yaml
  4. 13 7
      extensions-builtin/LDSR/scripts/ldsr_model.py
  5. 1 1
      extensions-builtin/Lora/extra_networks_lora.py
  6. 1 1
      extensions-builtin/Lora/scripts/lora_script.py
  7. 68 15
      extensions-builtin/ScuNET/scripts/scunet_model.py
  8. 30 91
      extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js
  9. 0 8
      javascript/contextMenus.js
  10. 31 7
      javascript/edit-attention.js
  11. 3 3
      javascript/generationParams.js
  12. 4 4
      javascript/hints.js
  13. 5 2
      javascript/imageviewer.js
  14. 1 1
      javascript/progressbar.js
  15. 6 6
      launch.py
  16. 6 10
      modules/api/api.py
  17. 6 2
      modules/devices.py
  18. 1 1
      modules/extra_networks_hypernet.py
  19. 6 0
      modules/generation_parameters_copypaste.py
  20. 27 4
      modules/images.py
  21. 6 3
      modules/img2img.py
  22. 2 2
      modules/interrogate.py
  23. 12 0
      modules/ngrok.py
  24. 7 2
      modules/postprocessing.py
  25. 31 6
      modules/processing.py
  26. 12 2
      modules/realesrgan_model.py
  27. 1 4
      modules/safe.py
  28. 10 0
      modules/sd_samplers_common.py
  29. 33 13
      modules/sd_samplers_kdiffusion.py
  30. 45 2
      modules/shared.py
  31. 5 7
      modules/styles.py
  32. 3 1
      modules/textual_inversion/preprocess.py
  33. 6 0
      modules/textual_inversion/textual_inversion.py
  34. 6 6
      modules/ui.py
  35. 1 1
      modules/ui_common.py
  36. 10 0
      modules/ui_components.py
  37. 14 6
      modules/ui_extensions.py
  38. 1 1
      modules/ui_postprocessing.py
  39. 2 2
      requirements.txt
  40. 4 4
      requirements_versions.txt
  41. 56 7
      scripts/custom_code.py
  42. 9 5
      scripts/postprocessing_upscale.py
  43. 55 25
      scripts/xyz_grid.py
  44. 4 0
      style.css
  45. 1 1
      webui-macos-env.sh
  46. 3 0
      webui-user.sh
  47. 48 5
      webui.py
  48. 20 4
      webui.sh

+ 1 - 1
.gitignore

@@ -32,4 +32,4 @@ notification.mp3
 /extensions
 /extensions
 /test/stdout.txt
 /test/stdout.txt
 /test/stderr.txt
 /test/stderr.txt
-/cache.json
+/cache.json*

+ 1 - 0
README.md

@@ -120,6 +120,7 @@ sudo pacman -S wget git python3
 bash <(wget -qO- https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui/master/webui.sh)
 bash <(wget -qO- https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui/master/webui.sh)
 ```
 ```
 3. Run `webui.sh`.
 3. Run `webui.sh`.
+4. Check `webui-user.sh` for options.
 ### Installation on Apple Silicon
 ### Installation on Apple Silicon
 
 
 Find the instructions [here](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Installation-on-Apple-Silicon).
 Find the instructions [here](https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Installation-on-Apple-Silicon).

+ 5 - 5
environment-wsl2.yaml

@@ -4,8 +4,8 @@ channels:
   - defaults
   - defaults
 dependencies:
 dependencies:
   - python=3.10
   - python=3.10
-  - pip=22.2.2
-  - cudatoolkit=11.3
-  - pytorch=1.12.1
-  - torchvision=0.13.1
-  - numpy=1.23.1
+  - pip=23.0
+  - cudatoolkit=11.8
+  - pytorch=2.0
+  - torchvision=0.15
+  - numpy=1.23

+ 13 - 7
extensions-builtin/LDSR/scripts/ldsr_model.py

@@ -25,22 +25,28 @@ class UpscalerLDSR(Upscaler):
         yaml_path = os.path.join(self.model_path, "project.yaml")
         yaml_path = os.path.join(self.model_path, "project.yaml")
         old_model_path = os.path.join(self.model_path, "model.pth")
         old_model_path = os.path.join(self.model_path, "model.pth")
         new_model_path = os.path.join(self.model_path, "model.ckpt")
         new_model_path = os.path.join(self.model_path, "model.ckpt")
-        safetensors_model_path = os.path.join(self.model_path, "model.safetensors")
+
+        local_model_paths = self.find_models(ext_filter=[".ckpt", ".safetensors"])
+        local_ckpt_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("model.ckpt")]), None)
+        local_safetensors_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("model.safetensors")]), None)
+        local_yaml_path = next(iter([local_model for local_model in local_model_paths if local_model.endswith("project.yaml")]), None)
+
         if os.path.exists(yaml_path):
         if os.path.exists(yaml_path):
             statinfo = os.stat(yaml_path)
             statinfo = os.stat(yaml_path)
             if statinfo.st_size >= 10485760:
             if statinfo.st_size >= 10485760:
                 print("Removing invalid LDSR YAML file.")
                 print("Removing invalid LDSR YAML file.")
                 os.remove(yaml_path)
                 os.remove(yaml_path)
+
         if os.path.exists(old_model_path):
         if os.path.exists(old_model_path):
             print("Renaming model from model.pth to model.ckpt")
             print("Renaming model from model.pth to model.ckpt")
             os.rename(old_model_path, new_model_path)
             os.rename(old_model_path, new_model_path)
-        if os.path.exists(safetensors_model_path):
-            model = safetensors_model_path
+
+        if local_safetensors_path is not None and os.path.exists(local_safetensors_path):
+            model = local_safetensors_path
         else:
         else:
-            model = load_file_from_url(url=self.model_url, model_dir=self.model_path,
-                                       file_name="model.ckpt", progress=True)
-        yaml = load_file_from_url(url=self.yaml_url, model_dir=self.model_path,
-                                  file_name="project.yaml", progress=True)
+            model = local_ckpt_path if local_ckpt_path is not None else load_file_from_url(url=self.model_url, model_dir=self.model_path, file_name="model.ckpt", progress=True)
+
+        yaml = local_yaml_path if local_yaml_path is not None else load_file_from_url(url=self.yaml_url, model_dir=self.model_path, file_name="project.yaml", progress=True)
 
 
         try:
         try:
             return LDSR(model, yaml)
             return LDSR(model, yaml)

+ 1 - 1
extensions-builtin/Lora/extra_networks_lora.py

@@ -8,7 +8,7 @@ class ExtraNetworkLora(extra_networks.ExtraNetwork):
     def activate(self, p, params_list):
     def activate(self, p, params_list):
         additional = shared.opts.sd_lora
         additional = shared.opts.sd_lora
 
 
-        if additional != "" and additional in lora.available_loras and len([x for x in params_list if x.items[0] == additional]) == 0:
+        if additional != "None" and additional in lora.available_loras and len([x for x in params_list if x.items[0] == additional]) == 0:
             p.all_prompts = [x + f"<lora:{additional}:{shared.opts.extra_networks_default_multiplier}>" for x in p.all_prompts]
             p.all_prompts = [x + f"<lora:{additional}:{shared.opts.extra_networks_default_multiplier}>" for x in p.all_prompts]
             params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier]))
             params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier]))
 
 

+ 1 - 1
extensions-builtin/Lora/scripts/lora_script.py

@@ -52,5 +52,5 @@ script_callbacks.on_before_ui(before_ui)
 
 
 
 
 shared.options_templates.update(shared.options_section(('extra_networks', "Extra Networks"), {
 shared.options_templates.update(shared.options_section(('extra_networks', "Extra Networks"), {
-    "sd_lora": shared.OptionInfo("None", "Add Lora to prompt", gr.Dropdown, lambda: {"choices": [""] + [x for x in lora.available_loras]}, refresh=lora.list_available_loras),
+    "sd_lora": shared.OptionInfo("None", "Add Lora to prompt", gr.Dropdown, lambda: {"choices": ["None"] + [x for x in lora.available_loras]}, refresh=lora.list_available_loras),
 }))
 }))

+ 68 - 15
extensions-builtin/ScuNET/scripts/scunet_model.py

@@ -5,11 +5,15 @@ import traceback
 import PIL.Image
 import PIL.Image
 import numpy as np
 import numpy as np
 import torch
 import torch
+from tqdm import tqdm
+
 from basicsr.utils.download_util import load_file_from_url
 from basicsr.utils.download_util import load_file_from_url
 
 
 import modules.upscaler
 import modules.upscaler
 from modules import devices, modelloader
 from modules import devices, modelloader
 from scunet_model_arch import SCUNet as net
 from scunet_model_arch import SCUNet as net
+from modules.shared import opts
+from modules import images
 
 
 
 
 class UpscalerScuNET(modules.upscaler.Upscaler):
 class UpscalerScuNET(modules.upscaler.Upscaler):
@@ -42,28 +46,78 @@ class UpscalerScuNET(modules.upscaler.Upscaler):
             scalers.append(scaler_data2)
             scalers.append(scaler_data2)
         self.scalers = scalers
         self.scalers = scalers
 
 
-    def do_upscale(self, img: PIL.Image, selected_file):
+    @staticmethod
+    @torch.no_grad()
+    def tiled_inference(img, model):
+        # test the image tile by tile
+        h, w = img.shape[2:]
+        tile = opts.SCUNET_tile
+        tile_overlap = opts.SCUNET_tile_overlap
+        if tile == 0:
+            return model(img)
+
+        device = devices.get_device_for('scunet')
+        assert tile % 8 == 0, "tile size should be a multiple of window_size"
+        sf = 1
+
+        stride = tile - tile_overlap
+        h_idx_list = list(range(0, h - tile, stride)) + [h - tile]
+        w_idx_list = list(range(0, w - tile, stride)) + [w - tile]
+        E = torch.zeros(1, 3, h * sf, w * sf, dtype=img.dtype, device=device)
+        W = torch.zeros_like(E, dtype=devices.dtype, device=device)
+
+        with tqdm(total=len(h_idx_list) * len(w_idx_list), desc="ScuNET tiles") as pbar:
+            for h_idx in h_idx_list:
+
+                for w_idx in w_idx_list:
+
+                    in_patch = img[..., h_idx: h_idx + tile, w_idx: w_idx + tile]
+
+                    out_patch = model(in_patch)
+                    out_patch_mask = torch.ones_like(out_patch)
+
+                    E[
+                        ..., h_idx * sf: (h_idx + tile) * sf, w_idx * sf: (w_idx + tile) * sf
+                    ].add_(out_patch)
+                    W[
+                        ..., h_idx * sf: (h_idx + tile) * sf, w_idx * sf: (w_idx + tile) * sf
+                    ].add_(out_patch_mask)
+                    pbar.update(1)
+        output = E.div_(W)
+
+        return output
+
+    def do_upscale(self, img: PIL.Image.Image, selected_file):
+
         torch.cuda.empty_cache()
         torch.cuda.empty_cache()
 
 
         model = self.load_model(selected_file)
         model = self.load_model(selected_file)
         if model is None:
         if model is None:
+            print(f"ScuNET: Unable to load model from {selected_file}", file=sys.stderr)
             return img
             return img
 
 
         device = devices.get_device_for('scunet')
         device = devices.get_device_for('scunet')
-        img = np.array(img)
-        img = img[:, :, ::-1]
-        img = np.moveaxis(img, 2, 0) / 255
-        img = torch.from_numpy(img).float()
-        img = img.unsqueeze(0).to(device)
-
-        with torch.no_grad():
-            output = model(img)
-        output = output.squeeze().float().cpu().clamp_(0, 1).numpy()
-        output = 255. * np.moveaxis(output, 0, 2)
-        output = output.astype(np.uint8)
-        output = output[:, :, ::-1]
+        tile = opts.SCUNET_tile
+        h, w = img.height, img.width
+        np_img = np.array(img)
+        np_img = np_img[:, :, ::-1]  # RGB to BGR
+        np_img = np_img.transpose((2, 0, 1)) / 255  # HWC to CHW
+        torch_img = torch.from_numpy(np_img).float().unsqueeze(0).to(device)  # type: ignore
+
+        if tile > h or tile > w:
+            _img = torch.zeros(1, 3, max(h, tile), max(w, tile), dtype=torch_img.dtype, device=torch_img.device)
+            _img[:, :, :h, :w] = torch_img # pad image
+            torch_img = _img
+
+        torch_output = self.tiled_inference(torch_img, model).squeeze(0)
+        torch_output = torch_output[:, :h * 1, :w * 1] # remove padding, if any
+        np_output: np.ndarray = torch_output.float().cpu().clamp_(0, 1).numpy()
+        del torch_img, torch_output
         torch.cuda.empty_cache()
         torch.cuda.empty_cache()
-        return PIL.Image.fromarray(output, 'RGB')
+
+        output = np_output.transpose((1, 2, 0))  # CHW to HWC
+        output = output[:, :, ::-1]  # BGR to RGB
+        return PIL.Image.fromarray((output * 255).astype(np.uint8))
 
 
     def load_model(self, path: str):
     def load_model(self, path: str):
         device = devices.get_device_for('scunet')
         device = devices.get_device_for('scunet')
@@ -84,4 +138,3 @@ class UpscalerScuNET(modules.upscaler.Upscaler):
         model = model.to(device)
         model = model.to(device)
 
 
         return model
         return model
-

+ 30 - 91
extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js

@@ -1,103 +1,42 @@
 // Stable Diffusion WebUI - Bracket checker
 // Stable Diffusion WebUI - Bracket checker
-// Version 1.0
-// By Hingashi no Florin/Bwin4L
+// By Hingashi no Florin/Bwin4L & @akx
 // Counts open and closed brackets (round, square, curly) in the prompt and negative prompt text boxes in the txt2img and img2img tabs.
 // 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.
 // If there's a mismatch, the keyword counter turns red and if you hover on it, a tooltip tells you what's wrong.
 
 
-function checkBrackets(evt, textArea, counterElt) {
-  errorStringParen  = '(...) - Different number of opening and closing parentheses detected.\n';
-  errorStringSquare = '[...] - Different number of opening and closing square brackets detected.\n';
-  errorStringCurly  = '{...} - Different number of opening and closing curly brackets detected.\n';
-
-  openBracketRegExp = /\(/g;
-  closeBracketRegExp = /\)/g;
-
-  openSquareBracketRegExp = /\[/g;
-  closeSquareBracketRegExp = /\]/g;
-
-  openCurlyBracketRegExp = /\{/g;
-  closeCurlyBracketRegExp = /\}/g;
-
-  totalOpenBracketMatches = 0;
-  totalCloseBracketMatches = 0;
-  totalOpenSquareBracketMatches = 0;
-  totalCloseSquareBracketMatches = 0;
-  totalOpenCurlyBracketMatches = 0;
-  totalCloseCurlyBracketMatches = 0;
-
-  openBracketMatches = textArea.value.match(openBracketRegExp);
-  if(openBracketMatches) {
-    totalOpenBracketMatches = openBracketMatches.length;
-  }
-
-  closeBracketMatches = textArea.value.match(closeBracketRegExp);
-  if(closeBracketMatches) {
-    totalCloseBracketMatches = closeBracketMatches.length;
-  }
-
-  openSquareBracketMatches = textArea.value.match(openSquareBracketRegExp);
-  if(openSquareBracketMatches) {
-    totalOpenSquareBracketMatches = openSquareBracketMatches.length;
-  }
-
-  closeSquareBracketMatches = textArea.value.match(closeSquareBracketRegExp);
-  if(closeSquareBracketMatches) {
-    totalCloseSquareBracketMatches = closeSquareBracketMatches.length;
-  }
-
-  openCurlyBracketMatches = textArea.value.match(openCurlyBracketRegExp);
-  if(openCurlyBracketMatches) {
-    totalOpenCurlyBracketMatches = openCurlyBracketMatches.length;
-  }
-
-  closeCurlyBracketMatches = textArea.value.match(closeCurlyBracketRegExp);
-  if(closeCurlyBracketMatches) {
-    totalCloseCurlyBracketMatches = closeCurlyBracketMatches.length;
-  }
-
-  if(totalOpenBracketMatches != totalCloseBracketMatches) {
-    if(!counterElt.title.includes(errorStringParen)) {
-      counterElt.title += errorStringParen;
-    }
-  } else {
-    counterElt.title = counterElt.title.replace(errorStringParen, '');
-  }
-
-  if(totalOpenSquareBracketMatches != totalCloseSquareBracketMatches) {
-    if(!counterElt.title.includes(errorStringSquare)) {
-      counterElt.title += errorStringSquare;
-    }
-  } else {
-    counterElt.title = counterElt.title.replace(errorStringSquare, '');
-  }
-
-  if(totalOpenCurlyBracketMatches != totalCloseCurlyBracketMatches) {
-    if(!counterElt.title.includes(errorStringCurly)) {
-      counterElt.title += errorStringCurly;
+function checkBrackets(textArea, counterElt) {
+  var counts = {};
+  (textArea.value.match(/[(){}\[\]]/g) || []).forEach(bracket => {
+    counts[bracket] = (counts[bracket] || 0) + 1;
+  });
+  var 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}.`
+      );
     }
     }
-  } else {
-    counterElt.title = counterElt.title.replace(errorStringCurly, '');
   }
   }
 
 
-  if(counterElt.title != '') {
-    counterElt.classList.add('error');
-  } else {
-    counterElt.classList.remove('error');
-  }
+  checkPair('(', ')', 'round brackets');
+  checkPair('[', ']', 'square brackets');
+  checkPair('{', '}', 'curly brackets');
+  counterElt.title = errors.join('\n');
+  counterElt.classList.toggle('error', errors.length !== 0);
 }
 }
 
 
-function setupBracketChecking(id_prompt, id_counter){
-    var textarea = gradioApp().querySelector("#" + id_prompt + " > label > textarea");
-    var counter = gradioApp().getElementById(id_counter)
+function setupBracketChecking(id_prompt, id_counter) {
+  var textarea = gradioApp().querySelector("#" + id_prompt + " > label > textarea");
+  var counter = gradioApp().getElementById(id_counter)
 
 
-    textarea.addEventListener("input", function(evt){
-        checkBrackets(evt, textarea, counter)
-    });
+  if (textarea && counter) {
+    textarea.addEventListener("input", () => checkBrackets(textarea, counter));
+  }
 }
 }
 
 
-onUiLoaded(function(){
-    setupBracketChecking('txt2img_prompt', 'txt2img_token_counter')
-    setupBracketChecking('txt2img_neg_prompt', 'txt2img_negative_token_counter')
-    setupBracketChecking('img2img_prompt', 'img2img_token_counter')
-    setupBracketChecking('img2img_neg_prompt', 'img2img_negative_token_counter')
-})
+onUiLoaded(function () {
+  setupBracketChecking('txt2img_prompt', 'txt2img_token_counter');
+  setupBracketChecking('txt2img_neg_prompt', 'txt2img_negative_token_counter');
+  setupBracketChecking('img2img_prompt', 'img2img_token_counter');
+  setupBracketChecking('img2img_neg_prompt', 'img2img_negative_token_counter');
+});

+ 0 - 8
javascript/contextMenus.js

@@ -161,14 +161,6 @@ addContextMenuEventListener = initResponse[2];
   appendContextMenuOption('#img2img_interrupt','Cancel generate forever',cancelGenerateForever)
   appendContextMenuOption('#img2img_interrupt','Cancel generate forever',cancelGenerateForever)
   appendContextMenuOption('#img2img_generate', 'Cancel generate forever',cancelGenerateForever)
   appendContextMenuOption('#img2img_generate', 'Cancel generate forever',cancelGenerateForever)
 
 
-  appendContextMenuOption('#roll','Roll three',
-    function(){
-      let rollbutton = get_uiCurrentTabContent().querySelector('#roll');
-      setTimeout(function(){rollbutton.click()},100)
-      setTimeout(function(){rollbutton.click()},200)
-      setTimeout(function(){rollbutton.click()},300)
-    }
-  )
 })();
 })();
 //End example Context Menu Items
 //End example Context Menu Items
 
 

+ 31 - 7
javascript/edit-attention.js

@@ -17,7 +17,7 @@ function keyupEditAttention(event){
 		// Find opening parenthesis around current cursor
 		// Find opening parenthesis around current cursor
 		const before = text.substring(0, selectionStart);
 		const before = text.substring(0, selectionStart);
 		let beforeParen = before.lastIndexOf(OPEN);
 		let beforeParen = before.lastIndexOf(OPEN);
-		if (beforeParen == -1) return  false;
+		if (beforeParen == -1) return false;
 		let beforeParenClose = before.lastIndexOf(CLOSE);
 		let beforeParenClose = before.lastIndexOf(CLOSE);
 		while (beforeParenClose !== -1 && beforeParenClose > beforeParen) {
 		while (beforeParenClose !== -1 && beforeParenClose > beforeParen) {
 			beforeParen = before.lastIndexOf(OPEN, beforeParen - 1);
 			beforeParen = before.lastIndexOf(OPEN, beforeParen - 1);
@@ -27,7 +27,7 @@ function keyupEditAttention(event){
 		// Find closing parenthesis around current cursor
 		// Find closing parenthesis around current cursor
 		const after = text.substring(selectionStart);
 		const after = text.substring(selectionStart);
 		let afterParen = after.indexOf(CLOSE);
 		let afterParen = after.indexOf(CLOSE);
-		if (afterParen == -1) return  false;
+		if (afterParen == -1) return false;
 		let afterParenOpen = after.indexOf(OPEN);
 		let afterParenOpen = after.indexOf(OPEN);
 		while (afterParenOpen !== -1 && afterParen > afterParenOpen) {
 		while (afterParenOpen !== -1 && afterParen > afterParenOpen) {
 			afterParen = after.indexOf(CLOSE, afterParen + 1);
 			afterParen = after.indexOf(CLOSE, afterParen + 1);
@@ -43,10 +43,28 @@ function keyupEditAttention(event){
 		target.setSelectionRange(selectionStart, selectionEnd);
 		target.setSelectionRange(selectionStart, selectionEnd);
 		return true;
 		return true;
     }
     }
+    
+    function selectCurrentWord(){
+        if (selectionStart !== selectionEnd) return false;
+        const delimiters = opts.keyedit_delimiters + " \r\n\t";
+        
+        // seek backward until to find beggining
+        while (!delimiters.includes(text[selectionStart - 1]) && selectionStart > 0) {
+            selectionStart--;
+        }
+        
+        // seek forward to find end
+        while (!delimiters.includes(text[selectionEnd]) && selectionEnd < text.length) {
+            selectionEnd++;
+        }
 
 
-	// If the user hasn't selected anything, let's select their current parenthesis block
-    if(! selectCurrentParenthesisBlock('<', '>')){
-        selectCurrentParenthesisBlock('(', ')')
+        target.setSelectionRange(selectionStart, selectionEnd);
+        return true;
+    }
+
+	// If the user hasn't selected anything, let's select their current parenthesis block or word
+    if (!selectCurrentParenthesisBlock('<', '>') && !selectCurrentParenthesisBlock('(', ')')) {
+        selectCurrentWord();
     }
     }
 
 
 	event.preventDefault();
 	event.preventDefault();
@@ -81,7 +99,13 @@ function keyupEditAttention(event){
 	weight = parseFloat(weight.toPrecision(12));
 	weight = parseFloat(weight.toPrecision(12));
 	if(String(weight).length == 1) weight += ".0"
 	if(String(weight).length == 1) weight += ".0"
 
 
-	text = text.slice(0, selectionEnd + 1) + weight + text.slice(selectionEnd + 1 + end - 1);
+    if (closeCharacter == ')' && weight == 1) {
+        text = text.slice(0, selectionStart - 1) + text.slice(selectionStart, selectionEnd) + text.slice(selectionEnd + 5);
+        selectionStart--;
+        selectionEnd--;
+    } else {
+        text = text.slice(0, selectionEnd + 1) + weight + text.slice(selectionEnd + 1 + end - 1);
+    }
 
 
 	target.focus();
 	target.focus();
 	target.value = text;
 	target.value = text;
@@ -93,4 +117,4 @@ function keyupEditAttention(event){
 
 
 addEventListener('keydown', (event) => {
 addEventListener('keydown', (event) => {
     keyupEditAttention(event);
     keyupEditAttention(event);
-});
+});

+ 3 - 3
javascript/generationParams.js

@@ -16,9 +16,9 @@ onUiUpdate(function(){
 
 
 let modalObserver = new MutationObserver(function(mutations) {
 let modalObserver = new MutationObserver(function(mutations) {
 	mutations.forEach(function(mutationRecord) {
 	mutations.forEach(function(mutationRecord) {
-		let selectedTab = gradioApp().querySelector('#tabs div button.bg-white')?.innerText
-		if (mutationRecord.target.style.display === 'none' && selectedTab === 'txt2img' || selectedTab === 'img2img')
-			gradioApp().getElementById(selectedTab+"_generation_info_button").click()
+		let selectedTab = gradioApp().querySelector('#tabs div button.selected')?.innerText
+		if (mutationRecord.target.style.display === 'none' && (selectedTab === 'txt2img' || selectedTab === 'img2img'))
+			gradioApp().getElementById(selectedTab+"_generation_info_button")?.click()
 	});
 	});
 });
 });
 
 

+ 4 - 4
javascript/hints.js

@@ -65,8 +65,8 @@ titles = {
 
 
     "Interrogate": "Reconstruct prompt from existing image and put it into the prompt field.",
     "Interrogate": "Reconstruct prompt from existing image and put it into the prompt field.",
 
 
-    "Images filename pattern": "Use following tags to define how filenames for images are chosen: [steps], [cfg], [prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
-    "Directory name pattern": "Use following tags to define how subdirectories for images and grids are chosen: [steps], [cfg],[prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp]; leave empty for default.",
+    "Images filename pattern": "Use following tags to define how filenames for images are chosen: [steps], [cfg], [prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp], [hasprompt<prompt1|default><prompt2>..]; leave empty for default.",
+    "Directory name pattern": "Use following tags to define how subdirectories for images and grids are chosen: [steps], [cfg],[prompt_hash], [prompt], [prompt_no_styles], [prompt_spaces], [width], [height], [styles], [sampler], [seed], [model_hash], [model_name], [prompt_words], [date], [datetime], [datetime<Format>], [datetime<Format><Time Zone>], [job_timestamp], [hasprompt<prompt1|default><prompt2>..]; leave empty for default.",
     "Max prompt words": "Set the maximum number of words to be used in the [prompt_words] option; ATTENTION: If the words are too long, they may exceed the maximum length of the file path that the system can handle",
     "Max prompt words": "Set the maximum number of words to be used in the [prompt_words] option; ATTENTION: If the words are too long, they may exceed the maximum length of the file path that the system can handle",
 
 
     "Loopback": "Performs img2img processing multiple times. Output images are used as input for the next loop.",
     "Loopback": "Performs img2img processing multiple times. Output images are used as input for the next loop.",
@@ -85,7 +85,6 @@ titles = {
     "vram": "Torch active: Peak amount of VRAM used by Torch during generation, excluding cached data.\nTorch reserved: Peak amount of VRAM allocated by Torch, including all active and cached data.\nSys VRAM: Peak amount of VRAM allocation across all applications / total GPU VRAM (peak utilization%).",
     "vram": "Torch active: Peak amount of VRAM used by Torch during generation, excluding cached data.\nTorch reserved: Peak amount of VRAM allocated by Torch, including all active and cached data.\nSys VRAM: Peak amount of VRAM allocation across all applications / total GPU VRAM (peak utilization%).",
 
 
     "Eta noise seed delta": "If this values is non-zero, it will be added to seed and used to initialize RNG for noises when using samplers with Eta. You can use this to produce even more variation of images, or you can use this to match images of other software if you know what you are doing.",
     "Eta noise seed delta": "If this values is non-zero, it will be added to seed and used to initialize RNG for noises when using samplers with Eta. You can use this to produce even more variation of images, or you can use this to match images of other software if you know what you are doing.",
-    "Do not add watermark to images": "If this option is enabled, watermark will not be added to created images. Warning: if you do not add watermark, you may be behaving in an unethical manner.",
 
 
     "Filename word regex": "This regular expression will be used extract words from filename, and they will be joined using the option below into label text used for training. Leave empty to keep filename text as it is.",
     "Filename word regex": "This regular expression will be used extract words from filename, and they will be joined using the option below into label text used for training. Leave empty to keep filename text as it is.",
     "Filename join string": "This string will be used to join split words into a single line if the option above is enabled.",
     "Filename join string": "This string will be used to join split words into a single line if the option above is enabled.",
@@ -111,7 +110,8 @@ titles = {
     "Resize height to": "Resizes image to this height. If 0, height is inferred from either of two nearby sliders.",
     "Resize height to": "Resizes image to this height. If 0, height is inferred from either of two nearby sliders.",
     "Multiplier for extra networks": "When adding extra network such as Hypernetwork or Lora to prompt, use this multiplier for it.",
     "Multiplier for extra networks": "When adding extra network such as Hypernetwork or Lora to prompt, use this multiplier for it.",
     "Discard weights with matching name": "Regular expression; if weights's name matches it, the weights is not written to the resulting checkpoint. Use ^model_ema to discard EMA weights.",
     "Discard weights with matching name": "Regular expression; if weights's name matches it, the weights is not written to the resulting checkpoint. Use ^model_ema to discard EMA weights.",
-    "Extra networks tab order": "Comma-separated list of tab names; tabs listed here will appear in the extra networks UI first and in order lsited."
+    "Extra networks tab order": "Comma-separated list of tab names; tabs listed here will appear in the extra networks UI first and in order lsited.",
+    "Negative Guidance minimum sigma": "Skip negative prompt for steps where image is already mostly denoised; the higher this value, the more skips there will be; provides increased performance in exchange for minor quality reduction."
 }
 }
 
 
 
 

+ 5 - 2
javascript/imageviewer.js

@@ -251,8 +251,11 @@ document.addEventListener("DOMContentLoaded", function() {
 
 
     modal.appendChild(modalNext)
     modal.appendChild(modalNext)
 
 
-    gradioApp().appendChild(modal)
-
+    try {
+		gradioApp().appendChild(modal);
+	} catch (e) {
+		gradioApp().body.appendChild(modal);
+	}
 
 
     document.body.appendChild(modal);
     document.body.appendChild(modal);
 
 

+ 1 - 1
javascript/progressbar.js

@@ -138,7 +138,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd, onProgre
                 return
                 return
             }
             }
 
 
-            if(elapsedFromStart > 5 && !res.queued && !res.active){
+            if(elapsedFromStart > 40 && !res.queued && !res.active){
                 removeProgressBar()
                 removeProgressBar()
                 return
                 return
             }
             }

+ 6 - 6
launch.py

@@ -120,12 +120,12 @@ def run_python(code, desc=None, errdesc=None):
     return run(f'"{python}" -c "{code}"', desc, errdesc)
     return run(f'"{python}" -c "{code}"', desc, errdesc)
 
 
 
 
-def run_pip(args, desc=None):
+def run_pip(args, desc=None, live=False):
     if "--skip-install" in sys.argv:
     if "--skip-install" in sys.argv:
         return
         return
 
 
     index_url_line = f' --index-url {index_url}' if index_url != '' else ''
     index_url_line = f' --index-url {index_url}' if index_url != '' else ''
-    return run(f'"{python}" -m pip {args} --prefer-binary{index_url_line}', desc=f"Installing {desc}", errdesc=f"Couldn't install {desc}")
+    return run(f'"{python}" -m pip {args} --prefer-binary{index_url_line}', desc=f"Installing {desc}", errdesc=f"Couldn't install {desc}", live=live)
 
 
 
 
 def check_run_python(code):
 def check_run_python(code):
@@ -222,10 +222,10 @@ def run_extensions_installers(settings_file):
 
 
 
 
 def prepare_environment():
 def prepare_environment():
-    torch_command = os.environ.get('TORCH_COMMAND', "pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117")
+    torch_command = os.environ.get('TORCH_COMMAND', "pip install torch==2.0.0 torchvision==0.15.1 --index-url https://download.pytorch.org/whl/cu118")
     requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt")
     requirements_file = os.environ.get('REQS_FILE', "requirements_versions.txt")
 
 
-    xformers_package = os.environ.get('XFORMERS_PACKAGE', 'xformers==0.0.16rc425')
+    xformers_package = os.environ.get('XFORMERS_PACKAGE', 'xformers==0.0.17')
     gfpgan_package = os.environ.get('GFPGAN_PACKAGE', "git+https://github.com/TencentARC/GFPGAN.git@8d2447a2d918f8eba5a4a01463fd48e45126a379")
     gfpgan_package = os.environ.get('GFPGAN_PACKAGE', "git+https://github.com/TencentARC/GFPGAN.git@8d2447a2d918f8eba5a4a01463fd48e45126a379")
     clip_package = os.environ.get('CLIP_PACKAGE', "git+https://github.com/openai/CLIP.git@d50d76daa670286dd6cacf3bcd80b5e4823fc8e1")
     clip_package = os.environ.get('CLIP_PACKAGE', "git+https://github.com/openai/CLIP.git@d50d76daa670286dd6cacf3bcd80b5e4823fc8e1")
     openclip_package = os.environ.get('OPENCLIP_PACKAGE', "git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b")
     openclip_package = os.environ.get('OPENCLIP_PACKAGE', "git+https://github.com/mlfoundations/open_clip.git@bb6e834e9c70d9c27d0dc3ecedeebeaeb1ffad6b")
@@ -268,7 +268,7 @@ def prepare_environment():
     if (not is_installed("xformers") or args.reinstall_xformers) and args.xformers:
     if (not is_installed("xformers") or args.reinstall_xformers) and args.xformers:
         if platform.system() == "Windows":
         if platform.system() == "Windows":
             if platform.python_version().startswith("3.10"):
             if platform.python_version().startswith("3.10"):
-                run_pip(f"install -U -I --no-deps {xformers_package}", "xformers")
+                run_pip(f"install -U -I --no-deps {xformers_package}", "xformers", live=True)
             else:
             else:
                 print("Installation of xformers is not supported in this version of Python.")
                 print("Installation of xformers is not supported in this version of Python.")
                 print("You can also check this and build manually: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Xformers#building-xformers-on-windows-by-duckness")
                 print("You can also check this and build manually: https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Xformers#building-xformers-on-windows-by-duckness")
@@ -293,7 +293,7 @@ def prepare_environment():
 
 
     if not os.path.isfile(requirements_file):
     if not os.path.isfile(requirements_file):
         requirements_file = os.path.join(script_path, requirements_file)
         requirements_file = os.path.join(script_path, requirements_file)
-    run_pip(f"install -r \"{requirements_file}\"", "requirements for Web UI")
+    run_pip(f"install -r \"{requirements_file}\"", "requirements")
 
 
     run_extensions_installers(settings_file=args.ui_settings_file)
     run_extensions_installers(settings_file=args.ui_settings_file)
 
 

+ 6 - 10
modules/api/api.py

@@ -6,7 +6,6 @@ import uvicorn
 import gradio as gr
 import gradio as gr
 from threading import Lock
 from threading import Lock
 from io import BytesIO
 from io import BytesIO
-from gradio.processing_utils import decode_base64_to_file
 from fastapi import APIRouter, Depends, FastAPI, Request, Response
 from fastapi import APIRouter, Depends, FastAPI, Request, Response
 from fastapi.security import HTTPBasic, HTTPBasicCredentials
 from fastapi.security import HTTPBasic, HTTPBasicCredentials
 from fastapi.exceptions import HTTPException
 from fastapi.exceptions import HTTPException
@@ -272,7 +271,9 @@ class Api:
                     raise HTTPException(status_code=422, detail=f"Cannot have a selectable script in the always on scripts params")
                     raise HTTPException(status_code=422, detail=f"Cannot have a selectable script in the always on scripts params")
                 # always on script with no arg should always run so you don't really need to add them to the requests
                 # always on script with no arg should always run so you don't really need to add them to the requests
                 if "args" in request.alwayson_scripts[alwayson_script_name]:
                 if "args" in request.alwayson_scripts[alwayson_script_name]:
-                    script_args[alwayson_script.args_from:alwayson_script.args_to] = request.alwayson_scripts[alwayson_script_name]["args"]
+                    # min between arg length in scriptrunner and arg length in the request
+                    for idx in range(0, min((alwayson_script.args_to - alwayson_script.args_from), len(request.alwayson_scripts[alwayson_script_name]["args"]))):
+                        script_args[alwayson_script.args_from + idx] = request.alwayson_scripts[alwayson_script_name]["args"][idx]
         return script_args
         return script_args
 
 
     def text2imgapi(self, txt2imgreq: StableDiffusionTxt2ImgProcessingAPI):
     def text2imgapi(self, txt2imgreq: StableDiffusionTxt2ImgProcessingAPI):
@@ -395,16 +396,11 @@ class Api:
     def extras_batch_images_api(self, req: ExtrasBatchImagesRequest):
     def extras_batch_images_api(self, req: ExtrasBatchImagesRequest):
         reqDict = setUpscalers(req)
         reqDict = setUpscalers(req)
 
 
-        def prepareFiles(file):
-            file = decode_base64_to_file(file.data, file_path=file.name)
-            file.orig_name = file.name
-            return file
-
-        reqDict['image_folder'] = list(map(prepareFiles, reqDict['imageList']))
-        reqDict.pop('imageList')
+        image_list = reqDict.pop('imageList', [])
+        image_folder = [decode_base64_to_image(x.data) for x in image_list]
 
 
         with self.queue_lock:
         with self.queue_lock:
-            result = postprocessing.run_extras(extras_mode=1, image="", input_dir="", output_dir="", save_output=False, **reqDict)
+            result = postprocessing.run_extras(extras_mode=1, image_folder=image_folder, image="", input_dir="", output_dir="", save_output=False, **reqDict)
 
 
         return ExtrasBatchImagesResponse(images=list(map(encode_pil_to_base64, result[0])), html_info=result[1])
         return ExtrasBatchImagesResponse(images=list(map(encode_pil_to_base64, result[0])), html_info=result[1])
 
 

+ 6 - 2
modules/devices.py

@@ -92,14 +92,18 @@ def cond_cast_float(input):
 
 
 
 
 def randn(seed, shape):
 def randn(seed, shape):
+    from modules.shared import opts
+
     torch.manual_seed(seed)
     torch.manual_seed(seed)
-    if device.type == 'mps':
+    if opts.randn_source == "CPU" or device.type == 'mps':
         return torch.randn(shape, device=cpu).to(device)
         return torch.randn(shape, device=cpu).to(device)
     return torch.randn(shape, device=device)
     return torch.randn(shape, device=device)
 
 
 
 
 def randn_without_seed(shape):
 def randn_without_seed(shape):
-    if device.type == 'mps':
+    from modules.shared import opts
+
+    if opts.randn_source == "CPU" or device.type == 'mps':
         return torch.randn(shape, device=cpu).to(device)
         return torch.randn(shape, device=cpu).to(device)
     return torch.randn(shape, device=device)
     return torch.randn(shape, device=device)
 
 

+ 1 - 1
modules/extra_networks_hypernet.py

@@ -9,7 +9,7 @@ class ExtraNetworkHypernet(extra_networks.ExtraNetwork):
     def activate(self, p, params_list):
     def activate(self, p, params_list):
         additional = shared.opts.sd_hypernetwork
         additional = shared.opts.sd_hypernetwork
 
 
-        if additional != "" and additional in shared.hypernetworks and len([x for x in params_list if x.items[0] == additional]) == 0:
+        if additional != "None" and additional in shared.hypernetworks and len([x for x in params_list if x.items[0] == additional]) == 0:
             p.all_prompts = [x + f"<hypernet:{additional}:{shared.opts.extra_networks_default_multiplier}>" for x in p.all_prompts]
             p.all_prompts = [x + f"<hypernet:{additional}:{shared.opts.extra_networks_default_multiplier}>" for x in p.all_prompts]
             params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier]))
             params_list.append(extra_networks.ExtraNetworkParams(items=[additional, shared.opts.extra_networks_default_multiplier]))
 
 

+ 6 - 0
modules/generation_parameters_copypaste.py

@@ -284,6 +284,10 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model
 
 
     restore_old_hires_fix_params(res)
     restore_old_hires_fix_params(res)
 
 
+    # Missing RNG means the default was set, which is GPU RNG
+    if "RNG" not in res:
+        res["RNG"] = "GPU"
+
     return res
     return res
 
 
 
 
@@ -304,6 +308,8 @@ infotext_to_setting_name_mapping = [
     ('UniPC skip type', 'uni_pc_skip_type'),
     ('UniPC skip type', 'uni_pc_skip_type'),
     ('UniPC order', 'uni_pc_order'),
     ('UniPC order', 'uni_pc_order'),
     ('UniPC lower order final', 'uni_pc_lower_order_final'),
     ('UniPC lower order final', 'uni_pc_lower_order_final'),
+    ('RNG', 'randn_source'),
+    ('NGMS', 's_min_uncond'),
 ]
 ]
 
 
 
 

+ 27 - 4
modules/images.py

@@ -318,6 +318,7 @@ re_nonletters = re.compile(r'[\s' + string.punctuation + ']+')
 re_pattern = re.compile(r"(.*?)(?:\[([^\[\]]+)\]|$)")
 re_pattern = re.compile(r"(.*?)(?:\[([^\[\]]+)\]|$)")
 re_pattern_arg = re.compile(r"(.*)<([^>]*)>$")
 re_pattern_arg = re.compile(r"(.*)<([^>]*)>$")
 max_filename_part_length = 128
 max_filename_part_length = 128
+NOTHING_AND_SKIP_PREVIOUS_TEXT = object()
 
 
 
 
 def sanitize_filename_part(text, replace_spaces=True):
 def sanitize_filename_part(text, replace_spaces=True):
@@ -352,6 +353,10 @@ class FilenameGenerator:
         'prompt_no_styles': lambda self: self.prompt_no_style(),
         'prompt_no_styles': lambda self: self.prompt_no_style(),
         'prompt_spaces': lambda self: sanitize_filename_part(self.prompt, replace_spaces=False),
         'prompt_spaces': lambda self: sanitize_filename_part(self.prompt, replace_spaces=False),
         'prompt_words': lambda self: self.prompt_words(),
         'prompt_words': lambda self: self.prompt_words(),
+        'batch_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.batch_size == 1 else self.p.batch_index + 1,
+        'generation_number': lambda self: NOTHING_AND_SKIP_PREVIOUS_TEXT if self.p.n_iter == 1 and self.p.batch_size == 1 else self.p.iteration * self.p.batch_size + self.p.batch_index + 1,
+        'hasprompt': lambda self, *args: self.hasprompt(*args),  # accepts formats:[hasprompt<prompt1|default><prompt2>..]
+        'clip_skip': lambda self: opts.data["CLIP_stop_at_last_layers"],
     }
     }
     default_time_format = '%Y%m%d%H%M%S'
     default_time_format = '%Y%m%d%H%M%S'
 
 
@@ -360,6 +365,22 @@ class FilenameGenerator:
         self.seed = seed
         self.seed = seed
         self.prompt = prompt
         self.prompt = prompt
         self.image = image
         self.image = image
+        
+    def hasprompt(self, *args):
+        lower = self.prompt.lower()
+        if self.p is None or self.prompt is None:
+            return None
+        outres = ""
+        for arg in args:
+            if arg != "":
+                division = arg.split("|")
+                expected = division[0].lower()
+                default = division[1] if len(division) > 1 else ""
+                if lower.find(expected) >= 0:
+                    outres = f'{outres}{expected}'
+                else:
+                    outres = outres if default == "" else f'{outres}{default}'
+        return sanitize_filename_part(outres)
 
 
     def prompt_no_style(self):
     def prompt_no_style(self):
         if self.p is None or self.prompt is None:
         if self.p is None or self.prompt is None:
@@ -403,9 +424,9 @@ class FilenameGenerator:
 
 
         for m in re_pattern.finditer(x):
         for m in re_pattern.finditer(x):
             text, pattern = m.groups()
             text, pattern = m.groups()
-            res += text
 
 
             if pattern is None:
             if pattern is None:
+                res += text
                 continue
                 continue
 
 
             pattern_args = []
             pattern_args = []
@@ -426,11 +447,13 @@ class FilenameGenerator:
                     print(f"Error adding [{pattern}] to filename", file=sys.stderr)
                     print(f"Error adding [{pattern}] to filename", file=sys.stderr)
                     print(traceback.format_exc(), file=sys.stderr)
                     print(traceback.format_exc(), file=sys.stderr)
 
 
-                if replacement is not None:
-                    res += str(replacement)
+                if replacement == NOTHING_AND_SKIP_PREVIOUS_TEXT:
+                    continue
+                elif replacement is not None:
+                    res += text + str(replacement)
                     continue
                     continue
 
 
-            res += f'[{pattern}]'
+            res += f'{text}[{pattern}]'
 
 
         return res
         return res
 
 

+ 6 - 3
modules/img2img.py

@@ -4,7 +4,7 @@ import sys
 import traceback
 import traceback
 
 
 import numpy as np
 import numpy as np
-from PIL import Image, ImageOps, ImageFilter, ImageEnhance, ImageChops
+from PIL import Image, ImageOps, ImageFilter, ImageEnhance, ImageChops, UnidentifiedImageError
 
 
 from modules import devices, sd_samplers
 from modules import devices, sd_samplers
 from modules.generation_parameters_copypaste import create_override_settings_dict
 from modules.generation_parameters_copypaste import create_override_settings_dict
@@ -46,7 +46,10 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args):
         if state.interrupted:
         if state.interrupted:
             break
             break
 
 
-        img = Image.open(image)
+        try:
+            img = Image.open(image)
+        except UnidentifiedImageError:
+            continue
         # Use the EXIF orientation of photos taken by smartphones.
         # Use the EXIF orientation of photos taken by smartphones.
         img = ImageOps.exif_transpose(img)
         img = ImageOps.exif_transpose(img)
         p.init_images = [img] * p.batch_size
         p.init_images = [img] * p.batch_size
@@ -151,7 +154,7 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s
         override_settings=override_settings,
         override_settings=override_settings,
     )
     )
 
 
-    p.scripts = modules.scripts.scripts_txt2img
+    p.scripts = modules.scripts.scripts_img2img
     p.script_args = args
     p.script_args = args
 
 
     if shared.cmd_opts.enable_console_prompts:
     if shared.cmd_opts.enable_console_prompts:

+ 2 - 2
modules/interrogate.py

@@ -32,7 +32,7 @@ def download_default_clip_interrogate_categories(content_dir):
     category_types = ["artists", "flavors", "mediums", "movements"]
     category_types = ["artists", "flavors", "mediums", "movements"]
 
 
     try:
     try:
-        os.makedirs(tmpdir)
+        os.makedirs(tmpdir, exist_ok=True)
         for category_type in category_types:
         for category_type in category_types:
             torch.hub.download_url_to_file(f"https://raw.githubusercontent.com/pharmapsychotic/clip-interrogator/main/clip_interrogator/data/{category_type}.txt", os.path.join(tmpdir, f"{category_type}.txt"))
             torch.hub.download_url_to_file(f"https://raw.githubusercontent.com/pharmapsychotic/clip-interrogator/main/clip_interrogator/data/{category_type}.txt", os.path.join(tmpdir, f"{category_type}.txt"))
         os.rename(tmpdir, content_dir)
         os.rename(tmpdir, content_dir)
@@ -41,7 +41,7 @@ def download_default_clip_interrogate_categories(content_dir):
         errors.display(e, "downloading default CLIP interrogate categories")
         errors.display(e, "downloading default CLIP interrogate categories")
     finally:
     finally:
         if os.path.exists(tmpdir):
         if os.path.exists(tmpdir):
-            os.remove(tmpdir)
+            os.removedirs(tmpdir)
 
 
 
 
 class InterrogateModels:
 class InterrogateModels:

+ 12 - 0
modules/ngrok.py

@@ -13,6 +13,18 @@ def connect(token, port, region):
     config = conf.PyngrokConfig(
     config = conf.PyngrokConfig(
         auth_token=token, region=region
         auth_token=token, region=region
     )
     )
+    
+    # Guard for existing tunnels
+    existing = ngrok.get_tunnels(pyngrok_config=config)
+    if existing:
+        for established in existing:
+            # Extra configuration in the case that the user is also using ngrok for other tunnels
+            if established.config['addr'][-4:] == str(port):
+                public_url = existing[0].public_url
+                print(f'ngrok has already been connected to localhost:{port}! URL: {public_url}\n'
+                    'You can use this link after the launch is complete.')
+                return
+    
     try:
     try:
         if account is None:
         if account is None:
             public_url = ngrok.connect(port, pyngrok_config=config, bind_tls=True).public_url
             public_url = ngrok.connect(port, pyngrok_config=config, bind_tls=True).public_url

+ 7 - 2
modules/postprocessing.py

@@ -18,9 +18,14 @@ def run_postprocessing(extras_mode, image, image_folder, input_dir, output_dir,
 
 
     if extras_mode == 1:
     if extras_mode == 1:
         for img in image_folder:
         for img in image_folder:
-            image = Image.open(img)
+            if isinstance(img, Image.Image):
+                image = img
+                fn = ''
+            else:
+                image = Image.open(os.path.abspath(img.name))
+                fn = os.path.splitext(img.orig_name)[0]
             image_data.append(image)
             image_data.append(image)
-            image_names.append(os.path.splitext(img.orig_name)[0])
+            image_names.append(fn)
     elif extras_mode == 2:
     elif extras_mode == 2:
         assert not shared.cmd_opts.hide_ui_dir_config, '--hide-ui-dir-config option must be disabled'
         assert not shared.cmd_opts.hide_ui_dir_config, '--hide-ui-dir-config option must be disabled'
         assert input_dir, 'input directory not selected'
         assert input_dir, 'input directory not selected'

+ 31 - 6
modules/processing.py

@@ -3,6 +3,7 @@ import math
 import os
 import os
 import sys
 import sys
 import warnings
 import warnings
+import hashlib
 
 
 import torch
 import torch
 import numpy as np
 import numpy as np
@@ -105,7 +106,7 @@ class StableDiffusionProcessing:
     """
     """
     The first set of paramaters: sd_models -> do_not_reload_embeddings represent the minimum required to create a StableDiffusionProcessing
     The first set of paramaters: sd_models -> do_not_reload_embeddings represent the minimum required to create a StableDiffusionProcessing
     """
     """
-    def __init__(self, sd_model=None, outpath_samples=None, outpath_grids=None, prompt: str = "", styles: List[str] = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, seed_enable_extras: bool = True, sampler_name: str = None, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: bool = False, do_not_save_samples: bool = False, do_not_save_grid: bool = False, extra_generation_params: Dict[Any, Any] = None, overlay_images: Any = None, negative_prompt: str = None, eta: float = None, do_not_reload_embeddings: bool = False, denoising_strength: float = 0, ddim_discretize: str = None, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.0, override_settings: Dict[str, Any] = None, override_settings_restore_afterwards: bool = True, sampler_index: int = None, script_args: list = None):
+    def __init__(self, sd_model=None, outpath_samples=None, outpath_grids=None, prompt: str = "", styles: List[str] = None, seed: int = -1, subseed: int = -1, subseed_strength: float = 0, seed_resize_from_h: int = -1, seed_resize_from_w: int = -1, seed_enable_extras: bool = True, sampler_name: str = None, batch_size: int = 1, n_iter: int = 1, steps: int = 50, cfg_scale: float = 7.0, width: int = 512, height: int = 512, restore_faces: bool = False, tiling: bool = False, do_not_save_samples: bool = False, do_not_save_grid: bool = False, extra_generation_params: Dict[Any, Any] = None, overlay_images: Any = None, negative_prompt: str = None, eta: float = None, do_not_reload_embeddings: bool = False, denoising_strength: float = 0, ddim_discretize: str = None, s_min_uncond: float = 0.0, s_churn: float = 0.0, s_tmax: float = None, s_tmin: float = 0.0, s_noise: float = 1.0, override_settings: Dict[str, Any] = None, override_settings_restore_afterwards: bool = True, sampler_index: int = None, script_args: list = None):
         if sampler_index is not None:
         if sampler_index is not None:
             print("sampler_index argument for StableDiffusionProcessing does not do anything; use sampler_name", file=sys.stderr)
             print("sampler_index argument for StableDiffusionProcessing does not do anything; use sampler_name", file=sys.stderr)
 
 
@@ -140,6 +141,7 @@ class StableDiffusionProcessing:
         self.denoising_strength: float = denoising_strength
         self.denoising_strength: float = denoising_strength
         self.sampler_noise_scheduler_override = None
         self.sampler_noise_scheduler_override = None
         self.ddim_discretize = ddim_discretize or opts.ddim_discretize
         self.ddim_discretize = ddim_discretize or opts.ddim_discretize
+        self.s_min_uncond = s_min_uncond or opts.s_min_uncond
         self.s_churn = s_churn or opts.s_churn
         self.s_churn = s_churn or opts.s_churn
         self.s_tmin = s_tmin or opts.s_tmin
         self.s_tmin = s_tmin or opts.s_tmin
         self.s_tmax = s_tmax or float('inf')  # not representable as a standard ui option
         self.s_tmax = s_tmax or float('inf')  # not representable as a standard ui option
@@ -162,6 +164,8 @@ class StableDiffusionProcessing:
         self.all_seeds = None
         self.all_seeds = None
         self.all_subseeds = None
         self.all_subseeds = None
         self.iteration = 0
         self.iteration = 0
+        self.is_hr_pass = False
+        
 
 
     @property
     @property
     def sd_model(self):
     def sd_model(self):
@@ -476,6 +480,9 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter
         "Conditional mask weight": getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None,
         "Conditional mask weight": getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None,
         "Clip skip": None if clip_skip <= 1 else clip_skip,
         "Clip skip": None if clip_skip <= 1 else clip_skip,
         "ENSD": None if opts.eta_noise_seed_delta == 0 else opts.eta_noise_seed_delta,
         "ENSD": None if opts.eta_noise_seed_delta == 0 else opts.eta_noise_seed_delta,
+        "Init image hash": getattr(p, 'init_img_hash', None),
+        "RNG": opts.randn_source if opts.randn_source != "GPU" else None,
+        "NGMS": None if p.s_min_uncond == 0 else p.s_min_uncond,
     }
     }
 
 
     generation_params.update(p.extra_generation_params)
     generation_params.update(p.extra_generation_params)
@@ -639,8 +646,14 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
                     processed = Processed(p, [], p.seed, "")
                     processed = Processed(p, [], p.seed, "")
                     file.write(processed.infotext(p, 0))
                     file.write(processed.infotext(p, 0))
 
 
-            uc = get_conds_with_caching(prompt_parser.get_learned_conditioning, negative_prompts, p.steps, cached_uc)
-            c = get_conds_with_caching(prompt_parser.get_multicond_learned_conditioning, prompts, p.steps, cached_c)
+            step_multiplier = 1
+            if not shared.opts.dont_fix_second_order_samplers_schedule:
+                try:
+                    step_multiplier = 2 if sd_samplers.all_samplers_map.get(p.sampler_name).aliases[0] in ['k_dpmpp_2s_a', 'k_dpmpp_2s_a_ka', 'k_dpmpp_sde', 'k_dpmpp_sde_ka', 'k_dpm_2', 'k_dpm_2_a', 'k_heun'] else 1
+                except:
+                    pass
+            uc = get_conds_with_caching(prompt_parser.get_learned_conditioning, negative_prompts, p.steps * step_multiplier, cached_uc)
+            c = get_conds_with_caching(prompt_parser.get_multicond_learned_conditioning, prompts, p.steps * step_multiplier, cached_c)
 
 
             if len(model_hijack.comments) > 0:
             if len(model_hijack.comments) > 0:
                 for comment in model_hijack.comments:
                 for comment in model_hijack.comments:
@@ -670,6 +683,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
                 p.scripts.postprocess_batch(p, x_samples_ddim, batch_number=n)
                 p.scripts.postprocess_batch(p, x_samples_ddim, batch_number=n)
 
 
             for i, x_sample in enumerate(x_samples_ddim):
             for i, x_sample in enumerate(x_samples_ddim):
+                p.batch_index = i
+
                 x_sample = 255. * np.moveaxis(x_sample.cpu().numpy(), 0, 2)
                 x_sample = 255. * np.moveaxis(x_sample.cpu().numpy(), 0, 2)
                 x_sample = x_sample.astype(np.uint8)
                 x_sample = x_sample.astype(np.uint8)
 
 
@@ -706,9 +721,9 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
                     image.info["parameters"] = text
                     image.info["parameters"] = text
                 output_images.append(image)
                 output_images.append(image)
 
 
-                if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay:
+                if hasattr(p, 'mask_for_overlay') and p.mask_for_overlay and any([opts.save_mask, opts.save_mask_composite, opts.return_mask, opts.return_mask_composite]):
                     image_mask = p.mask_for_overlay.convert('RGB')
                     image_mask = p.mask_for_overlay.convert('RGB')
-                    image_mask_composite = Image.composite(image.convert('RGBA').convert('RGBa'), Image.new('RGBa', image.size), p.mask_for_overlay.convert('L')).convert('RGBA')
+                    image_mask_composite = Image.composite(image.convert('RGBA').convert('RGBa'), Image.new('RGBa', image.size), images.resize_image(2, p.mask_for_overlay, image.width, image.height).convert('L')).convert('RGBA')
 
 
                     if opts.save_mask:
                     if opts.save_mask:
                         images.save_image(image_mask, p.outpath_samples, "", seeds[i], prompts[i], opts.samples_format, info=infotext(n, i), p=p, suffix="-mask")
                         images.save_image(image_mask, p.outpath_samples, "", seeds[i], prompts[i], opts.samples_format, info=infotext(n, i), p=p, suffix="-mask")
@@ -718,7 +733,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed:
 
 
                     if opts.return_mask:
                     if opts.return_mask:
                         output_images.append(image_mask)
                         output_images.append(image_mask)
-                    
+
                     if opts.return_mask_composite:
                     if opts.return_mask_composite:
                         output_images.append(image_mask_composite)
                         output_images.append(image_mask_composite)
 
 
@@ -871,6 +886,8 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing):
         if not self.enable_hr:
         if not self.enable_hr:
             return samples
             return samples
 
 
+        self.is_hr_pass = True
+
         target_width = self.hr_upscale_to_x
         target_width = self.hr_upscale_to_x
         target_height = self.hr_upscale_to_y
         target_height = self.hr_upscale_to_y
 
 
@@ -940,6 +957,8 @@ class StableDiffusionProcessingTxt2Img(StableDiffusionProcessing):
 
 
         samples = self.sampler.sample_img2img(self, samples, noise, conditioning, unconditional_conditioning, steps=self.hr_second_pass_steps or self.steps, image_conditioning=image_conditioning)
         samples = self.sampler.sample_img2img(self, samples, noise, conditioning, unconditional_conditioning, steps=self.hr_second_pass_steps or self.steps, image_conditioning=image_conditioning)
 
 
+        self.is_hr_pass = False
+
         return samples
         return samples
 
 
 
 
@@ -1007,6 +1026,12 @@ class StableDiffusionProcessingImg2Img(StableDiffusionProcessing):
             self.color_corrections = []
             self.color_corrections = []
         imgs = []
         imgs = []
         for img in self.init_images:
         for img in self.init_images:
+
+            # Save init image
+            if opts.save_init_img:
+                self.init_img_hash = hashlib.md5(img.tobytes()).hexdigest()
+                images.save_image(img, path=opts.outdir_init_images, basename=None, forced_filename=self.init_img_hash, save_to_dirs=False)
+
             image = images.flatten(img, opts.img2img_background_color)
             image = images.flatten(img, opts.img2img_background_color)
 
 
             if crop_region is None and self.resize_mode != 3:
             if crop_region is None and self.resize_mode != 3:

+ 12 - 2
modules/realesrgan_model.py

@@ -9,7 +9,7 @@ from realesrgan import RealESRGANer
 
 
 from modules.upscaler import Upscaler, UpscalerData
 from modules.upscaler import Upscaler, UpscalerData
 from modules.shared import cmd_opts, opts
 from modules.shared import cmd_opts, opts
-
+from modules import modelloader
 
 
 class UpscalerRealESRGAN(Upscaler):
 class UpscalerRealESRGAN(Upscaler):
     def __init__(self, path):
     def __init__(self, path):
@@ -23,7 +23,15 @@ class UpscalerRealESRGAN(Upscaler):
             self.enable = True
             self.enable = True
             self.scalers = []
             self.scalers = []
             scalers = self.load_models(path)
             scalers = self.load_models(path)
+
+            local_model_paths = self.find_models(ext_filter=[".pth"])
             for scaler in scalers:
             for scaler in scalers:
+                if scaler.local_data_path.startswith("http"):
+                    filename = modelloader.friendly_name(scaler.local_data_path)
+                    local = next(iter([local_model for local_model in local_model_paths if local_model.endswith(filename + '.pth')]), None)
+                    if local:
+                        scaler.local_data_path = local
+
                 if scaler.name in opts.realesrgan_enabled_models:
                 if scaler.name in opts.realesrgan_enabled_models:
                     self.scalers.append(scaler)
                     self.scalers.append(scaler)
 
 
@@ -64,7 +72,9 @@ class UpscalerRealESRGAN(Upscaler):
                 print(f"Unable to find model info: {path}")
                 print(f"Unable to find model info: {path}")
                 return None
                 return None
 
 
-            info.local_data_path = load_file_from_url(url=info.data_path, model_dir=self.model_path, progress=True)
+            if info.local_data_path.startswith("http"):
+                info.local_data_path = load_file_from_url(url=info.data_path, model_dir=self.model_path, progress=True)
+
             return info
             return info
         except Exception as e:
         except Exception as e:
             print(f"Error making Real-ESRGAN models list: {e}", file=sys.stderr)
             print(f"Error making Real-ESRGAN models list: {e}", file=sys.stderr)

+ 1 - 4
modules/safe.py

@@ -1,6 +1,5 @@
 # this code is adapted from the script contributed by anon from /h/
 # this code is adapted from the script contributed by anon from /h/
 
 
-import io
 import pickle
 import pickle
 import collections
 import collections
 import sys
 import sys
@@ -12,11 +11,9 @@ import _codecs
 import zipfile
 import zipfile
 import re
 import re
 
 
-
 # PyTorch 1.13 and later have _TypedStorage renamed to TypedStorage
 # PyTorch 1.13 and later have _TypedStorage renamed to TypedStorage
 TypedStorage = torch.storage.TypedStorage if hasattr(torch.storage, 'TypedStorage') else torch.storage._TypedStorage
 TypedStorage = torch.storage.TypedStorage if hasattr(torch.storage, 'TypedStorage') else torch.storage._TypedStorage
 
 
-
 def encode(*args):
 def encode(*args):
     out = _codecs.encode(*args)
     out = _codecs.encode(*args)
     return out
     return out
@@ -27,7 +24,7 @@ class RestrictedUnpickler(pickle.Unpickler):
 
 
     def persistent_load(self, saved_id):
     def persistent_load(self, saved_id):
         assert saved_id[0] == 'storage'
         assert saved_id[0] == 'storage'
-        return TypedStorage()
+        return TypedStorage(_internal=True)
 
 
     def find_class(self, module, name):
     def find_class(self, module, name):
         if self.extra_handler is not None:
         if self.extra_handler is not None:

+ 10 - 0
modules/sd_samplers_common.py

@@ -60,3 +60,13 @@ def store_latent(decoded):
 
 
 class InterruptedException(BaseException):
 class InterruptedException(BaseException):
     pass
     pass
+
+
+if opts.randn_source == "CPU":
+    import torchsde._brownian.brownian_interval
+
+    def torchsde_randn(size, dtype, device, seed):
+        generator = torch.Generator(devices.cpu).manual_seed(int(seed))
+        return torch.randn(size, dtype=dtype, device=devices.cpu, generator=generator).to(device)
+
+    torchsde._brownian.brownian_interval._randn = torchsde_randn

+ 33 - 13
modules/sd_samplers_kdiffusion.py

@@ -76,7 +76,7 @@ class CFGDenoiser(torch.nn.Module):
 
 
         return denoised
         return denoised
 
 
-    def forward(self, x, sigma, uncond, cond, cond_scale, image_cond):
+    def forward(self, x, sigma, uncond, cond, cond_scale, s_min_uncond, image_cond):
         if state.interrupted or state.skipped:
         if state.interrupted or state.skipped:
             raise sd_samplers_common.InterruptedException
             raise sd_samplers_common.InterruptedException
 
 
@@ -115,12 +115,21 @@ class CFGDenoiser(torch.nn.Module):
         sigma_in = denoiser_params.sigma
         sigma_in = denoiser_params.sigma
         tensor = denoiser_params.text_cond
         tensor = denoiser_params.text_cond
         uncond = denoiser_params.text_uncond
         uncond = denoiser_params.text_uncond
+        skip_uncond = False
 
 
-        if tensor.shape[1] == uncond.shape[1]:
-            if not is_edit_model:
-                cond_in = torch.cat([tensor, uncond])
-            else:
+        # alternating uncond allows for higher thresholds without the quality loss normally expected from raising it
+        if self.step % 2 and s_min_uncond > 0 and sigma[0] < s_min_uncond and not is_edit_model:
+            skip_uncond = True
+            x_in = x_in[:-batch_size]
+            sigma_in = sigma_in[:-batch_size]
+
+        if tensor.shape[1] == uncond.shape[1] or skip_uncond:
+            if is_edit_model:
                 cond_in = torch.cat([tensor, uncond, uncond])
                 cond_in = torch.cat([tensor, uncond, uncond])
+            elif skip_uncond:
+                cond_in = tensor
+            else:
+                cond_in = torch.cat([tensor, uncond])
 
 
             if shared.batch_cond_uncond:
             if shared.batch_cond_uncond:
                 x_out = self.inner_model(x_in, sigma_in, cond=make_condition_dict([cond_in], image_cond_in))
                 x_out = self.inner_model(x_in, sigma_in, cond=make_condition_dict([cond_in], image_cond_in))
@@ -144,7 +153,13 @@ class CFGDenoiser(torch.nn.Module):
 
 
                 x_out[a:b] = self.inner_model(x_in[a:b], sigma_in[a:b], cond=make_condition_dict(c_crossattn, image_cond_in[a:b]))
                 x_out[a:b] = self.inner_model(x_in[a:b], sigma_in[a:b], cond=make_condition_dict(c_crossattn, image_cond_in[a:b]))
 
 
-            x_out[-uncond.shape[0]:] = self.inner_model(x_in[-uncond.shape[0]:], sigma_in[-uncond.shape[0]:], cond=make_condition_dict([uncond], image_cond_in[-uncond.shape[0]:]))
+            if not skip_uncond:
+                x_out[-uncond.shape[0]:] = self.inner_model(x_in[-uncond.shape[0]:], sigma_in[-uncond.shape[0]:], cond=make_condition_dict([uncond], image_cond_in[-uncond.shape[0]:]))
+
+        denoised_image_indexes = [x[0][0] for x in conds_list]
+        if skip_uncond:
+            fake_uncond = torch.cat([x_out[i:i+1] for i in denoised_image_indexes])
+            x_out = torch.cat([x_out, fake_uncond])  # we skipped uncond denoising, so we put cond-denoised image to where the uncond-denoised image should be
 
 
         denoised_params = CFGDenoisedParams(x_out, state.sampling_step, state.sampling_steps)
         denoised_params = CFGDenoisedParams(x_out, state.sampling_step, state.sampling_steps)
         cfg_denoised_callback(denoised_params)
         cfg_denoised_callback(denoised_params)
@@ -152,20 +167,21 @@ class CFGDenoiser(torch.nn.Module):
         devices.test_for_nans(x_out, "unet")
         devices.test_for_nans(x_out, "unet")
 
 
         if opts.live_preview_content == "Prompt":
         if opts.live_preview_content == "Prompt":
-            sd_samplers_common.store_latent(x_out[0:uncond.shape[0]])
+            sd_samplers_common.store_latent(torch.cat([x_out[i:i+1] for i in denoised_image_indexes]))
         elif opts.live_preview_content == "Negative prompt":
         elif opts.live_preview_content == "Negative prompt":
             sd_samplers_common.store_latent(x_out[-uncond.shape[0]:])
             sd_samplers_common.store_latent(x_out[-uncond.shape[0]:])
 
 
-        if not is_edit_model:
-            denoised = self.combine_denoised(x_out, conds_list, uncond, cond_scale)
-        else:
+        if is_edit_model:
             denoised = self.combine_denoised_for_edit_model(x_out, cond_scale)
             denoised = self.combine_denoised_for_edit_model(x_out, cond_scale)
+        elif skip_uncond:
+            denoised = self.combine_denoised(x_out, conds_list, uncond, 1.0)
+        else:
+            denoised = self.combine_denoised(x_out, conds_list, uncond, cond_scale)
 
 
         if self.mask is not None:
         if self.mask is not None:
             denoised = self.init_latent * self.mask + self.nmask * denoised
             denoised = self.init_latent * self.mask + self.nmask * denoised
 
 
         self.step += 1
         self.step += 1
-
         return denoised
         return denoised
 
 
 
 
@@ -190,7 +206,7 @@ class TorchHijack:
             if noise.shape == x.shape:
             if noise.shape == x.shape:
                 return noise
                 return noise
 
 
-        if x.device.type == 'mps':
+        if opts.randn_source == "CPU" or x.device.type == 'mps':
             return torch.randn_like(x, device=devices.cpu).to(x.device)
             return torch.randn_like(x, device=devices.cpu).to(x.device)
         else:
         else:
             return torch.randn_like(x)
             return torch.randn_like(x)
@@ -210,6 +226,7 @@ class KDiffusionSampler:
         self.eta = None
         self.eta = None
         self.config = None
         self.config = None
         self.last_latent = None
         self.last_latent = None
+        self.s_min_uncond = None
 
 
         self.conditioning_key = sd_model.model.conditioning_key
         self.conditioning_key = sd_model.model.conditioning_key
 
 
@@ -244,6 +261,7 @@ class KDiffusionSampler:
         self.model_wrap_cfg.step = 0
         self.model_wrap_cfg.step = 0
         self.model_wrap_cfg.image_cfg_scale = getattr(p, 'image_cfg_scale', None)
         self.model_wrap_cfg.image_cfg_scale = getattr(p, 'image_cfg_scale', None)
         self.eta = p.eta if p.eta is not None else opts.eta_ancestral
         self.eta = p.eta if p.eta is not None else opts.eta_ancestral
+        self.s_min_uncond = getattr(p, 's_min_uncond', 0.0)
 
 
         k_diffusion.sampling.torch = TorchHijack(self.sampler_noises if self.sampler_noises is not None else [])
         k_diffusion.sampling.torch = TorchHijack(self.sampler_noises if self.sampler_noises is not None else [])
 
 
@@ -326,6 +344,7 @@ class KDiffusionSampler:
             'image_cond': image_conditioning, 
             'image_cond': image_conditioning, 
             'uncond': unconditional_conditioning, 
             'uncond': unconditional_conditioning, 
             'cond_scale': p.cfg_scale,
             'cond_scale': p.cfg_scale,
+            's_min_uncond': self.s_min_uncond
         }
         }
 
 
         samples = self.launch_sampling(t_enc + 1, lambda: self.func(self.model_wrap_cfg, xi, extra_args=extra_args, disable=False, callback=self.callback_state, **extra_params_kwargs))
         samples = self.launch_sampling(t_enc + 1, lambda: self.func(self.model_wrap_cfg, xi, extra_args=extra_args, disable=False, callback=self.callback_state, **extra_params_kwargs))
@@ -359,7 +378,8 @@ class KDiffusionSampler:
             'cond': conditioning, 
             'cond': conditioning, 
             'image_cond': image_conditioning, 
             'image_cond': image_conditioning, 
             'uncond': unconditional_conditioning, 
             'uncond': unconditional_conditioning, 
-            'cond_scale': p.cfg_scale
+            'cond_scale': p.cfg_scale,
+            's_min_uncond': self.s_min_uncond
         }, disable=False, callback=self.callback_state, **extra_params_kwargs))
         }, disable=False, callback=self.callback_state, **extra_params_kwargs))
 
 
         return samples
         return samples

+ 45 - 2
modules/shared.py

@@ -4,6 +4,7 @@ import json
 import os
 import os
 import sys
 import sys
 import time
 import time
+import requests
 
 
 from PIL import Image
 from PIL import Image
 import gradio as gr
 import gradio as gr
@@ -39,6 +40,7 @@ restricted_opts = {
     "outdir_grids",
     "outdir_grids",
     "outdir_txt2img_grids",
     "outdir_txt2img_grids",
     "outdir_save",
     "outdir_save",
+    "outdir_init_images"
 }
 }
 
 
 ui_reorder_categories = [
 ui_reorder_categories = [
@@ -54,6 +56,21 @@ ui_reorder_categories = [
     "scripts",
     "scripts",
 ]
 ]
 
 
+# https://huggingface.co/datasets/freddyaboulton/gradio-theme-subdomains/resolve/main/subdomains.json
+gradio_hf_hub_themes = [
+    "gradio/glass",
+    "gradio/monochrome",
+    "gradio/seafoam",
+    "gradio/soft",
+    "freddyaboulton/dracula_revamped",
+    "gradio/dracula_test",
+    "abidlabs/dracula_test",
+    "abidlabs/pakistan",
+    "dawood/microsoft_windows",
+    "ysharma/steampunk"
+]
+
+
 cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or cmd_opts.server_name) and not cmd_opts.enable_insecure_extension_access
 cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or cmd_opts.server_name) and not cmd_opts.enable_insecure_extension_access
 
 
 devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = \
 devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = \
@@ -252,7 +269,7 @@ options_templates.update(options_section(('saving-images', "Saving images/grids"
     "use_original_name_batch": OptionInfo(True, "Use original name for output filename during batch process in extras tab"),
     "use_original_name_batch": OptionInfo(True, "Use original name for output filename during batch process in extras tab"),
     "use_upscaler_name_as_suffix": OptionInfo(False, "Use upscaler name as filename suffix in the extras tab"),
     "use_upscaler_name_as_suffix": OptionInfo(False, "Use upscaler name as filename suffix in the extras tab"),
     "save_selected_only": OptionInfo(True, "When using 'Save' button, only save a single selected image"),
     "save_selected_only": OptionInfo(True, "When using 'Save' button, only save a single selected image"),
-    "do_not_add_watermark": OptionInfo(False, "Do not add watermark to images"),
+    "save_init_img": OptionInfo(False, "Save init images when using img2img"),
 
 
     "temp_dir":  OptionInfo("", "Directory for temporary images; leave empty for default"),
     "temp_dir":  OptionInfo("", "Directory for temporary images; leave empty for default"),
     "clean_temp_dir_at_start": OptionInfo(False, "Cleanup non-default temporary directory when starting webui"),
     "clean_temp_dir_at_start": OptionInfo(False, "Cleanup non-default temporary directory when starting webui"),
@@ -268,6 +285,7 @@ options_templates.update(options_section(('saving-paths', "Paths for saving"), {
     "outdir_txt2img_grids": OptionInfo("outputs/txt2img-grids", 'Output directory for txt2img grids', component_args=hide_dirs),
     "outdir_txt2img_grids": OptionInfo("outputs/txt2img-grids", 'Output directory for txt2img grids', component_args=hide_dirs),
     "outdir_img2img_grids": OptionInfo("outputs/img2img-grids", 'Output directory for img2img grids', component_args=hide_dirs),
     "outdir_img2img_grids": OptionInfo("outputs/img2img-grids", 'Output directory for img2img grids', component_args=hide_dirs),
     "outdir_save": OptionInfo("log/images", "Directory for saving images using the Save button", component_args=hide_dirs),
     "outdir_save": OptionInfo("log/images", "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),
 }))
 }))
 
 
 options_templates.update(options_section(('saving-to-dirs', "Saving to a directory"), {
 options_templates.update(options_section(('saving-to-dirs', "Saving to a directory"), {
@@ -283,6 +301,8 @@ options_templates.update(options_section(('upscaling', "Upscaling"), {
     "ESRGAN_tile_overlap": OptionInfo(8, "Tile overlap, in pixels for ESRGAN upscalers. Low values = visible seam.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}),
     "ESRGAN_tile_overlap": OptionInfo(8, "Tile overlap, in pixels for ESRGAN upscalers. Low values = visible seam.", gr.Slider, {"minimum": 0, "maximum": 48, "step": 1}),
     "realesrgan_enabled_models": OptionInfo(["R-ESRGAN 4x+", "R-ESRGAN 4x+ Anime6B"], "Select which Real-ESRGAN models to show in the web UI. (Requires restart)", gr.CheckboxGroup, lambda: {"choices": shared_items.realesrgan_models_names()}),
     "realesrgan_enabled_models": OptionInfo(["R-ESRGAN 4x+", "R-ESRGAN 4x+ Anime6B"], "Select which Real-ESRGAN models to show in the web UI. (Requires restart)", gr.CheckboxGroup, lambda: {"choices": shared_items.realesrgan_models_names()}),
     "upscaler_for_img2img": OptionInfo(None, "Upscaler for img2img", gr.Dropdown, lambda: {"choices": [x.name for x in sd_upscalers]}),
     "upscaler_for_img2img": OptionInfo(None, "Upscaler for img2img", gr.Dropdown, lambda: {"choices": [x.name for x in sd_upscalers]}),
+    "SCUNET_tile": OptionInfo(256, "Tile size for SCUNET upscalers. 0 = no tiling.", gr.Slider, {"minimum": 0, "maximum": 512, "step": 16}),
+    "SCUNET_tile_overlap": OptionInfo(8, "Tile overlap, in pixels for SCUNET upscalers. Low values = visible seam.", gr.Slider, {"minimum": 0, "maximum": 64, "step": 1}),
 }))
 }))
 
 
 options_templates.update(options_section(('face-restoration', "Face restoration"), {
 options_templates.update(options_section(('face-restoration', "Face restoration"), {
@@ -331,6 +351,7 @@ options_templates.update(options_section(('sd', "Stable Diffusion"), {
     "comma_padding_backtrack": OptionInfo(20, "Increase coherency by padding from the last comma within n tokens when using more than 75 tokens", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1 }),
     "comma_padding_backtrack": OptionInfo(20, "Increase coherency by padding from the last comma within n tokens when using more than 75 tokens", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1 }),
     "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}),
     "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}),
     "upcast_attn": OptionInfo(False, "Upcast cross attention layer to float32"),
     "upcast_attn": OptionInfo(False, "Upcast cross attention layer to float32"),
+    "randn_source": OptionInfo("GPU", "Random number generator source. Changes seeds drastically. Use CPU to produce the same picture across different vidocard vendors.", gr.Radio, {"choices": ["GPU", "CPU"]}),
 }))
 }))
 
 
 options_templates.update(options_section(('compatibility', "Compatibility"), {
 options_templates.update(options_section(('compatibility', "Compatibility"), {
@@ -338,6 +359,7 @@ options_templates.update(options_section(('compatibility', "Compatibility"), {
     "use_old_karras_scheduler_sigmas": OptionInfo(False, "Use old karras scheduler sigmas (0.1 to 10)."),
     "use_old_karras_scheduler_sigmas": OptionInfo(False, "Use old karras scheduler sigmas (0.1 to 10)."),
     "no_dpmpp_sde_batch_determinism": OptionInfo(False, "Do not make DPM++ SDE deterministic across different batch sizes."),
     "no_dpmpp_sde_batch_determinism": OptionInfo(False, "Do not make DPM++ SDE deterministic across different batch sizes."),
     "use_old_hires_fix_width_height": OptionInfo(False, "For hires fix, use width/height sliders to set final resolution rather than first pass (disables Upscale by, Resize width/height to)."),
     "use_old_hires_fix_width_height": OptionInfo(False, "For hires fix, use width/height sliders to set final resolution rather than first pass (disables Upscale by, Resize width/height to)."),
+    "dont_fix_second_order_samplers_schedule": OptionInfo(False, "Do not fix prompt schedule for second order samplers."),
 }))
 }))
 
 
 options_templates.update(options_section(('interrogate', "Interrogate Options"), {
 options_templates.update(options_section(('interrogate', "Interrogate Options"), {
@@ -361,7 +383,7 @@ options_templates.update(options_section(('extra_networks', "Extra Networks"), {
     "extra_networks_card_width": OptionInfo(0, "Card width for Extra Networks (px)"),
     "extra_networks_card_width": OptionInfo(0, "Card width for Extra Networks (px)"),
     "extra_networks_card_height": OptionInfo(0, "Card height for Extra Networks (px)"),
     "extra_networks_card_height": OptionInfo(0, "Card height for Extra Networks (px)"),
     "extra_networks_add_text_separator": OptionInfo(" ", "Extra text to add before <...> when adding extra network to prompt"),
     "extra_networks_add_text_separator": OptionInfo(" ", "Extra text to add before <...> when adding extra network to prompt"),
-    "sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": [""] + [x for x in hypernetworks.keys()]}, refresh=reload_hypernetworks),
+    "sd_hypernetwork": OptionInfo("None", "Add hypernetwork to prompt", gr.Dropdown, lambda: {"choices": ["None"] + [x for x in hypernetworks.keys()]}, refresh=reload_hypernetworks),
 }))
 }))
 
 
 options_templates.update(options_section(('ui', "User interface"), {
 options_templates.update(options_section(('ui', "User interface"), {
@@ -382,11 +404,13 @@ options_templates.update(options_section(('ui', "User interface"), {
     "dimensions_and_batch_together": OptionInfo(True, "Show Width/Height and Batch sliders in same row"),
     "dimensions_and_batch_together": OptionInfo(True, "Show Width/Height and Batch sliders in same row"),
     "keyedit_precision_attention": OptionInfo(0.1, "Ctrl+up/down precision when editing (attention:1.1)", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}),
     "keyedit_precision_attention": OptionInfo(0.1, "Ctrl+up/down precision when editing (attention:1.1)", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}),
     "keyedit_precision_extra": OptionInfo(0.05, "Ctrl+up/down precision when editing <extra networks:0.9>", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}),
     "keyedit_precision_extra": OptionInfo(0.05, "Ctrl+up/down precision when editing <extra networks:0.9>", gr.Slider, {"minimum": 0.01, "maximum": 0.2, "step": 0.001}),
+    "keyedit_delimiters": OptionInfo(".,\/!?%^*;:{}=`~()", "Ctrl+up/down word delimiters"),
     "quicksettings": OptionInfo("sd_model_checkpoint", "Quicksettings list"),
     "quicksettings": OptionInfo("sd_model_checkpoint", "Quicksettings list"),
     "hidden_tabs": OptionInfo([], "Hidden UI tabs (requires restart)", ui_components.DropdownMulti, lambda: {"choices": [x for x in tab_names]}),
     "hidden_tabs": OptionInfo([], "Hidden UI tabs (requires restart)", ui_components.DropdownMulti, lambda: {"choices": [x for x in tab_names]}),
     "ui_reorder": OptionInfo(", ".join(ui_reorder_categories), "txt2img/img2img UI item order"),
     "ui_reorder": OptionInfo(", ".join(ui_reorder_categories), "txt2img/img2img UI item order"),
     "ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order"),
     "ui_extra_networks_tab_reorder": OptionInfo("", "Extra networks tab order"),
     "localization": OptionInfo("None", "Localization (requires restart)", gr.Dropdown, lambda: {"choices": ["None"] + list(localization.localizations.keys())}, refresh=lambda: localization.list_localizations(cmd_opts.localizations_dir)),
     "localization": OptionInfo("None", "Localization (requires restart)", gr.Dropdown, lambda: {"choices": ["None"] + list(localization.localizations.keys())}, refresh=lambda: localization.list_localizations(cmd_opts.localizations_dir)),
+    "gradio_theme": OptionInfo("Default", "Gradio theme (requires restart)", ui_components.DropdownEditable, lambda: {"choices": ["Default"] + gradio_hf_hub_themes})
 }))
 }))
 
 
 options_templates.update(options_section(('ui', "Live previews"), {
 options_templates.update(options_section(('ui', "Live previews"), {
@@ -405,6 +429,7 @@ options_templates.update(options_section(('sampler-params', "Sampler parameters"
     "eta_ancestral": OptionInfo(1.0, "eta (noise multiplier) for ancestral samplers", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     "eta_ancestral": OptionInfo(1.0, "eta (noise multiplier) for ancestral samplers", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     "ddim_discretize": OptionInfo('uniform', "img2img DDIM discretize", gr.Radio, {"choices": ['uniform', 'quad']}),
     "ddim_discretize": OptionInfo('uniform', "img2img DDIM discretize", gr.Radio, {"choices": ['uniform', 'quad']}),
     's_churn': OptionInfo(0.0, "sigma churn", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     's_churn': OptionInfo(0.0, "sigma churn", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
+    's_min_uncond': OptionInfo(0, "Negative Guidance minimum sigma", gr.Slider, {"minimum": 0.0, "maximum": 4.0, "step": 0.01}),
     's_tmin':  OptionInfo(0.0, "sigma tmin",  gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     's_tmin':  OptionInfo(0.0, "sigma tmin",  gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     's_noise': OptionInfo(1.0, "sigma noise", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     's_noise': OptionInfo(1.0, "sigma noise", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01}),
     'eta_noise_seed_delta': OptionInfo(0, "Eta noise seed delta", gr.Number, {"precision": 0}),
     'eta_noise_seed_delta': OptionInfo(0, "Eta noise seed delta", gr.Number, {"precision": 0}),
@@ -600,6 +625,24 @@ clip_model = None
 
 
 progress_print_out = sys.stdout
 progress_print_out = sys.stdout
 
 
+gradio_theme = gr.themes.Base()
+
+
+def reload_gradio_theme(theme_name=None):
+    global gradio_theme
+    if not theme_name:
+        theme_name = opts.gradio_theme
+
+    if theme_name == "Default":
+        gradio_theme = gr.themes.Default()
+    else:
+        try:
+            gradio_theme = gr.themes.ThemeClass.from_hub(theme_name)
+        except requests.exceptions.ConnectionError:
+            print("Can't access HuggingFace Hub, falling back to default Gradio theme")
+            gradio_theme = gr.themes.Default()
+
+
 
 
 class TotalTQDM:
 class TotalTQDM:
     def __init__(self):
     def __init__(self):

+ 5 - 7
modules/styles.py

@@ -72,16 +72,14 @@ class StyleDatabase:
         return apply_styles_to_prompt(prompt, [self.styles.get(x, self.no_style).negative_prompt for x in styles])
         return apply_styles_to_prompt(prompt, [self.styles.get(x, self.no_style).negative_prompt for x in styles])
 
 
     def save_styles(self, path: str) -> None:
     def save_styles(self, path: str) -> None:
-        # Write to temporary file first, so we don't nuke the file if something goes wrong
-        fd, temp_path = tempfile.mkstemp(".csv")
+        # Always keep a backup file around
+        if os.path.exists(path):
+            shutil.copy(path, path + ".bak")
+
+        fd = os.open(path, os.O_RDWR|os.O_CREAT)
         with os.fdopen(fd, "w", encoding="utf-8-sig", newline='') as file:
         with os.fdopen(fd, "w", encoding="utf-8-sig", newline='') as file:
             # _fields is actually part of the public API: typing.NamedTuple is a replacement for collections.NamedTuple,
             # _fields is actually part of the public API: typing.NamedTuple is a replacement for collections.NamedTuple,
             # and collections.NamedTuple has explicit documentation for accessing _fields. Same goes for _asdict()
             # and collections.NamedTuple has explicit documentation for accessing _fields. Same goes for _asdict()
             writer = csv.DictWriter(file, fieldnames=PromptStyle._fields)
             writer = csv.DictWriter(file, fieldnames=PromptStyle._fields)
             writer.writeheader()
             writer.writeheader()
             writer.writerows(style._asdict() for k,     style in self.styles.items())
             writer.writerows(style._asdict() for k,     style in self.styles.items())
-
-        # Always keep a backup file around
-        if os.path.exists(path):
-            shutil.move(path, path + ".bak")
-        shutil.move(temp_path, path)

+ 3 - 1
modules/textual_inversion/preprocess.py

@@ -161,7 +161,9 @@ def preprocess_work(process_src, process_dst, process_width, process_height, pre
         params.subindex = 0
         params.subindex = 0
         filename = os.path.join(src, imagefile)
         filename = os.path.join(src, imagefile)
         try:
         try:
-            img = Image.open(filename).convert("RGB")
+            img = Image.open(filename)
+            img = ImageOps.exif_transpose(img)
+            img = img.convert("RGB")
         except Exception:
         except Exception:
             continue
             continue
 
 

+ 6 - 0
modules/textual_inversion/textual_inversion.py

@@ -233,6 +233,12 @@ class EmbeddingDatabase:
             self.load_from_dir(embdir)
             self.load_from_dir(embdir)
             embdir.update()
             embdir.update()
 
 
+        # re-sort word_embeddings because load_from_dir may not load in alphabetic order.
+        # using a temporary copy so we don't reinitialize self.word_embeddings in case other objects have a reference to it.
+        sorted_word_embeddings = {e.name: e for e in sorted(self.word_embeddings.values(), key=lambda e: e.name.lower())}
+        self.word_embeddings.clear()
+        self.word_embeddings.update(sorted_word_embeddings)
+
         displayed_embeddings = (tuple(self.word_embeddings.keys()), tuple(self.skipped_embeddings.keys()))
         displayed_embeddings = (tuple(self.word_embeddings.keys()), tuple(self.skipped_embeddings.keys()))
         if self.previously_displayed_embeddings != displayed_embeddings:
         if self.previously_displayed_embeddings != displayed_embeddings:
             self.previously_displayed_embeddings = displayed_embeddings
             self.previously_displayed_embeddings = displayed_embeddings

+ 6 - 6
modules/ui.py

@@ -171,8 +171,8 @@ def create_seed_inputs(target_interface):
     with FormRow(elem_id=target_interface + '_seed_row', variant="compact"):
     with FormRow(elem_id=target_interface + '_seed_row', variant="compact"):
         seed = (gr.Textbox if cmd_opts.use_textbox_seed else gr.Number)(label='Seed', value=-1, elem_id=target_interface + '_seed')
         seed = (gr.Textbox if cmd_opts.use_textbox_seed else gr.Number)(label='Seed', value=-1, elem_id=target_interface + '_seed')
         seed.style(container=False)
         seed.style(container=False)
-        random_seed = ToolButton(random_symbol, elem_id=target_interface + '_random_seed')
-        reuse_seed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_seed')
+        random_seed = ToolButton(random_symbol, elem_id=target_interface + '_random_seed', label='Random seed')
+        reuse_seed = ToolButton(reuse_symbol, elem_id=target_interface + '_reuse_seed', label='Reuse seed')
 
 
         seed_checkbox = gr.Checkbox(label='Extra', elem_id=target_interface + '_subseed_show', value=False)
         seed_checkbox = gr.Checkbox(label='Extra', elem_id=target_interface + '_subseed_show', value=False)
 
 
@@ -468,7 +468,7 @@ def create_ui():
                                 height = gr.Slider(minimum=64, maximum=2048, step=8, label="Height", value=512, elem_id="txt2img_height")
                                 height = gr.Slider(minimum=64, maximum=2048, step=8, label="Height", value=512, elem_id="txt2img_height")
 
 
                             with gr.Column(elem_id="txt2img_dimensions_row", scale=1, elem_classes="dimensions-tools"):
                             with gr.Column(elem_id="txt2img_dimensions_row", scale=1, elem_classes="dimensions-tools"):
-                                res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="txt2img_res_switch_btn")
+                                res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="txt2img_res_switch_btn", label="Switch dims")
 
 
                             if opts.dimensions_and_batch_together:
                             if opts.dimensions_and_batch_together:
                                 with gr.Column(elem_id="txt2img_column_batch"):
                                 with gr.Column(elem_id="txt2img_column_batch"):
@@ -1204,7 +1204,7 @@ def create_ui():
 
 
             with gr.Column(elem_id='ti_gallery_container'):
             with gr.Column(elem_id='ti_gallery_container'):
                 ti_output = gr.Text(elem_id="ti_output", value="", show_label=False)
                 ti_output = gr.Text(elem_id="ti_output", value="", show_label=False)
-                ti_gallery = gr.Gallery(label='Output', show_label=False, elem_id='ti_gallery').style(grid=4)
+                ti_gallery = gr.Gallery(label='Output', show_label=False, elem_id='ti_gallery').style(columns=4)
                 ti_progress = gr.HTML(elem_id="ti_progress", value="")
                 ti_progress = gr.HTML(elem_id="ti_progress", value="")
                 ti_outcome = gr.HTML(elem_id="ti_error", value="")
                 ti_outcome = gr.HTML(elem_id="ti_error", value="")
 
 
@@ -1565,7 +1565,7 @@ def create_ui():
     for _interface, label, _ifid in interfaces:
     for _interface, label, _ifid in interfaces:
         shared.tab_names.append(label)
         shared.tab_names.append(label)
 
 
-    with gr.Blocks(analytics_enabled=False, title="Stable Diffusion") as demo:
+    with gr.Blocks(theme=shared.gradio_theme, analytics_enabled=False, title="Stable Diffusion") as demo:
         with gr.Row(elem_id="quicksettings", variant="compact"):
         with gr.Row(elem_id="quicksettings", variant="compact"):
             for i, k, item in sorted(quicksettings_list, key=lambda x: quicksettings_names.get(x[1], x[0])):
             for i, k, item in sorted(quicksettings_list, key=lambda x: quicksettings_names.get(x[1], x[0])):
                 component = create_setting_component(k, is_quicksettings=True)
                 component = create_setting_component(k, is_quicksettings=True)
@@ -1705,7 +1705,7 @@ def create_ui():
                 if init_field is not None:
                 if init_field is not None:
                     init_field(saved_value)
                     init_field(saved_value)
 
 
-        if type(x) in [gr.Slider, gr.Radio, gr.Checkbox, gr.Textbox, gr.Number, gr.Dropdown] and x.visible:
+        if type(x) in [gr.Slider, gr.Radio, gr.Checkbox, gr.Textbox, gr.Number, gr.Dropdown, ToolButton] and x.visible:
             apply_field(x, 'visible')
             apply_field(x, 'visible')
 
 
         if type(x) == gr.Slider:
         if type(x) == gr.Slider:

+ 1 - 1
modules/ui_common.py

@@ -125,7 +125,7 @@ Requested path was: {f}
 
 
     with gr.Column(variant='panel', elem_id=f"{tabname}_results"):
     with gr.Column(variant='panel', elem_id=f"{tabname}_results"):
         with gr.Group(elem_id=f"{tabname}_gallery_container"):
         with gr.Group(elem_id=f"{tabname}_gallery_container"):
-            result_gallery = gr.Gallery(label='Output', show_label=False, elem_id=f"{tabname}_gallery").style(grid=4)
+            result_gallery = gr.Gallery(label='Output', show_label=False, elem_id=f"{tabname}_gallery").style(columns=4)
 
 
         generation_info = None
         generation_info = None
         with gr.Column():
         with gr.Column():

+ 10 - 0
modules/ui_components.py

@@ -62,3 +62,13 @@ class DropdownMulti(FormComponent, gr.Dropdown):
 
 
     def get_block_name(self):
     def get_block_name(self):
         return "dropdown"
         return "dropdown"
+
+
+class DropdownEditable(FormComponent, gr.Dropdown):
+    """Same as gr.Dropdown but allows editing value"""
+    def __init__(self, **kwargs):
+        super().__init__(allow_custom_value=True, **kwargs)
+
+    def get_block_name(self):
+        return "dropdown"
+

+ 14 - 6
modules/ui_extensions.py

@@ -129,7 +129,7 @@ def normalize_git_url(url):
     return url
     return url
 
 
 
 
-def install_extension_from_url(dirname, url):
+def install_extension_from_url(dirname, url, branch_name=None):
     check_access()
     check_access()
 
 
     assert url, 'No URL specified'
     assert url, 'No URL specified'
@@ -150,10 +150,17 @@ def install_extension_from_url(dirname, url):
 
 
     try:
     try:
         shutil.rmtree(tmpdir, True)
         shutil.rmtree(tmpdir, True)
-        with git.Repo.clone_from(url, tmpdir) as repo:
-            repo.remote().fetch()
-            for submodule in repo.submodules:
-                submodule.update()
+        if not branch_name:
+            # if no branch is specified, use the default branch
+            with git.Repo.clone_from(url, tmpdir) as repo:
+                repo.remote().fetch()
+                for submodule in repo.submodules:
+                    submodule.update()
+        else:
+            with git.Repo.clone_from(url, tmpdir, branch=branch_name) as repo:
+                repo.remote().fetch()
+                for submodule in repo.submodules:
+                    submodule.update()
         try:
         try:
             os.rename(tmpdir, target_dir)
             os.rename(tmpdir, target_dir)
         except OSError as err:
         except OSError as err:
@@ -376,13 +383,14 @@ def create_ui():
 
 
             with gr.TabItem("Install from URL"):
             with gr.TabItem("Install from URL"):
                 install_url = gr.Text(label="URL for extension's git repository")
                 install_url = gr.Text(label="URL for extension's git repository")
+                install_branch = gr.Text(label="Specific branch name", placeholder="Leave empty for default main branch")
                 install_dirname = gr.Text(label="Local directory name", placeholder="Leave empty for auto")
                 install_dirname = gr.Text(label="Local directory name", placeholder="Leave empty for auto")
                 install_button = gr.Button(value="Install", variant="primary")
                 install_button = gr.Button(value="Install", variant="primary")
                 install_result = gr.HTML(elem_id="extension_install_result")
                 install_result = gr.HTML(elem_id="extension_install_result")
 
 
                 install_button.click(
                 install_button.click(
                     fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
                     fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
-                    inputs=[install_dirname, install_url],
+                    inputs=[install_dirname, install_url, install_branch],
                     outputs=[extensions_table, install_result],
                     outputs=[extensions_table, install_result],
                 )
                 )
 
 

+ 1 - 1
modules/ui_postprocessing.py

@@ -13,7 +13,7 @@ def create_ui():
                     extras_image = gr.Image(label="Source", source="upload", interactive=True, type="pil", elem_id="extras_image")
                     extras_image = gr.Image(label="Source", source="upload", interactive=True, type="pil", elem_id="extras_image")
 
 
                 with gr.TabItem('Batch Process', elem_id="extras_batch_process_tab") as tab_batch:
                 with gr.TabItem('Batch Process', elem_id="extras_batch_process_tab") as tab_batch:
-                    image_batch = gr.File(label="Batch Process", file_count="multiple", interactive=True, type="file", elem_id="extras_image_batch")
+                    image_batch = gr.Files(label="Batch Process", interactive=True, elem_id="extras_image_batch")
 
 
                 with gr.TabItem('Batch from Directory', elem_id="extras_batch_directory_tab") as tab_batch_dir:
                 with gr.TabItem('Batch from Directory', elem_id="extras_batch_directory_tab") as tab_batch_dir:
                     extras_batch_input_dir = gr.Textbox(label="Input directory", **shared.hide_dirs, placeholder="A directory on the same machine where the server is running.", elem_id="extras_batch_input_dir")
                     extras_batch_input_dir = gr.Textbox(label="Input directory", **shared.hide_dirs, placeholder="A directory on the same machine where the server is running.", elem_id="extras_batch_input_dir")

+ 2 - 2
requirements.txt

@@ -1,11 +1,11 @@
+astunparse
 blendmodes
 blendmodes
 accelerate
 accelerate
 basicsr
 basicsr
 fonts
 fonts
 font-roboto
 font-roboto
 gfpgan
 gfpgan
-gradio==3.23
-invisible-watermark
+gradio==3.27
 numpy
 numpy
 omegaconf
 omegaconf
 opencv-contrib-python
 opencv-contrib-python

+ 4 - 4
requirements_versions.txt

@@ -1,10 +1,10 @@
 blendmodes==2022
 blendmodes==2022
 transformers==4.25.1
 transformers==4.25.1
-accelerate==0.12.0
+accelerate==0.18.0
 basicsr==1.4.2
 basicsr==1.4.2
 gfpgan==1.3.8
 gfpgan==1.3.8
-gradio==3.23
-numpy==1.23.3
+gradio==3.27
+numpy==1.23.5
 Pillow==9.4.0
 Pillow==9.4.0
 realesrgan==0.3.0
 realesrgan==0.3.0
 torch
 torch
@@ -25,6 +25,6 @@ lark==1.1.2
 inflection==0.5.1
 inflection==0.5.1
 GitPython==3.1.30
 GitPython==3.1.30
 torchsde==0.2.5
 torchsde==0.2.5
-safetensors==0.3.0
+safetensors==0.3.1
 httpcore<=0.15
 httpcore<=0.15
 fastapi==0.94.0
 fastapi==0.94.0

+ 56 - 7
scripts/custom_code.py

@@ -1,9 +1,40 @@
 import modules.scripts as scripts
 import modules.scripts as scripts
 import gradio as gr
 import gradio as gr
+import ast
+import copy
 
 
 from modules.processing import Processed
 from modules.processing import Processed
 from modules.shared import opts, cmd_opts, state
 from modules.shared import opts, cmd_opts, state
 
 
+
+def convertExpr2Expression(expr):
+    expr.lineno = 0
+    expr.col_offset = 0
+    result = ast.Expression(expr.value, lineno=0, col_offset = 0)
+
+    return result
+
+
+def exec_with_return(code, module):
+    """
+    like exec() but can return values
+    https://stackoverflow.com/a/52361938/5862977
+    """
+    code_ast = ast.parse(code)
+
+    init_ast = copy.deepcopy(code_ast)
+    init_ast.body = code_ast.body[:-1]
+
+    last_ast = copy.deepcopy(code_ast)
+    last_ast.body = code_ast.body[-1:]
+
+    exec(compile(init_ast, "<ast>", "exec"), module.__dict__)
+    if type(last_ast.body[0]) == ast.Expr:
+        return eval(compile(convertExpr2Expression(last_ast.body[0]), "<ast>", "eval"), module.__dict__)
+    else:
+        exec(compile(last_ast, "<ast>", "exec"), module.__dict__)
+
+
 class Script(scripts.Script):
 class Script(scripts.Script):
 
 
     def title(self):
     def title(self):
@@ -13,12 +44,23 @@ class Script(scripts.Script):
         return cmd_opts.allow_code
         return cmd_opts.allow_code
 
 
     def ui(self, is_img2img):
     def ui(self, is_img2img):
-        code = gr.Textbox(label="Python code", lines=1, elem_id=self.elem_id("code"))
+        example = """from modules.processing import process_images
+
+p.width = 768
+p.height = 768
+p.batch_size = 2
+p.steps = 10
+
+return process_images(p)
+"""
+
 
 
-        return [code]
+        code = gr.Code(value=example, language="python", label="Python code", elem_id=self.elem_id("code"))
+        indent_level = gr.Number(label='Indent level', value=2, precision=0, elem_id=self.elem_id("indent_level"))
 
 
+        return [code, indent_level]
 
 
-    def run(self, p, code):
+    def run(self, p, code, indent_level):
         assert cmd_opts.allow_code, '--allow-code option must be enabled'
         assert cmd_opts.allow_code, '--allow-code option must be enabled'
 
 
         display_result_data = [[], -1, ""]
         display_result_data = [[], -1, ""]
@@ -29,13 +71,20 @@ class Script(scripts.Script):
             display_result_data[2] = i
             display_result_data[2] = i
 
 
         from types import ModuleType
         from types import ModuleType
-        compiled = compile(code, '', 'exec')
         module = ModuleType("testmodule")
         module = ModuleType("testmodule")
         module.__dict__.update(globals())
         module.__dict__.update(globals())
         module.p = p
         module.p = p
         module.display = display
         module.display = display
-        exec(compiled, module.__dict__)
+
+        indent = " " * indent_level
+        indented = code.replace('\n', '\n' + indent)
+        body = f"""def __webuitemp__():
+{indent}{indented}
+__webuitemp__()"""
+
+        result = exec_with_return(body, module)
+
+        if isinstance(result, Processed):
+            return result
 
 
         return Processed(p, *display_result_data)
         return Processed(p, *display_result_data)
-    
-    

+ 9 - 5
scripts/postprocessing_upscale.py

@@ -4,8 +4,8 @@ import numpy as np
 from modules import scripts_postprocessing, shared
 from modules import scripts_postprocessing, shared
 import gradio as gr
 import gradio as gr
 
 
-from modules.ui_components import FormRow
-
+from modules.ui_components import FormRow, ToolButton
+from modules.ui import switch_values_symbol
 
 
 upscale_cache = {}
 upscale_cache = {}
 
 
@@ -25,9 +25,12 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing):
 
 
                     with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to:
                     with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to:
                         with FormRow():
                         with FormRow():
-                            upscaling_resize_w = gr.Number(label="Width", value=512, precision=0, elem_id="extras_upscaling_resize_w")
-                            upscaling_resize_h = gr.Number(label="Height", value=512, precision=0, elem_id="extras_upscaling_resize_h")
-                            upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop")
+                            with gr.Column(elem_id="upscaling_column_size", scale=4):
+                                upscaling_resize_w = gr.Slider(minimum=64, maximum=2048, step=8, label="Width", value=512, elem_id="extras_upscaling_resize_w")
+                                upscaling_resize_h = gr.Slider(minimum=64, maximum=2048, step=8, label="Height", value=512, elem_id="extras_upscaling_resize_h")
+                            with gr.Column(elem_id="upscaling_dimensions_row", scale=1, elem_classes="dimensions-tools"):
+                                upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn")
+                                upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop")
 
 
             with FormRow():
             with FormRow():
                 extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name)
                 extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name)
@@ -36,6 +39,7 @@ class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing):
                 extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name)
                 extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name)
                 extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility")
                 extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility")
 
 
+        upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False)
         tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab])
         tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab])
         tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab])
         tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab])
 
 

+ 55 - 25
scripts/xyz_grid.py

@@ -211,7 +211,8 @@ axis_options = [
     AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list),
     AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list),
     AxisOptionTxt2Img("Sampler", str, apply_sampler, format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers]),
     AxisOptionTxt2Img("Sampler", str, apply_sampler, format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers]),
     AxisOptionImg2Img("Sampler", str, apply_sampler, format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img]),
     AxisOptionImg2Img("Sampler", str, apply_sampler, format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img]),
-    AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_value, confirm=confirm_checkpoints, cost=1.0, choices=lambda: list(sd_models.checkpoints_list)),
+    AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_value, confirm=confirm_checkpoints, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list, key=str.casefold)),
+    AxisOption("Negative Guidance minimum sigma", float, apply_field("s_min_uncond")),
     AxisOption("Sigma Churn", float, apply_field("s_churn")),
     AxisOption("Sigma Churn", float, apply_field("s_churn")),
     AxisOption("Sigma min", float, apply_field("s_tmin")),
     AxisOption("Sigma min", float, apply_field("s_tmin")),
     AxisOption("Sigma max", float, apply_field("s_tmax")),
     AxisOption("Sigma max", float, apply_field("s_tmax")),
@@ -374,16 +375,19 @@ class Script(scripts.Script):
                 with gr.Row():
                 with gr.Row():
                     x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type"))
                     x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type"))
                     x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values"))
                     x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values"))
+                    x_values_dropdown = gr.Dropdown(label="X values",visible=False,multiselect=True,interactive=True)
                     fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False)
                     fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False)
 
 
                 with gr.Row():
                 with gr.Row():
                     y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type"))
                     y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type"))
                     y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values"))
                     y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values"))
+                    y_values_dropdown = gr.Dropdown(label="Y values",visible=False,multiselect=True,interactive=True)
                     fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False)
                     fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False)
 
 
                 with gr.Row():
                 with gr.Row():
                     z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type"))
                     z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type"))
                     z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values"))
                     z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values"))
+                    z_values_dropdown = gr.Dropdown(label="Z values",visible=False,multiselect=True,interactive=True)
                     fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False)
                     fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False)
 
 
         with gr.Row(variant="compact", elem_id="axis_options"):
         with gr.Row(variant="compact", elem_id="axis_options"):
@@ -401,54 +405,74 @@ class Script(scripts.Script):
             swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button")
             swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button")
             swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button")
             swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button")
 
 
-        def swap_axes(axis1_type, axis1_values, axis2_type, axis2_values):
-            return self.current_axis_options[axis2_type].label, axis2_values, self.current_axis_options[axis1_type].label, axis1_values
+        def swap_axes(axis1_type, axis1_values, axis1_values_dropdown, axis2_type, axis2_values, axis2_values_dropdown):
+            return self.current_axis_options[axis2_type].label, axis2_values, axis2_values_dropdown, self.current_axis_options[axis1_type].label, axis1_values, axis1_values_dropdown
 
 
-        xy_swap_args = [x_type, x_values, y_type, y_values]
+        xy_swap_args = [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown]
         swap_xy_axes_button.click(swap_axes, inputs=xy_swap_args, outputs=xy_swap_args)
         swap_xy_axes_button.click(swap_axes, inputs=xy_swap_args, outputs=xy_swap_args)
-        yz_swap_args = [y_type, y_values, z_type, z_values]
+        yz_swap_args = [y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown]
         swap_yz_axes_button.click(swap_axes, inputs=yz_swap_args, outputs=yz_swap_args)
         swap_yz_axes_button.click(swap_axes, inputs=yz_swap_args, outputs=yz_swap_args)
-        xz_swap_args = [x_type, x_values, z_type, z_values]
+        xz_swap_args = [x_type, x_values, x_values_dropdown, z_type, z_values, z_values_dropdown]
         swap_xz_axes_button.click(swap_axes, inputs=xz_swap_args, outputs=xz_swap_args)
         swap_xz_axes_button.click(swap_axes, inputs=xz_swap_args, outputs=xz_swap_args)
 
 
         def fill(x_type):
         def fill(x_type):
             axis = self.current_axis_options[x_type]
             axis = self.current_axis_options[x_type]
-            return ", ".join(axis.choices()) if axis.choices else gr.update()
-
-        fill_x_button.click(fn=fill, inputs=[x_type], outputs=[x_values])
-        fill_y_button.click(fn=fill, inputs=[y_type], outputs=[y_values])
-        fill_z_button.click(fn=fill, inputs=[z_type], outputs=[z_values])
-
-        def select_axis(x_type):
-            return gr.Button.update(visible=self.current_axis_options[x_type].choices is not None)
-
-        x_type.change(fn=select_axis, inputs=[x_type], outputs=[fill_x_button])
-        y_type.change(fn=select_axis, inputs=[y_type], outputs=[fill_y_button])
-        z_type.change(fn=select_axis, inputs=[z_type], outputs=[fill_z_button])
+            return axis.choices() if axis.choices else gr.update()
+
+        fill_x_button.click(fn=fill, inputs=[x_type], outputs=[x_values_dropdown])
+        fill_y_button.click(fn=fill, inputs=[y_type], outputs=[y_values_dropdown])
+        fill_z_button.click(fn=fill, inputs=[z_type], outputs=[z_values_dropdown])
+
+        def select_axis(axis_type,axis_values_dropdown):
+            choices = self.current_axis_options[axis_type].choices
+            has_choices = choices is not None
+            current_values = axis_values_dropdown
+            if has_choices:
+                choices = choices()
+                if isinstance(current_values,str):
+                    current_values = current_values.split(",")
+                current_values = list(filter(lambda x: x in choices, current_values))
+            return gr.Button.update(visible=has_choices),gr.Textbox.update(visible=not has_choices),gr.update(choices=choices if has_choices else None,visible=has_choices,value=current_values)
+
+        x_type.change(fn=select_axis, inputs=[x_type,x_values_dropdown], outputs=[fill_x_button,x_values,x_values_dropdown])
+        y_type.change(fn=select_axis, inputs=[y_type,y_values_dropdown], outputs=[fill_y_button,y_values,y_values_dropdown])
+        z_type.change(fn=select_axis, inputs=[z_type,z_values_dropdown], outputs=[fill_z_button,z_values,z_values_dropdown])
+
+        def get_dropdown_update_from_params(axis,params):
+            val_key = axis + " Values"
+            vals = params.get(val_key,"")
+            valslist = [x.strip() for x in chain.from_iterable(csv.reader(StringIO(vals))) if x]
+            return gr.update(value = valslist)
 
 
         self.infotext_fields = (
         self.infotext_fields = (
             (x_type, "X Type"),
             (x_type, "X Type"),
             (x_values, "X Values"),
             (x_values, "X Values"),
+            (x_values_dropdown, lambda params:get_dropdown_update_from_params("X",params)),
             (y_type, "Y Type"),
             (y_type, "Y Type"),
             (y_values, "Y Values"),
             (y_values, "Y Values"),
+            (y_values_dropdown, lambda params:get_dropdown_update_from_params("Y",params)),
             (z_type, "Z Type"),
             (z_type, "Z Type"),
             (z_values, "Z Values"),
             (z_values, "Z Values"),
+            (z_values_dropdown, lambda params:get_dropdown_update_from_params("Z",params)),
         )
         )
 
 
-        return [x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size]
+        return [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size]
 
 
-    def run(self, p, x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size):
+    def run(self, p, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size):
         if not no_fixed_seeds:
         if not no_fixed_seeds:
             modules.processing.fix_seed(p)
             modules.processing.fix_seed(p)
 
 
         if not opts.return_grid:
         if not opts.return_grid:
             p.batch_size = 1
             p.batch_size = 1
 
 
-        def process_axis(opt, vals):
+        def process_axis(opt, vals, vals_dropdown):
             if opt.label == 'Nothing':
             if opt.label == 'Nothing':
                 return [0]
                 return [0]
 
 
-            valslist = [x.strip() for x in chain.from_iterable(csv.reader(StringIO(vals))) if x]
+            if opt.choices is not None:
+                valslist = vals_dropdown
+            else:
+                valslist = [x.strip() for x in chain.from_iterable(csv.reader(StringIO(vals))) if x]
 
 
             if opt.type == int:
             if opt.type == int:
                 valslist_ext = []
                 valslist_ext = []
@@ -506,13 +530,19 @@ class Script(scripts.Script):
             return valslist
             return valslist
 
 
         x_opt = self.current_axis_options[x_type]
         x_opt = self.current_axis_options[x_type]
-        xs = process_axis(x_opt, x_values)
+        if x_opt.choices is not None:
+            x_values = ",".join(x_values_dropdown)
+        xs = process_axis(x_opt, x_values, x_values_dropdown)
 
 
         y_opt = self.current_axis_options[y_type]
         y_opt = self.current_axis_options[y_type]
-        ys = process_axis(y_opt, y_values)
+        if y_opt.choices is not None:
+            y_values = ",".join(y_values_dropdown)
+        ys = process_axis(y_opt, y_values, y_values_dropdown)
 
 
         z_opt = self.current_axis_options[z_type]
         z_opt = self.current_axis_options[z_type]
-        zs = process_axis(z_opt, z_values)
+        if z_opt.choices is not None:
+            z_values = ",".join(z_values_dropdown)
+        zs = process_axis(z_opt, z_values, z_values_dropdown)
 
 
         # this could be moved to common code, but unlikely to be ever triggered anywhere else
         # this could be moved to common code, but unlikely to be ever triggered anywhere else
         Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes
         Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes

+ 4 - 0
style.css

@@ -312,6 +312,10 @@ div.dimensions-tools{
     align-content: center;
     align-content: center;
 }
 }
 
 
+div#extras_scale_to_tab div.form{
+    flex-direction: row;
+}
+
 #mode_img2img .gradio-image > div.fixed-height, #mode_img2img .gradio-image > div.fixed-height img{
 #mode_img2img .gradio-image > div.fixed-height, #mode_img2img .gradio-image > div.fixed-height img{
     height: 480px !important;
     height: 480px !important;
     max-height: 480px !important;
     max-height: 480px !important;

+ 1 - 1
webui-macos-env.sh

@@ -11,7 +11,7 @@ fi
 
 
 export install_dir="$HOME"
 export install_dir="$HOME"
 export COMMANDLINE_ARGS="--skip-torch-cuda-test --upcast-sampling --no-half-vae --use-cpu interrogate"
 export COMMANDLINE_ARGS="--skip-torch-cuda-test --upcast-sampling --no-half-vae --use-cpu interrogate"
-export TORCH_COMMAND="pip install torch==1.12.1 torchvision==0.13.1"
+export TORCH_COMMAND="pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/cu118"
 export K_DIFFUSION_REPO="https://github.com/brkirch/k-diffusion.git"
 export K_DIFFUSION_REPO="https://github.com/brkirch/k-diffusion.git"
 export K_DIFFUSION_COMMIT_HASH="51c9778f269cedb55a4d88c79c0246d35bdadb71"
 export K_DIFFUSION_COMMIT_HASH="51c9778f269cedb55a4d88c79c0246d35bdadb71"
 export PYTORCH_ENABLE_MPS_FALLBACK=1
 export PYTORCH_ENABLE_MPS_FALLBACK=1

+ 3 - 0
webui-user.sh

@@ -43,4 +43,7 @@
 # Uncomment to enable accelerated launch
 # Uncomment to enable accelerated launch
 #export ACCELERATE="True"
 #export ACCELERATE="True"
 
 
+# Uncomment to disable TCMalloc
+#export NO_TCMALLOC="True"
+
 ###########################################
 ###########################################

+ 48 - 5
webui.py

@@ -20,6 +20,9 @@ startup_timer = timer.Timer()
 import torch
 import torch
 import pytorch_lightning # pytorch_lightning should be imported after torch, but it re-enables warnings on import so import once to disable them
 import pytorch_lightning # pytorch_lightning should be imported after torch, but it re-enables warnings on import so import once to disable them
 warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="pytorch_lightning")
 warnings.filterwarnings(action="ignore", category=DeprecationWarning, module="pytorch_lightning")
+warnings.filterwarnings(action="ignore", category=UserWarning, module="torchvision")
+
+
 startup_timer.record("import torch")
 startup_timer.record("import torch")
 
 
 import gradio
 import gradio
@@ -67,11 +70,51 @@ else:
     server_name = "0.0.0.0" if cmd_opts.listen else None
     server_name = "0.0.0.0" if cmd_opts.listen else None
 
 
 
 
+def fix_asyncio_event_loop_policy():
+    """
+        The default `asyncio` event loop policy only automatically creates
+        event loops in the main threads. Other threads must create event
+        loops explicitly or `asyncio.get_event_loop` (and therefore
+        `.IOLoop.current`) will fail. Installing this policy allows event
+        loops to be created automatically on any thread, matching the
+        behavior of Tornado versions prior to 5.0 (or 5.0 on Python 2).
+    """
+
+    import asyncio
+
+    if sys.platform == "win32" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
+        # "Any thread" and "selector" should be orthogonal, but there's not a clean
+        # interface for composing policies so pick the right base.
+        _BasePolicy = asyncio.WindowsSelectorEventLoopPolicy  # type: ignore
+    else:
+        _BasePolicy = asyncio.DefaultEventLoopPolicy
+
+    class AnyThreadEventLoopPolicy(_BasePolicy):  # type: ignore
+        """Event loop policy that allows loop creation on any thread.
+        Usage::
+
+            asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
+        """
+
+        def get_event_loop(self) -> asyncio.AbstractEventLoop:
+            try:
+                return super().get_event_loop()
+            except (RuntimeError, AssertionError):
+                # This was an AssertionError in python 3.4.2 (which ships with debian jessie)
+                # and changed to a RuntimeError in 3.4.3.
+                # "There is no current event loop in thread %r"
+                loop = self.new_event_loop()
+                self.set_event_loop(loop)
+                return loop
+
+    asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
+
+
 def check_versions():
 def check_versions():
     if shared.cmd_opts.skip_version_check:
     if shared.cmd_opts.skip_version_check:
         return
         return
 
 
-    expected_torch_version = "1.13.1"
+    expected_torch_version = "2.0.0"
 
 
     if version.parse(torch.__version__) < version.parse(expected_torch_version):
     if version.parse(torch.__version__) < version.parse(expected_torch_version):
         errors.print_error_explanation(f"""
         errors.print_error_explanation(f"""
@@ -84,7 +127,7 @@ there are reports of issues with training tab on the latest version.
 Use --skip-version-check commandline argument to disable this check.
 Use --skip-version-check commandline argument to disable this check.
         """.strip())
         """.strip())
 
 
-    expected_xformers_version = "0.0.16rc425"
+    expected_xformers_version = "0.0.17"
     if shared.xformers_available:
     if shared.xformers_available:
         import xformers
         import xformers
 
 
@@ -99,6 +142,8 @@ Use --skip-version-check commandline argument to disable this check.
 
 
 
 
 def initialize():
 def initialize():
+    fix_asyncio_event_loop_policy()
+
     check_versions()
     check_versions()
 
 
     extensions.list_extensions()
     extensions.list_extensions()
@@ -126,9 +171,6 @@ def initialize():
     modules.scripts.load_scripts()
     modules.scripts.load_scripts()
     startup_timer.record("load scripts")
     startup_timer.record("load scripts")
 
 
-    modelloader.load_upscalers()
-    startup_timer.record("load upscalers")
-
     modules.sd_vae.refresh_vae_list()
     modules.sd_vae.refresh_vae_list()
     startup_timer.record("refresh VAE")
     startup_timer.record("refresh VAE")
 
 
@@ -150,6 +192,7 @@ def initialize():
     shared.opts.onchange("sd_vae", wrap_queued_call(lambda: modules.sd_vae.reload_vae_weights()), call=False)
     shared.opts.onchange("sd_vae", wrap_queued_call(lambda: modules.sd_vae.reload_vae_weights()), call=False)
     shared.opts.onchange("sd_vae_as_default", wrap_queued_call(lambda: modules.sd_vae.reload_vae_weights()), call=False)
     shared.opts.onchange("sd_vae_as_default", wrap_queued_call(lambda: modules.sd_vae.reload_vae_weights()), call=False)
     shared.opts.onchange("temp_dir", ui_tempdir.on_tmpdir_changed)
     shared.opts.onchange("temp_dir", ui_tempdir.on_tmpdir_changed)
+    shared.opts.onchange("gradio_theme", shared.reload_gradio_theme)
     startup_timer.record("opts onchange")
     startup_timer.record("opts onchange")
 
 
     shared.reload_hypernetworks()
     shared.reload_hypernetworks()

+ 20 - 4
webui.sh

@@ -23,7 +23,7 @@ fi
 # Install directory without trailing slash
 # Install directory without trailing slash
 if [[ -z "${install_dir}" ]]
 if [[ -z "${install_dir}" ]]
 then
 then
-    install_dir="/home/$(whoami)"
+    install_dir="${HOME}"
 fi
 fi
 
 
 # Name of the subdirectory (defaults to stable-diffusion-webui)
 # Name of the subdirectory (defaults to stable-diffusion-webui)
@@ -113,12 +113,13 @@ case "$gpu_info" in
         printf "Experimental support for Renoir: make sure to have at least 4GB of VRAM and 10GB of RAM or enable cpu mode: --use-cpu all --no-half"
         printf "Experimental support for Renoir: make sure to have at least 4GB of VRAM and 10GB of RAM or enable cpu mode: --use-cpu all --no-half"
         printf "\n%s\n" "${delimiter}"
         printf "\n%s\n" "${delimiter}"
     ;;
     ;;
-    *) 
+    *)
     ;;
     ;;
 esac
 esac
 if echo "$gpu_info" | grep -q "AMD" && [[ -z "${TORCH_COMMAND}" ]]
 if echo "$gpu_info" | grep -q "AMD" && [[ -z "${TORCH_COMMAND}" ]]
 then
 then
-    export TORCH_COMMAND="pip install torch torchvision --extra-index-url https://download.pytorch.org/whl/rocm5.2"
+    # AMD users will still use torch 1.13 because 2.0 does not seem to work.
+    export TORCH_COMMAND="pip install torch==1.13.1+rocm5.2 torchvision==0.14.1+rocm5.2 --index-url https://download.pytorch.org/whl/rocm5.2"
 fi  
 fi  
 
 
 for preq in "${GIT}" "${python_cmd}"
 for preq in "${GIT}" "${python_cmd}"
@@ -172,15 +173,30 @@ else
     exit 1
     exit 1
 fi
 fi
 
 
+# Try using TCMalloc on Linux
+prepare_tcmalloc() {
+    if [[ "${OSTYPE}" == "linux"* ]] && [[ -z "${NO_TCMALLOC}" ]] && [[ -z "${LD_PRELOAD}" ]]; then
+        TCMALLOC="$(ldconfig -p | grep -Po "libtcmalloc.so.\d" | head -n 1)"
+        if [[ ! -z "${TCMALLOC}" ]]; then
+            echo "Using TCMalloc: ${TCMALLOC}"
+            export LD_PRELOAD="${TCMALLOC}"
+        else
+            printf "\e[1m\e[31mCannot locate TCMalloc (improves CPU memory usage)\e[0m\n"
+        fi
+    fi
+}
+
 if [[ ! -z "${ACCELERATE}" ]] && [ ${ACCELERATE}="True" ] && [ -x "$(command -v accelerate)" ]
 if [[ ! -z "${ACCELERATE}" ]] && [ ${ACCELERATE}="True" ] && [ -x "$(command -v accelerate)" ]
 then
 then
     printf "\n%s\n" "${delimiter}"
     printf "\n%s\n" "${delimiter}"
     printf "Accelerating launch.py..."
     printf "Accelerating launch.py..."
     printf "\n%s\n" "${delimiter}"
     printf "\n%s\n" "${delimiter}"
+    prepare_tcmalloc
     exec accelerate launch --num_cpu_threads_per_process=6 "${LAUNCH_SCRIPT}" "$@"
     exec accelerate launch --num_cpu_threads_per_process=6 "${LAUNCH_SCRIPT}" "$@"
 else
 else
     printf "\n%s\n" "${delimiter}"
     printf "\n%s\n" "${delimiter}"
     printf "Launching launch.py..."
     printf "Launching launch.py..."
-    printf "\n%s\n" "${delimiter}"      
+    printf "\n%s\n" "${delimiter}"
+    prepare_tcmalloc
     exec "${python_cmd}" "${LAUNCH_SCRIPT}" "$@"
     exec "${python_cmd}" "${LAUNCH_SCRIPT}" "$@"
 fi
 fi