aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js177
-rw-r--r--modules/errors.py26
-rw-r--r--modules/generation_parameters_copypaste.py8
-rw-r--r--modules/launch_utils.py6
-rw-r--r--modules/sd_hijack_optimizations.py2
-rw-r--r--modules/shared.py22
-rw-r--r--modules/styles.py67
-rw-r--r--modules/sysinfo.py162
-rw-r--r--modules/ui.py17
-rw-r--r--modules/ui_extensions.py2
-rw-r--r--modules/ui_settings.py28
-rw-r--r--modules/ui_tempdir.py2
-rw-r--r--style.css13
-rw-r--r--webui.bat2
-rwxr-xr-xwebui.sh34
15 files changed, 502 insertions, 66 deletions
diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
index 41d9ddf4..2a2ed999 100644
--- a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
+++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
@@ -1,5 +1,3 @@
-// Main
-
// Helper functions
// Get active tab
function getActiveTab(elements, all = false) {
@@ -14,6 +12,17 @@ function getActiveTab(elements, all = false) {
}
}
+// Get tab ID
+function getTabId(elements, elementIDs) {
+ const activeTab = getActiveTab(elements);
+ const tabIdLookup = {
+ "Sketch": elementIDs.sketch,
+ "Inpaint sketch": elementIDs.inpaintSketch,
+ "Inpaint": elementIDs.inpaint
+ };
+ return tabIdLookup[activeTab.innerText];
+}
+
// Wait until opts loaded
async function waitForOpts() {
return new Promise(resolve => {
@@ -63,6 +72,48 @@ function createHotkeyConfig(defaultHotkeysConfig, hotkeysConfigOpts) {
return result;
}
+/**
+ * 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
+ * temporarily sets the display property to 'block' and then hides the mask again after 300 milliseconds
+ * to avoid breaking the canvas. Additionally, the function adjusts the mask to work correctly on
+ * very long images.
+ */
+
+function restoreImgRedMask(elements, elementIDs) {
+ const mainTabId = getTabId(elements, elementIDs);
+
+ if (!mainTabId) return;
+
+ const mainTab = gradioApp().querySelector(mainTabId);
+ const img = mainTab.querySelector("img");
+ const imageARPreview = gradioApp().querySelector("#imageARPreview");
+
+ if (!img || !imageARPreview) return;
+
+ imageARPreview.style.transform = "";
+ if (parseFloat(mainTab.style.width) > 865) {
+ const transformString = mainTab.style.transform;
+ const scaleMatch = transformString.match(/scale\(([-+]?[0-9]*\.?[0-9]+)\)/);
+ let zoom = 1; // default zoom
+
+ if (scaleMatch && scaleMatch[1]) {
+ zoom = Number(scaleMatch[1]);
+ }
+
+ imageARPreview.style.transformOrigin = "0 0";
+ imageARPreview.style.transform = `scale(${zoom})`;
+ }
+
+ if (img.style.display !== "none") return;
+
+ img.style.display = "block";
+
+ setTimeout(() => {
+ img.style.display = "none";
+ }, 400);
+}
+
// Main
onUiLoaded(async() => {
const hotkeysConfigOpts = await waitForOpts();
@@ -84,17 +135,19 @@ onUiLoaded(async() => {
let isMoving = false;
let mouseX, mouseY;
+ let activeElement;
const elementIDs = {
sketch: "#img2img_sketch",
inpaint: "#img2maskimg",
inpaintSketch: "#inpaint_sketch",
- img2imgTabs: "#mode_img2img .tab-nav"
+ img2imgTabs: "#mode_img2img .tab-nav",
+ rangeGroup: "#img2img_column_size"
};
async function getElements() {
const elements = await Promise.all(
- Object.values(elementIDs).map(id => document.querySelector(id))
+ Object.values(elementIDs).map(id => gradioApp().querySelector(id))
);
return Object.fromEntries(
Object.keys(elementIDs).map((key, index) => [key, elements[index]])
@@ -102,10 +155,36 @@ onUiLoaded(async() => {
}
const elements = await getElements();
+ const elemData = {};
+
+ // Apply functionality to the range inputs. Restore redmask and correct for long images.
+ const rangeInputs = elements.rangeGroup ? elements.rangeGroup.querySelectorAll("input") :
+ [
+ gradioApp().querySelector("#img2img_width input[type='range']"),
+ gradioApp().querySelector("#img2img_height input[type='range']")
+ ];
+
+ rangeInputs.forEach(input => {
+ if (input) {
+ input.addEventListener("input", () => restoreImgRedMask(elements, elementIDs));
+ }
+ });
+
+ function applyZoomAndPan(elemId) {
+ const targetElement = gradioApp().querySelector(elemId);
+
+ if (!targetElement) {
+ console.log("Element not found");
+ return;
+ }
- function applyZoomAndPan(targetElement, elemId) {
targetElement.style.transformOrigin = "0 0";
- let [zoomLevel, panX, panY] = [1, 0, 0];
+
+ elemData[elemId] = {
+ zoom: 1,
+ panX: 0,
+ panY: 0
+ };
let fullScreenMode = false;
// Create tooltip
@@ -124,7 +203,7 @@ onUiLoaded(async() => {
const tooltipContent = document.createElement("div");
tooltipContent.className = "tooltip-content";
- // Add info about hotkets
+ // Add info about hotkeys
const zoomKey = hotkeysConfig.canvas_swap_controls ? "Ctrl" : "Shift";
const adjustKey = hotkeysConfig.canvas_swap_controls ? "Shift" : "Ctrl";
@@ -132,21 +211,15 @@ onUiLoaded(async() => {
{key: `${zoomKey} + wheel`, action: "Zoom canvas"},
{key: `${adjustKey} + wheel`, action: "Adjust brush size"},
{
- key: hotkeysConfig.canvas_hotkey_reset.charAt(
- hotkeysConfig.canvas_hotkey_reset.length - 1
- ),
+ key: hotkeysConfig.canvas_hotkey_reset.charAt(hotkeysConfig.canvas_hotkey_reset.length - 1),
action: "Reset zoom"
},
{
- key: hotkeysConfig.canvas_hotkey_fullscreen.charAt(
- hotkeysConfig.canvas_hotkey_fullscreen.length - 1
- ),
+ key: hotkeysConfig.canvas_hotkey_fullscreen.charAt(hotkeysConfig.canvas_hotkey_fullscreen.length - 1),
action: "Fullscreen mode"
},
{
- key: hotkeysConfig.canvas_hotkey_move.charAt(
- hotkeysConfig.canvas_hotkey_move.length - 1
- ),
+ key: hotkeysConfig.canvas_hotkey_move.charAt(hotkeysConfig.canvas_hotkey_move.length - 1),
action: "Move canvas"
}
];
@@ -186,12 +259,14 @@ onUiLoaded(async() => {
// Reset the zoom level and pan position of the target element to their initial values
function resetZoom() {
- zoomLevel = 1;
- panX = 0;
- panY = 0;
+ elemData[elemId] = {
+ zoomLevel: 1,
+ panX: 0,
+ panY: 0
+ };
fixCanvas();
- targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`;
+ targetElement.style.transform = `scale(${elemData[elemId].zoomLevel}) translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px)`;
const canvas = gradioApp().querySelector(
`${elemId} canvas[key="interface"]`
@@ -269,11 +344,14 @@ onUiLoaded(async() => {
// Update the zoom level and pan position of the target element based on the values of the zoomLevel, panX and panY variables
function updateZoom(newZoomLevel, mouseX, mouseY) {
newZoomLevel = Math.max(0.5, Math.min(newZoomLevel, 15));
- panX += mouseX - (mouseX * newZoomLevel) / zoomLevel;
- panY += mouseY - (mouseY * newZoomLevel) / zoomLevel;
+
+ elemData[elemId].panX +=
+ mouseX - (mouseX * newZoomLevel) / elemData[elemId].zoomLevel;
+ elemData[elemId].panY +=
+ mouseY - (mouseY * newZoomLevel) / elemData[elemId].zoomLevel;
targetElement.style.transformOrigin = "0 0";
- targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`;
+ targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${newZoomLevel})`;
toggleOverlap("on");
return newZoomLevel;
@@ -289,9 +367,9 @@ onUiLoaded(async() => {
let zoomPosX, zoomPosY;
let delta = 0.2;
- if (zoomLevel > 7) {
+ if (elemData[elemId].zoomLevel > 7) {
delta = 0.9;
- } else if (zoomLevel > 2) {
+ } else if (elemData[elemId].zoomLevel > 2) {
delta = 0.6;
}
@@ -299,8 +377,9 @@ onUiLoaded(async() => {
zoomPosY = e.clientY;
fullScreenMode = false;
- zoomLevel = updateZoom(
- zoomLevel + (operation === "+" ? delta : -delta),
+ elemData[elemId].zoomLevel = updateZoom(
+ elemData[elemId].zoomLevel +
+ (operation === "+" ? delta : -delta),
zoomPosX - targetElement.getBoundingClientRect().left,
zoomPosY - targetElement.getBoundingClientRect().top
);
@@ -351,9 +430,9 @@ onUiLoaded(async() => {
targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// Update global variables
- zoomLevel = scale;
- panX = offsetX;
- panY = offsetY;
+ elemData[elemId].zoomLevel = scale;
+ elemData[elemId].panX = offsetX;
+ elemData[elemId].panY = offsetY;
fullScreenMode = false;
toggleOverlap("off");
@@ -427,9 +506,9 @@ onUiLoaded(async() => {
targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
// Update global variables
- zoomLevel = scale;
- panX = offsetX;
- panY = offsetY;
+ elemData[elemId].zoomLevel = scale;
+ elemData[elemId].panX = offsetX;
+ elemData[elemId].panY = offsetY;
fullScreenMode = true;
toggleOverlap("on");
@@ -465,6 +544,8 @@ onUiLoaded(async() => {
if (!isKeyDownHandlerAttached) {
document.addEventListener("keydown", handleKeyDown);
isKeyDownHandlerAttached = true;
+
+ activeElement = elemId;
}
}
@@ -472,6 +553,8 @@ onUiLoaded(async() => {
if (isKeyDownHandlerAttached) {
document.removeEventListener("keydown", handleKeyDown);
isKeyDownHandlerAttached = false;
+
+ activeElement = null;
}
}
@@ -528,21 +611,24 @@ onUiLoaded(async() => {
// Detect zoom level and update the pan speed.
function updatePanPosition(movementX, movementY) {
- let panSpeed = 1.5;
+ let panSpeed = 2;
- if (zoomLevel > 8) {
- panSpeed = 2.5;
+ if (elemData[elemId].zoomLevel > 8) {
+ panSpeed = 3.5;
}
- panX = panX + movementX * panSpeed;
- panY = panY + movementY * panSpeed;
+ elemData[elemId].panX += movementX * panSpeed;
+ elemData[elemId].panY += movementY * panSpeed;
- targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`;
- toggleOverlap("on");
+ // Delayed redraw of an element
+ requestAnimationFrame(() => {
+ targetElement.style.transform = `translate(${elemData[elemId].panX}px, ${elemData[elemId].panY}px) scale(${elemData[elemId].zoomLevel})`;
+ toggleOverlap("on");
+ });
}
function handleMoveByKey(e) {
- if (isMoving) {
+ if (isMoving && elemId === activeElement) {
updatePanPosition(e.movementX, e.movementY);
targetElement.style.pointerEvents = "none";
} else {
@@ -558,7 +644,10 @@ onUiLoaded(async() => {
gradioApp().addEventListener("mousemove", handleMoveByKey);
}
- applyZoomAndPan(elements.sketch, elementIDs.sketch);
- applyZoomAndPan(elements.inpaint, elementIDs.inpaint);
- applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch);
+ applyZoomAndPan(elementIDs.sketch);
+ applyZoomAndPan(elementIDs.inpaint);
+ applyZoomAndPan(elementIDs.inpaintSketch);
+
+ // Make the function global so that other extensions can take advantage of this solution
+ window.applyZoomAndPan = applyZoomAndPan;
});
diff --git a/modules/errors.py b/modules/errors.py
index e408f500..5271a9fe 100644
--- a/modules/errors.py
+++ b/modules/errors.py
@@ -3,10 +3,30 @@ import textwrap
import traceback
+exception_records = []
+
+
+def record_exception():
+ _, e, tb = sys.exc_info()
+ if e is None:
+ return
+
+ if exception_records and exception_records[-1] == e:
+ return
+
+ exception_records.append((e, tb))
+
+ if len(exception_records) > 5:
+ exception_records.pop(0)
+
+
def report(message: str, *, exc_info: bool = False) -> None:
"""
Print an error message to stderr, with optional traceback.
"""
+
+ record_exception()
+
for line in message.splitlines():
print("***", line, file=sys.stderr)
if exc_info:
@@ -15,6 +35,8 @@ def report(message: str, *, exc_info: bool = False) -> None:
def print_error_explanation(message):
+ record_exception()
+
lines = message.strip().split("\n")
max_len = max([len(x) for x in lines])
@@ -25,6 +47,8 @@ def print_error_explanation(message):
def display(e: Exception, task, *, full_traceback=False):
+ record_exception()
+
print(f"{task or 'error'}: {type(e).__name__}", file=sys.stderr)
te = traceback.TracebackException.from_exception(e)
if full_traceback:
@@ -44,6 +68,8 @@ already_displayed = {}
def display_once(e: Exception, task):
+ record_exception()
+
if task in already_displayed:
return
diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py
index 071bd9ea..4c420e5f 100644
--- a/modules/generation_parameters_copypaste.py
+++ b/modules/generation_parameters_copypaste.py
@@ -265,6 +265,14 @@ Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model
else:
prompt += ("" if prompt == "" else "\n") + line
+ if shared.opts.infotext_styles != "Ignore":
+ found_styles, prompt, negative_prompt = shared.prompt_styles.extract_styles_from_prompt(prompt, negative_prompt)
+
+ if shared.opts.infotext_styles == "Apply":
+ res["Styles array"] = found_styles
+ elif shared.opts.infotext_styles == "Apply if any" and found_styles:
+ res["Styles array"] = found_styles
+
res["Prompt"] = prompt
res["Negative prompt"] = negative_prompt
diff --git a/modules/launch_utils.py b/modules/launch_utils.py
index 0c8c4db0..af8d8b37 100644
--- a/modules/launch_utils.py
+++ b/modules/launch_utils.py
@@ -244,6 +244,12 @@ def prepare_environment():
codeformer_commit_hash = os.environ.get('CODEFORMER_COMMIT_HASH', "c5b4593074ba6214284d6acd5f1719b6c5d739af")
blip_commit_hash = os.environ.get('BLIP_COMMIT_HASH', "48211a1594f1321b00f14c9f7a5b4813144b2fb9")
+ try:
+ # the existance of this file is a signal to webui.sh/bat that webui needs to be restarted when it stops execution
+ os.remove(os.path.join(script_path, "tmp", "restart"))
+ except OSError:
+ pass
+
if not args.skip_python_version_check:
check_python_version()
diff --git a/modules/sd_hijack_optimizations.py b/modules/sd_hijack_optimizations.py
index b41aa419..3c71e6b5 100644
--- a/modules/sd_hijack_optimizations.py
+++ b/modules/sd_hijack_optimizations.py
@@ -46,7 +46,7 @@ class SdOptimizationXformers(SdOptimization):
priority = 100
def is_available(self):
- return shared.cmd_opts.force_enable_xformers or (shared.xformers_available and torch.version.cuda and (6, 0) <= torch.cuda.get_device_capability(shared.device) <= (9, 0))
+ return shared.cmd_opts.force_enable_xformers or (shared.xformers_available and torch.cuda.is_available() and (6, 0) <= torch.cuda.get_device_capability(shared.device) <= (9, 0))
def apply(self):
ldm.modules.attention.CrossAttention.forward = xformers_attention_forward
diff --git a/modules/shared.py b/modules/shared.py
index 7025a754..7d056a4d 100644
--- a/modules/shared.py
+++ b/modules/shared.py
@@ -260,6 +260,10 @@ class OptionInfo:
self.comment_after += f"<span class='info'>({info})</span>"
return self
+ def html(self, html):
+ self.comment_after += html
+ return self
+
def needs_restart(self):
self.comment_after += " <span class='info'>(requires restart)</span>"
return self
@@ -488,7 +492,14 @@ 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_version_to_infotext": OptionInfo(True, "Add program version to generation information"),
- "disable_weights_auto_swap": OptionInfo(True, "When reading generation parameters from text into UI (from PNG info or pasted text), do not change the selected model/checkpoint."),
+ "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'>
+<li>Ignore: keep prompt and styles dropdown as it is.</li>
+<li>Apply: remove style text from prompt, always replace styles dropdown value with found styles (even if none are found).</li>
+<li>Discard: remove style text from prompt, keep styles dropdown as it is.</li>
+<li>Apply if any: remove style text from prompt; if any styles are found in prompt, put them into styles dropdown, otherwise keep it as it is.</li>
+</ul>"""),
+
}))
options_templates.update(options_section(('ui', "Live previews"), {
@@ -841,3 +852,12 @@ def walk_files(path, allowed_extensions=None):
continue
yield os.path.join(root, filename)
+
+
+def restart_program():
+ """creates file tmp/restart and immediately stops the process, which webui.bat/webui.sh interpret as a command to start webui again"""
+
+ with open(os.path.join(script_path, "tmp", "restart"), "w"):
+ pass
+
+ os._exit(0)
diff --git a/modules/styles.py b/modules/styles.py
index 34e1b5e1..ec0e1bc5 100644
--- a/modules/styles.py
+++ b/modules/styles.py
@@ -1,6 +1,7 @@
import csv
import os
import os.path
+import re
import typing
import shutil
@@ -28,6 +29,44 @@ def apply_styles_to_prompt(prompt, styles):
return prompt
+re_spaces = re.compile(" +")
+
+
+def extract_style_text_from_prompt(style_text, prompt):
+ stripped_prompt = re.sub(re_spaces, " ", prompt.strip())
+ stripped_style_text = re.sub(re_spaces, " ", style_text.strip())
+ if "{prompt}" in stripped_style_text:
+ left, right = stripped_style_text.split("{prompt}", 2)
+ if stripped_prompt.startswith(left) and stripped_prompt.endswith(right):
+ prompt = stripped_prompt[len(left):len(stripped_prompt)-len(right)]
+ return True, prompt
+ else:
+ if stripped_prompt.endswith(stripped_style_text):
+ prompt = stripped_prompt[:len(stripped_prompt)-len(stripped_style_text)]
+
+ if prompt.endswith(', '):
+ prompt = prompt[:-2]
+
+ return True, prompt
+
+ return False, prompt
+
+
+def extract_style_from_prompts(style: PromptStyle, prompt, negative_prompt):
+ if not style.prompt and not style.negative_prompt:
+ return False, prompt, negative_prompt
+
+ match_positive, extracted_positive = extract_style_text_from_prompt(style.prompt, prompt)
+ if not match_positive:
+ return False, prompt, negative_prompt
+
+ match_negative, extracted_negative = extract_style_text_from_prompt(style.negative_prompt, negative_prompt)
+ if not match_negative:
+ return False, prompt, negative_prompt
+
+ return True, extracted_positive, extracted_negative
+
+
class StyleDatabase:
def __init__(self, path: str):
self.no_style = PromptStyle("None", "", "")
@@ -67,10 +106,34 @@ class StyleDatabase:
if os.path.exists(path):
shutil.copy(path, f"{path}.bak")
- fd = os.open(path, os.O_RDWR|os.O_CREAT)
+ fd = os.open(path, os.O_RDWR | os.O_CREAT)
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,
# and collections.NamedTuple has explicit documentation for accessing _fields. Same goes for _asdict()
writer = csv.DictWriter(file, fieldnames=PromptStyle._fields)
writer.writeheader()
- writer.writerows(style._asdict() for k, style in self.styles.items())
+ writer.writerows(style._asdict() for k, style in self.styles.items())
+
+ def extract_styles_from_prompt(self, prompt, negative_prompt):
+ extracted = []
+
+ applicable_styles = list(self.styles.values())
+
+ while True:
+ found_style = None
+
+ for style in applicable_styles:
+ is_match, new_prompt, new_neg_prompt = extract_style_from_prompts(style, prompt, negative_prompt)
+ if is_match:
+ found_style = style
+ prompt = new_prompt
+ negative_prompt = new_neg_prompt
+ break
+
+ if not found_style:
+ break
+
+ applicable_styles.remove(found_style)
+ extracted.append(found_style.name)
+
+ return list(reversed(extracted)), prompt, negative_prompt
diff --git a/modules/sysinfo.py b/modules/sysinfo.py
new file mode 100644
index 00000000..5f15ac4f
--- /dev/null
+++ b/modules/sysinfo.py
@@ -0,0 +1,162 @@
+import json
+import os
+import sys
+import traceback
+
+import platform
+import hashlib
+import pkg_resources
+import psutil
+import re
+
+import launch
+from modules import paths_internal, timer
+
+checksum_token = "DontStealMyGamePlz__WINNERS_DONT_USE_DRUGS__DONT_COPY_THAT_FLOPPY"
+environment_whitelist = {
+ "GIT",
+ "INDEX_URL",
+ "WEBUI_LAUNCH_LIVE_OUTPUT",
+ "GRADIO_ANALYTICS_ENABLED",
+ "PYTHONPATH",
+ "TORCH_INDEX_URL",
+ "TORCH_COMMAND",
+ "REQS_FILE",
+ "XFORMERS_PACKAGE",
+ "GFPGAN_PACKAGE",
+ "CLIP_PACKAGE",
+ "OPENCLIP_PACKAGE",
+ "STABLE_DIFFUSION_REPO",
+ "K_DIFFUSION_REPO",
+ "CODEFORMER_REPO",
+ "BLIP_REPO",
+ "STABLE_DIFFUSION_COMMIT_HASH",
+ "K_DIFFUSION_COMMIT_HASH",
+ "CODEFORMER_COMMIT_HASH",
+ "BLIP_COMMIT_HASH",
+ "COMMANDLINE_ARGS",
+ "IGNORE_CMD_ARGS_ERRORS",
+}
+
+
+def pretty_bytes(num, suffix="B"):
+ for unit in ["", "K", "M", "G", "T", "P", "E", "Z", "Y"]:
+ if abs(num) < 1024 or unit == 'Y':
+ return f"{num:.0f}{unit}{suffix}"
+ num /= 1024
+
+
+def get():
+ res = get_dict()
+
+ text = json.dumps(res, ensure_ascii=False, indent=4)
+
+ h = hashlib.sha256(text.encode("utf8"))
+ text = text.replace(checksum_token, h.hexdigest())
+
+ return text
+
+
+re_checksum = re.compile(r'"Checksum": "([0-9a-fA-F]{64})"')
+
+
+def check(x):
+ m = re.search(re_checksum, x)
+ if not m:
+ return False
+
+ replaced = re.sub(re_checksum, f'"Checksum": "{checksum_token}"', x)
+
+ h = hashlib.sha256(replaced.encode("utf8"))
+ return h.hexdigest() == m.group(1)
+
+
+def get_dict():
+ ram = psutil.virtual_memory()
+
+ res = {
+ "Platform": platform.platform(),
+ "Python": platform.python_version(),
+ "Version": launch.git_tag(),
+ "Commit": launch.commit_hash(),
+ "Script path": paths_internal.script_path,
+ "Data path": paths_internal.data_path,
+ "Extensions dir": paths_internal.extensions_dir,
+ "Checksum": checksum_token,
+ "Commandline": sys.argv,
+ "Torch env info": get_torch_sysinfo(),
+ "Exceptions": get_exceptions(),
+ "CPU": {
+ "model": platform.processor(),
+ "count logical": psutil.cpu_count(logical=True),
+ "count physical": psutil.cpu_count(logical=False),
+ },
+ "RAM": {
+ x: pretty_bytes(getattr(ram, x, 0)) for x in ["total", "used", "free", "active", "inactive", "buffers", "cached", "shared"] if getattr(ram, x, 0) != 0
+ },
+ "Extensions": get_extensions(enabled=True),
+ "Inactive extensions": get_extensions(enabled=False),
+ "Environment": get_environment(),
+ "Config": get_config(),
+ "Startup": timer.startup_record,
+ "Packages": sorted([f"{pkg.key}=={pkg.version}" for pkg in pkg_resources.working_set]),
+ }
+
+ return res
+
+
+def format_traceback(tb):
+ return [[f"{x.filename}, line {x.lineno}, {x.name}", x.line] for x in traceback.extract_tb(tb)]
+
+
+def get_exceptions():
+ try:
+ from modules import errors
+
+ return [{"exception": str(e), "traceback": format_traceback(tb)} for e, tb in reversed(errors.exception_records)]
+ except Exception as e:
+ return str(e)
+
+
+def get_environment():
+ return {k: os.environ[k] for k in sorted(os.environ) if k in environment_whitelist}
+
+
+re_newline = re.compile(r"\r*\n")
+
+
+def get_torch_sysinfo():
+ try:
+ import torch.utils.collect_env
+ info = torch.utils.collect_env.get_env_info()._asdict()
+
+ return {k: re.split(re_newline, str(v)) if "\n" in str(v) else v for k, v in info.items()}
+ except Exception as e:
+ return str(e)
+
+
+def get_extensions(*, enabled):
+
+ try:
+ from modules import extensions
+
+ def to_json(x: extensions.Extension):
+ return {
+ "name": x.name,
+ "path": x.path,
+ "version": x.version,
+ "branch": x.branch,
+ "remote": x.remote,
+ }
+
+ return [to_json(x) for x in extensions.extensions if not x.is_builtin and x.enabled == enabled]
+ except Exception as e:
+ return str(e)
+
+
+def get_config():
+ try:
+ from modules import shared
+ return shared.opts.data
+ except Exception as e:
+ return str(e)
diff --git a/modules/ui.py b/modules/ui.py
index 988b2003..4c0fd4d5 100644
--- a/modules/ui.py
+++ b/modules/ui.py
@@ -1,3 +1,4 @@
+import datetime
import json
import mimetypes
import os
@@ -11,7 +12,7 @@ import numpy as np
from PIL import Image, PngImagePlugin # noqa: F401
from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call, wrap_gradio_call
-from modules import sd_hijack, sd_models, script_callbacks, ui_extensions, deepbooru, sd_vae, extra_networks, ui_common, ui_postprocessing, progress, ui_loadsave, errors, shared_items, ui_settings, timer
+from modules import sd_hijack, sd_models, script_callbacks, ui_extensions, deepbooru, sd_vae, extra_networks, ui_common, ui_postprocessing, progress, ui_loadsave, errors, shared_items, ui_settings, timer, sysinfo
from modules.ui_components import FormRow, FormGroup, ToolButton, FormHTML
from modules.paths import script_path
from modules.ui_common import create_refresh_button
@@ -621,6 +622,7 @@ def create_ui():
(subseed_strength, "Variation seed strength"),
(seed_resize_from_w, "Seed resize from-1"),
(seed_resize_from_h, "Seed resize from-2"),
+ (txt2img_prompt_styles, lambda d: d["Styles array"] if isinstance(d.get("Styles array"), list) else gr.update()),
(denoising_strength, "Denoising strength"),
(enable_hr, lambda d: "Denoising strength" in d),
(hr_options, lambda d: gr.Row.update(visible="Denoising strength" in d)),
@@ -1036,6 +1038,7 @@ def create_ui():
(subseed_strength, "Variation seed strength"),
(seed_resize_from_w, "Seed resize from-1"),
(seed_resize_from_h, "Seed resize from-2"),
+ (img2img_prompt_styles, lambda d: d["Styles array"] if isinstance(d.get("Styles array"), list) else gr.update()),
(denoising_strength, "Denoising strength"),
(mask_blur, "Mask blur"),
*modules.scripts.scripts_img2img.infotext_fields
@@ -1598,3 +1601,15 @@ def setup_ui_api(app):
app.add_api_route("/internal/ping", lambda: {}, methods=["GET"])
app.add_api_route("/internal/profile-startup", lambda: timer.startup_record, methods=["GET"])
+
+ def download_sysinfo(attachment=False):
+ from fastapi.responses import PlainTextResponse
+
+ text = sysinfo.get()
+ filename = f"sysinfo-{datetime.datetime.utcnow().strftime('%Y-%m-%d-%H-%M')}.txt"
+
+ return PlainTextResponse(text, headers={'Content-Disposition': f'{"attachment" if attachment else "inline"}; filename="{filename}"'})
+
+ app.add_api_route("/internal/sysinfo", download_sysinfo, methods=["GET"])
+ app.add_api_route("/internal/sysinfo-download", lambda: download_sysinfo(attachment=True), methods=["GET"])
+
diff --git a/modules/ui_extensions.py b/modules/ui_extensions.py
index 3140ed64..5580dfaf 100644
--- a/modules/ui_extensions.py
+++ b/modules/ui_extensions.py
@@ -49,7 +49,7 @@ def apply_and_restart(disable_list, update_list, disable_all):
shared.opts.disabled_extensions = disabled
shared.opts.disable_all_extensions = disable_all
shared.opts.save(shared.config_filename)
- shared.state.request_restart()
+ shared.restart_program()
def save_config_state(name):
diff --git a/modules/ui_settings.py b/modules/ui_settings.py
index 7874298e..892c5e1a 100644
--- a/modules/ui_settings.py
+++ b/modules/ui_settings.py
@@ -1,6 +1,6 @@
import gradio as gr
-from modules import ui_common, shared, script_callbacks, scripts, sd_models
+from modules import ui_common, shared, script_callbacks, scripts, sd_models, sysinfo
from modules.call_queue import wrap_gradio_call
from modules.shared import opts
from modules.ui_components import FormRow
@@ -157,6 +157,17 @@ class UiSettings:
with gr.TabItem("Defaults", id="defaults", elem_id="settings_tab_defaults"):
loadsave.create_ui()
+ with gr.TabItem("Sysinfo", id="sysinfo", elem_id="settings_tab_sysinfo"):
+ gr.HTML('<a href="./internal/sysinfo-download" class="sysinfo_big_link" download>Download system info</a><br /><a href="./internal/sysinfo">(or open as text in a new page)</a>', elem_id="sysinfo_download")
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ sysinfo_check_file = gr.File(label="Check system info for validity", type='binary')
+ with gr.Column(scale=1):
+ sysinfo_check_output = gr.HTML("", elem_id="sysinfo_validity")
+ with gr.Column(scale=100):
+ pass
+
with gr.TabItem("Actions", id="actions", elem_id="settings_tab_actions"):
request_notifications = gr.Button(value='Request browser notifications', elem_id="request_notifications")
download_localization = gr.Button(value='Download localization template', elem_id="download_localization")
@@ -215,6 +226,21 @@ class UiSettings:
outputs=[],
)
+ def check_file(x):
+ if x is None:
+ return ''
+
+ if sysinfo.check(x.decode('utf8', errors='ignore')):
+ return 'Valid'
+
+ return 'Invalid'
+
+ sysinfo_check_file.change(
+ fn=check_file,
+ inputs=[sysinfo_check_file],
+ outputs=[sysinfo_check_output],
+ )
+
self.interface = settings_interface
def add_quicksettings(self):
diff --git a/modules/ui_tempdir.py b/modules/ui_tempdir.py
index 9fc7d764..fb75137e 100644
--- a/modules/ui_tempdir.py
+++ b/modules/ui_tempdir.py
@@ -31,7 +31,7 @@ def check_tmp_file(gradio, filename):
return False
-def save_pil_to_file(self, pil_image, dir=None):
+def save_pil_to_file(self, pil_image, dir=None, format="png"):
already_saved_as = getattr(pil_image, 'already_saved_as', None)
if already_saved_as and os.path.isfile(already_saved_as):
register_tmp_file(shared.demo, already_saved_as)
diff --git a/style.css b/style.css
index ba081b56..e1df716f 100644
--- a/style.css
+++ b/style.css
@@ -450,6 +450,19 @@ table.popup-table .link{
opacity: 0.75;
}
+#sysinfo_download a.sysinfo_big_link{
+ font-size: 24pt;
+}
+
+#sysinfo_download a{
+ text-decoration: underline;
+}
+
+#sysinfo_validity{
+ font-size: 18pt;
+}
+
+
/* live preview */
.progressDiv{
position: relative;
diff --git a/webui.bat b/webui.bat
index 209d972b..961fc7d4 100644
--- a/webui.bat
+++ b/webui.bat
@@ -51,12 +51,14 @@ if EXIST %ACCELERATE% goto :accelerate_launch
:launch
%PYTHON% launch.py %*
+if EXIST tmp/restart goto :skip_venv
pause
exit /b
:accelerate_launch
echo Accelerating
%ACCELERATE% launch --num_cpu_threads_per_process=6 launch.py
+if EXIST tmp/restart goto :skip_venv
pause
exit /b
diff --git a/webui.sh b/webui.sh
index 1e728813..c407b3ef 100755
--- a/webui.sh
+++ b/webui.sh
@@ -203,17 +203,23 @@ prepare_tcmalloc() {
fi
}
-if [[ ! -z "${ACCELERATE}" ]] && [ ${ACCELERATE}="True" ] && [ -x "$(command -v accelerate)" ]
-then
- printf "\n%s\n" "${delimiter}"
- printf "Accelerating launch.py..."
- printf "\n%s\n" "${delimiter}"
- prepare_tcmalloc
- exec accelerate launch --num_cpu_threads_per_process=6 "${LAUNCH_SCRIPT}" "$@"
-else
- printf "\n%s\n" "${delimiter}"
- printf "Launching launch.py..."
- printf "\n%s\n" "${delimiter}"
- prepare_tcmalloc
- exec "${python_cmd}" "${LAUNCH_SCRIPT}" "$@"
-fi
+KEEP_GOING=1
+while [[ "$KEEP_GOING" -eq "1" ]]; do
+ if [[ ! -z "${ACCELERATE}" ]] && [ ${ACCELERATE}="True" ] && [ -x "$(command -v accelerate)" ]; then
+ printf "\n%s\n" "${delimiter}"
+ printf "Accelerating launch.py..."
+ printf "\n%s\n" "${delimiter}"
+ prepare_tcmalloc
+ accelerate launch --num_cpu_threads_per_process=6 "${LAUNCH_SCRIPT}" "$@"
+ else
+ printf "\n%s\n" "${delimiter}"
+ printf "Launching launch.py..."
+ printf "\n%s\n" "${delimiter}"
+ prepare_tcmalloc
+ "${python_cmd}" "${LAUNCH_SCRIPT}" "$@"
+ fi
+
+ if [[ ! -f tmp/restart ]]; then
+ KEEP_GOING=0
+ fi
+done