aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/run_tests.yaml2
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js222
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py9
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/style.css10
-rw-r--r--javascript/extensions.js18
-rw-r--r--javascript/hints.js4
-rw-r--r--modules/api/api.py19
-rw-r--r--modules/call_queue.py3
-rw-r--r--modules/cmd_args.py2
-rw-r--r--modules/codeformer_model.py4
-rw-r--r--modules/extensions.py3
-rw-r--r--modules/generation_parameters_copypaste.py1
-rw-r--r--modules/gfpgan_model.py5
-rw-r--r--modules/images.py2
-rw-r--r--modules/img2img.py5
-rw-r--r--modules/launch_utils.py4
-rw-r--r--modules/modelloader.py3
-rw-r--r--modules/processing.py3
-rw-r--r--modules/scripts.py3
-rw-r--r--modules/sd_models.py10
-rw-r--r--modules/sd_samplers_kdiffusion.py11
-rw-r--r--modules/shared.py4
-rw-r--r--modules/textual_inversion/autocrop.py3
-rw-r--r--modules/txt2img.py6
-rw-r--r--modules/ui.py4
-rw-r--r--modules/ui_extensions.py16
-rw-r--r--webui.py9
27 files changed, 276 insertions, 109 deletions
diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml
index 226cf759..96546011 100644
--- a/.github/workflows/run_tests.yaml
+++ b/.github/workflows/run_tests.yaml
@@ -50,7 +50,7 @@ jobs:
python -m pytest -vv --junitxml=test/results.xml --cov . --cov-report=xml --verify-base-url test
- name: Kill test server
if: always()
- run: curl -vv -XPOST http://127.0.0.1:7860/_stop && sleep 10
+ run: curl -vv -XPOST http://127.0.0.1:7860/sdapi/v1/server-stop && sleep 10
- name: Show coverage
run: |
python -m coverage combine .coverage*
diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
index 4ecb3d36..5ebd2073 100644
--- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
+++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
@@ -4,12 +4,12 @@ onUiLoaded(async() => {
inpaint: "#img2maskimg",
inpaintSketch: "#inpaint_sketch",
rangeGroup: "#img2img_column_size",
- sketch: "#img2img_sketch",
+ sketch: "#img2img_sketch"
};
const tabNameToElementId = {
"Inpaint sketch": elementIDs.inpaintSketch,
"Inpaint": elementIDs.inpaint,
- "Sketch": elementIDs.sketch,
+ "Sketch": elementIDs.sketch
};
// Helper functions
@@ -42,43 +42,110 @@ onUiLoaded(async() => {
}
}
- // Check is hotkey valid
- function isSingleLetter(value) {
+ // Function for defining the "Ctrl", "Shift" and "Alt" keys
+ function isModifierKey(event, key) {
+ switch (key) {
+ case "Ctrl":
+ return event.ctrlKey;
+ case "Shift":
+ return event.shiftKey;
+ case "Alt":
+ return event.altKey;
+ default:
+ return false;
+ }
+ }
+
+ // Check if hotkey is valid
+ function isValidHotkey(value) {
+ const specialKeys = ["Ctrl", "Alt", "Shift", "Disable"];
return (
- typeof value === "string" && value.length === 1 && /[a-z]/i.test(value)
+ (typeof value === "string" &&
+ value.length === 1 &&
+ /[a-z]/i.test(value)) ||
+ specialKeys.includes(value)
);
}
- // Create hotkeyConfig from opts
+ // Normalize hotkey
+ function normalizeHotkey(hotkey) {
+ return hotkey.length === 1 ? "Key" + hotkey.toUpperCase() : hotkey;
+ }
+
+ // Format hotkey for display
+ function formatHotkeyForDisplay(hotkey) {
+ return hotkey.startsWith("Key") ? hotkey.slice(3) : hotkey;
+ }
+
+ // Create hotkey configuration with the provided options
function createHotkeyConfig(defaultHotkeysConfig, hotkeysConfigOpts) {
- const result = {};
- const usedKeys = new Set();
+ const result = {}; // Resulting hotkey configuration
+ const usedKeys = new Set(); // Set of used hotkeys
+ // Iterate through defaultHotkeysConfig keys
for (const key in defaultHotkeysConfig) {
- if (typeof hotkeysConfigOpts[key] === "boolean") {
- result[key] = hotkeysConfigOpts[key];
- continue;
- }
+ const userValue = hotkeysConfigOpts[key]; // User-provided hotkey value
+ const defaultValue = defaultHotkeysConfig[key]; // Default hotkey value
+
+ // Apply appropriate value for undefined, boolean, or object userValue
if (
- hotkeysConfigOpts[key] &&
- isSingleLetter(hotkeysConfigOpts[key]) &&
- !usedKeys.has(hotkeysConfigOpts[key].toUpperCase())
+ userValue === undefined ||
+ typeof userValue === "boolean" ||
+ typeof userValue === "object" ||
+ userValue === "disable"
) {
- // If the property passed the test and has not yet been used, add 'Key' before it and save it
- result[key] = "Key" + hotkeysConfigOpts[key].toUpperCase();
- usedKeys.add(hotkeysConfigOpts[key].toUpperCase());
+ result[key] =
+ userValue === undefined ? defaultValue : userValue;
+ } else if (isValidHotkey(userValue)) {
+ const normalizedUserValue = normalizeHotkey(userValue);
+
+ // Check for conflicting hotkeys
+ if (!usedKeys.has(normalizedUserValue)) {
+ usedKeys.add(normalizedUserValue);
+ result[key] = normalizedUserValue;
+ } else {
+ console.error(
+ `Hotkey: ${formatHotkeyForDisplay(
+ userValue
+ )} for ${key} is repeated and conflicts with another hotkey. The default hotkey is used: ${formatHotkeyForDisplay(
+ defaultValue
+ )}`
+ );
+ result[key] = defaultValue;
+ }
} else {
- // If the property does not pass the test or has already been used, we keep the default value
console.error(
- `Hotkey: ${hotkeysConfigOpts[key]} for ${key} is repeated and conflicts with another hotkey or is not 1 letter. The default hotkey is used: ${defaultHotkeysConfig[key][3]}`
+ `Hotkey: ${formatHotkeyForDisplay(
+ userValue
+ )} for ${key} is not valid. The default hotkey is used: ${formatHotkeyForDisplay(
+ defaultValue
+ )}`
);
- result[key] = defaultHotkeysConfig[key];
+ result[key] = defaultValue;
}
}
return result;
}
+ // Disables functions in the config object based on the provided list of function names
+ function disableFunctions(config, disabledFunctions) {
+ // Bind the hasOwnProperty method to the functionMap object to avoid errors
+ const hasOwnProperty =
+ Object.prototype.hasOwnProperty.bind(functionMap);
+
+ // Loop through the disabledFunctions array and disable the corresponding functions in the config object
+ disabledFunctions.forEach(funcName => {
+ if (hasOwnProperty(funcName)) {
+ const key = functionMap[funcName];
+ config[key] = "disable";
+ }
+ });
+
+ // Return the updated config object
+ return config;
+ }
+
/**
* The restoreImgRedMask function displays a red mask around an image to indicate the aspect ratio.
* If the image display property is set to 'none', the mask breaks. To fix this, the function
@@ -100,7 +167,9 @@ onUiLoaded(async() => {
imageARPreview.style.transform = "";
if (parseFloat(mainTab.style.width) > 865) {
const transformString = mainTab.style.transform;
- const scaleMatch = transformString.match(/scale\(([-+]?[0-9]*\.?[0-9]+)\)/);
+ const scaleMatch = transformString.match(
+ /scale\(([-+]?[0-9]*\.?[0-9]+)\)/
+ );
let zoom = 1; // default zoom
if (scaleMatch && scaleMatch[1]) {
@@ -124,31 +193,52 @@ onUiLoaded(async() => {
// Default config
const defaultHotkeysConfig = {
+ canvas_hotkey_zoom: "Alt",
+ canvas_hotkey_adjust: "Ctrl",
canvas_hotkey_reset: "KeyR",
canvas_hotkey_fullscreen: "KeyS",
canvas_hotkey_move: "KeyF",
canvas_hotkey_overlap: "KeyO",
- canvas_show_tooltip: true,
- canvas_swap_controls: false
+ canvas_disabled_functions: [],
+ canvas_show_tooltip: true
};
- // swap the actions for ctr + wheel and shift + wheel
- const hotkeysConfig = createHotkeyConfig(
+
+ const functionMap = {
+ "Zoom": "canvas_hotkey_zoom",
+ "Adjust brush size": "canvas_hotkey_adjust",
+ "Moving canvas": "canvas_hotkey_move",
+ "Fullscreen": "canvas_hotkey_fullscreen",
+ "Reset Zoom": "canvas_hotkey_reset",
+ "Overlap": "canvas_hotkey_overlap"
+ };
+
+ // Loading the configuration from opts
+ const preHotkeysConfig = createHotkeyConfig(
defaultHotkeysConfig,
hotkeysConfigOpts
);
+ // Disable functions that are not needed by the user
+ const hotkeysConfig = disableFunctions(
+ preHotkeysConfig,
+ preHotkeysConfig.canvas_disabled_functions
+ );
+
let isMoving = false;
let mouseX, mouseY;
let activeElement;
- const elements = Object.fromEntries(Object.keys(elementIDs).map((id) => [
- id,
- gradioApp().querySelector(elementIDs[id]),
- ]));
+ const elements = Object.fromEntries(
+ Object.keys(elementIDs).map(id => [
+ id,
+ gradioApp().querySelector(elementIDs[id])
+ ])
+ );
const elemData = {};
// Apply functionality to the range inputs. Restore redmask and correct for long images.
- const rangeInputs = elements.rangeGroup ? Array.from(elements.rangeGroup.querySelectorAll("input")) :
+ const rangeInputs = elements.rangeGroup ?
+ Array.from(elements.rangeGroup.querySelectorAll("input")) :
[
gradioApp().querySelector("#img2img_width input[type='range']"),
gradioApp().querySelector("#img2img_height input[type='range']")
@@ -180,38 +270,56 @@ onUiLoaded(async() => {
const toolTipElemnt =
targetElement.querySelector(".image-container");
const tooltip = document.createElement("div");
- tooltip.className = "tooltip";
+ tooltip.className = "canvas-tooltip";
// Creating an item of information
const info = document.createElement("i");
- info.className = "tooltip-info";
+ info.className = "canvas-tooltip-info";
info.textContent = "";
// Create a container for the contents of the tooltip
const tooltipContent = document.createElement("div");
- tooltipContent.className = "tooltip-content";
-
- // Add info about hotkeys
- const zoomKey = hotkeysConfig.canvas_swap_controls ? "Ctrl" : "Shift";
- const adjustKey = hotkeysConfig.canvas_swap_controls ? "Shift" : "Ctrl";
+ tooltipContent.className = "canvas-tooltip-content";
- const hotkeys = [
- {key: `${zoomKey} + wheel`, action: "Zoom canvas"},
- {key: `${adjustKey} + wheel`, action: "Adjust brush size"},
+ // Define an array with hotkey information and their actions
+ const hotkeysInfo = [
{
- key: hotkeysConfig.canvas_hotkey_reset.charAt(hotkeysConfig.canvas_hotkey_reset.length - 1),
- action: "Reset zoom"
+ configKey: "canvas_hotkey_zoom",
+ action: "Zoom canvas",
+ keySuffix: " + wheel"
},
{
- key: hotkeysConfig.canvas_hotkey_fullscreen.charAt(hotkeysConfig.canvas_hotkey_fullscreen.length - 1),
- action: "Fullscreen mode"
+ configKey: "canvas_hotkey_adjust",
+ action: "Adjust brush size",
+ keySuffix: " + wheel"
},
+ {configKey: "canvas_hotkey_reset", action: "Reset zoom"},
{
- key: hotkeysConfig.canvas_hotkey_move.charAt(hotkeysConfig.canvas_hotkey_move.length - 1),
- action: "Move canvas"
- }
+ configKey: "canvas_hotkey_fullscreen",
+ action: "Fullscreen mode"
+ },
+ {configKey: "canvas_hotkey_move", action: "Move canvas"},
+ {configKey: "canvas_hotkey_overlap", action: "Overlap"}
];
+
+ // Create hotkeys array with disabled property based on the config values
+ const hotkeys = hotkeysInfo.map(info => {
+ const configValue = hotkeysConfig[info.configKey];
+ const key = info.keySuffix ?
+ `${configValue}${info.keySuffix}` :
+ configValue.charAt(configValue.length - 1);
+ return {
+ key,
+ action: info.action,
+ disabled: configValue === "disable"
+ };
+ });
+
for (const hotkey of hotkeys) {
+ if (hotkey.disabled) {
+ continue;
+ }
+
const p = document.createElement("p");
p.innerHTML = `<b>${hotkey.key}</b> - ${hotkey.action}`;
tooltipContent.appendChild(p);
@@ -346,10 +454,7 @@ onUiLoaded(async() => {
// Change the zoom level based on user interaction
function changeZoomLevel(operation, e) {
- if (
- (!hotkeysConfig.canvas_swap_controls && e.shiftKey) ||
- (hotkeysConfig.canvas_swap_controls && e.ctrlKey)
- ) {
+ if (isModifierKey(e, hotkeysConfig.canvas_hotkey_zoom)) {
e.preventDefault();
let zoomPosX, zoomPosY;
@@ -514,6 +619,13 @@ onUiLoaded(async() => {
event.preventDefault();
action(event);
}
+
+ if (
+ isModifierKey(event, hotkeysConfig.canvas_hotkey_zoom) ||
+ isModifierKey(event, hotkeysConfig.canvas_hotkey_adjust)
+ ) {
+ event.preventDefault();
+ }
}
// Get Mouse position
@@ -564,11 +676,7 @@ onUiLoaded(async() => {
changeZoomLevel(operation, e);
// Handle brush size adjustment with ctrl key pressed
- if (
- (hotkeysConfig.canvas_swap_controls && e.shiftKey) ||
- (!hotkeysConfig.canvas_swap_controls &&
- (e.ctrlKey || e.metaKey))
- ) {
+ if (isModifierKey(e, hotkeysConfig.canvas_hotkey_adjust)) {
e.preventDefault();
// Increase or decrease brush size based on scroll direction
diff --git a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
index d83e14da..1b6683aa 100644
--- a/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
+++ b/extensions-builtin/canvas-zoom-and-pan/scripts/hotkey_config.py
@@ -1,10 +1,13 @@
+import gradio as gr
from modules import shared
shared.options_templates.update(shared.options_section(('canvas_hotkey', "Canvas Hotkeys"), {
- "canvas_hotkey_move": shared.OptionInfo("F", "Moving the canvas"),
+ "canvas_hotkey_zoom": shared.OptionInfo("Alt", "Zoom canvas", gr.Radio, {"choices": ["Shift","Ctrl", "Alt"]}).info("If you choose 'Shift' you cannot scroll horizontally, 'Alt' can cause a little trouble in firefox"),
+ "canvas_hotkey_adjust": shared.OptionInfo("Ctrl", "Adjust brush size", gr.Radio, {"choices": ["Shift","Ctrl", "Alt"]}).info("If you choose 'Shift' you cannot scroll horizontally, 'Alt' can cause a little trouble in firefox"),
+ "canvas_hotkey_move": shared.OptionInfo("F", "Moving the canvas").info("To work correctly in firefox, turn off 'Automatically search the page text when typing' in the browser settings"),
"canvas_hotkey_fullscreen": shared.OptionInfo("S", "Fullscreen Mode, maximizes the picture so that it fits into the screen and stretches it to its full width "),
"canvas_hotkey_reset": shared.OptionInfo("R", "Reset zoom and canvas positon"),
- "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap ( Technical button, neededs for testing )"),
+ "canvas_hotkey_overlap": shared.OptionInfo("O", "Toggle overlap").info("Technical button, neededs for testing"),
"canvas_show_tooltip": shared.OptionInfo(True, "Enable tooltip on the canvas"),
- "canvas_swap_controls": shared.OptionInfo(False, "Swap hotkey combinations for Zoom and Adjust brush resize"),
+ "canvas_disabled_functions": shared.OptionInfo(["Overlap"], "Disable function that you don't use", gr.CheckboxGroup, {"choices": ["Zoom","Adjust brush size", "Moving canvas","Fullscreen","Reset Zoom","Overlap"]}),
}))
diff --git a/extensions-builtin/canvas-zoom-and-pan/style.css b/extensions-builtin/canvas-zoom-and-pan/style.css
index 5b131d50..6bcc9570 100644
--- a/extensions-builtin/canvas-zoom-and-pan/style.css
+++ b/extensions-builtin/canvas-zoom-and-pan/style.css
@@ -1,4 +1,4 @@
-.tooltip-info {
+.canvas-tooltip-info {
position: absolute;
top: 10px;
left: 10px;
@@ -15,7 +15,7 @@
z-index: 100;
}
-.tooltip-info::after {
+.canvas-tooltip-info::after {
content: '';
display: block;
width: 2px;
@@ -24,7 +24,7 @@
margin-top: 2px;
}
-.tooltip-info::before {
+.canvas-tooltip-info::before {
content: '';
display: block;
width: 2px;
@@ -32,7 +32,7 @@
background-color: white;
}
-.tooltip-content {
+.canvas-tooltip-content {
display: none;
background-color: #f9f9f9;
color: #333;
@@ -50,7 +50,7 @@
z-index: 100;
}
-.tooltip:hover .tooltip-content {
+.canvas-tooltip:hover .canvas-tooltip-content {
display: block;
animation: fadeIn 0.5s;
opacity: 1;
diff --git a/javascript/extensions.js b/javascript/extensions.js
index efeaf3a5..1f7254c5 100644
--- a/javascript/extensions.js
+++ b/javascript/extensions.js
@@ -72,3 +72,21 @@ function config_state_confirm_restore(_, config_state_name, config_restore_type)
}
return [confirmed, config_state_name, config_restore_type];
}
+
+function toggle_all_extensions(event) {
+ gradioApp().querySelectorAll('#extensions .extension_toggle').forEach(function(checkbox_el) {
+ checkbox_el.checked = event.target.checked;
+ });
+}
+
+function toggle_extension() {
+ let all_extensions_toggled = true;
+ for (const checkbox_el of gradioApp().querySelectorAll('#extensions .extension_toggle')) {
+ if (!checkbox_el.checked) {
+ all_extensions_toggled = false;
+ break;
+ }
+ }
+
+ gradioApp().querySelector('#extensions .all_extensions_toggle').checked = all_extensions_toggled;
+}
diff --git a/javascript/hints.js b/javascript/hints.js
index 05ae5f22..dc75ce31 100644
--- a/javascript/hints.js
+++ b/javascript/hints.js
@@ -15,7 +15,7 @@ var titles = {
"CFG Scale": "Classifier Free Guidance Scale - how strongly the image should conform to prompt - lower values produce more creative results",
"Seed": "A value that determines the output of random number generator - if you create an image with same parameters and seed as another image, you'll get the same result",
"\u{1f3b2}\ufe0f": "Set seed to -1, which will cause a new random number to be used every time",
- "\u267b\ufe0f": "Reuse seed from last generation, mostly useful if it was randomed",
+ "\u267b\ufe0f": "Reuse seed from last generation, mostly useful if it was randomized",
"\u2199\ufe0f": "Read generation parameters from prompt or last generation if prompt is empty into user interface.",
"\u{1f4c2}": "Open images output directory",
"\u{1f4be}": "Save style",
@@ -112,7 +112,7 @@ var titles = {
"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.",
"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 listed.",
"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."
};
diff --git a/modules/api/api.py b/modules/api/api.py
index 2e49526e..80d45ca7 100644
--- a/modules/api/api.py
+++ b/modules/api/api.py
@@ -14,7 +14,7 @@ from fastapi.encoders import jsonable_encoder
from secrets import compare_digest
import modules.shared as shared
-from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors
+from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing, errors, restart
from modules.api import models
from modules.shared import opts
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
@@ -209,6 +209,11 @@ class Api:
self.add_api_route("/sdapi/v1/scripts", self.get_scripts_list, methods=["GET"], response_model=models.ScriptsList)
self.add_api_route("/sdapi/v1/script-info", self.get_script_info, methods=["GET"], response_model=List[models.ScriptInfo])
+ if shared.cmd_opts.add_stop_route:
+ self.add_api_route("/sdapi/v1/server-kill", self.kill_webui, methods=["POST"])
+ self.add_api_route("/sdapi/v1/server-restart", self.restart_webui, methods=["POST"])
+ self.add_api_route("/sdapi/v1/server-stop", self.stop_webui, methods=["POST"])
+
self.default_script_arg_txt2img = []
self.default_script_arg_img2img = []
@@ -715,3 +720,15 @@ class Api:
def launch(self, server_name, port):
self.app.include_router(self.router)
uvicorn.run(self.app, host=server_name, port=port, timeout_keep_alive=0)
+
+ def kill_webui(self):
+ restart.stop_program()
+
+ def restart_webui(self):
+ if restart.is_restartable():
+ restart.restart_program()
+ return Response(status_code=501)
+
+ def stop_webui(request):
+ shared.state.server_command = "stop"
+ return Response("Stopping.")
diff --git a/modules/call_queue.py b/modules/call_queue.py
index 1b5e5273..69bf63d2 100644
--- a/modules/call_queue.py
+++ b/modules/call_queue.py
@@ -1,3 +1,4 @@
+from functools import wraps
import html
import threading
import time
@@ -18,6 +19,7 @@ def wrap_queued_call(func):
def wrap_gradio_gpu_call(func, extra_outputs=None):
+ @wraps(func)
def f(*args, **kwargs):
# if the first argument is a string that says "task(...)", it is treated as a job id
@@ -45,6 +47,7 @@ def wrap_gradio_gpu_call(func, extra_outputs=None):
def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
+ @wraps(func)
def f(*args, extra_outputs_array=extra_outputs, **kwargs):
run_memmon = shared.opts.memmon_poll_rate > 0 and not shared.mem_mon.disabled and add_stats
if run_memmon:
diff --git a/modules/cmd_args.py b/modules/cmd_args.py
index de905caa..624dcb4f 100644
--- a/modules/cmd_args.py
+++ b/modules/cmd_args.py
@@ -106,4 +106,4 @@ parser.add_argument("--skip-version-check", action='store_true', help="Do not ch
parser.add_argument("--no-hashing", action='store_true', help="disable sha256 hashing of checkpoints to help loading performance", default=False)
parser.add_argument("--no-download-sd-model", action='store_true', help="don't download SD1.5 model even if no model is found in --ckpt-dir", default=False)
parser.add_argument('--subpath', type=str, help='customize the subpath for gradio, use with reverse proxy')
-parser.add_argument('--add-stop-route', action='store_true', help='add /_stop route to stop server')
+parser.add_argument('--add-stop-route', action='store_true', help='enable server stop/restart/kill via api')
diff --git a/modules/codeformer_model.py b/modules/codeformer_model.py
index 4260b016..d974e4b8 100644
--- a/modules/codeformer_model.py
+++ b/modules/codeformer_model.py
@@ -20,9 +20,7 @@ codeformer = None
def setup_model(dirname):
- global model_path
- if not os.path.exists(model_path):
- os.makedirs(model_path)
+ os.makedirs(model_path, exist_ok=True)
path = modules.paths.paths.get("CodeFormer", None)
if path is None:
diff --git a/modules/extensions.py b/modules/extensions.py
index 8608584b..abc6e2b1 100644
--- a/modules/extensions.py
+++ b/modules/extensions.py
@@ -7,8 +7,7 @@ from modules.paths_internal import extensions_dir, extensions_builtin_dir, scrip
extensions = []
-if not os.path.exists(extensions_dir):
- os.makedirs(extensions_dir)
+os.makedirs(extensions_dir, exist_ok=True)
def active():
diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py
index a638f912..dd30a1b5 100644
--- a/modules/generation_parameters_copypaste.py
+++ b/modules/generation_parameters_copypaste.py
@@ -357,6 +357,7 @@ infotext_to_setting_name_mapping = [
('Token merging ratio hr', 'token_merging_ratio_hr'),
('RNG', 'randn_source'),
('NGMS', 's_min_uncond'),
+ ('Pad conds', 'pad_cond_uncond'),
]
diff --git a/modules/gfpgan_model.py b/modules/gfpgan_model.py
index e239a09d..6ecd295c 100644
--- a/modules/gfpgan_model.py
+++ b/modules/gfpgan_model.py
@@ -70,11 +70,8 @@ gfpgan_constructor = None
def setup_model(dirname):
- global model_path
- if not os.path.exists(model_path):
- os.makedirs(model_path)
-
try:
+ os.makedirs(model_path, exist_ok=True)
from gfpgan import GFPGANer
from facexlib import detection, parsing # noqa: F401
global user_path
diff --git a/modules/images.py b/modules/images.py
index 7bbfc3e0..1906e2ab 100644
--- a/modules/images.py
+++ b/modules/images.py
@@ -372,8 +372,8 @@ class FilenameGenerator:
'hasprompt': lambda self, *args: self.hasprompt(*args), # accepts formats:[hasprompt<prompt1|default><prompt2>..]
'clip_skip': lambda self: opts.data["CLIP_stop_at_last_layers"],
'denoising': lambda self: self.p.denoising_strength if self.p and self.p.denoising_strength else NOTHING_AND_SKIP_PREVIOUS_TEXT,
+ 'user': lambda self: self.p.user,
'vae_filename': lambda self: self.get_vae_filename(),
-
}
default_time_format = '%Y%m%d%H%M%S'
diff --git a/modules/img2img.py b/modules/img2img.py
index 2c497020..b07d7f2f 100644
--- a/modules/img2img.py
+++ b/modules/img2img.py
@@ -3,6 +3,7 @@ from pathlib import Path
import numpy as np
from PIL import Image, ImageOps, ImageFilter, ImageEnhance, ImageChops, UnidentifiedImageError
+import gradio as gr
from modules import sd_samplers
from modules.generation_parameters_copypaste import create_override_settings_dict
@@ -97,7 +98,7 @@ def process_batch(p, input_dir, output_dir, inpaint_mask_dir, args, to_scale=Fal
processed_image.save(os.path.join(output_dir, filename))
-def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, steps: int, sampler_index: int, mask_blur: int, mask_alpha: float, inpainting_fill: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, denoising_strength: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, *args):
+def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_styles, init_img, sketch, init_img_with_mask, inpaint_color_sketch, inpaint_color_sketch_orig, init_img_inpaint, init_mask_inpaint, steps: int, sampler_index: int, mask_blur: int, mask_alpha: float, inpainting_fill: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, image_cfg_scale: float, denoising_strength: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, selected_scale_tab: int, height: int, width: int, scale_by: float, resize_mode: int, inpaint_full_res: bool, inpaint_full_res_padding: int, inpainting_mask_invert: int, img2img_batch_input_dir: str, img2img_batch_output_dir: str, img2img_batch_inpaint_mask_dir: str, override_settings_texts, request: gr.Request, *args):
override_settings = create_override_settings_dict(override_settings_texts)
is_batch = mode == 5
@@ -180,6 +181,8 @@ def img2img(id_task: str, mode: int, prompt: str, negative_prompt: str, prompt_s
p.scripts = modules.scripts.scripts_img2img
p.script_args = args
+ p.user = request.username
+
if shared.cmd_opts.enable_console_prompts:
print(f"\nimg2img: {prompt}", file=shared.progress_print_out)
diff --git a/modules/launch_utils.py b/modules/launch_utils.py
index 609a181e..97539e68 100644
--- a/modules/launch_utils.py
+++ b/modules/launch_utils.py
@@ -147,10 +147,10 @@ def git_clone(url, dir, name, commithash=None):
return
run(f'"{git}" -C "{dir}" fetch', f"Fetching updates for {name}...", f"Couldn't fetch {name}")
- run(f'"{git}" -C "{dir}" checkout {commithash}', f"Checking out commit for {name} with hash: {commithash}...", f"Couldn't checkout commit {commithash} for {name}")
+ run(f'"{git}" -C "{dir}" checkout {commithash}', f"Checking out commit for {name} with hash: {commithash}...", f"Couldn't checkout commit {commithash} for {name}", live=True)
return
- run(f'"{git}" clone "{url}" "{dir}"', f"Cloning {name} into {dir}...", f"Couldn't clone {name}")
+ run(f'"{git}" clone "{url}" "{dir}"', f"Cloning {name} into {dir}...", f"Couldn't clone {name}", live=True)
if commithash is not None:
run(f'"{git}" -C "{dir}" checkout {commithash}', None, "Couldn't checkout {name}'s hash: {commithash}")
diff --git a/modules/modelloader.py b/modules/modelloader.py
index be23071a..75f01247 100644
--- a/modules/modelloader.py
+++ b/modules/modelloader.py
@@ -95,8 +95,7 @@ def cleanup_models():
def move_files(src_path: str, dest_path: str, ext_filter: str = None):
try:
- if not os.path.exists(dest_path):
- os.makedirs(dest_path)
+ os.makedirs(dest_path, exist_ok=True)
if os.path.exists(src_path):
for file in os.listdir(src_path):
fullpath = os.path.join(src_path, file)
diff --git a/modules/processing.py b/modules/processing.py
index 8da73884..d7df5db0 100644
--- a/modules/processing.py
+++ b/modules/processing.py
@@ -184,6 +184,8 @@ class StableDiffusionProcessing:
self.uc = None
self.c = None
+ self.user = None
+
@property
def sd_model(self):
return shared.sd_model
@@ -585,6 +587,7 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter
"NGMS": None if p.s_min_uncond == 0 else p.s_min_uncond,
**p.extra_generation_params,
"Version": program_version() if opts.add_version_to_infotext else None,
+ "User": p.user if opts.add_user_name_to_info else None,
}
generation_params_text = ", ".join([k if k == v else f'{k}: {generation_parameters_copypaste.quote(v)}' for k, v in generation_params.items() if v is not None])
diff --git a/modules/scripts.py b/modules/scripts.py
index 99bf836a..49e4a611 100644
--- a/modules/scripts.py
+++ b/modules/scripts.py
@@ -1,6 +1,7 @@
import os
import re
import sys
+import inspect
from collections import namedtuple
import gradio as gr
@@ -249,7 +250,7 @@ def load_scripts():
def register_scripts_from_module(module):
for script_class in module.__dict__.values():
- if type(script_class) != type:
+ if not inspect.isclass(script_class):
continue
if issubclass(script_class, Script):
diff --git a/modules/sd_models.py b/modules/sd_models.py
index 918f6fd6..f65f4e36 100644
--- a/modules/sd_models.py
+++ b/modules/sd_models.py
@@ -95,8 +95,7 @@ except Exception:
def setup_model():
- if not os.path.exists(model_path):
- os.makedirs(model_path)
+ os.makedirs(model_path, exist_ok=True)
enable_midas_autodownload()
@@ -248,7 +247,12 @@ def read_state_dict(checkpoint_file, print_global_state=False, map_location=None
_, extension = os.path.splitext(checkpoint_file)
if extension.lower() == ".safetensors":
device = map_location or shared.weight_load_location or devices.get_optimal_device_name()
- pl_sd = safetensors.torch.load_file(checkpoint_file, device=device)
+
+ if not shared.opts.disable_mmap_load_safetensors:
+ pl_sd = safetensors.torch.load_file(checkpoint_file, device=device)
+ else:
+ pl_sd = safetensors.torch.load(open(checkpoint_file, 'rb').read())
+ pl_sd = {k: v.to(device) for k, v in pl_sd.items()}
else:
pl_sd = torch.load(checkpoint_file, map_location=map_location or shared.weight_load_location)
diff --git a/modules/sd_samplers_kdiffusion.py b/modules/sd_samplers_kdiffusion.py
index f8a0c7ba..71581b76 100644
--- a/modules/sd_samplers_kdiffusion.py
+++ b/modules/sd_samplers_kdiffusion.py
@@ -69,6 +69,7 @@ class CFGDenoiser(torch.nn.Module):
self.init_latent = None
self.step = 0
self.image_cfg_scale = None
+ self.padded_cond_uncond = False
def combine_denoised(self, x_out, conds_list, uncond, cond_scale):
denoised_uncond = x_out[-uncond.shape[0]:]
@@ -133,15 +134,17 @@ class CFGDenoiser(torch.nn.Module):
x_in = x_in[:-batch_size]
sigma_in = sigma_in[:-batch_size]
- # TODO add infotext entry
+ self.padded_cond_uncond = False
if shared.opts.pad_cond_uncond and tensor.shape[1] != uncond.shape[1]:
empty = shared.sd_model.cond_stage_model_empty_prompt
num_repeats = (tensor.shape[1] - uncond.shape[1]) // empty.shape[1]
if num_repeats < 0:
tensor = torch.cat([tensor, empty.repeat((tensor.shape[0], -num_repeats, 1))], axis=1)
+ self.padded_cond_uncond = True
elif num_repeats > 0:
uncond = torch.cat([uncond, empty.repeat((uncond.shape[0], num_repeats, 1))], axis=1)
+ self.padded_cond_uncond = True
if tensor.shape[1] == uncond.shape[1] or skip_uncond:
if is_edit_model:
@@ -405,6 +408,9 @@ class KDiffusionSampler:
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))
+ if self.model_wrap_cfg.padded_cond_uncond:
+ p.extra_generation_params["Pad conds"] = True
+
return samples
def sample(self, p, x, conditioning, unconditional_conditioning, steps=None, image_conditioning=None):
@@ -438,5 +444,8 @@ class KDiffusionSampler:
's_min_uncond': self.s_min_uncond
}, disable=False, callback=self.callback_state, **extra_params_kwargs))
+ if self.model_wrap_cfg.padded_cond_uncond:
+ p.extra_generation_params["Pad conds"] = True
+
return samples
diff --git a/modules/shared.py b/modules/shared.py
index 91c31d55..203ee1b9 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -376,6 +376,7 @@ options_templates.update(options_section(('system', "System"), {
"multiple_tqdm": OptionInfo(True, "Add a second progress bar to the console that shows progress for an entire job."),
"print_hypernet_extra": OptionInfo(False, "Print extra hypernetwork information to console."),
"list_hidden_files": OptionInfo(True, "Load models/files in hidden directories").info("directory is hidden if its name starts with \".\""),
+ "disable_mmap_load_safetensors": OptionInfo(False, "Disable memmapping for loading .safetensors files.").info("fixes very slow loading speed in some cases"),
}))
options_templates.update(options_section(('training', "Training"), {
@@ -409,7 +410,7 @@ options_templates.update(options_section(('sd', "Stable Diffusion"), {
"enable_emphasis": OptionInfo(True, "Enable emphasis").info("use (text) to make model pay more attention to text and [text] to make it pay less attention"),
"enable_batch_seeds": OptionInfo(True, "Make K-diffusion samplers produce same images in a batch as when making a single image"),
"comma_padding_backtrack": OptionInfo(20, "Prompt word wrap length limit", gr.Slider, {"minimum": 0, "maximum": 74, "step": 1}).info("in tokens - for texts shorter than specified, if they don't fit into 75 token limit, move them to the next 75 token chunk"),
- "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}).link("wiki", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#clip-skip").info("ignore last layers of CLIP nrtwork; 1 ignores none, 2 ignores one layer"),
+ "CLIP_stop_at_last_layers": OptionInfo(1, "Clip skip", gr.Slider, {"minimum": 1, "maximum": 12, "step": 1}).link("wiki", "https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Features#clip-skip").info("ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer"),
"upcast_attn": OptionInfo(False, "Upcast cross attention layer to float32"),
"randn_source": OptionInfo("GPU", "Random number generator source.", gr.Radio, {"choices": ["GPU", "CPU"]}).info("changes seeds drastically; use CPU to produce the same picture across different videocard vendors"),
}))
@@ -493,6 +494,7 @@ options_templates.update(options_section(('ui', "User interface"), {
options_templates.update(options_section(('infotext', "Infotext"), {
"add_model_hash_to_info": OptionInfo(True, "Add model hash to generation information"),
"add_model_name_to_info": OptionInfo(True, "Add model name to generation information"),
+ "add_user_name_to_info": OptionInfo(False, "Add user name to generation information when authenticated"),
"add_version_to_infotext": OptionInfo(True, "Add program version to generation information"),
"disable_weights_auto_swap": OptionInfo(True, "Disregard checkpoint information from pasted infotext").info("when reading generation parameters from text into UI"),
"infotext_styles": OptionInfo("Apply if any", "Infer styles from prompts of pasted infotext", gr.Radio, {"choices": ["Ignore", "Apply", "Discard", "Apply if any"]}).info("when reading generation parameters from text into UI)").html("""<ul style='margin-left: 1.5em'>
diff --git a/modules/textual_inversion/autocrop.py b/modules/textual_inversion/autocrop.py
index 75705459..1675e39a 100644
--- a/modules/textual_inversion/autocrop.py
+++ b/modules/textual_inversion/autocrop.py
@@ -298,8 +298,7 @@ def download_and_cache_models(dirname):
download_url = 'https://github.com/opencv/opencv_zoo/blob/91fb0290f50896f38a0ab1e558b74b16bc009428/models/face_detection_yunet/face_detection_yunet_2022mar.onnx?raw=true'
model_file_name = 'face_detection_yunet.onnx'
- if not os.path.exists(dirname):
- os.makedirs(dirname)
+ os.makedirs(dirname, exist_ok=True)
cache_file = os.path.join(dirname, model_file_name)
if not os.path.exists(cache_file):
diff --git a/modules/txt2img.py b/modules/txt2img.py
index 2e7d202d..6aa79f23 100644
--- a/modules/txt2img.py
+++ b/modules/txt2img.py
@@ -4,10 +4,10 @@ from modules.generation_parameters_copypaste import create_override_settings_dic
from modules.shared import opts, cmd_opts
import modules.shared as shared
from modules.ui import plaintext_to_html
+import gradio as gr
-
-def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, hr_sampler_index: int, hr_prompt: str, hr_negative_prompt, override_settings_texts, *args):
+def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, steps: int, sampler_index: int, restore_faces: bool, tiling: bool, n_iter: int, batch_size: int, cfg_scale: float, seed: int, subseed: int, subseed_strength: float, seed_resize_from_h: int, seed_resize_from_w: int, seed_enable_extras: bool, height: int, width: int, enable_hr: bool, denoising_strength: float, hr_scale: float, hr_upscaler: str, hr_second_pass_steps: int, hr_resize_x: int, hr_resize_y: int, hr_sampler_index: int, hr_prompt: str, hr_negative_prompt, override_settings_texts, request: gr.Request, *args):
override_settings = create_override_settings_dict(override_settings_texts)
p = processing.StableDiffusionProcessingTxt2Img(
@@ -48,6 +48,8 @@ def txt2img(id_task: str, prompt: str, negative_prompt: str, prompt_styles, step
p.scripts = modules.scripts.scripts_txt2img
p.script_args = args
+ p.user = request.username
+
if cmd_opts.enable_console_prompts:
print(f"\ntxt2img: {prompt}", file=shared.progress_print_out)
diff --git a/modules/ui.py b/modules/ui.py
index e2e3b6da..e3c8e43a 100644
--- a/modules/ui.py
+++ b/modules/ui.py
@@ -773,7 +773,7 @@ def create_ui():
selected_scale_tab = gr.State(value=0)
with gr.Tabs():
- with gr.Tab(label="Resize to") as tab_scale_to:
+ with gr.Tab(label="Resize to", elem_id="img2img_tab_resize_to") as tab_scale_to:
with FormRow():
with gr.Column(elem_id="img2img_column_size", scale=4):
width = gr.Slider(minimum=64, maximum=2048, step=8, label="Width", value=512, elem_id="img2img_width")
@@ -782,7 +782,7 @@ def create_ui():
res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="img2img_res_switch_btn")
detect_image_size_btn = ToolButton(value=detect_image_size_symbol, elem_id="img2img_detect_image_size_btn")
- with gr.Tab(label="Resize by") as tab_scale_by:
+ with gr.Tab(label="Resize by", elem_id="img2img_tab_resize_by") as tab_scale_by:
scale_by = gr.Slider(minimum=0.05, maximum=4.0, step=0.05, label="Scale", value=1.0, elem_id="img2img_scale")
with FormRow():
diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py
index 4379a641..278bf5e4 100644
--- a/modules/ui_extensions.py
+++ b/modules/ui_extensions.py
@@ -138,7 +138,10 @@ def extension_table():
<table id="extensions">
<thead>
<tr>
- <th><abbr title="Use checkbox to enable the extension; it will be enabled or disabled when you click apply button">Extension</abbr></th>
+ <th>
+ <input class="gr-check-radio gr-checkbox all_extensions_toggle" type="checkbox" {'checked="checked"' if all(ext.enabled for ext in extensions.extensions) else ''} onchange="toggle_all_extensions(event)" />
+ <abbr title="Use checkbox to enable the extension; it will be enabled or disabled when you click apply button">Extension</abbr>
+ </th>
<th>URL</th>
<th>Branch</th>
<th>Version</th>
@@ -170,7 +173,7 @@ def extension_table():
code += f"""
<tr>
- <td><label{style}><input class="gr-check-radio gr-checkbox" name="enable_{html.escape(ext.name)}" type="checkbox" {'checked="checked"' if ext.enabled else ''}>{html.escape(ext.name)}</label></td>
+ <td><label{style}><input class="gr-check-radio gr-checkbox extension_toggle" name="enable_{html.escape(ext.name)}" type="checkbox" {'checked="checked"' if ext.enabled else ''} onchange="toggle_extension(event)" />{html.escape(ext.name)}</label></td>
<td>{remote}</td>
<td>{ext.branch}</td>
<td>{version_link}</td>
@@ -325,6 +328,11 @@ def normalize_git_url(url):
def install_extension_from_url(dirname, url, branch_name=None):
check_access()
+ if isinstance(dirname, str):
+ dirname = dirname.strip()
+ if isinstance(url, str):
+ url = url.strip()
+
assert url, 'No URL specified'
if dirname is None or dirname == "":
@@ -563,9 +571,9 @@ def create_ui():
available_extensions_table = gr.HTML()
refresh_available_extensions_button.click(
- fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update()]),
+ fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update(), gr.update()]),
inputs=[available_extensions_index, hide_tags, sort_column],
- outputs=[available_extensions_index, available_extensions_table, hide_tags, install_result, search_extensions_text],
+ outputs=[available_extensions_index, available_extensions_table, hide_tags, search_extensions_text, install_result],
)
install_extension_button.click(
diff --git a/webui.py b/webui.py
index 136d036d..bad29f28 100644
--- a/webui.py
+++ b/webui.py
@@ -11,7 +11,7 @@ import json
from threading import Thread
from typing import Iterable
-from fastapi import FastAPI, Response
+from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from packaging import version
@@ -362,11 +362,6 @@ def api_only():
api.launch(server_name="0.0.0.0" if cmd_opts.listen else "127.0.0.1", port=cmd_opts.port if cmd_opts.port else 7861)
-def stop_route(request):
- shared.state.server_command = "stop"
- return Response("Stopping.")
-
-
def webui():
launch_api = cmd_opts.api
initialize()
@@ -404,8 +399,6 @@ def webui():
"redoc_url": "/redoc",
},
)
- if cmd_opts.add_stop_route:
- app.add_route("/_stop", stop_route, methods=["POST"])
# after initial launch, disable --autolaunch for subsequent restarts
cmd_opts.autolaunch = False