diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 3a8b9953..9cc16d01 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -43,8 +43,8 @@ body:
- type: input
id: commit
attributes:
- label: Commit where the problem happens
- description: Which commit are you running ? (Do not write *Latest version/repo/commit*, as this means nothing and will have changed by the time we read your issue. Rather, copy the **Commit** link at the bottom of the UI, or from the cmd/terminal if you can't launch it.)
+ label: Version or Commit where the problem happens
+ description: "Which webui version or commit are you running ? (Do not write *Latest Version/repo/commit*, as this means nothing and will have changed by the time we read your issue. Rather, copy the **Version: v1.2.3** link at the bottom of the UI, or from the cmd/terminal if you can't launch it.)"
validations:
required: true
- type: dropdown
diff --git a/extensions-builtin/LDSR/scripts/ldsr_model.py b/extensions-builtin/LDSR/scripts/ldsr_model.py
index c4da79f3..95f1669d 100644
--- a/extensions-builtin/LDSR/scripts/ldsr_model.py
+++ b/extensions-builtin/LDSR/scripts/ldsr_model.py
@@ -1,9 +1,8 @@
import os
-import sys
-import traceback
from basicsr.utils.download_util import load_file_from_url
+from modules.errors import print_error
from modules.upscaler import Upscaler, UpscalerData
from ldsr_model_arch import LDSR
from modules import shared, script_callbacks
@@ -51,10 +50,8 @@ class UpscalerLDSR(Upscaler):
try:
return LDSR(model, yaml)
-
except Exception:
- print("Error importing LDSR:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error("Error importing LDSR", exc_info=True)
return None
def do_upscale(self, img, path):
diff --git a/extensions-builtin/LDSR/sd_hijack_autoencoder.py b/extensions-builtin/LDSR/sd_hijack_autoencoder.py
index 81c5101b..27a86e13 100644
--- a/extensions-builtin/LDSR/sd_hijack_autoencoder.py
+++ b/extensions-builtin/LDSR/sd_hijack_autoencoder.py
@@ -10,7 +10,7 @@ from contextlib import contextmanager
from torch.optim.lr_scheduler import LambdaLR
from ldm.modules.ema import LitEma
-from taming.modules.vqvae.quantize import VectorQuantizer2 as VectorQuantizer
+from vqvae_quantize import VectorQuantizer2 as VectorQuantizer
from ldm.modules.diffusionmodules.model import Encoder, Decoder
from ldm.util import instantiate_from_config
diff --git a/extensions-builtin/LDSR/vqvae_quantize.py b/extensions-builtin/LDSR/vqvae_quantize.py
new file mode 100644
index 00000000..dd14b8fd
--- /dev/null
+++ b/extensions-builtin/LDSR/vqvae_quantize.py
@@ -0,0 +1,147 @@
+# Vendored from https://raw.githubusercontent.com/CompVis/taming-transformers/24268930bf1dce879235a7fddd0b2355b84d7ea6/taming/modules/vqvae/quantize.py,
+# where the license is as follows:
+#
+# Copyright (c) 2020 Patrick Esser and Robin Rombach and Björn Ommer
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
+# OR OTHER DEALINGS IN THE SOFTWARE./
+
+import torch
+import torch.nn as nn
+import numpy as np
+from einops import rearrange
+
+
+class VectorQuantizer2(nn.Module):
+ """
+ Improved version over VectorQuantizer, can be used as a drop-in replacement. Mostly
+ avoids costly matrix multiplications and allows for post-hoc remapping of indices.
+ """
+
+ # NOTE: due to a bug the beta term was applied to the wrong term. for
+ # backwards compatibility we use the buggy version by default, but you can
+ # specify legacy=False to fix it.
+ def __init__(self, n_e, e_dim, beta, remap=None, unknown_index="random",
+ sane_index_shape=False, legacy=True):
+ super().__init__()
+ self.n_e = n_e
+ self.e_dim = e_dim
+ self.beta = beta
+ self.legacy = legacy
+
+ self.embedding = nn.Embedding(self.n_e, self.e_dim)
+ self.embedding.weight.data.uniform_(-1.0 / self.n_e, 1.0 / self.n_e)
+
+ self.remap = remap
+ if self.remap is not None:
+ self.register_buffer("used", torch.tensor(np.load(self.remap)))
+ self.re_embed = self.used.shape[0]
+ self.unknown_index = unknown_index # "random" or "extra" or integer
+ if self.unknown_index == "extra":
+ self.unknown_index = self.re_embed
+ self.re_embed = self.re_embed + 1
+ print(f"Remapping {self.n_e} indices to {self.re_embed} indices. "
+ f"Using {self.unknown_index} for unknown indices.")
+ else:
+ self.re_embed = n_e
+
+ self.sane_index_shape = sane_index_shape
+
+ def remap_to_used(self, inds):
+ ishape = inds.shape
+ assert len(ishape) > 1
+ inds = inds.reshape(ishape[0], -1)
+ used = self.used.to(inds)
+ match = (inds[:, :, None] == used[None, None, ...]).long()
+ new = match.argmax(-1)
+ unknown = match.sum(2) < 1
+ if self.unknown_index == "random":
+ new[unknown] = torch.randint(0, self.re_embed, size=new[unknown].shape).to(device=new.device)
+ else:
+ new[unknown] = self.unknown_index
+ return new.reshape(ishape)
+
+ def unmap_to_all(self, inds):
+ ishape = inds.shape
+ assert len(ishape) > 1
+ inds = inds.reshape(ishape[0], -1)
+ used = self.used.to(inds)
+ if self.re_embed > self.used.shape[0]: # extra token
+ inds[inds >= self.used.shape[0]] = 0 # simply set to zero
+ back = torch.gather(used[None, :][inds.shape[0] * [0], :], 1, inds)
+ return back.reshape(ishape)
+
+ def forward(self, z, temp=None, rescale_logits=False, return_logits=False):
+ assert temp is None or temp == 1.0, "Only for interface compatible with Gumbel"
+ assert rescale_logits is False, "Only for interface compatible with Gumbel"
+ assert return_logits is False, "Only for interface compatible with Gumbel"
+ # reshape z -> (batch, height, width, channel) and flatten
+ z = rearrange(z, 'b c h w -> b h w c').contiguous()
+ z_flattened = z.view(-1, self.e_dim)
+ # distances from z to embeddings e_j (z - e)^2 = z^2 + e^2 - 2 e * z
+
+ d = torch.sum(z_flattened ** 2, dim=1, keepdim=True) + \
+ torch.sum(self.embedding.weight ** 2, dim=1) - 2 * \
+ torch.einsum('bd,dn->bn', z_flattened, rearrange(self.embedding.weight, 'n d -> d n'))
+
+ min_encoding_indices = torch.argmin(d, dim=1)
+ z_q = self.embedding(min_encoding_indices).view(z.shape)
+ perplexity = None
+ min_encodings = None
+
+ # compute loss for embedding
+ if not self.legacy:
+ loss = self.beta * torch.mean((z_q.detach() - z) ** 2) + \
+ torch.mean((z_q - z.detach()) ** 2)
+ else:
+ loss = torch.mean((z_q.detach() - z) ** 2) + self.beta * \
+ torch.mean((z_q - z.detach()) ** 2)
+
+ # preserve gradients
+ z_q = z + (z_q - z).detach()
+
+ # reshape back to match original input shape
+ z_q = rearrange(z_q, 'b h w c -> b c h w').contiguous()
+
+ if self.remap is not None:
+ min_encoding_indices = min_encoding_indices.reshape(z.shape[0], -1) # add batch axis
+ min_encoding_indices = self.remap_to_used(min_encoding_indices)
+ min_encoding_indices = min_encoding_indices.reshape(-1, 1) # flatten
+
+ if self.sane_index_shape:
+ min_encoding_indices = min_encoding_indices.reshape(
+ z_q.shape[0], z_q.shape[2], z_q.shape[3])
+
+ return z_q, loss, (perplexity, min_encodings, min_encoding_indices)
+
+ def get_codebook_entry(self, indices, shape):
+ # shape specifying (batch, height, width, channel)
+ if self.remap is not None:
+ indices = indices.reshape(shape[0], -1) # add batch axis
+ indices = self.unmap_to_all(indices)
+ indices = indices.reshape(-1) # flatten again
+
+ # get quantized latent vectors
+ z_q = self.embedding(indices)
+
+ if shape is not None:
+ z_q = z_q.view(shape)
+ # reshape back to match original input shape
+ z_q = z_q.permute(0, 3, 1, 2).contiguous()
+
+ return z_q
diff --git a/extensions-builtin/ScuNET/scripts/scunet_model.py b/extensions-builtin/ScuNET/scripts/scunet_model.py
index 45d9297b..dd1b822e 100644
--- a/extensions-builtin/ScuNET/scripts/scunet_model.py
+++ b/extensions-builtin/ScuNET/scripts/scunet_model.py
@@ -1,6 +1,5 @@
import os.path
import sys
-import traceback
import PIL.Image
import numpy as np
@@ -12,6 +11,8 @@ from basicsr.utils.download_util import load_file_from_url
import modules.upscaler
from modules import devices, modelloader, script_callbacks
from scunet_model_arch import SCUNet as net
+
+from modules.errors import print_error
from modules.shared import opts
@@ -38,8 +39,7 @@ class UpscalerScuNET(modules.upscaler.Upscaler):
scaler_data = modules.upscaler.UpscalerData(name, file, self, 4)
scalers.append(scaler_data)
except Exception:
- print(f"Error loading ScuNET model: {file}", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error loading ScuNET model: {file}", exc_info=True)
if add_model2:
scaler_data2 = modules.upscaler.UpscalerData(self.model_name2, self.model_url2, self)
scalers.append(scaler_data2)
diff --git a/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
new file mode 100644
index 00000000..f555960d
--- /dev/null
+++ b/extensions-builtin/canvas-zoom-and-pan/javascript/zoom.js
@@ -0,0 +1,431 @@
+// Main
+
+// Helper functions
+// Get active tab
+function getActiveTab(elements, all = false) {
+ const tabs = elements.img2imgTabs.querySelectorAll("button");
+
+ if (all) return tabs;
+
+ for (let tab of tabs) {
+ if (tab.classList.contains("selected")) {
+ return tab;
+ }
+ }
+}
+
+onUiLoaded(async() => {
+ const hotkeysConfig = {
+ resetZoom: "KeyR",
+ fitToScreen: "KeyS",
+ moveKey: "KeyF",
+ overlap: "KeyO"
+ };
+
+ let isMoving = false;
+ let mouseX, mouseY;
+
+ const elementIDs = {
+ sketch: "#img2img_sketch",
+ inpaint: "#img2maskimg",
+ inpaintSketch: "#inpaint_sketch",
+ img2imgTabs: "#mode_img2img .tab-nav"
+ };
+
+ async function getElements() {
+ const elements = await Promise.all(
+ Object.values(elementIDs).map(id => document.querySelector(id))
+ );
+ return Object.fromEntries(
+ Object.keys(elementIDs).map((key, index) => [key, elements[index]])
+ );
+ }
+
+ const elements = await getElements();
+
+ function applyZoomAndPan(targetElement, elemId) {
+ targetElement.style.transformOrigin = "0 0";
+ let [zoomLevel, panX, panY] = [1, 0, 0];
+ let fullScreenMode = false;
+
+ // In the course of research, it was found that the tag img is very harmful when zooming and creates white canvases. This hack allows you to almost never think about this problem, it has no effect on webui.
+ function fixCanvas() {
+ const activeTab = getActiveTab(elements).textContent.trim();
+
+ if (activeTab !== "img2img") {
+ const img = targetElement.querySelector(`${elemId} img`);
+
+ if (img && img.style.display !== "none") {
+ img.style.display = "none";
+ img.style.visibility = "hidden";
+ }
+ }
+ }
+
+ // Reset the zoom level and pan position of the target element to their initial values
+ function resetZoom() {
+ zoomLevel = 1;
+ panX = 0;
+ panY = 0;
+
+ fixCanvas();
+ targetElement.style.transform = `scale(${zoomLevel}) translate(${panX}px, ${panY}px)`;
+
+ const canvas = gradioApp().querySelector(
+ `${elemId} canvas[key="interface"]`
+ );
+
+ toggleOverlap("off");
+ fullScreenMode = false;
+
+ if (
+ canvas &&
+ parseFloat(canvas.style.width) > 865 &&
+ parseFloat(targetElement.style.width) > 865
+ ) {
+ fitToElement();
+ return;
+ }
+
+ targetElement.style.width = "";
+ if (canvas) {
+ targetElement.style.height = canvas.style.height;
+ }
+ }
+
+ // Toggle the zIndex of the target element between two values, allowing it to overlap or be overlapped by other elements
+ function toggleOverlap(forced = "") {
+ const zIndex1 = "0";
+ const zIndex2 = "998";
+
+ targetElement.style.zIndex =
+ targetElement.style.zIndex !== zIndex2 ? zIndex2 : zIndex1;
+
+ if (forced === "off") {
+ targetElement.style.zIndex = zIndex1;
+ } else if (forced === "on") {
+ targetElement.style.zIndex = zIndex2;
+ }
+ }
+
+ // Adjust the brush size based on the deltaY value from a mouse wheel event
+ function adjustBrushSize(
+ elemId,
+ deltaY,
+ withoutValue = false,
+ percentage = 5
+ ) {
+ const input =
+ gradioApp().querySelector(
+ `${elemId} input[aria-label='Brush radius']`
+ ) ||
+ gradioApp().querySelector(
+ `${elemId} button[aria-label="Use brush"]`
+ );
+
+ if (input) {
+ input.click();
+ if (!withoutValue) {
+ const maxValue =
+ parseFloat(input.getAttribute("max")) || 100;
+ const changeAmount = maxValue * (percentage / 100);
+ const newValue =
+ parseFloat(input.value) +
+ (deltaY > 0 ? -changeAmount : changeAmount);
+ input.value = Math.min(Math.max(newValue, 0), maxValue);
+ input.dispatchEvent(new Event("change"));
+ }
+ }
+ }
+
+ // Reset zoom when uploading a new image
+ const fileInput = gradioApp().querySelector(
+ `${elemId} input[type="file"][accept="image/*"].svelte-116rqfv`
+ );
+ fileInput.addEventListener("click", resetZoom);
+
+ // 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;
+
+ targetElement.style.transformOrigin = "0 0";
+ targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${newZoomLevel})`;
+
+ toggleOverlap("on");
+ return newZoomLevel;
+ }
+
+ // Change the zoom level based on user interaction
+ function changeZoomLevel(operation, e) {
+ if (e.shiftKey) {
+ e.preventDefault();
+
+ let zoomPosX, zoomPosY;
+ let delta = 0.2;
+ if (zoomLevel > 7) {
+ delta = 0.9;
+ } else if (zoomLevel > 2) {
+ delta = 0.6;
+ }
+
+ zoomPosX = e.clientX;
+ zoomPosY = e.clientY;
+
+ fullScreenMode = false;
+ zoomLevel = updateZoom(
+ zoomLevel + (operation === "+" ? delta : -delta),
+ zoomPosX - targetElement.getBoundingClientRect().left,
+ zoomPosY - targetElement.getBoundingClientRect().top
+ );
+ }
+ }
+
+ /**
+ * This function fits the target element to the screen by calculating
+ * the required scale and offsets. It also updates the global variables
+ * zoomLevel, panX, and panY to reflect the new state.
+ */
+
+ function fitToElement() {
+ //Reset Zoom
+ targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
+
+ // Get element and screen dimensions
+ const elementWidth = targetElement.offsetWidth;
+ const elementHeight = targetElement.offsetHeight;
+ const parentElement = targetElement.parentElement;
+ const screenWidth = parentElement.clientWidth;
+ const screenHeight = parentElement.clientHeight;
+
+ // Get element's coordinates relative to the parent element
+ const elementRect = targetElement.getBoundingClientRect();
+ const parentRect = parentElement.getBoundingClientRect();
+ const elementX = elementRect.x - parentRect.x;
+
+ // Calculate scale and offsets
+ const scaleX = screenWidth / elementWidth;
+ const scaleY = screenHeight / elementHeight;
+ const scale = Math.min(scaleX, scaleY);
+
+ const transformOrigin =
+ window.getComputedStyle(targetElement).transformOrigin;
+ const [originX, originY] = transformOrigin.split(" ");
+ const originXValue = parseFloat(originX);
+ const originYValue = parseFloat(originY);
+
+ const offsetX =
+ (screenWidth - elementWidth * scale) / 2 -
+ originXValue * (1 - scale);
+ const offsetY =
+ (screenHeight - elementHeight * scale) / 2.5 -
+ originYValue * (1 - scale);
+
+ // Apply scale and offsets to the element
+ targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
+
+ // Update global variables
+ zoomLevel = scale;
+ panX = offsetX;
+ panY = offsetY;
+
+ fullScreenMode = false;
+ toggleOverlap("off");
+ }
+
+ /**
+ * This function fits the target element to the screen by calculating
+ * the required scale and offsets. It also updates the global variables
+ * zoomLevel, panX, and panY to reflect the new state.
+ */
+
+ // Fullscreen mode
+ function fitToScreen() {
+ const canvas = gradioApp().querySelector(
+ `${elemId} canvas[key="interface"]`
+ );
+
+ if (!canvas) return;
+
+ if (canvas.offsetWidth > 862) {
+ targetElement.style.width = canvas.offsetWidth + "px";
+ }
+
+ if (fullScreenMode) {
+ resetZoom();
+ fullScreenMode = false;
+ return;
+ }
+
+ //Reset Zoom
+ targetElement.style.transform = `translate(${0}px, ${0}px) scale(${1})`;
+
+ // Get scrollbar width to right-align the image
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
+
+ // Get element and screen dimensions
+ const elementWidth = targetElement.offsetWidth;
+ const elementHeight = targetElement.offsetHeight;
+ const screenWidth = window.innerWidth - scrollbarWidth;
+ const screenHeight = window.innerHeight;
+
+ // Get element's coordinates relative to the page
+ const elementRect = targetElement.getBoundingClientRect();
+ const elementY = elementRect.y;
+ const elementX = elementRect.x;
+
+ // Calculate scale and offsets
+ const scaleX = screenWidth / elementWidth;
+ const scaleY = screenHeight / elementHeight;
+ const scale = Math.min(scaleX, scaleY);
+
+ // Get the current transformOrigin
+ const computedStyle = window.getComputedStyle(targetElement);
+ const transformOrigin = computedStyle.transformOrigin;
+ const [originX, originY] = transformOrigin.split(" ");
+ const originXValue = parseFloat(originX);
+ const originYValue = parseFloat(originY);
+
+ // Calculate offsets with respect to the transformOrigin
+ const offsetX =
+ (screenWidth - elementWidth * scale) / 2 -
+ elementX -
+ originXValue * (1 - scale);
+ const offsetY =
+ (screenHeight - elementHeight * scale) / 2 -
+ elementY -
+ originYValue * (1 - scale);
+
+ // Apply scale and offsets to the element
+ targetElement.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
+
+ // Update global variables
+ zoomLevel = scale;
+ panX = offsetX;
+ panY = offsetY;
+
+ fullScreenMode = true;
+ toggleOverlap("on");
+ }
+
+ // Handle keydown events
+ function handleKeyDown(event) {
+ const hotkeyActions = {
+ [hotkeysConfig.resetZoom]: resetZoom,
+ [hotkeysConfig.overlap]: toggleOverlap,
+ [hotkeysConfig.fitToScreen]: fitToScreen
+ // [hotkeysConfig.moveKey] : moveCanvas,
+ };
+
+ const action = hotkeyActions[event.code];
+ if (action) {
+ event.preventDefault();
+ action(event);
+ }
+ }
+
+ // Get Mouse position
+ function getMousePosition(e) {
+ mouseX = e.offsetX;
+ mouseY = e.offsetY;
+ }
+
+ targetElement.addEventListener("mousemove", getMousePosition);
+
+ // Handle events only inside the targetElement
+ let isKeyDownHandlerAttached = false;
+
+ function handleMouseMove() {
+ if (!isKeyDownHandlerAttached) {
+ document.addEventListener("keydown", handleKeyDown);
+ isKeyDownHandlerAttached = true;
+ }
+ }
+
+ function handleMouseLeave() {
+ if (isKeyDownHandlerAttached) {
+ document.removeEventListener("keydown", handleKeyDown);
+ isKeyDownHandlerAttached = false;
+ }
+ }
+
+ // Add mouse event handlers
+ targetElement.addEventListener("mousemove", handleMouseMove);
+ targetElement.addEventListener("mouseleave", handleMouseLeave);
+
+ // Reset zoom when click on another tab
+ elements.img2imgTabs.addEventListener("click", resetZoom);
+ elements.img2imgTabs.addEventListener("click", () => {
+ // targetElement.style.width = "";
+ if (parseInt(targetElement.style.width) > 865) {
+ setTimeout(fitToElement, 0);
+ }
+ });
+
+ targetElement.addEventListener("wheel", e => {
+ // change zoom level
+ const operation = e.deltaY > 0 ? "-" : "+";
+ changeZoomLevel(operation, e);
+
+ // Handle brush size adjustment with ctrl key pressed
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault();
+
+ // Increase or decrease brush size based on scroll direction
+ adjustBrushSize(elemId, e.deltaY);
+ }
+ });
+
+ /**
+ * Handle the move event for pan functionality. Updates the panX and panY variables and applies the new transform to the target element.
+ * @param {MouseEvent} e - The mouse event.
+ */
+ function handleMoveKeyDown(e) {
+ if (e.code === hotkeysConfig.moveKey) {
+ if (!e.ctrlKey && !e.metaKey) {
+ isMoving = true;
+ }
+ }
+ }
+
+ function handleMoveKeyUp(e) {
+ if (e.code === hotkeysConfig.moveKey) {
+ isMoving = false;
+ }
+ }
+
+ document.addEventListener("keydown", handleMoveKeyDown);
+ document.addEventListener("keyup", handleMoveKeyUp);
+
+ // Detect zoom level and update the pan speed.
+ function updatePanPosition(movementX, movementY) {
+ let panSpeed = 1.5;
+
+ if (zoomLevel > 8) {
+ panSpeed = 2.5;
+ }
+
+ panX = panX + movementX * panSpeed;
+ panY = panY + movementY * panSpeed;
+
+ targetElement.style.transform = `translate(${panX}px, ${panY}px) scale(${zoomLevel})`;
+ toggleOverlap("on");
+ }
+
+ function handleMoveByKey(e) {
+ if (isMoving) {
+ updatePanPosition(e.movementX, e.movementY);
+ targetElement.style.pointerEvents = "none";
+ } else {
+ targetElement.style.pointerEvents = "auto";
+ }
+ }
+
+ gradioApp().addEventListener("mousemove", handleMoveByKey);
+ }
+
+ applyZoomAndPan(elements.sketch, elementIDs.sketch);
+ applyZoomAndPan(elements.inpaint, elementIDs.inpaint);
+ applyZoomAndPan(elements.inpaintSketch, elementIDs.inpaintSketch);
+});
diff --git a/javascript/imageviewerGamepad.js b/javascript/imageviewerGamepad.js
index 31d226de..a22c7e6e 100644
--- a/javascript/imageviewerGamepad.js
+++ b/javascript/imageviewerGamepad.js
@@ -1,7 +1,9 @@
+let gamepads = [];
+
window.addEventListener('gamepadconnected', (e) => {
const index = e.gamepad.index;
let isWaiting = false;
- setInterval(async() => {
+ gamepads[index] = setInterval(async() => {
if (!opts.js_modal_lightbox_gamepad || isWaiting) return;
const gamepad = navigator.getGamepads()[index];
const xValue = gamepad.axes[0];
@@ -24,6 +26,10 @@ window.addEventListener('gamepadconnected', (e) => {
}, 10);
});
+window.addEventListener('gamepaddisconnected', (e) => {
+ clearInterval(gamepads[e.gamepad.index]);
+});
+
/*
Primarily for vr controller type pointer devices.
I use the wheel event because there's currently no way to do it properly with web xr.
diff --git a/modules/api/api.py b/modules/api/api.py
index 6a456861..fbd616a3 100644
--- a/modules/api/api.py
+++ b/modules/api/api.py
@@ -16,6 +16,7 @@ from secrets import compare_digest
import modules.shared as shared
from modules import sd_samplers, deepbooru, sd_hijack, images, scripts, ui, postprocessing
from modules.api import models
+from modules.errors import print_error
from modules.shared import opts
from modules.processing import StableDiffusionProcessingTxt2Img, StableDiffusionProcessingImg2Img, process_images
from modules.textual_inversion.textual_inversion import create_embedding, train_embedding
@@ -23,6 +24,7 @@ from modules.textual_inversion.preprocess import preprocess
from modules.hypernetworks.hypernetwork import create_hypernetwork, train_hypernetwork
from PIL import PngImagePlugin,Image
from modules.sd_models import checkpoints_list, unload_model_weights, reload_model_weights
+from modules.sd_vae import vae_dict
from modules.sd_models_config import find_checkpoint_config_near_filename
from modules.realesrgan_model import get_realesrgan_models
from modules import devices
@@ -108,7 +110,6 @@ def api_middleware(app: FastAPI):
from rich.console import Console
console = Console()
except Exception:
- import traceback
rich_available = False
@app.middleware("http")
@@ -139,11 +140,12 @@ def api_middleware(app: FastAPI):
"errors": str(e),
}
if not isinstance(e, HTTPException): # do not print backtrace on known httpexceptions
- print(f"API error: {request.method}: {request.url} {err}")
+ message = f"API error: {request.method}: {request.url} {err}"
if rich_available:
+ print(message)
console.print_exception(show_locals=True, max_frames=2, extra_lines=1, suppress=[anyio, starlette], word_wrap=False, width=min([console.width, 200]))
else:
- traceback.print_exc()
+ print_error(message, exc_info=True)
return JSONResponse(status_code=vars(e).get('status_code', 500), content=jsonable_encoder(err))
@app.middleware("http")
@@ -189,6 +191,7 @@ class Api:
self.add_api_route("/sdapi/v1/samplers", self.get_samplers, methods=["GET"], response_model=List[models.SamplerItem])
self.add_api_route("/sdapi/v1/upscalers", self.get_upscalers, methods=["GET"], response_model=List[models.UpscalerItem])
self.add_api_route("/sdapi/v1/sd-models", self.get_sd_models, methods=["GET"], response_model=List[models.SDModelItem])
+ self.add_api_route("/sdapi/v1/sd-vae", self.get_sd_vaes, methods=["GET"], response_model=List[models.SDVaeItem])
self.add_api_route("/sdapi/v1/hypernetworks", self.get_hypernetworks, methods=["GET"], response_model=List[models.HypernetworkItem])
self.add_api_route("/sdapi/v1/face-restorers", self.get_face_restorers, methods=["GET"], response_model=List[models.FaceRestorerItem])
self.add_api_route("/sdapi/v1/realesrgan-models", self.get_realesrgan_models, methods=["GET"], response_model=List[models.RealesrganItem])
@@ -541,6 +544,9 @@ class Api:
def get_sd_models(self):
return [{"title": x.title, "model_name": x.model_name, "hash": x.shorthash, "sha256": x.sha256, "filename": x.filename, "config": find_checkpoint_config_near_filename(x)} for x in checkpoints_list.values()]
+ def get_sd_vaes(self):
+ return [{"model_name": x, "filename": vae_dict[x]} for x in vae_dict.keys()]
+
def get_hypernetworks(self):
return [{"name": name, "path": shared.hypernetworks[name]} for name in shared.hypernetworks]
diff --git a/modules/api/models.py b/modules/api/models.py
index 1ff2fb33..47fdede2 100644
--- a/modules/api/models.py
+++ b/modules/api/models.py
@@ -249,6 +249,10 @@ class SDModelItem(BaseModel):
filename: str = Field(title="Filename")
config: Optional[str] = Field(title="Config file")
+class SDVaeItem(BaseModel):
+ model_name: str = Field(title="Model Name")
+ filename: str = Field(title="Filename")
+
class HypernetworkItem(BaseModel):
name: str = Field(title="Name")
path: Optional[str] = Field(title="Path")
diff --git a/modules/call_queue.py b/modules/call_queue.py
index 447bb764..dba2a9b4 100644
--- a/modules/call_queue.py
+++ b/modules/call_queue.py
@@ -1,10 +1,9 @@
import html
-import sys
import threading
-import traceback
import time
from modules import shared, progress
+from modules.errors import print_error
queue_lock = threading.Lock()
@@ -56,16 +55,14 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
try:
res = list(func(*args, **kwargs))
except Exception as e:
- # When printing out our debug argument list, do not print out more than a MB of text
- max_debug_str_len = 131072 # (1024*1024)/8
-
- print("Error completing request", file=sys.stderr)
- argStr = f"Arguments: {args} {kwargs}"
- print(argStr[:max_debug_str_len], file=sys.stderr)
- if len(argStr) > max_debug_str_len:
- print(f"(Argument list truncated at {max_debug_str_len}/{len(argStr)} characters)", file=sys.stderr)
-
- print(traceback.format_exc(), file=sys.stderr)
+ # When printing out our debug argument list,
+ # do not print out more than a 100 KB of text
+ max_debug_str_len = 131072
+ message = "Error completing request"
+ arg_str = f"Arguments: {args} {kwargs}"[:max_debug_str_len]
+ if len(arg_str) > max_debug_str_len:
+ arg_str += f" (Argument list truncated at {max_debug_str_len}/{len(arg_str)} characters)"
+ print_error(f"{message}\n{arg_str}", exc_info=True)
shared.state.job = ""
shared.state.job_count = 0
@@ -108,4 +105,3 @@ def wrap_gradio_call(func, extra_outputs=None, add_stats=False):
return tuple(res)
return f
-
diff --git a/modules/cmd_args.py b/modules/cmd_args.py
index 3eeb84d5..0974056d 100644
--- a/modules/cmd_args.py
+++ b/modules/cmd_args.py
@@ -11,7 +11,7 @@ parser.add_argument("--skip-python-version-check", action='store_true', help="la
parser.add_argument("--skip-torch-cuda-test", action='store_true', help="launch.py argument: do not check if CUDA is able to work properly")
parser.add_argument("--reinstall-xformers", action='store_true', help="launch.py argument: install the appropriate version of xformers even if you have some version already installed")
parser.add_argument("--reinstall-torch", action='store_true', help="launch.py argument: install the appropriate version of torch even if you have some version already installed")
-parser.add_argument("--update-check", action='store_true', help="launch.py argument: chck for updates at startup")
+parser.add_argument("--update-check", action='store_true', help="launch.py argument: check for updates at startup")
parser.add_argument("--test-server", action='store_true', help="launch.py argument: configure server for testing")
parser.add_argument("--skip-prepare-environment", action='store_true', help="launch.py argument: skip all environment preparation")
parser.add_argument("--skip-install", action='store_true', help="launch.py argument: skip installation of packages")
diff --git a/modules/codeformer_model.py b/modules/codeformer_model.py
index ececdbae..76143e9f 100644
--- a/modules/codeformer_model.py
+++ b/modules/codeformer_model.py
@@ -1,6 +1,4 @@
import os
-import sys
-import traceback
import cv2
import torch
@@ -8,6 +6,7 @@ import torch
import modules.face_restoration
import modules.shared
from modules import shared, devices, modelloader
+from modules.errors import print_error
from modules.paths import models_path
# codeformer people made a choice to include modified basicsr library to their project which makes
@@ -105,8 +104,8 @@ def setup_model(dirname):
restored_face = tensor2img(output, rgb2bgr=True, min_max=(-1, 1))
del output
torch.cuda.empty_cache()
- except Exception as error:
- print(f'\tFailed inference for CodeFormer: {error}', file=sys.stderr)
+ except Exception:
+ print_error('Failed inference for CodeFormer', exc_info=True)
restored_face = tensor2img(cropped_face_t, rgb2bgr=True, min_max=(-1, 1))
restored_face = restored_face.astype('uint8')
@@ -135,7 +134,6 @@ def setup_model(dirname):
shared.face_restorers.append(codeformer)
except Exception:
- print("Error setting up CodeFormer:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error("Error setting up CodeFormer", exc_info=True)
# sys.path = stored_sys_path
diff --git a/modules/config_states.py b/modules/config_states.py
index db65bcdb..faeaf28b 100644
--- a/modules/config_states.py
+++ b/modules/config_states.py
@@ -3,8 +3,6 @@ Supports saving and restoring webui and extensions from a known working set of c
"""
import os
-import sys
-import traceback
import json
import time
import tqdm
@@ -14,6 +12,7 @@ from collections import OrderedDict
import git
from modules import shared, extensions
+from modules.errors import print_error
from modules.paths_internal import script_path, config_states_dir
@@ -53,8 +52,7 @@ def get_webui_config():
if os.path.exists(os.path.join(script_path, ".git")):
webui_repo = git.Repo(script_path)
except Exception:
- print(f"Error reading webui git info from {script_path}:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error reading webui git info from {script_path}", exc_info=True)
webui_remote = None
webui_commit_hash = None
@@ -134,8 +132,7 @@ def restore_webui_config(config):
if os.path.exists(os.path.join(script_path, ".git")):
webui_repo = git.Repo(script_path)
except Exception:
- print(f"Error reading webui git info from {script_path}:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error reading webui git info from {script_path}", exc_info=True)
return
try:
@@ -143,8 +140,7 @@ def restore_webui_config(config):
webui_repo.git.reset(webui_commit_hash, hard=True)
print(f"* Restored webui to commit {webui_commit_hash}.")
except Exception:
- print(f"Error restoring webui to commit {webui_commit_hash}:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error restoring webui to commit{webui_commit_hash}")
def restore_extension_config(config):
diff --git a/modules/errors.py b/modules/errors.py
index da4694f8..41d8dc93 100644
--- a/modules/errors.py
+++ b/modules/errors.py
@@ -1,7 +1,23 @@
import sys
+import textwrap
import traceback
+def print_error(
+ message: str,
+ *,
+ exc_info: bool = False,
+) -> None:
+ """
+ Print an error message to stderr, with optional traceback.
+ """
+ for line in message.splitlines():
+ print("***", line, file=sys.stderr)
+ if exc_info:
+ print(textwrap.indent(traceback.format_exc(), " "), file=sys.stderr)
+ print("---")
+
+
def print_error_explanation(message):
lines = message.strip().split("\n")
max_len = max([len(x) for x in lines])
diff --git a/modules/extensions.py b/modules/extensions.py
index 624832a0..92f93ad9 100644
--- a/modules/extensions.py
+++ b/modules/extensions.py
@@ -1,11 +1,9 @@
import os
-import sys
import threading
-import traceback
-
-import git
from modules import shared
+from modules.errors import print_error
+from modules.gitpython_hack import Repo
from modules.paths_internal import extensions_dir, extensions_builtin_dir, script_path # noqa: F401
extensions = []
@@ -54,10 +52,9 @@ class Extension:
repo = None
try:
if os.path.exists(os.path.join(self.path, ".git")):
- repo = git.Repo(self.path)
+ repo = Repo(self.path)
except Exception:
- print(f"Error reading github repository info from {self.path}:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error reading github repository info from {self.path}", exc_info=True)
if repo is None or repo.bare:
self.remote = None
@@ -72,8 +69,8 @@ class Extension:
self.commit_hash = commit.hexsha
self.version = self.commit_hash[:8]
- except Exception as ex:
- print(f"Failed reading extension data from Git repository ({self.name}): {ex}", file=sys.stderr)
+ except Exception:
+ print_error(f"Failed reading extension data from Git repository ({self.name})", exc_info=True)
self.remote = None
self.have_info_from_repo = True
@@ -94,7 +91,7 @@ class Extension:
return res
def check_updates(self):
- repo = git.Repo(self.path)
+ repo = Repo(self.path)
for fetch in repo.remote().fetch(dry_run=True):
if fetch.flags != fetch.HEAD_UPTODATE:
self.can_update = True
@@ -116,7 +113,7 @@ class Extension:
self.status = "latest"
def fetch_and_reset_hard(self, commit='origin'):
- repo = git.Repo(self.path)
+ repo = Repo(self.path)
# Fix: `error: Your local changes to the following files would be overwritten by merge`,
# because WSL2 Docker set 755 file permissions instead of 644, this results to the error.
repo.git.fetch(all=True)
diff --git a/modules/gfpgan_model.py b/modules/gfpgan_model.py
index 0131dea4..d2f647fe 100644
--- a/modules/gfpgan_model.py
+++ b/modules/gfpgan_model.py
@@ -1,12 +1,11 @@
import os
-import sys
-import traceback
import facexlib
import gfpgan
import modules.face_restoration
from modules import paths, shared, devices, modelloader
+from modules.errors import print_error
model_dir = "GFPGAN"
user_path = None
@@ -112,5 +111,4 @@ def setup_model(dirname):
shared.face_restorers.append(FaceRestorerGFPGAN())
except Exception:
- print("Error setting up GFPGAN:", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error("Error setting up GFPGAN", exc_info=True)
diff --git a/modules/gitpython_hack.py b/modules/gitpython_hack.py
new file mode 100644
index 00000000..e537c1df
--- /dev/null
+++ b/modules/gitpython_hack.py
@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+import io
+import subprocess
+
+import git
+
+
+class Git(git.Git):
+ """
+ Git subclassed to never use persistent processes.
+ """
+
+ def _get_persistent_cmd(self, attr_name, cmd_name, *args, **kwargs):
+ raise NotImplementedError(f"Refusing to use persistent process: {attr_name} ({cmd_name} {args} {kwargs})")
+
+ def get_object_header(self, ref: str | bytes) -> tuple[str, str, int]:
+ ret = subprocess.check_output(
+ [self.GIT_PYTHON_GIT_EXECUTABLE, "cat-file", "--batch-check"],
+ input=self._prepare_ref(ref),
+ cwd=self._working_dir,
+ timeout=2,
+ )
+ return self._parse_object_header(ret)
+
+ def stream_object_data(self, ref: str) -> tuple[str, str, int, "Git.CatFileContentStream"]:
+ # Not really streaming, per se; this buffers the entire object in memory.
+ # Shouldn't be a problem for our use case, since we're only using this for
+ # object headers (commit objects).
+ ret = subprocess.check_output(
+ [self.GIT_PYTHON_GIT_EXECUTABLE, "cat-file", "--batch"],
+ input=self._prepare_ref(ref),
+ cwd=self._working_dir,
+ timeout=30,
+ )
+ bio = io.BytesIO(ret)
+ hexsha, typename, size = self._parse_object_header(bio.readline())
+ return (hexsha, typename, size, self.CatFileContentStream(size, bio))
+
+
+class Repo(git.Repo):
+ GitCommandWrapperType = Git
diff --git a/modules/hypernetworks/hypernetwork.py b/modules/hypernetworks/hypernetwork.py
index 570b5603..fcc1ef20 100644
--- a/modules/hypernetworks/hypernetwork.py
+++ b/modules/hypernetworks/hypernetwork.py
@@ -2,8 +2,6 @@ import datetime
import glob
import html
import os
-import sys
-import traceback
import inspect
import modules.textual_inversion.dataset
@@ -12,6 +10,7 @@ import tqdm
from einops import rearrange, repeat
from ldm.util import default
from modules import devices, processing, sd_models, shared, sd_samplers, hashes, sd_hijack_checkpoint
+from modules.errors import print_error
from modules.textual_inversion import textual_inversion, logging
from modules.textual_inversion.learn_schedule import LearnRateScheduler
from torch import einsum
@@ -325,17 +324,14 @@ def load_hypernetwork(name):
if path is None:
return None
- hypernetwork = Hypernetwork()
-
try:
+ hypernetwork = Hypernetwork()
hypernetwork.load(path)
+ return hypernetwork
except Exception:
- print(f"Error loading hypernetwork {path}", file=sys.stderr)
- print(traceback.format_exc(), file=sys.stderr)
+ print_error(f"Error loading hypernetwork {path}", exc_info=True)
return None
- return hypernetwork
-
def load_hypernetworks(names, multipliers=None):
already_loaded = {}
@@ -770,7 +766,7 @@ Last saved image: {html.escape(last_saved_image)}