diff --git a/How to install and run.txt b/How to install and run.txt index af783b64..8e83ab7c 100644 --- a/How to install and run.txt +++ b/How to install and run.txt @@ -5,10 +5,10 @@ If you haven't downloaded Stable Diffusion UI yet, please download from https:// After downloading, to install please follow these instructions: For Windows: -- Please double-click the "Start Stable Diffusion UI.cmd" file inside the "stable-diffusion-ui" folder. +- Please double-click the "Easy-Diffusion-Windows.exe" file and follow the instructions. For Linux: -- Please open a terminal, and go to the "stable-diffusion-ui" directory. Then run ./start.sh +- Please open a terminal, unzip the Easy-Diffusion-Linux.zip file and go to the "easy-diffusion" directory. Then run ./start.sh That file will automatically install everything. After that it will start the Stable Diffusion interface in a web browser. @@ -21,4 +21,4 @@ If you have any problems, please: 3. Or, file an issue at https://github.com/easydiffusion/easydiffusion/issues Thanks -cmdr2 (and contributors to the project) \ No newline at end of file +cmdr2 (and contributors to the project) diff --git a/README.md b/README.md index b97c35d1..8acafd76 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Does not require technical knowledge, does not require pre-installed software. 1 Click the download button for your operating system:

- - - + + +

**Hardware requirements:** @@ -23,6 +23,7 @@ Click the download button for your operating system: - Minimum 8 GB of system RAM. - Atleast 25 GB of space on the hard disk. + The installer will take care of whatever is needed. If you face any problems, you can join the friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) and ask for assistance. ## On Windows: @@ -132,6 +133,15 @@ We could really use help on these aspects (click to view tasks that need your he If you have any code contributions in mind, please feel free to say Hi to us on the [discord server](https://discord.com/invite/u9yhsFmEkB). We use the Discord server for development-related discussions, and for helping users. +# Credits +* Stable Diffusion: https://github.com/Stability-AI/stablediffusion +* CodeFormer: https://github.com/sczhou/CodeFormer (license: https://github.com/sczhou/CodeFormer/blob/master/LICENSE) +* GFPGAN: https://github.com/TencentARC/GFPGAN +* RealESRGAN: https://github.com/xinntao/Real-ESRGAN +* k-diffusion: https://github.com/crowsonkb/k-diffusion +* Code contributors and artists on the cmdr2 UI: https://github.com/cmdr2/stable-diffusion-ui and Discord (https://discord.com/invite/u9yhsFmEkB) +* Lots of contributors on the internet + # Disclaimer The authors of this project are not responsible for any content generated using this interface. diff --git a/scripts/check_modules.py b/scripts/check_modules.py index bc043c7c..d549a16d 100644 --- a/scripts/check_modules.py +++ b/scripts/check_modules.py @@ -18,7 +18,7 @@ os_name = platform.system() modules_to_check = { "torch": ("1.11.0", "1.13.1", "2.0.0"), "torchvision": ("0.12.0", "0.14.1", "0.15.1"), - "sdkit": "1.0.156", + "sdkit": "1.0.165", "stable-diffusion-sdkit": "2.1.4", "rich": "12.6.0", "uvicorn": "0.19.0", diff --git a/ui/easydiffusion/model_manager.py b/ui/easydiffusion/model_manager.py index 63f79859..845e9126 100644 --- a/ui/easydiffusion/model_manager.py +++ b/ui/easydiffusion/model_manager.py @@ -148,7 +148,7 @@ def reload_models_if_necessary(context: Context, models_data: ModelsData, models models_to_reload = { model_type: path for model_type, path in models_data.model_paths.items() - if context.model_paths.get(model_type) != path + if context.model_paths.get(model_type) != path or (path is not None and context.models.get(model_type) is None) } if models_data.model_paths.get("codeformer"): diff --git a/ui/easydiffusion/tasks/render_images.py b/ui/easydiffusion/tasks/render_images.py index 8df208b6..bdf6e3ac 100644 --- a/ui/easydiffusion/tasks/render_images.py +++ b/ui/easydiffusion/tasks/render_images.py @@ -15,6 +15,7 @@ from sdkit.utils import ( img_to_base64_str, img_to_buffer, latent_samples_to_images, + log, ) from .task import Task @@ -93,15 +94,27 @@ class RenderTask(Task): return model["params"].get(param_name) != new_val def trt_needs_reload(self, context): - if not self.has_param_changed(context, "convert_to_tensorrt"): + if not context.test_diffusers: return False + if "stable-diffusion" not in context.models or "params" not in context.models["stable-diffusion"]: + return True model = context.models["stable-diffusion"] - pipe = model["default"] - if hasattr(pipe.unet, "_allocate_trt_buffers"): # TRT already loaded - return False - return True + # curr_convert_to_trt = model["params"].get("convert_to_tensorrt") + new_convert_to_trt = self.models_data.model_params.get("stable-diffusion", {}).get("convert_to_tensorrt", False) + + pipe = model["default"] + is_trt_loaded = hasattr(pipe.unet, "_allocate_trt_buffers") or hasattr( + pipe.unet, "_allocate_trt_buffers_backup" + ) + if new_convert_to_trt and not is_trt_loaded: + return True + + curr_build_config = model["params"].get("trt_build_config") + new_build_config = self.models_data.model_params.get("stable-diffusion", {}).get("trt_build_config", {}) + + return new_convert_to_trt and curr_build_config != new_build_config def make_images( @@ -210,17 +223,29 @@ def generate_images_internal( if req.init_image is not None and not context.test_diffusers: req.sampler_name = "ddim" + req.width, req.height = map(lambda x: x - x % 8, (req.width, req.height)) # clamp to 8 + if req.control_image and task_data.control_filter_to_apply: req.control_image = filter_images(context, req.control_image, task_data.control_filter_to_apply)[0] if context.test_diffusers: pipe = context.models["stable-diffusion"]["default"] + if hasattr(pipe.unet, "_allocate_trt_buffers_backup"): + setattr(pipe.unet, "_allocate_trt_buffers", pipe.unet._allocate_trt_buffers_backup) + delattr(pipe.unet, "_allocate_trt_buffers_backup") + if hasattr(pipe.unet, "_allocate_trt_buffers"): convert_to_trt = models_data.model_params["stable-diffusion"].get("convert_to_tensorrt", False) - pipe.unet.forward = pipe.unet._trt_forward if convert_to_trt else pipe.unet._non_trt_forward - # pipe.vae.decoder.forward = ( - # pipe.vae.decoder._trt_forward if convert_to_trt else pipe.vae.decoder._non_trt_forward - # ) + if convert_to_trt: + pipe.unet.forward = pipe.unet._trt_forward + # pipe.vae.decoder.forward = pipe.vae.decoder._trt_forward + log.info(f"Setting unet.forward to TensorRT") + else: + log.info(f"Not using TensorRT for unet.forward") + pipe.unet.forward = pipe.unet._non_trt_forward + # pipe.vae.decoder.forward = pipe.vae.decoder._non_trt_forward + setattr(pipe.unet, "_allocate_trt_buffers_backup", pipe.unet._allocate_trt_buffers) + delattr(pipe.unet, "_allocate_trt_buffers") images = generate_images(context, callback=callback, **req.dict()) user_stopped = False diff --git a/ui/easydiffusion/types.py b/ui/easydiffusion/types.py index 181a9505..fe936ca2 100644 --- a/ui/easydiffusion/types.py +++ b/ui/easydiffusion/types.py @@ -226,6 +226,9 @@ def convert_legacy_render_req_to_new(old_req: dict): model_params["stable-diffusion"] = { "clip_skip": bool(old_req.get("clip_skip", False)), "convert_to_tensorrt": bool(old_req.get("convert_to_tensorrt", False)), + "trt_build_config": old_req.get( + "trt_build_config", {"batch_size_range": (1, 1), "dimensions_range": [(768, 1024)]} + ), } # move the filter params diff --git a/ui/easydiffusion/utils/save_utils.py b/ui/easydiffusion/utils/save_utils.py index 49743554..89dae991 100644 --- a/ui/easydiffusion/utils/save_utils.py +++ b/ui/easydiffusion/utils/save_utils.py @@ -21,6 +21,8 @@ TASK_TEXT_MAPPING = { "seed": "Seed", "use_stable_diffusion_model": "Stable Diffusion model", "clip_skip": "Clip Skip", + "use_controlnet_model": "ControlNet model", + "control_filter_to_apply": "ControlNet Filter", "use_vae_model": "VAE model", "sampler_name": "Sampler", "width": "Width", @@ -260,10 +262,12 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output del metadata["lora_alpha"] if task_data.use_upscale != "latent_upscaler" and "latent_upscaler_steps" in metadata: del metadata["latent_upscaler_steps"] + if task_data.use_controlnet_model is None and "control_filter_to_apply" in metadata: + del metadata["control_filter_to_apply"] if not using_diffusers: for key in ( - x for x in ["use_lora_model", "lora_alpha", "clip_skip", "tiling", "latent_upscaler_steps"] if x in metadata + x for x in ["use_lora_model", "lora_alpha", "clip_skip", "tiling", "latent_upscaler_steps", "use_controlnet_model", "control_filter_to_apply"] if x in metadata ): del metadata[key] diff --git a/ui/index.html b/ui/index.html index 2e158780..faee0ff2 100644 --- a/ui/index.html +++ b/ui/index.html @@ -163,60 +163,63 @@ Click to learn more about Clip Skip - -
- - - -
- - Click to learn more about ControlNets -
- - -
- -
- -
- + + + +
+ + + +
+ + Click to learn more about ControlNets +
+ + +
+ +
+ +
+ + Click to learn more about VAEs @@ -323,7 +326,7 @@ - + diff --git a/ui/media/css/main.css b/ui/media/css/main.css index b6dc85c4..be3e92f0 100644 --- a/ui/media/css/main.css +++ b/ui/media/css/main.css @@ -1852,4 +1852,9 @@ div#enlarge-buttons { } #controlnet_model { width: 77%; +} + +/* hack for fixing Image Modifier Improvements plugin */ +#imageTagPopupContainer { + position: absolute; } \ No newline at end of file diff --git a/ui/media/js/auto-save.js b/ui/media/js/auto-save.js index ebdf4927..5ada13d5 100644 --- a/ui/media/js/auto-save.js +++ b/ui/media/js/auto-save.js @@ -55,6 +55,7 @@ const SETTINGS_IDS_LIST = [ "zip_toggle", "tree_toggle", "json_toggle", + "extract_lora_from_prompt", ] const IGNORE_BY_DEFAULT = ["prompt"] diff --git a/ui/media/js/main.js b/ui/media/js/main.js index 47c5da00..f340a79c 100644 --- a/ui/media/js/main.js +++ b/ui/media/js/main.js @@ -186,6 +186,8 @@ let undoBuffer = [] const UNDO_LIMIT = 20 const MAX_IMG_UNDO_ENTRIES = 5 +let IMAGE_STEP_SIZE = 64 + let loraModels = [] imagePreview.addEventListener("drop", function(ev) { @@ -1453,15 +1455,21 @@ function getCurrentUserRequest() { let numOutputsParallel = parseInt(numOutputsParallelField.value) const seed = randomSeedField.checked ? Math.floor(Math.random() * (2 ** 32 - 1)) : parseInt(seedField.value) - if ( - testDiffusers.checked && - document.getElementById("toggle-tensorrt-install").innerHTML == "Uninstall" && - document.querySelector("#convert_to_tensorrt").checked - ) { - // TRT enabled + // if ( + // testDiffusers.checked && + // document.getElementById("toggle-tensorrt-install").innerHTML == "Uninstall" && + // document.querySelector("#convert_to_tensorrt").checked + // ) { + // // TRT enabled - numOutputsParallel = 1 // force 1 parallel - } + // numOutputsParallel = 1 // force 1 parallel + // } + + // clamp to multiple of 8 + let width = parseInt(widthField.value) + let height = parseInt(heightField.value) + width = width - (width % IMAGE_STEP_SIZE) + height = height - (height % IMAGE_STEP_SIZE) const newTask = { batchesDone: 0, @@ -1475,8 +1483,8 @@ function getCurrentUserRequest() { num_outputs: numOutputsParallel, num_inference_steps: parseInt(numInferenceStepsField.value), guidance_scale: parseFloat(guidanceScaleField.value), - width: parseInt(widthField.value), - height: parseInt(heightField.value), + width: width, + height: height, // allow_nsfw: allowNSFWField.checked, vram_usage_level: vramUsageLevelField.value, sampler_name: samplerField.value, @@ -1550,6 +1558,22 @@ function getCurrentUserRequest() { if (testDiffusers.checked && document.getElementById("toggle-tensorrt-install").innerHTML == "Uninstall") { // TRT is installed newTask.reqBody.convert_to_tensorrt = document.querySelector("#convert_to_tensorrt").checked + let trtBuildConfig = { + batch_size_range: [ + parseInt(document.querySelector("#trt-build-min-batch").value), + parseInt(document.querySelector("#trt-build-max-batch").value), + ], + dimensions_range: [], + } + + let sizes = [512, 768, 1024, 1280, 1536] + sizes.forEach((i) => { + let el = document.querySelector("#trt-build-res-" + i) + if (el.checked) { + trtBuildConfig["dimensions_range"].push([i, i + 256]) + } + }) + newTask.reqBody.trt_build_config = trtBuildConfig } if (controlnetModelField.value !== "" && IMAGE_REGEX.test(controlImagePreview.src)) { newTask.reqBody.use_controlnet_model = controlnetModelField.value @@ -2238,6 +2262,7 @@ function checkRandomSeed() { randomSeedField.addEventListener("input", checkRandomSeed) checkRandomSeed() +// warning: the core plugin `image-editor-improvements.js:172` replaces loadImg2ImgFromFile() with a custom version function loadImg2ImgFromFile() { if (initImageSelector.files.length === 0) { return @@ -2320,6 +2345,9 @@ controlImageSelector.addEventListener("change", loadControlnetImageFromFile) function controlImageLoad() { let w = controlImagePreview.naturalWidth let h = controlImagePreview.naturalHeight + w = w - (w % IMAGE_STEP_SIZE) + h = h - (h % IMAGE_STEP_SIZE) + addImageSizeOption(w) addImageSizeOption(h) @@ -2481,6 +2509,7 @@ function packagesUpdate(event) { if (document.getElementById("toggle-tensorrt-install").innerHTML == "Uninstall") { document.querySelector("#enable_trt_config").classList.remove("displayNone") + document.querySelector("#trt-build-config").classList.remove("displayNone") if (!trtSettingsForced) { // settings for demo @@ -2492,8 +2521,8 @@ function packagesUpdate(event) { seedField.disabled = false stableDiffusionModelField.value = "sd-v1-4" - numOutputsParallelField.classList.add("displayNone") - document.querySelector("#num_outputs_parallel_label").classList.add("displayNone") + // numOutputsParallelField.classList.add("displayNone") + // document.querySelector("#num_outputs_parallel_label").classList.add("displayNone") trtSettingsForced = true } diff --git a/ui/media/js/parameters.js b/ui/media/js/parameters.js index caab8817..51d73c7a 100644 --- a/ui/media/js/parameters.js +++ b/ui/media/js/parameters.js @@ -121,6 +121,15 @@ var PARAMETERS = [ icon: "fa-arrow-down-short-wide", default: false, }, + { + id: "extract_lora_from_prompt", + type: ParameterType.checkbox, + label: "Extract LoRA tags from the prompt", + note: + "Automatically extract lora tags like <lora:name:0.4> from the prompt, and apply the correct LoRA (if present)", + icon: "fa-code", + default: true, + }, { id: "ui_open_browser_on_start", type: ParameterType.checkbox, @@ -258,7 +267,19 @@ var PARAMETERS = [ label: "NVIDIA TensorRT", note: `Faster image generation by converting your Stable Diffusion models to the NVIDIA TensorRT format. You can choose the models to convert. Download size: approximately 2 GB.

- Early access version: support for LoRA is still under development.`, + Early access version: support for LoRA is still under development. +
+

Build Config:

+ Batch size range: + +

+ Build for resolutions:
+ 512x512 to 768x768
+ 768x768 to 1024x1024
+ 1024x1024 to 1280x1280
+ 1280x1280 to 1536x1536
+ 1536x1536 to 1792x1792
+
`, icon: "fa-angles-up", render: () => '', table: installExtrasTable, @@ -460,15 +481,22 @@ async function getAppConfig() { if (!testDiffusersEnabled) { document.querySelector("#lora_model_container").style.display = "none" document.querySelector("#tiling_container").style.display = "none" + document.querySelector("#controlnet_model_container").style.display = "none" + document.querySelector("#hypernetwork_model_container").style.display = "" + document.querySelector("#hypernetwork_strength_container").style.display = "" document.querySelectorAll("#sampler_name option.diffusers-only").forEach((option) => { option.style.display = "none" }) - customWidthField.step=64 - customHeightField.step=64 + IMAGE_STEP_SIZE = 64 + customWidthField.step = IMAGE_STEP_SIZE + customHeightField.step = IMAGE_STEP_SIZE } else { document.querySelector("#lora_model_container").style.display = "" document.querySelector("#tiling_container").style.display = "" + document.querySelector("#controlnet_model_container").style.display = "" + document.querySelector("#hypernetwork_model_container").style.display = "none" + document.querySelector("#hypernetwork_strength_container").style.display = "none" document.querySelectorAll("#sampler_name option.k_diffusion-only").forEach((option) => { option.style.display = "none" @@ -476,8 +504,9 @@ async function getAppConfig() { document.querySelector("#clip_skip_config").classList.remove("displayNone") document.querySelector("#embeddings-button").classList.remove("displayNone") document.querySelector("#negative-embeddings-button").classList.remove("displayNone") - customWidthField.step=8 - customHeightField.step=8 + IMAGE_STEP_SIZE = 8 + customWidthField.step = IMAGE_STEP_SIZE + customHeightField.step = IMAGE_STEP_SIZE } console.log("get config status response", config) diff --git a/ui/plugins/ui/image-editor-improvements.plugin.js b/ui/plugins/ui/image-editor-improvements.plugin.js index 7435df8b..fbc7ad80 100644 --- a/ui/plugins/ui/image-editor-improvements.plugin.js +++ b/ui/plugins/ui/image-editor-improvements.plugin.js @@ -124,35 +124,17 @@ // Draw the image with centered coordinates context.drawImage(imageObj, x, y, this.width, this.height); - initImagePreview.src = canvas.toDataURL('image/png'); + let bestWidth = maxCroppedWidth - maxCroppedWidth % IMAGE_STEP_SIZE + let bestHeight = maxCroppedHeight - maxCroppedHeight % IMAGE_STEP_SIZE - // Get the options from widthField and heightField - const widthOptions = Array.from(widthField.options).map(option => parseInt(option.value)); - const heightOptions = Array.from(heightField.options).map(option => parseInt(option.value)); - - // Find the closest aspect ratio and closest to original dimensions - let bestWidth = widthOptions[0]; - let bestHeight = heightOptions[0]; - let minDifference = Math.abs(maxCroppedWidth / maxCroppedHeight - bestWidth / bestHeight); - let minDistance = Math.abs(maxCroppedWidth - bestWidth) + Math.abs(maxCroppedHeight - bestHeight); - - for (const width of widthOptions) { - for (const height of heightOptions) { - const difference = Math.abs(maxCroppedWidth / maxCroppedHeight - width / height); - const distance = Math.abs(maxCroppedWidth - width) + Math.abs(maxCroppedHeight - height); - - if (difference < minDifference || (difference === minDifference && distance < minDistance)) { - minDifference = difference; - minDistance = distance; - bestWidth = width; - bestHeight = height; - } - } - } + addImageSizeOption(bestWidth) + addImageSizeOption(bestHeight) // Set the width and height to the closest aspect ratio and closest to original dimensions widthField.value = bestWidth; heightField.value = bestHeight; + + initImagePreview.src = canvas.toDataURL('image/png'); }; function handlePaste(e) { diff --git a/ui/plugins/ui/lora-prompt-parser.plugin.js b/ui/plugins/ui/lora-prompt-parser.plugin.js new file mode 100644 index 00000000..201d49af --- /dev/null +++ b/ui/plugins/ui/lora-prompt-parser.plugin.js @@ -0,0 +1,119 @@ +/* + LoRA Prompt Parser 1.0 + by Patrice + + Copying and pasting a prompt with a LoRA tag will automatically select the corresponding option in the Easy Diffusion dropdown and remove the LoRA tag from the prompt. The LoRA must be already available in the corresponding Easy Diffusion dropdown (this is not a LoRA downloader). +*/ +(function() { + "use strict" + + promptField.addEventListener('input', function(e) { + let loraExtractSetting = document.getElementById("extract_lora_from_prompt") + if (!loraExtractSetting.checked) { + return + } + + const { LoRA, prompt } = extractLoraTags(e.target.value); + //console.log('e.target: ' + JSON.stringify(LoRA)); + + if (LoRA !== null && LoRA.length > 0) { + promptField.value = prompt.replace(/,+$/, ''); // remove any trailing , + + if (testDiffusers?.checked === false) { + showToast("LoRA's are only supported with diffusers. Just stripping the LoRA tag from the prompt.") + } + } + + if (LoRA !== null && LoRA.length > 0 && testDiffusers?.checked) { + for (let i = 0; i < LoRA.length; i++) { + //if (loraModelField.value !== LoRA[0].lora_model) { + // Set the new LoRA value + //console.log("Loading info"); + //console.log(LoRA[0].lora_model_0); + //console.log(JSON.stringify(LoRa)); + + let lora = `lora_model_${i}`; + let alpha = `lora_alpha_${i}`; + let loramodel = document.getElementById(lora); + let alphavalue = document.getElementById(alpha); + loramodel.setAttribute("data-path", LoRA[i].lora_model_0); + loramodel.value = LoRA[i].lora_model_0; + alphavalue.value = LoRA[i].lora_alpha_0; + if (i != LoRA.length - 1) + createLoraEntry(); + } + //loraAlphaSlider.value = loraAlphaField.value * 100; + //TBD.value = LoRA[0].blockweights; // block weights not supported by ED at this time + //} + showToast("Prompt successfully processed", LoRA[0].lora_model_0); + //console.log('LoRa: ' + LoRA[0].lora_model_0); + //showToast("Prompt successfully processed", lora_model_0.value); + + } + + //promptField.dispatchEvent(new Event('change')); + }); + + function isModelAvailable(array, searchString) { + const foundItem = array.find(function(item) { + item = item.toString().toLowerCase(); + return item === searchString.toLowerCase() + }); + + return foundItem || ""; + } + + // extract LoRA tags from strings + function extractLoraTags(prompt) { + // Define the regular expression for the tags + const regex = /<(?:lora|lyco):([^:>]+)(?::([^:>]*))?(?::([^:>]*))?>/gi + + // Initialize an array to hold the matches + let matches = [] + + // Iterate over the string, finding matches + for (const match of prompt.matchAll(regex)) { + const modelFileName = isModelAvailable(modelsCache.options.lora, match[1].trim()) + if (modelFileName !== "") { + // Initialize an object to hold a match + let loraTag = { + lora_model_0: modelFileName, + } + //console.log("Model:" + modelFileName); + + // If weight is provided, add it to the loraTag object + if (match[2] !== undefined && match[2] !== '') { + loraTag.lora_alpha_0 = parseFloat(match[2].trim()) + } + else + { + loraTag.lora_alpha_0 = 0.5 + } + + + // If blockweights are provided, add them to the loraTag object + if (match[3] !== undefined && match[3] !== '') { + loraTag.blockweights = match[3].trim() + } + + // Add the loraTag object to the array of matches + matches.push(loraTag); + //console.log(JSON.stringify(matches)); + } + else + { + showToast("LoRA not found: " + match[1].trim(), 5000, true) + } + } + + // Clean up the prompt string, e.g. from "apple, banana, , orange, , pear , " to "apple, banana, orange, pear" + let cleanedPrompt = prompt.replace(regex, '').replace(/(\s*,\s*(?=\s*,|$))|(^\s*,\s*)|\s+/g, ' ').trim(); + //console.log('Matches: ' + JSON.stringify(matches)); + + // Return the array of matches and cleaned prompt string + return { + LoRA: matches, + prompt: cleanedPrompt + } + } +})()