Merge pull request #1527 from easydiffusion/beta

v3
This commit is contained in:
cmdr2 2023-08-29 12:27:35 +05:30 committed by GitHub
commit c8de1cd49b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3824 additions and 3394 deletions

View File

@ -712,3 +712,411 @@ FileSaver.js is licensed under the MIT license:
SOFTWARE.
[1]: http://eligrey.com
croppr.js
=========
https://github.com/jamesssooi/Croppr.js
croppr.js is licensed under the MIT license:
MIT License
Copyright (c) 2017 James Ooi
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.
ExifReader
==========
https://github.com/mattiasw/ExifReader
ExifReader is licensed under the Mozilla Public License:
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -1,5 +1,44 @@
# What's new?
## v3.0
### Major Changes
- **ControlNet** - Full support for ControlNet, with native integration of the common ControlNet models. Just select a control image, then choose the ControlNet filter/model and run. No additional configuration or download necessary. Supports custom ControlNets as well.
- **SDXL** - Full support for SDXL. No configuration necessary, just put the SDXL model in the `models/stable-diffusion` folder.
- **Multiple LoRAs** - Use multiple LoRAs, including SDXL and SD2-compatible LoRAs. Put them in the `models/lora` folder.
- **Embeddings** - Use textual inversion embeddings easily, by putting them in the `models/embeddings` folder and using their names in the prompt (or by clicking the `+ Embeddings` button to select embeddings visually). Thanks @JeLuf.
- **Seamless Tiling** - Generate repeating textures that can be useful for games and other art projects. Works best in 512x512 resolution. Thanks @JeLuf.
- **Inpainting Models** - Full support for inpainting models, including custom inpainting models. No configuration (or yaml files) necessary.
- **Faster than v2.5** - Nearly 40% faster than Easy Diffusion v2.5, and can be even faster if you enable xFormers.
- **Even less VRAM usage** - Less than 2 GB for 512x512 images on 'low' VRAM usage setting (SD 1.5). Can generate large images with SDXL.
- **WebP images** - Supports saving images in the lossless webp format.
- **Undo/Redo in the UI** - Remove tasks or images from the queue easily, and undo the action if you removed anything accidentally. Thanks @JeLuf.
- **Three new samplers, and latent upscaler** - Added `DEIS`, `DDPM` and `DPM++ 2m SDE` as additional samplers. Thanks @ogmaresca and @rbertus2000.
- **Significantly faster 'Upscale' and 'Fix Faces' buttons on the images**
- **Major rewrite of the code** - We've switched to using diffusers under-the-hood, which allows us to release new features faster, and focus on making the UI and installer even easier to use.
### Detailed changelog
* 3.0.2 - 29 Aug 2023 - Fixed incorrect matching of embeddings from prompts.
* 3.0.2 - 24 Aug 2023 - Fix broken seamless tiling.
* 3.0.2 - 23 Aug 2023 - Fix styling on mobile devices.
* 3.0.2 - 22 Aug 2023 - Full support for inpainting models, including custom models. Support SD 1.x and SD 2.x inpainting models. Does not require you to specify a yaml config file.
* 3.0.2 - 22 Aug 2023 - Reduce VRAM consumption of controlnet in 'low' VRAM mode, and allow accelerating controlnets using xformers.
* 3.0.2 - 22 Aug 2023 - Improve auto-detection of SD 2.0 and 2.1 models, removing the need for custom yaml files for SD 2.x models. Improve the model load time by speeding-up the black image test.
* 3.0.1 - 18 Aug 2023 - Rotate an image if EXIF rotation is present. For e.g. this is common in images taken with a smartphone.
* 3.0.1 - 18 Aug 2023 - Resize control images to the task dimensions, to avoid memory errors with high-res control images.
* 3.0.1 - 18 Aug 2023 - Show controlnet filter preview in the task entry.
* 3.0.1 - 18 Aug 2023 - Fix drag-and-drop and 'Use these Settings' for LoRA and ControlNet.
* 3.0.1 - 18 Aug 2023 - Auto-save LoRA models and strengths.
* 3.0.1 - 17 Aug 2023 - Automatically use the correct yaml config file for custom SDXL models, even if a yaml file isn't present in the folder.
* 3.0.1 - 17 Aug 2023 - Fix broken embeddings with SDXL.
* 3.0.1 - 16 Aug 2023 - Fix broken LoRA with SDXL.
* 3.0.1 - 15 Aug 2023 - Fix broken seamless tiling.
* 3.0.1 - 15 Aug 2023 - Fix textual inversion embeddings not working in `low` VRAM usage mode.
* 3.0.1 - 15 Aug 2023 - Fix for custom VAEs not working in `low` VRAM usage mode.
* 3.0.1 - 14 Aug 2023 - Slider to change the image dimensions proportionally (in Image Settings). Thanks @JeLuf.
* 3.0.1 - 14 Aug 2023 - Show an error to the user if an embedding isn't compatible with the model, instead of failing silently without informing the user. Thanks @JeLuf.
* 3.0.1 - 14 Aug 2023 - Disable watermarking for SDXL img2img. Thanks @AvidGameFan.
* 3.0.0 - 3 Aug 2023 - Enabled diffusers for everyone by default. The old v2 engine can be used by disabling the "Use v3 engine" option in the Settings tab.
## v2.5
### Major Changes
- **Nearly twice as fast** - significantly faster speed of image generation. Code contributions are welcome to make our project even faster: https://github.com/easydiffusion/sdkit/#is-it-fast

View File

@ -1,11 +1,13 @@
# Easy Diffusion 2.5
# Easy Diffusion 3.0
### The easiest way to install and use [Stable Diffusion](https://github.com/CompVis/stable-diffusion) on your computer.
Does not require technical knowledge, does not require pre-installed software. 1-click install, powerful features, friendly community.
[Installation guide](#installation) | [Troubleshooting guide](https://github.com/easydiffusion/easydiffusion/wiki/Troubleshooting) | <sub>[![Discord Server](https://img.shields.io/discord/1014774730907209781?label=Discord)](https://discord.com/invite/u9yhsFmEkB)</sub> <sup>(for support queries, and development discussions)</sup>
![t2i](https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/assets/stable-samples/txt2img/768/merged-0006.png)
---
![262597678-11089485-2514-4a11-88fb-c3acc81fc9ec](https://github.com/easydiffusion/easydiffusion/assets/844287/050b5e15-e909-45bf-8162-a38234830e38)
# Installation
Click the download button for your operating system:
@ -62,17 +64,19 @@ Just delete the `EasyDiffusion` folder to uninstall all the downloaded packages.
- **UI Themes**: Customize the program to your liking.
- **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
### Image generation
- **Supports**: "*Text to Image*" and "*Image to Image*".
- **21 Samplers**: `ddim`, `plms`, `heun`, `euler`, `euler_a`, `dpm2`, `dpm2_a`, `lms`, `dpm_solver_stability`, `dpmpp_2s_a`, `dpmpp_2m`, `dpmpp_sde`, `dpm_fast`, `dpm_adaptive`, `ddpm`, `deis`, `unipc_snr`, `unipc_tu`, `unipc_tq`, `unipc_snr_2`, `unipc_tu_2`.
- **In-Painting**: Specify areas of your image to paint into.
### Powerful image generation
- **Supports**: "*Text to Image*", "*Image to Image*" and "*InPainting*"
- **ControlNet**: For advanced control over the image, e.g. by setting the pose or drawing the outline for the AI to fill in.
- **16 Samplers**: `PLMS`, `DDIM`, `DEIS`, `Heun`, `Euler`, `Euler Ancestral`, `DPM2`, `DPM2 Ancestral`, `LMS`, `DPM Solver`, `DPM++ 2s Ancestral`, `DPM++ 2m`, `DPM++ 2m SDE`, `DPM++ SDE`, `DDPM`, `UniPC`.
- **Stable Diffusion XL and 2.1**: Generate higher-quality images using the latest Stable Diffusion XL models.
- **Textual Inversion Embeddings**: For guiding the AI strongly towards a particular concept.
- **Simple Drawing Tool**: Draw basic images to guide the AI, without needing an external drawing program.
- **Face Correction (GFPGAN)**
- **Upscaling (RealESRGAN)**
- **Loopback**: Use the output image as the input image for the next img2img task.
- **Loopback**: Use the output image as the input image for the next image task.
- **Negative Prompt**: Specify aspects of the image to *remove*.
- **Attention/Emphasis**: () in the prompt increases the model's attention to enclosed words, and [] decreases it.
- **Weighted Prompts**: Use weights for specific words in your prompt to change their importance, e.g. `red:2.4 dragon:1.2`.
- **Attention/Emphasis**: `+` in the prompt increases the model's attention to enclosed words, and `-` decreases it. E.g. `apple++ falling from a tree`.
- **Weighted Prompts**: Use weights for specific words in your prompt to change their importance, e.g. `(red)2.4 (dragon)1.2`.
- **Prompt Matrix**: Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut riding a horse | illustration | cinematic lighting`.
- **Prompt Set**: Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut on the {moon,earth}`
- **1-click Upscale/Face Correction**: Upscale or correct an image after it has been generated.
@ -82,10 +86,11 @@ Just delete the `EasyDiffusion` folder to uninstall all the downloaded packages.
### Advanced features
- **Custom Models**: Use your own `.ckpt` or `.safetensors` file, by placing it inside the `models/stable-diffusion` folder!
- **Stable Diffusion 2.1 support**
- **Stable Diffusion XL and 2.1 support**
- **Merge Models**
- **Use custom VAE models**
- **Use pre-trained Hypernetworks**
- **Textual Inversion Embeddings**
- **ControlNet**
- **Use custom GFPGAN models**
- **UI Plugins**: Choose from a growing list of [community-generated UI plugins](https://github.com/easydiffusion/easydiffusion/wiki/UI-Plugins), or write your own plugin to add features to the project!
@ -103,18 +108,8 @@ Just delete the `EasyDiffusion` folder to uninstall all the downloaded packages.
----
## Easy for new users:
![Screenshot of the initial UI](https://user-images.githubusercontent.com/844287/217043152-29454d15-0387-4228-b70d-9a4b84aeb8ba.png)
## Powerful features for advanced users:
![Screenshot of advanced settings](https://user-images.githubusercontent.com/844287/217042588-fc53c975-bacd-4a9c-af88-37408734ade3.png)
## Live Preview
Useful for judging (and stopping) an image quickly, without waiting for it to finish rendering.
![live-512](https://user-images.githubusercontent.com/844287/192097249-729a0a1e-a677-485e-9ccc-16a9e848fabe.gif)
## Easy for new users, powerful features for advanced users:
![image](https://github.com/easydiffusion/easydiffusion/assets/844287/efbbac9f-42ce-4aef-8625-fd23c74a8241)
## Task Queue
![Screenshot of task queue](https://user-images.githubusercontent.com/844287/217043984-0b35f73b-1318-47cb-9eed-a2a91b430490.png)
@ -128,12 +123,6 @@ Please refer to our [guide](https://github.com/easydiffusion/easydiffusion/wiki/
# Bugs reports and code contributions welcome
If there are any problems or suggestions, please feel free to ask on the [discord server](https://discord.com/invite/u9yhsFmEkB) or [file an issue](https://github.com/easydiffusion/easydiffusion/issues).
We could really use help on these aspects (click to view tasks that need your help):
* [User Interface](https://github.com/users/cmdr2/projects/1/views/1)
* [Engine](https://github.com/users/cmdr2/projects/3/views/1)
* [Installer](https://github.com/users/cmdr2/projects/4/views/1)
* [Documentation](https://github.com/users/cmdr2/projects/5/views/1)
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

BIN
patch.patch Normal file

Binary file not shown.

View File

@ -18,13 +18,15 @@ 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.165",
"sdkit": "2.0.3",
"stable-diffusion-sdkit": "2.1.4",
"rich": "12.6.0",
"uvicorn": "0.19.0",
"fastapi": "0.85.1",
"pycloudflared": "0.2.0",
"ruamel.yaml": "0.17.21",
"sqlalchemy": "2.0.19",
"python-multipart": "0.0.6",
# "xformers": "0.0.16",
}
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]

View File

@ -46,6 +46,8 @@ if "%update_branch%"=="" (
@cd sd-ui-files
@call git add -A .
@call git stash
@call git reset --hard
@call git -c advice.detachedHead=false checkout "%update_branch%"
@call git pull

View File

@ -29,6 +29,8 @@ if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/in
cd sd-ui-files
git add -A .
git stash
git reset --hard
git -c advice.detachedHead=false checkout "$update_branch"
git pull

View File

@ -38,6 +38,7 @@ SD_UI_DIR = os.getenv("SD_UI_PATH", None)
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts"))
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models"))
BUCKET_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "bucket"))
USER_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "plugins"))
CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins"))
@ -60,6 +61,7 @@ APP_CONFIG_DEFAULTS = {
"ui": {
"open_browser_on_start": True,
},
"test_diffusers": True,
}
IMAGE_EXTENSIONS = [
@ -115,7 +117,7 @@ def getConfig(default_val=APP_CONFIG_DEFAULTS):
def set_config_on_startup(config: dict):
if getConfig.__test_diffusers_on_startup is None:
getConfig.__test_diffusers_on_startup = config.get("test_diffusers", False)
getConfig.__test_diffusers_on_startup = config.get("test_diffusers", True)
config["config_on_startup"] = {"test_diffusers": getConfig.__test_diffusers_on_startup}
if os.path.isfile(config_yaml_path):

View File

@ -0,0 +1,102 @@
from typing import List
from fastapi import Depends, FastAPI, HTTPException, Response, File
from sqlalchemy.orm import Session
from easydiffusion.easydb import crud, models, schemas
from easydiffusion.easydb.database import SessionLocal, engine
from requests.compat import urlparse
import base64, json
MIME_TYPES = {
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"png": "image/png",
"webp": "image/webp",
"js": "text/javascript",
"htm": "text/html",
"html": "text/html",
"css": "text/css",
"json": "application/json",
"mjs": "application/json",
"yaml": "application/yaml",
"svg": "image/svg+xml",
"txt": "text/plain",
}
def init():
from easydiffusion.server import server_api
models.BucketBase.metadata.create_all(bind=engine)
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@server_api.get("/bucket/{obj_path:path}")
def bucket_get_object(obj_path: str, db: Session = Depends(get_db)):
filename = get_filename_from_url(obj_path)
path = get_path_from_url(obj_path)
if filename==None:
bucket = crud.get_bucket_by_path(db, path=path)
if bucket == None:
raise HTTPException(status_code=404, detail="Bucket not found")
bucketfiles = db.query(models.BucketFile).with_entities(models.BucketFile.filename).filter(models.BucketFile.bucket_id == bucket.id).all()
bucketfiles = [ x.filename for x in bucketfiles ]
return bucketfiles
else:
bucket_id = crud.get_bucket_by_path(db, path).id
bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id == bucket_id, models.BucketFile.filename == filename).first()
suffix = get_suffix_from_filename(filename)
return Response(content=bucketfile.data, media_type=MIME_TYPES.get(suffix, "application/octet-stream"))
@server_api.post("/bucket/{obj_path:path}")
def bucket_post_object(obj_path: str, file: bytes = File(), db: Session = Depends(get_db)):
filename = get_filename_from_url(obj_path)
path = get_path_from_url(obj_path)
bucket = crud.get_bucket_by_path(db, path)
if bucket == None:
bucket = crud.create_bucket(db=db, bucket=schemas.BucketCreate(path=path))
bucket_id = bucket.id
bucketfile = schemas.BucketFileCreate(filename=filename, data=file)
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
result.data = base64.encodestring(result.data)
return result
@server_api.post("/buckets/{bucket_id}/items/", response_model=schemas.BucketFile)
def create_bucketfile_in_bucket(
bucket_id: int, bucketfile: schemas.BucketFileCreate, db: Session = Depends(get_db)
):
bucketfile.data = base64.decodestring(bucketfile.data)
result = crud.create_bucketfile(db=db, bucketfile=bucketfile, bucket_id=bucket_id)
result.data = base64.encodestring(result.data)
return result
def get_filename_from_url(url):
path = urlparse(url).path
name = path[path.rfind('/')+1:]
return name or None
def get_path_from_url(url):
path = urlparse(url).path
path = path[0:path.rfind('/')]
return path or None
def get_suffix_from_filename(filename):
return filename[filename.rfind('.')+1:]

View File

@ -0,0 +1,24 @@
from sqlalchemy.orm import Session
from easydiffusion.easydb import models, schemas
def get_bucket_by_path(db: Session, path: str):
return db.query(models.Bucket).filter(models.Bucket.path == path).first()
def create_bucket(db: Session, bucket: schemas.BucketCreate):
db_bucket = models.Bucket(path=bucket.path)
db.add(db_bucket)
db.commit()
db.refresh(db_bucket)
return db_bucket
def create_bucketfile(db: Session, bucketfile: schemas.BucketFileCreate, bucket_id: int):
db_bucketfile = models.BucketFile(**bucketfile.dict(), bucket_id=bucket_id)
db.merge(db_bucketfile)
db.commit()
db_bucketfile = db.query(models.BucketFile).filter(models.BucketFile.bucket_id==bucket_id, models.BucketFile.filename==bucketfile.filename).first()
return db_bucketfile

View File

@ -0,0 +1,14 @@
import os
from easydiffusion import app
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
os.makedirs(app.BUCKET_DIR, exist_ok=True)
SQLALCHEMY_DATABASE_URL = "sqlite:///"+os.path.join(app.BUCKET_DIR, "bucket.db")
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
BucketBase = declarative_base()

View File

@ -0,0 +1,25 @@
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, BLOB
from sqlalchemy.orm import relationship
from easydiffusion.easydb.database import BucketBase
class Bucket(BucketBase):
__tablename__ = "bucket"
id = Column(Integer, primary_key=True, index=True)
path = Column(String, unique=True, index=True)
bucketfiles = relationship("BucketFile", back_populates="bucket")
class BucketFile(BucketBase):
__tablename__ = "bucketfile"
filename = Column(String, index=True, primary_key=True)
bucket_id = Column(Integer, ForeignKey("bucket.id"), primary_key=True)
data = Column(BLOB, index=False)
bucket = relationship("Bucket", back_populates="bucketfiles")

View File

@ -0,0 +1,36 @@
from typing import List, Union
from pydantic import BaseModel
class BucketFileBase(BaseModel):
filename: str
data: bytes
class BucketFileCreate(BucketFileBase):
pass
class BucketFile(BucketFileBase):
bucket_id: int
class Config:
orm_mode = True
class BucketBase(BaseModel):
path: str
class BucketCreate(BucketBase):
pass
class Bucket(BucketBase):
id: int
bucketfiles: List[BucketFile] = []
class Config:
orm_mode = True

View File

@ -11,6 +11,7 @@ from sdkit import Context
from sdkit.models import load_model, scan_model, unload_model, download_model, get_model_info_from_db
from sdkit.models.model_loader.controlnet_filters import filters as cn_filters
from sdkit.utils import hash_file_quick
from sdkit.models.model_loader.embeddings import get_embedding_token
KNOWN_MODEL_TYPES = [
"stable-diffusion",
@ -29,7 +30,7 @@ MODEL_EXTENSIONS = {
"hypernetwork": [".pt", ".safetensors"],
"gfpgan": [".pth"],
"realesrgan": [".pth"],
"lora": [".ckpt", ".safetensors"],
"lora": [".ckpt", ".safetensors", ".pt"],
"codeformer": [".pth"],
"embeddings": [".pt", ".bin", ".safetensors"],
"controlnet": [".pth", ".safetensors"],
@ -65,9 +66,6 @@ def load_default_models(context: Context):
runtime.set_vram_optimizations(context)
config = app.getConfig()
context.embeddings_path = os.path.join(app.MODELS_DIR, "embeddings")
# init default model paths
for model_type in MODELS_TO_LOAD_ON_START:
context.model_paths[model_type] = resolve_model_to_use(model_type=model_type, fail_if_not_found=False)
@ -102,7 +100,16 @@ def unload_all(context: Context):
def resolve_model_to_use(model_name: Union[str, list] = None, model_type: str = None, fail_if_not_found: bool = True):
model_names = model_name if isinstance(model_name, list) else [model_name]
model_paths = [resolve_model_to_use_single(m, model_type, fail_if_not_found) for m in model_names]
model_paths = []
for m in model_names:
if model_type == "embeddings":
try:
resolve_model_to_use_single(m, model_type)
except FileNotFoundError: # try with spaces
m = m.replace("_", " ")
path = resolve_model_to_use_single(m, model_type, fail_if_not_found)
model_paths.append(path)
return model_paths[0] if len(model_paths) == 1 else model_paths
@ -141,7 +148,9 @@ def resolve_model_to_use_single(model_name: str = None, model_type: str = None,
return default_model_path
if model_name and fail_if_not_found:
raise Exception(f"Could not find the desired model {model_name}! Is it present in the {model_dir} folder?")
raise FileNotFoundError(
f"Could not find the desired model {model_name}! Is it present in the {model_dir} folder?"
)
def reload_models_if_necessary(context: Context, models_data: ModelsData, models_to_force_reload: list = []):
@ -315,6 +324,7 @@ def getModels(scan_for_malicious: bool = True):
{"control_v11p_sd15_mlsd": "Straight Lines"},
{"control_v11p_sd15_seg": "Segment"},
{"control_v11e_sd15_shuffle": "Shuffle"},
{"control_v11f1e_sd15_tile": "Tile"},
],
},
}
@ -324,9 +334,11 @@ def getModels(scan_for_malicious: bool = True):
class MaliciousModelException(Exception):
"Raised when picklescan reports a problem with a model"
def scan_directory(directory, suffixes, directoriesFirst: bool = True, default_entries=[]):
tree = list(default_entries)
def scan_directory(directory, suffixes, directoriesFirst: bool = True, default_entries=[], nameFilter=None):
nonlocal models_scanned
tree = list(default_entries)
for entry in sorted(
os.scandir(directory),
key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower()),
@ -345,7 +357,11 @@ def getModels(scan_for_malicious: bool = True):
raise MaliciousModelException(entry.path)
if scan_for_malicious:
known_models[entry.path] = mtime
model_id = entry.name[: -len(matching_suffix)]
if callable(nameFilter):
model_id = nameFilter(model_id)
model_exists = False
for m in tree: # allows default "named" models, like CodeFormer and known ControlNet models
if (isinstance(m, str) and model_id == m) or (isinstance(m, dict) and model_id in m):
@ -353,14 +369,15 @@ def getModels(scan_for_malicious: bool = True):
break
if not model_exists:
tree.append(model_id)
elif entry.is_dir():
scan = scan_directory(entry.path, suffixes, directoriesFirst=False)
scan = scan_directory(entry.path, suffixes, directoriesFirst=False, nameFilter=nameFilter)
if len(scan) != 0:
tree.append((entry.name, scan))
return tree
def listModels(model_type):
def listModels(model_type, nameFilter=None):
nonlocal models_scanned
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
@ -370,7 +387,9 @@ def getModels(scan_for_malicious: bool = True):
try:
default_tree = models["options"].get(model_type, [])
models["options"][model_type] = scan_directory(models_dir, model_extensions, default_entries=default_tree)
models["options"][model_type] = scan_directory(
models_dir, model_extensions, default_entries=default_tree, nameFilter=nameFilter
)
except MaliciousModelException as e:
models["scan-error"] = str(e)
@ -382,7 +401,7 @@ def getModels(scan_for_malicious: bool = True):
listModels(model_type="hypernetwork")
listModels(model_type="gfpgan")
listModels(model_type="lora")
listModels(model_type="embeddings")
listModels(model_type="embeddings", nameFilter=get_embedding_token)
listModels(model_type="controlnet")
if scan_for_malicious and models_scanned > 0:

View File

@ -12,9 +12,9 @@ from easydiffusion import app
manifest = {
"tensorrt": {
"install": [
"nvidia-cudnn --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com",
"tensorrt-libs --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com",
"tensorrt --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com",
"nvidia-cudnn --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
"tensorrt-libs --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
"tensorrt --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
],
"uninstall": ["tensorrt"],
# TODO also uninstall tensorrt-libs and nvidia-cudnn, but do it upon restarting (avoid 'file in use' error)
@ -25,7 +25,7 @@ installing = []
# remove this once TRT releases on pypi
if platform.system() == "Windows":
trt_dir = os.path.join(app.ROOT_DIR, "tensorrt")
if os.path.exists(trt_dir):
if os.path.exists(trt_dir) and os.path.isdir(trt_dir) and len(os.listdir(trt_dir)) > 0:
files = os.listdir(trt_dir)
packages = manifest["tensorrt"]["install"]
@ -61,6 +61,10 @@ def install(module_name):
raise RuntimeError(f"Can't install unknown package: {module_name}!")
commands = manifest[module_name]["install"]
if module_name == "tensorrt":
commands += [
"protobuf==3.20.3 polygraphy==0.47.1 onnx==1.14.0 --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com"
]
commands = [f"python -m pip install --upgrade {cmd}" for cmd in commands]
installing.append(module_name)

View File

@ -30,9 +30,7 @@ def init(device):
from easydiffusion import app
app_config = app.getConfig()
context.test_diffusers = (
app_config.get("test_diffusers", False) and app_config.get("update_branch", "main") != "main"
)
context.test_diffusers = app_config.get("test_diffusers", True)
log.info("Device usage during initialization:")
get_device_usage(device, log_info=True, process_usage_only=False)

View File

@ -63,7 +63,7 @@ class SetAppConfigRequest(BaseModel, extra=Extra.allow):
ui_open_browser_on_start: bool = None
listen_to_network: bool = None
listen_port: int = None
test_diffusers: bool = False
test_diffusers: bool = True
def init():
@ -139,6 +139,10 @@ def init():
def modify_package(package_name: str, req: dict):
return modify_package_internal(package_name, req)
@server_api.get("/sha256/{obj_path:path}")
def get_sha256(obj_path: str):
return get_sha256_internal(obj_path)
@server_api.get("/")
def read_root():
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
@ -451,3 +455,26 @@ def modify_package_internal(package_name: str, req: dict):
log.error(str(e))
log.error(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))
def get_sha256_internal(obj_path):
import hashlib
from easydiffusion.utils import sha256sum
path = obj_path.split("/")
type = path.pop(0)
try:
model_path = model_manager.resolve_model_to_use("/".join(path), type)
except Exception as e:
log.error(str(e))
log.error(traceback.format_exc())
return HTTPException(status_code=404)
try:
digest = sha256sum(model_path)
return {"digest": digest}
except Exception as e:
log.error(str(e))
log.error(traceback.format_exc())
return HTTPException(status_code=500, detail=str(e))

View File

@ -3,7 +3,7 @@ import pprint
from sdkit.filter import apply_filters
from sdkit.models import load_model
from sdkit.utils import img_to_base64_str, log
from sdkit.utils import img_to_base64_str, get_image, log
from easydiffusion import model_manager, runtime
from easydiffusion.types import FilterImageRequest, FilterImageResponse, ModelsData, OutputFormatData
@ -42,7 +42,12 @@ class FilterTask(Task):
print_task_info(self.request, self.models_data, self.output_format)
images = filter_images(context, self.request.image, self.request.filter, self.request.filter_params)
if isinstance(self.request.image, list):
images = [get_image(img) for img in self.request.image]
else:
images = get_image(self.request.image)
images = filter_images(context, images, self.request.filter, self.request.filter_params)
output_format = self.output_format
images = [

View File

@ -15,6 +15,8 @@ from sdkit.utils import (
img_to_base64_str,
img_to_buffer,
latent_samples_to_images,
resize_img,
get_image,
log,
)
@ -226,8 +228,13 @@ def generate_images_internal(
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 = get_image(req.control_image)
req.control_image = resize_img(req.control_image.convert("RGB"), req.width, req.height, clamp_to_8=True)
req.control_image = filter_images(context, req.control_image, task_data.control_filter_to_apply)[0]
if req.init_image is not None and int(req.num_inference_steps * req.prompt_strength) == 0:
req.prompt_strength = 1 / req.num_inference_steps if req.num_inference_steps > 0 else 1
if context.test_diffusers:
pipe = context.models["stable-diffusion"]["default"]
if hasattr(pipe.unet, "_allocate_trt_buffers_backup"):

View File

@ -73,6 +73,7 @@ class TaskData(BaseModel):
use_hypernetwork_model: Union[str, List[str]] = None
use_lora_model: Union[str, List[str]] = None
use_controlnet_model: Union[str, List[str]] = None
use_embeddings_model: Union[str, List[str]] = None
filters: List[str] = []
filter_params: Dict[str, Dict[str, Any]] = {}
control_filter_to_apply: Union[str, List[str]] = None
@ -200,6 +201,7 @@ def convert_legacy_render_req_to_new(old_req: dict):
model_paths["hypernetwork"] = old_req.get("use_hypernetwork_model")
model_paths["lora"] = old_req.get("use_lora_model")
model_paths["controlnet"] = old_req.get("use_controlnet_model")
model_paths["embeddings"] = old_req.get("use_embeddings_model")
model_paths["gfpgan"] = old_req.get("use_face_correction", "")
model_paths["gfpgan"] = model_paths["gfpgan"] if "gfpgan" in model_paths["gfpgan"].lower() else None

View File

@ -6,3 +6,15 @@ from .save_utils import (
save_images_to_disk,
get_printable_request,
)
def sha256sum(filename):
sha256 = hashlib.sha256()
with open(filename, "rb") as f:
while True:
data = f.read(8192) # Read in chunks of 8192 bytes
if not data:
break
sha256.update(data)
return sha256.hexdigest()

View File

@ -10,6 +10,7 @@ from easydiffusion import app
from easydiffusion.types import GenerateImageRequest, TaskData, OutputFormatData
from numpy import base_repr
from sdkit.utils import save_dicts, save_images
from sdkit.models.model_loader.embeddings import get_embedding_token
filename_regex = re.compile("[^a-zA-Z0-9._-]")
img_number_regex = re.compile("([0-9]{5,})")
@ -34,7 +35,7 @@ TASK_TEXT_MAPPING = {
"lora_alpha": "LoRA Strength",
"use_hypernetwork_model": "Hypernetwork model",
"hypernetwork_strength": "Hypernetwork Strength",
"use_embedding_models": "Embedding models",
"use_embeddings_model": "Embedding models",
"tiling": "Seamless Tiling",
"use_face_correction": "Use Face Correction",
"use_upscale": "Use Upscaling",
@ -219,7 +220,7 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
task_data_metadata.update(output_format.dict())
app_config = app.getConfig()
using_diffusers = app_config.get("test_diffusers", False)
using_diffusers = app_config.get("test_diffusers", True)
# Save the metadata in the order defined in TASK_TEXT_MAPPING
metadata = {}
@ -228,20 +229,18 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
metadata[key] = req_metadata[key]
elif key in task_data_metadata:
metadata[key] = task_data_metadata[key]
elif key == "use_embedding_models" and using_diffusers:
if key == "use_embeddings_model" and using_diffusers:
embeddings_extensions = {".pt", ".bin", ".safetensors"}
def scan_directory(directory_path: str):
used_embeddings = []
for entry in os.scandir(directory_path):
if entry.is_file():
entry_extension = os.path.splitext(entry.name)[1]
if entry_extension not in embeddings_extensions:
# Check if the filename has the right extension
if not any(map(lambda ext: entry.name.endswith(ext), embeddings_extensions)):
continue
embedding_name_regex = regex.compile(
r"(^|[\s,])" + regex.escape(os.path.splitext(entry.name)[0]) + r"([+-]*$|[\s,]|[+-]+[\s,])"
)
embedding_name_regex = regex.compile(r"(^|[\s,])" + regex.escape(get_embedding_token(entry.name)) + r"([+-]*$|[\s,]|[+-]+[\s,])")
if embedding_name_regex.search(req.prompt) or embedding_name_regex.search(req.negative_prompt):
used_embeddings.append(entry.path)
elif entry.is_dir():
@ -249,7 +248,7 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
return used_embeddings
used_embeddings = scan_directory(os.path.join(app.MODELS_DIR, "embeddings"))
metadata["use_embedding_models"] = used_embeddings if len(used_embeddings) > 0 else None
metadata["use_embeddings_model"] = used_embeddings if len(used_embeddings) > 0 else None
# Clean up the metadata
if req.init_image is None and "prompt_strength" in metadata:
@ -265,7 +264,10 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
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:
if using_diffusers:
for key in (x for x in ["use_hypernetwork_model", "hypernetwork_strength"] if x in metadata):
del metadata[key]
else:
for key in (
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
):

View File

@ -18,12 +18,15 @@
<link rel="stylesheet" href="/media/css/image-modal.css">
<link rel="stylesheet" href="/media/css/plugins.css">
<link rel="stylesheet" href="/media/css/animations.css">
<link rel="stylesheet" href="/media/css/croppr.css" rel="stylesheet"/>
<link rel="manifest" href="/media/manifest.webmanifest">
<script src="/media/js/jquery-3.6.1.min.js"></script>
<script src="/media/js/jquery-confirm.min.js"></script>
<script src="/media/js/jszip.min.js"></script>
<script src="/media/js/FileSaver.min.js"></script>
<script src="/media/js/marked.min.js"></script>
<script src="/media/js/croppr.js"></script>
<script src="/media/js/exif-reader.js"></script>
</head>
<body>
<div id="container">
@ -32,7 +35,7 @@
<h1>
<img id="logo_img" src="/media/images/icon-512x512.png" >
Easy Diffusion
<small><span id="version">v2.5.48</span> <span id="updateBranchLabel"></span></small>
<small><span id="version">v3.0.2</span> <span id="updateBranchLabel"></span></small>
</h1>
</div>
<div id="server-status">
@ -59,7 +62,14 @@
<div id="editor-inputs-prompt" class="row">
<div id="prompt-toolbar" class="split-toolbar">
<div id="prompt-toolbar-left" class="toolbar-left">
<label for="prompt"><b>Enter Prompt</b></label> <small>or</small> <button id="promptsFromFileBtn" class="tertiaryButton smallButton">Load from a file</button>
<label for="prompt"><b>Enter Prompt</b>
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">
You can type your prompts in the below textbox or load them from a file. You can also
reload tasks from metadata embedded in PNG, WEBP and JPEG images (enable embedding from the Settings).
</span></i>
</label>
<small>or</small>
<button id="promptsFromFileBtn" class="tertiaryButton smallButton">Load from a file</button>
</div>
<div id="prompt-toolbar-right" class="toolbar-right">
<button id="image-modifier-dropdown" class="tertiaryButton smallButton">+ Image Modifiers</button>
@ -73,7 +83,7 @@
<a href="https://github.com/easydiffusion/easydiffusion/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top">Click to learn more about Negative Prompts</span></i></a>
<small>(optional)</small>
</label>
<button id="negative-embeddings-button" class="tertiaryButton smallButton displayNone">+ Embedding</button>
<button id="negative-embeddings-button" class="tertiaryButton smallButton displayNone">+ Negative Embedding</button>
<div class="collapsible-content">
<textarea id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)"></textarea>
</div>
@ -81,6 +91,11 @@
<div id="editor-inputs-init-image" class="row">
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label>
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top">
Add img2img source image using the Browse button, via drag & drop from external file or browser image (incl.
rendered image) or by pasting an image from the clipboard using Ctrl+V.<br /><br />
You may also reload the metadata embedded in a PNG, WEBP or JPEG image (enable embedding from the Settings).
</span></i>
<div id="init_image_preview_container" class="image_preview_container">
<div id="init_image_wrapper" class="preview_image_wrapper">
@ -141,7 +156,12 @@
<div><table>
<tr><b class="settings-subheader">Image Settings</b></tr>
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="0" onkeypress="preventNonNumericalInput(event)"> <input id="random_seed" name="random_seed" type="checkbox" checked><label for="random_seed">Random</label></td></tr>
<tr class="pl-5"><td><label for="num_outputs_total">Number of Images:</label></td><td><input id="num_outputs_total" name="num_outputs_total" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label><small>(total)</small></label> <input id="num_outputs_parallel" name="num_outputs_parallel" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label id="num_outputs_parallel_label" for="num_outputs_parallel"><small>(in parallel)</small></label></td></tr>
<tr class="pl-5"><td><label for="num_outputs_total">Number of Images:</label></td>
<td><input id="num_outputs_total" name="num_outputs_total" value="1" type="number" value="1" min="1" step="1" onkeypres"="preventNonNumericalInput(event)">
<label><small>(total)</small></label>
<input id="num_outputs_parallel" name="num_outputs_parallel" value="1" type="number" value="1" min="1" step="1" onkeypress="preventNonNumericalInput(event)">
<label id="num_outputs_parallel_label" for="num_outputs_parallel"><small>(in parallel)</small></label></td>
</tr>
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td class="model-input">
<input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<button id="reload-models" class="secondaryButton reloadModels"><i class='fa-solid fa-rotate'></i></button>
@ -270,7 +290,7 @@
<option value="1792">1792</option>
<option value="2048">2048</option>
</select>
<label for="width"><small>(width)</small></label>
<label id="widthLabel" for="width"><small><span>(width)</span></small></label>
<span id="swap-width-height" class="clickable smallButton" style="margin-left: 2px; margin-right:2px;"><i class="fa-solid fa-right-left"><span class="simple-tooltip top-left"> Swap width and height </span></i></span>
<select id="height" name="height" value="512">
<option value="128">128</option>
@ -293,7 +313,7 @@
<option value="1792">1792</option>
<option value="2048">2048</option>
</select>
<label for="height"><small>(height)</small></label>
<label id="heightLabel" for="height"><small><span>(height)</span></small></label>
<div id="recent-resolutions-container">
<span id="recent-resolutions-button" class="clickable"><i class="fa-solid fa-sliders"><span class="simple-tooltip top-left"> Advanced sizes </span></i></span>
<div id="recent-resolutions-popup" class="displayNone">
@ -301,12 +321,22 @@
<input id="custom-width" name="custom-width" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)">
&times;
<input id="custom-height" name="custom-height" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)"><br>
<small>Resize:</small><br>
<input id="resize-slider" name="resize-slider" class="editor-slider" value="1" type="range" min="0.4" max="2" step="0.005" style="width:100%;"><br>
<div id="enlarge-buttons"><button data-factor="0.5" class="tertiaryButton smallButton">×0.5</button>&nbsp;<button data-factor="1.2" class="tertiaryButton smallButton">×1.2</button>&nbsp;<button data-factor="1.5" class="tertiaryButton smallButton">×1.5</button>&nbsp;<button data-factor="2" class="tertiaryButton smallButton">×2</button>&nbsp;<button data-factor="3" class="tertiaryButton smallButton">×3</button></div>
<small>Enlarge:</small><br>
<div id="enlarge-buttons"><button id="enlarge15" class="tertiaryButton smallButton">×1.5</button>&nbsp;<button id="enlarge2" class="tertiaryButton smallButton">×2</button>&nbsp;<button id="enlarge3" class="tertiaryButton smallButton">×3</button></div>
<div class="two-column">
<div class="left-column">
<small>Recently&nbsp;used:</small><br>
<div id="recent-resolution-list">
</div>
</div>
<div class="right-column">
<small>Common&nbsp;sizes:</small><br>
<div id="common-resolution-list">
</div>
</div>
<small>Recently&nbsp;used:</small><br>
<div id="recent-resolution-list">
</div>
</div>
</div>
@ -320,11 +350,10 @@
<label for="lora_model">LoRA:</label>
</td>
<td class="diffusers-restart-needed">
<div class="model_entries"></div>
<button class="add_model_entry"><i class="fa-solid fa-plus"></i> add another LoRA</button>
<div id="lora_model" data-path=""></div>
</td>
</tr>
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
<tr id="hypernetwork_model_container" class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</label></td><td>
<input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
</td></tr>
<tr id="hypernetwork_strength_container" class="pl-5">
@ -389,7 +418,7 @@
</div>
</div>
<label><small><b>Note:</b> The Image Modifiers section has moved to the <code>+ Image Modifiers</code> button at the top, just below the Prompt textbox.</small></label>
<label><small><b>Note:</b> The Image Modifiers section has moved to the <code>+ Image Modifiers</code> button at the top, just above the Prompt textbox.</small></label>
</div>
<div id="preview" class="col-free">
@ -403,7 +432,7 @@
<div id="preview-content">
<div id="preview-tools" class="displayNone">
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can icon"></i> Clear All</button>
<button class="tertiaryButton" id="show-download-popup"><i class="fa-solid fa-download"></i> Download images</button>
<button class="tertiaryButton" id="show-download-popup"><i class="fa-solid fa-download"></i><span> Download images</span></button>
<div class="display-settings">
<button id="undo" class="displayNone primaryButton">
Undo <i class="fa-solid fa-rotate-left icon"></i>
@ -432,6 +461,9 @@
</div>
<div class="clearfix" style="clear: both;"></div>
</div>
<div id="supportBanner" class="displayNone">
If you found this project useful and want to help keep it alive, please consider <a href="https://ko-fi.com/easydiffusion" target="_blank">buying me a coffee</a> or <a href="https://www.patreon.com/EasyDiffusion" target="_blank">supporting me on Patreon</a> to help cover the cost of development and maintenance! Or even better, <a href="https://cmdr2.itch.io/easydiffusion" target="_blank">purchasing it at the full price</a>. Thank you for your support!
</div>
</div>
</div>
</div>
@ -669,6 +701,8 @@
<span>
</div>
<div id="embeddings-dialog-header-right">
<button id="add-embeddings-thumb" class="tertiaryButton smallButton" style="background-color: var(--background-color4);"><i class="fa-solid fa-folder-plus"></i> Add thumbnail</button>
<input id="add-embeddings-thumb-input" name="add-embeddings-thumb-input" type="file" class="displayNone">
<i id="embeddings-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
</div>
</div>
@ -679,6 +713,15 @@
</button>
<i class="fa-solid fa-magnifying-glass"></i>
<input id="embeddings-search-box" type="text" spellcheck="false" autocomplete="off" placeholder="Search...">
<label for="embedding-card-size-selector"><small>Thumbnail Size:</small></label>
<select id="embedding-card-size-selector" name="embedding-card-size-selector">
<option value="-2">0</option>
<option value="-1" selected>1</option>
<option value="0">2</option>
<option value="1">3</option>
<option value="2">4</option>
<option value="3">5</option>
</select>
<span style="float:right;"><label>Mode:</label>&nbsp;<select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
</div>
<div id="embeddings-list">
@ -686,6 +729,34 @@
</div>
</dialog>
<dialog id="use-as-thumb-dialog">
<div id="use-as-thumb-dialog-header" class="dialog-header">
<div id="use-as-thumb-dialog-header-left" class="dialog-header-left">
<h4>Use as thumbnail</h4>
<span>Use a pictures as thumbnail for embeddings, LORAs, etc.</span>
</div>
<div id="use-as-thumb-dialog-header-right">
<i id="use-as-thumb-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
</div>
</div>
<div>
<div class="use-as-thumb-grid">
<div class="use-as-thumb-preview">
<div id="use-as-thumb-img-container"><img id="use-as-thumb-image" src="/media/images/noimg.png" width="512" height="512"></div>
</div>
<div class="use-as-thumb-select">
<label for="use-as-thumb-select">Use the thumbnail for:</label><br>
<select id="use-as-thumb-select" size="16" multiple>
</select>
</div>
<div class="use-as-thumb-buttons">
<button class="tertiaryButton" id="use-as-thumb-save">Save thumbnail</button>
<button class="tertiaryButton" id="use-as-thumb-cancel">Cancel</button>
</div>
</div>
</div>
</dialog>
<div id="image-editor" class="popup image-editor-popup">
<div>
<i class="close-button fa-solid fa-xmark"></i>
@ -721,7 +792,6 @@
<div id="footer-spacer"></div>
<div id="footer">
<div class="line-separator">&nbsp;</div>
<p>If you found this project useful and want to help keep it alive, please <a href="https://ko-fi.com/easydiffusion" target="_blank"><img src="/media/images/kofi.png" id="coffeeButton"></a> to help cover the cost of development and maintenance! Thank you for your support!</p>
<p>Please feel free to join the <a href="https://discord.com/invite/u9yhsFmEkB" target="_blank">discord community</a> or <a href="https://github.com/easydiffusion/easydiffusion/issues" target="_blank">file an issue</a> if you have any problems or suggestions in using this interface.</p>
<div id="footer-legal">
<p><b>Disclaimer:</b> The authors of this project are not responsible for any content generated using this interface.</p>
@ -739,6 +809,8 @@
<script src="media/js/auto-save.js"></script>
<script src="media/js/searchable-models.js"></script>
<script src="media/js/multi-model-selector.js"></script>
<script src="media/js/task-manager.js"></script>
<script src="media/js/main.js"></script>
<script src="media/js/plugins.js"></script>
<script src="media/js/themes.js"></script>

View File

@ -1,4 +1,4 @@
from easydiffusion import model_manager, app, server
from easydiffusion import model_manager, app, server, bucket_manager
from easydiffusion.server import server_api # required for uvicorn
app.init()
@ -8,6 +8,7 @@ server.init()
# Init the app
model_manager.init()
app.init_render_threads()
bucket_manager.init()
# start the browser ui
app.open_browser()

58
ui/media/css/croppr.css Normal file
View File

@ -0,0 +1,58 @@
.croppr-container * {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}
.croppr-container img {
vertical-align: middle;
max-width: 100%;
}
.croppr {
position: relative;
display: inline-block;
}
.croppr-overlay {
background: rgba(0,0,0,0.5);
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
cursor: crosshair;
}
.croppr-region {
border: 1px dashed rgba(0, 0, 0, 0.5);
position: absolute;
z-index: 3;
cursor: move;
top: 0;
}
.croppr-imageClipped {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
pointer-events: none;
}
.croppr-handle {
border: 1px solid black;
background-color: white;
width: 10px;
height: 10px;
position: absolute;
z-index: 4;
top: 0;
}

View File

@ -230,3 +230,26 @@
.inpainter .load_mask {
display: flex;
}
.editor-canvas-overlay {
cursor: none;
}
.image-brush-preview {
position: fixed;
background: black;
opacity: 0.3;
borderRadius: 50%;
cursor: none;
pointer-events: none;
transform: translate(-50%, -50%);
}
.editor-options-container > * > *:not(.active):not(.button) {
border: 1px dotted slategray;
}
.image_editor_opacity .editor-options-container > * > *:not(.active):not(.button) {
border: 1px dotted slategray;
}

View File

@ -34,6 +34,7 @@ code {
width: 32px;
height: 32px;
transform: translateY(4px);
cursor: pointer;
}
#prompt {
width: 100%;
@ -476,6 +477,7 @@ dialog {
background: var(--background-color2);
color: var(--text-color);
border-radius: 6px;
box-shadow: 0px 0px 30px black;
border: 2px solid rgb(255 255 255 / 10%);
padding: 0px;
}
@ -1088,7 +1090,7 @@ input::file-selector-button {
.tab-content-inner {
margin: 0px;
}
.tab {
#top-nav .tab {
font-size: 0;
}
.tab .icon {
@ -1114,6 +1116,9 @@ input::file-selector-button {
#preview-tools button .icon {
font-size: 12pt;
}
#show-download-popup .fa-solid {
font-size: 12pt;
}
}
@media screen and (max-width: 500px) {
@ -1418,6 +1423,10 @@ div.task-fs-initimage {
display: none;
position: absolute;
}
div.task-fs-initimage img {
max-height: 70vH;
max-width: 70vW;
}
div.task-initimg:hover div.task-fs-initimage {
display: block;
position: absolute;
@ -1433,9 +1442,13 @@ div.top-right {
right: 8px;
}
.task-fs-initimage .top-right button {
margin-top: 6px;
}
#small_image_warning {
font-size: smaller;
color: var(--status-orange);
font-size: smaller;
color: var(--status-orange);
}
button#save-system-settings-btn {
@ -1460,6 +1473,9 @@ button#save-system-settings-btn {
cursor: pointer;;
}
.validation-failed {
border: solid 2px red;
}
/* SCROLLBARS */
:root {
--scrollbar-width: 14px;
@ -1650,6 +1666,35 @@ body.wait-pause {
}
}
.spinner-container {
width: 80px;
height: 100px;
margin: 100px auto;
margin-top: 30vH;
}
.spinner-block {
position: relative;
box-sizing: border-box;
float: left;
margin: 0 10px 10px 0;
width: 12px;
height: 12px;
border-radius: 3px;
background: var(--accent-color);
}
.spinner-block:nth-child(4n+1) { animation: spinner-wave 2s ease .0s infinite; }
.spinner-block:nth-child(4n+2) { animation: spinner-wave 2s ease .2s infinite; }
.spinner-block:nth-child(4n+3) { animation: spinner-wave 2s ease .4s infinite; }
.spinner-block:nth-child(4n+4) { animation: spinner-wave 2s ease .6s infinite; margin-right: 0; }
@keyframes spinner-wave {
0% { top: 0; opacity: 1; }
50% { top: 30px; opacity: .2; }
100% { top: 0; opacity: 1; }
}
#embeddings-dialog {
overflow: clip;
}
@ -1664,6 +1709,12 @@ body.wait-pause {
overflow-y: scroll;
}
@media screen and (max-width: 1400px) {
#embeddings-list {
width: 80vW;
}
}
#embeddings-list button {
margin: 2px;
color: var(--button-color);
@ -1741,6 +1792,32 @@ body.wait-pause {
float: right;
}
.use-as-thumb-grid { display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
gap: 8px 8px;
grid-auto-flow: row;
grid-template-areas:
"uat-preview uat-select"
"uat-preview uat-buttons";
}
.use-as-thumb-preview {
justify-self: center;
align-self: center;
grid-area: uat-preview;
}
.use-as-thumb-select {
grid-area: uat-select;
}
.use-as-thumb-buttons {
justify-self: center;
grid-area: uat-buttons;
}
.diffusers-disabled-on-startup .diffusers-restart-needed {
font-size: 0;
}
@ -1778,6 +1855,10 @@ div#recent-resolutions-popup small {
opacity: 0.7;
}
div#common-resolution-list button {
background: var(--background-color1);
}
td#image-size-options small {
margin-right: 0px !important;
}
@ -1794,6 +1875,27 @@ div#enlarge-buttons {
text-align: center;
}
.two-column { display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
gap: 0px 0.5em;
grid-auto-flow: row;
grid-template-areas:
"left-column right-column";
}
.left-column {
justify-self: center;
align-self: center;
grid-area: left-column;
}
.right-column {
justify-self: center;
align-self: center;
grid-area: right-column;
}
.clickable {
cursor: pointer;
}
@ -1828,7 +1930,100 @@ div#enlarge-buttons {
width: 77%;
}
.drop-area {
width: 45%;
height: 50px;
border: 2px dashed #ccc;
text-align: center;
line-height: 50px;
font-size: small;
color: #ccc;
border-radius: 10px;
display: none;
margin: 12px 10px;
}
#num_outputs_total {
width: 42pt;
}
#num_outputs_parallel {
width: 42pt;
}
.model_entry .model_weight {
width: 50pt;
}
/* hack for fixing Image Modifier Improvements plugin */
#imageTagPopupContainer {
position: absolute;
}
@media screen and (max-width: 400px) {
.editor-slider {
width: 40%;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
/* Firefox */
}
#num_outputs_total {
width: 27pt;
}
#num_outputs_parallel {
width: 27pt;
margin-left: -4pt;
}
.model_entry .model_weight {
width: 30pt;
}
#width {
width: 50pt;
}
#height {
width: 50pt;
}
}
@media screen and (max-width: 460px) {
#widthLabel small span {
display: none;
}
#widthLabel small:after {
content: "(w)";
}
#heightLabel small span {
display: none;
}
#heightLabel small:after {
content: "(h)";
}
#prompt-toolbar-right {
text-align: right;
}
#editor-settings label {
font-size: 9pt;
}
#editor-settings .model-filter {
width: 56%;
}
#vae_model {
width: 65% !important;
}
.model_entry .model_name {
width: 60% !important;
}
}
#supportBanner {
font-size: 9pt;
padding: 5pt;
border: 1px solid var(--background-color2);
margin-bottom: 5pt;
border-radius: 4pt;
padding-top: 6pt;
color: var(--small-label-color);
}

BIN
ui/media/images/noimg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -15,14 +15,12 @@ const SETTINGS_IDS_LIST = [
"stable_diffusion_model",
"clip_skip",
"vae_model",
"hypernetwork_model",
"sampler_name",
"width",
"height",
"num_inference_steps",
"guidance_scale",
"prompt_strength",
"hypernetwork_strength",
"tiling",
"output_format",
"output_quality",
@ -45,6 +43,7 @@ const SETTINGS_IDS_LIST = [
"sound_toggle",
"vram_usage_level",
"confirm_dangerous_actions",
"profileName",
"metadata_output_format",
"auto_save_settings",
"apply_color_correction",
@ -54,10 +53,18 @@ const SETTINGS_IDS_LIST = [
"zip_toggle",
"tree_toggle",
"json_toggle",
"extract_lora_from_prompt",
"embedding-card-size-selector",
"lora_model",
]
const IGNORE_BY_DEFAULT = ["prompt"]
if (!testDiffusers.checked) {
SETTINGS_IDS_LIST.push("hypernetwork_model")
SETTINGS_IDS_LIST.push("hypernetwork_strength")
}
const SETTINGS_SECTIONS = [
// gets the "keys" property filled in with an ordered list of settings in this section via initSettings
{ id: "editor-inputs", name: "Prompt" },

1189
ui/media/js/croppr.js Executable file

File diff suppressed because it is too large Load Diff

View File

@ -289,42 +289,56 @@ const TASK_MAPPING = {
readUI: () => vaeModelField.value,
parse: (val) => val,
},
use_controlnet_model: {
name: "ControlNet model",
setUI: (use_controlnet_model) => {
controlnetModelField.value = getModelPath(use_controlnet_model, [".pth", ".safetensors"])
},
readUI: () => controlnetModelField.value,
parse: (val) => val,
},
control_filter_to_apply: {
name: "ControlNet Filter",
setUI: (control_filter_to_apply) => {
controlImageFilterField.value = control_filter_to_apply
},
readUI: () => controlImageFilterField.value,
parse: (val) => val,
},
use_lora_model: {
name: "LoRA model",
setUI: (use_lora_model) => {
// create rows
for (let i = loraModels.length; i < use_lora_model.length; i++) {
createLoraEntry()
}
use_lora_model.forEach((model_name, i) => {
let field = loraModels[i][0]
const oldVal = field.value
if (model_name !== "") {
model_name = getModelPath(model_name, [".ckpt", ".safetensors"])
model_name = model_name !== "" ? model_name : oldVal
let modelPaths = []
use_lora_model = Array.isArray(use_lora_model) ? use_lora_model : [use_lora_model]
use_lora_model.forEach((m) => {
if (m.includes("models\\lora\\")) {
m = m.split("models\\lora\\")[1]
} else if (m.includes("models\\\\lora\\\\")) {
m = m.split("models\\\\lora\\\\")[1]
} else if (m.includes("models/lora/")) {
m = m.split("models/lora/")[1]
}
field.value = model_name
m = m.replaceAll("\\\\", "/")
m = getModelPath(m, [".ckpt", ".safetensors"])
modelPaths.push(m)
})
// clear the remaining entries
let container = document.querySelector("#lora_model_container .model_entries")
for (let i = use_lora_model.length; i < loraModels.length; i++) {
let modelEntry = loraModels[i][2]
container.removeChild(modelEntry)
}
loraModels.splice(use_lora_model.length)
loraModelField.modelNames = modelPaths
},
readUI: () => {
let values = loraModels.map((e) => e[0].value)
values = values.filter((e) => e.trim() !== "")
values = values.length > 0 ? values : "None"
return values
return loraModelField.modelNames
},
parse: (val) => {
val = !val || val === "None" ? "" : val
if (typeof val === "string" && val.includes(",")) {
val = val.split(",")
val = val.map((v) => v.trim())
val = val.map((v) => v.replaceAll("\\", "\\\\"))
val = val.map((v) => v.replaceAll('"', ""))
val = val.map((v) => v.replaceAll("'", ""))
val = val.map((v) => '"' + v + '"')
val = "[" + val + "]"
val = JSON.parse(val)
}
val = Array.isArray(val) ? val : [val]
return val
},
@ -332,31 +346,17 @@ const TASK_MAPPING = {
lora_alpha: {
name: "LoRA Strength",
setUI: (lora_alpha) => {
for (let i = loraModels.length; i < lora_alpha.length; i++) {
createLoraEntry()
}
lora_alpha.forEach((model_strength, i) => {
let field = loraModels[i][1]
field.value = model_strength
})
// clear the remaining entries
let container = document.querySelector("#lora_model_container .model_entries")
for (let i = lora_alpha.length; i < loraModels.length; i++) {
let modelEntry = loraModels[i][2]
container.removeChild(modelEntry)
}
loraModels.splice(lora_alpha.length)
lora_alpha = Array.isArray(lora_alpha) ? lora_alpha : [lora_alpha]
loraModelField.modelWeights = lora_alpha
},
readUI: () => {
let models = loraModels.filter((e) => e[0].value.trim() !== "")
let values = models.map((e) => e[1].value)
values = values.length > 0 ? values : 0
return values
return loraModelField.modelWeights
},
parse: (val) => {
if (typeof val === "string" && val.includes(",")) {
val = "[" + val.replaceAll("'", '"') + "]"
val = JSON.parse(val)
}
val = Array.isArray(val) ? val : [val]
val = val.map((e) => parseFloat(e))
return val
@ -472,11 +472,8 @@ function restoreTaskToUI(task, fieldsToSkip) {
}
if (!("use_lora_model" in task.reqBody)) {
loraModels.forEach((e) => {
e[0].value = ""
e[1].value = 0
e[0].dispatchEvent(new Event("change"))
})
loraModelField.modelNames = []
loraModelField.modelWeights = []
}
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
@ -519,10 +516,23 @@ function restoreTaskToUI(task, fieldsToSkip) {
)
initImagePreview.src = task.reqBody.init_image
}
// hide/show controlnet picture as needed
if (IMAGE_REGEX.test(controlImagePreview.src) && task.reqBody.control_image == undefined) {
// hide source image
controlImageClearBtn.dispatchEvent(new Event("click"))
} else if (task.reqBody.control_image !== undefined) {
// listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpai
controlImagePreview.src = task.reqBody.control_image
}
}
function readUI() {
const reqBody = {}
for (const key in TASK_MAPPING) {
if (testDiffusers.checked && (key === "use_hypernetwork_model" || key === "hypernetwork_strength")) {
continue
}
reqBody[key] = TASK_MAPPING[key].readUI()
}
return {
@ -569,6 +579,10 @@ const TASK_TEXT_MAPPING = {
use_stable_diffusion_model: "Stable Diffusion model",
use_hypernetwork_model: "Hypernetwork model",
hypernetwork_strength: "Hypernetwork Strength",
use_lora_model: "LoRA model",
lora_alpha: "LoRA Strength",
use_controlnet_model: "ControlNet model",
control_filter_to_apply: "ControlNet Filter",
}
function parseTaskFromText(str) {
const taskReqBody = {}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,256 @@
/**
* A component consisting of multiple model dropdowns, along with a "weight" field per model.
*
* Behaves like a single input element, giving an object in response to the .value field.
*
* Inspired by the design of the ModelDropdown component (searchable-models.js).
*/
class MultiModelSelector {
root
modelType
modelNameFriendly
defaultWeight
weightStep
modelContainer
addNewButton
counter = 0
/* MIMIC A REGULAR INPUT FIELD */
get id() {
return this.root.id
}
get parentElement() {
return this.root.parentElement
}
get parentNode() {
return this.root.parentNode
}
get value() {
return { modelNames: this.modelNames, modelWeights: this.modelWeights }
}
set value(modelData) {
if (typeof modelData !== "object") {
throw new Error("Multi-model selector expects an object containing modelNames and modelWeights as keys!")
}
if (!("modelNames" in modelData) || !("modelWeights" in modelData)) {
throw new Error("modelNames or modelWeights not present in the data passed to the multi-model selector")
}
let newModelNames = modelData["modelNames"]
let newModelWeights = modelData["modelWeights"]
if (newModelNames.length !== newModelWeights.length) {
throw new Error("Need to pass an equal number of modelNames and modelWeights!")
}
// update weight first, name second.
// for some unholy reason this order matters for dispatch chains
// the root of all this unholiness is because searchable-models automatically dispatches an update event
// as soon as the value is updated via JS, which is against the DOM pattern of not dispatching an event automatically
// unless the caller explicitly dispatches the event.
this.modelWeights = newModelWeights
this.modelNames = newModelNames
}
get disabled() {
return false
}
set disabled(state) {
// do nothing
}
getModelElements(ignoreEmpty = false) {
let entries = this.root.querySelectorAll(".model_entry")
entries = [...entries]
let elements = entries.map((e) => {
let modelName = e.querySelector(".model_name").field
let modelWeight = e.querySelector(".model_weight")
if (ignoreEmpty && modelName.value.trim() === "") {
return null
}
return { name: modelName, weight: modelWeight }
})
elements = elements.filter((e) => e !== null)
return elements
}
addEventListener(type, listener, options) {
// do nothing
}
dispatchEvent(event) {
// do nothing
}
appendChild(option) {
// do nothing
}
// remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html
bind(f, obj) {
return function() {
return f.apply(obj, arguments)
}
}
constructor(root, modelType, modelNameFriendly = undefined, defaultWeight = 0.5, weightStep = 0.02) {
this.root = root
this.modelType = modelType
this.modelNameFriendly = modelNameFriendly || modelType
this.defaultWeight = defaultWeight
this.weightStep = weightStep
let self = this
document.addEventListener("refreshModels", function() {
setTimeout(self.bind(self.populateModels, self), 1)
})
this.createStructure()
this.populateModels()
}
createStructure() {
this.modelContainer = document.createElement("div")
this.modelContainer.className = "model_entries"
this.root.appendChild(this.modelContainer)
this.addNewButton = document.createElement("button")
this.addNewButton.className = "add_model_entry"
this.addNewButton.innerHTML = '<i class="fa-solid fa-plus"></i> add another ' + this.modelNameFriendly
this.addNewButton.addEventListener("click", this.bind(this.addModelEntry, this))
this.root.appendChild(this.addNewButton)
}
populateModels() {
if (this.root.dataset.path === "") {
if (this.length === 0) {
this.addModelEntry() // create a single blank entry
}
} else {
this.value = JSON.parse(this.root.dataset.path)
}
}
addModelEntry() {
let idx = this.counter++
let currLength = this.length
const modelElement = document.createElement("div")
modelElement.className = "model_entry"
modelElement.innerHTML = `
<input id="${this.modelType}_${idx}" class="model_name model-filter" type="text" spellcheck="false" autocomplete="off" data-path="" />
<input class="model_weight" type="number" step="${this.weightStep}" value="${this.defaultWeight}" pattern="^-?[0-9]*\.?[0-9]*$" onkeypress="preventNonNumericalInput(event)">
`
this.modelContainer.appendChild(modelElement)
let modelNameEl = modelElement.querySelector(".model_name")
modelNameEl.field = new ModelDropdown(modelNameEl, this.modelType, "None")
let modelWeightEl = modelElement.querySelector(".model_weight")
let self = this
function makeUpdateEvent(type) {
return function(e) {
e.stopPropagation()
let modelData = self.value
self.root.dataset.path = JSON.stringify(modelData)
self.root.dispatchEvent(new Event(type))
}
}
modelNameEl.addEventListener("change", makeUpdateEvent("change"))
modelNameEl.addEventListener("input", makeUpdateEvent("input"))
modelWeightEl.addEventListener("change", makeUpdateEvent("change"))
modelWeightEl.addEventListener("input", makeUpdateEvent("input"))
let removeBtn = document.createElement("button")
removeBtn.className = "remove_model_btn"
removeBtn.setAttribute("title", "Remove model")
removeBtn.innerHTML = '<i class="fa-solid fa-minus"></i>'
if (currLength === 0) {
removeBtn.classList.add("displayNone")
}
removeBtn.addEventListener(
"click",
this.bind(function(e) {
this.modelContainer.removeChild(modelElement)
makeUpdateEvent("change")(e)
}, this)
)
modelElement.appendChild(removeBtn)
}
removeModelEntry() {
if (this.length === 0) {
return
}
let lastEntry = this.modelContainer.lastElementChild
lastEntry.remove()
}
get length() {
return this.getModelElements().length
}
get modelNames() {
return this.getModelElements(true).map((e) => e.name.value)
}
set modelNames(newModelNames) {
this.resizeEntryList(newModelNames.length)
if (newModelNames.length === 0) {
this.getModelElements()[0].name.value = ""
}
// assign to the corresponding elements
let currElements = this.getModelElements()
for (let i = 0; i < newModelNames.length; i++) {
let curr = currElements[i]
curr.name.value = newModelNames[i]
}
}
get modelWeights() {
return this.getModelElements(true).map((e) => e.weight.value)
}
set modelWeights(newModelWeights) {
this.resizeEntryList(newModelWeights.length)
if (newModelWeights.length === 0) {
this.getModelElements()[0].weight.value = this.defaultWeight
}
// assign to the corresponding elements
let currElements = this.getModelElements()
for (let i = 0; i < newModelWeights.length; i++) {
let curr = currElements[i]
curr.weight.value = newModelWeights[i]
}
}
resizeEntryList(newLength) {
if (newLength === 0) {
newLength = 1
}
let currLength = this.length
if (currLength < newLength) {
for (let i = currLength; i < newLength; i++) {
this.addModelEntry()
}
} else {
for (let i = newLength; i < currLength; i++) {
this.removeModelEntry()
}
}
}
}

View File

@ -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 &lt;lora:name:0.4&gt; from the prompt, and apply the correct LoRA (if present)",
icon: "fa-code",
default: true,
},
{
id: "ui_open_browser_on_start",
type: ParameterType.checkbox,
@ -185,6 +194,17 @@ var PARAMETERS = [
icon: "fa-check-double",
default: true,
},
{
id: "profileName",
type: ParameterType.custom,
label: "Profile Name",
note:
"Name of the profile for model manager settings, e.g. thumbnails for embeddings. Use this to have different settings for different users.",
render: (parameter) => {
return `<input id="${parameter.id}" name="${parameter.id}" value="default" size="12">`
},
icon: "fa-user-gear",
},
{
id: "listen_to_network",
type: ParameterType.checkbox,
@ -220,11 +240,11 @@ var PARAMETERS = [
{
id: "test_diffusers",
type: ParameterType.checkbox,
label: "Test Diffusers",
label: "Use the new v3 engine (diffusers)",
note:
"<b>Experimental! Can have bugs!</b> Use upcoming features (like LoRA) in our new engine. Please press Save, then restart the program after changing this.",
"Use our new v3 engine, with additional features like LoRA, ControlNet, SDXL, Embeddings, Tiling and lots more! Please press Save, then restart the program after changing this.",
icon: "fa-bolt",
default: false,
default: true,
saveInAppConfig: true,
},
{
@ -401,6 +421,7 @@ let useBetaChannelField = document.querySelector("#use_beta_channel")
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
let testDiffusers = document.querySelector("#test_diffusers")
let profileNameField = document.querySelector("#profileName")
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
@ -432,8 +453,6 @@ async function getAppConfig() {
if (config.update_branch === "beta") {
useBetaChannelField.checked = true
document.querySelector("#updateBranchLabel").innerText = "(beta)"
} else {
getParameterSettingsEntry("test_diffusers").classList.add("displayNone")
}
if (config.ui && config.ui.open_browser_on_start === false) {
uiOpenBrowserOnStartField.checked = false
@ -445,11 +464,14 @@ async function getAppConfig() {
listenPortField.value = config.net.listen_port
}
const testDiffusersEnabled = config.test_diffusers && config.update_branch !== "main"
let testDiffusersEnabled = true
if (config.test_diffusers === false) {
testDiffusersEnabled = false
}
testDiffusers.checked = testDiffusersEnabled
if (config.config_on_startup) {
if (config.config_on_startup?.test_diffusers && config.update_branch !== "main") {
if (config.config_on_startup?.test_diffusers) {
document.body.classList.add("diffusers-enabled-on-startup")
document.body.classList.remove("diffusers-disabled-on-startup")
} else {
@ -462,7 +484,9 @@ async function getAppConfig() {
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.querySelector("#negative-embeddings-button").style.display = "none"
document.querySelectorAll("#sampler_name option.diffusers-only").forEach((option) => {
option.style.display = "none"
@ -474,6 +498,7 @@ async function getAppConfig() {
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) => {
@ -481,7 +506,6 @@ 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")
IMAGE_STEP_SIZE = 8
customWidthField.step = IMAGE_STEP_SIZE
customHeightField.step = IMAGE_STEP_SIZE
@ -794,11 +818,3 @@ navigator.permissions.query({ name: "clipboard-write" }).then(function(result) {
})
document.addEventListener("system_info_update", (e) => setDeviceInfo(e.detail))
useBetaChannelField.addEventListener("change", (e) => {
if (e.target.checked) {
getParameterSettingsEntry("test_diffusers").classList.remove("displayNone")
} else {
getParameterSettingsEntry("test_diffusers").classList.add("displayNone")
}
})

View File

@ -118,13 +118,16 @@ class ModelDropdown {
)
}
saveCurrentSelection(elem, value, path) {
saveCurrentSelection(elem, value, path, dispatchEvent = true) {
this.currentSelection.elem = elem
this.currentSelection.value = value
this.currentSelection.path = path
this.modelFilter.dataset.path = path
this.modelFilter.value = value
this.modelFilter.dispatchEvent(new Event("change"))
if (dispatchEvent) {
this.modelFilter.dispatchEvent(new Event("change"))
}
}
processClick(e) {
@ -348,13 +351,13 @@ class ModelDropdown {
}
}
selectEntry(path) {
selectEntry(path, dispatchEvent = true) {
if (path !== undefined) {
const entries = this.modelElements
for (const elem of entries) {
if (elem.dataset.path == path) {
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path, dispatchEvent)
this.highlightedModelEntry = elem
elem.scrollIntoView({ block: "nearest" })
break
@ -529,7 +532,7 @@ class ModelDropdown {
rootModelList.style.minWidth = modelFilterStyle.width
})
this.selectEntry(this.activeModel)
this.selectEntry(this.activeModel, false)
}
/**

409
ui/media/js/task-manager.js Normal file
View File

@ -0,0 +1,409 @@
const htmlTaskMap = new WeakMap()
const pauseBtn = document.querySelector("#pause")
const resumeBtn = document.querySelector("#resume")
const processOrder = document.querySelector("#process_order_toggle")
let pauseClient = false
async function onIdle() {
const serverCapacity = SD.serverCapacity
if (pauseClient === true) {
await resumeClient()
}
for (const taskEntry of getUncompletedTaskEntries()) {
if (SD.activeTasks.size >= serverCapacity) {
break
}
const task = htmlTaskMap.get(taskEntry)
if (!task) {
const taskStatusLabel = taskEntry.querySelector(".taskStatusLabel")
taskStatusLabel.style.display = "none"
continue
}
await onTaskStart(task)
}
}
function getUncompletedTaskEntries() {
const taskEntries = Array.from(document.querySelectorAll("#preview .imageTaskContainer .taskStatusLabel"))
.filter((taskLabel) => taskLabel.style.display !== "none")
.map(function(taskLabel) {
let imageTaskContainer = taskLabel.parentNode
while (!imageTaskContainer.classList.contains("imageTaskContainer") && imageTaskContainer.parentNode) {
imageTaskContainer = imageTaskContainer.parentNode
}
return imageTaskContainer
})
if (!processOrder.checked) {
taskEntries.reverse()
}
return taskEntries
}
async function onTaskStart(task) {
if (!task.isProcessing || task.batchesDone >= task.batchCount) {
return
}
if (typeof task.startTime !== "number") {
task.startTime = Date.now()
}
if (!("instances" in task)) {
task["instances"] = []
}
task["stopTask"].innerHTML = '<i class="fa-solid fa-circle-stop"></i> Stop'
task["taskStatusLabel"].innerText = "Starting"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
if (task.previewTaskReq !== undefined) {
let controlImagePreview = task.taskConfig.querySelector(".controlnet-img-preview > img")
try {
let result = await SD.filter(task.previewTaskReq)
controlImagePreview.src = result.output[0]
let controlImageLargePreview = task.taskConfig.querySelector(
".controlnet-img-preview .task-fs-initimage img"
)
controlImageLargePreview.src = controlImagePreview.src
} catch (error) {
console.log("filter error", error)
}
delete task.previewTaskReq
}
let newTaskReqBody = task.reqBody
if (task.batchCount > 1) {
// Each output render batch needs it's own task reqBody instance to avoid altering the other runs after they are completed.
newTaskReqBody = Object.assign({}, task.reqBody)
if (task.batchesDone == task.batchCount - 1) {
// Last batch of the task
// If the number of parallel jobs is no factor of the total number of images, the last batch must create less than "parallel jobs count" images
// E.g. with numOutputsTotal = 6 and num_outputs = 5, the last batch shall only generate 1 image.
newTaskReqBody.num_outputs = task.numOutputsTotal - task.reqBody.num_outputs * (task.batchCount - 1)
}
}
const startSeed = task.seed || newTaskReqBody.seed
const genSeeds = Boolean(
typeof newTaskReqBody.seed !== "number" || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1)
)
if (genSeeds) {
newTaskReqBody.seed = parseInt(startSeed) + task.batchesDone * task.reqBody.num_outputs
}
const outputContainer = document.createElement("div")
outputContainer.className = "img-batch"
task.outputContainer.insertBefore(outputContainer, task.outputContainer.firstChild)
const eventInfo = { reqBody: newTaskReqBody }
const callbacksPromises = PLUGINS["TASK_CREATE"].map((hook) => {
if (typeof hook !== "function") {
console.error("The provided TASK_CREATE hook is not a function. Hook: %o", hook)
return Promise.reject(new Error("hook is not a function."))
}
try {
return Promise.resolve(hook.call(task, eventInfo))
} catch (err) {
console.error(err)
return Promise.reject(err)
}
})
await Promise.allSettled(callbacksPromises)
let instance = eventInfo.instance
if (!instance) {
const factory = PLUGINS.OUTPUTS_FORMATS.get(eventInfo.reqBody?.output_format || newTaskReqBody.output_format)
if (factory) {
instance = await Promise.resolve(factory(eventInfo.reqBody || newTaskReqBody))
}
if (!instance) {
console.error(
`${factory ? "Factory " + String(factory) : "No factory defined"} for output format ${eventInfo.reqBody
?.output_format || newTaskReqBody.output_format}. Instance is ${instance ||
"undefined"}. Using default renderer.`
)
instance = new SD.RenderTask(eventInfo.reqBody || newTaskReqBody)
}
}
task["instances"].push(instance)
task.batchesDone++
document.dispatchEvent(new CustomEvent("before_task_start", { detail: { task: task } }))
instance.enqueue(getTaskUpdater(task, newTaskReqBody, outputContainer)).then(
(renderResult) => {
onRenderTaskCompleted(task, newTaskReqBody, instance, outputContainer, renderResult)
},
(reason) => {
onTaskErrorHandler(task, newTaskReqBody, instance, reason)
}
)
document.dispatchEvent(new CustomEvent("after_task_start", { detail: { task: task } }))
}
function getTaskUpdater(task, reqBody, outputContainer) {
const outputMsg = task["outputMsg"]
const progressBar = task["progressBar"]
const progressBarInner = progressBar.querySelector("div")
const batchCount = task.batchCount
let lastStatus = undefined
return async function(event) {
if (this.status !== lastStatus) {
lastStatus = this.status
switch (this.status) {
case SD.TaskStatus.pending:
task["taskStatusLabel"].innerText = "Pending"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
break
case SD.TaskStatus.waiting:
task["taskStatusLabel"].innerText = "Waiting"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
task["taskStatusLabel"].classList.remove("activeTaskLabel")
break
case SD.TaskStatus.processing:
case SD.TaskStatus.completed:
task["taskStatusLabel"].innerText = "Processing"
task["taskStatusLabel"].classList.add("activeTaskLabel")
task["taskStatusLabel"].classList.remove("waitingTaskLabel")
break
case SD.TaskStatus.stopped:
break
case SD.TaskStatus.failed:
if (!SD.isServerAvailable()) {
logError(
"Stable Diffusion is still starting up, please wait. If this goes on beyond a few minutes, Stable Diffusion has probably crashed. Please check the error message in the command-line window.",
event,
outputMsg
)
} else if (typeof event?.response === "object") {
let msg = "Stable Diffusion had an error reading the response:<br/><pre>"
if (this.exception) {
msg += `Error: ${this.exception.message}<br/>`
}
try {
// 'Response': body stream already read
msg += "Read: " + (await event.response.text())
} catch (e) {
msg += "Unexpected end of stream. "
}
const bufferString = event.reader.bufferedString
if (bufferString) {
msg += "Buffered data: " + bufferString
}
msg += "</pre>"
logError(msg, event, outputMsg)
}
break
}
}
if ("update" in event) {
const stepUpdate = event.update
if (!("step" in stepUpdate)) {
return
}
// task.instances can be a mix of different tasks with uneven number of steps (Render Vs Filter Tasks)
const instancesWithProgressUpdates = task.instances.filter((instance) => instance.step !== undefined)
const overallStepCount =
instancesWithProgressUpdates.reduce(
(sum, instance) =>
sum +
(instance.isPending
? Math.max(0, instance.step || stepUpdate.step) /
(instance.total_steps || stepUpdate.total_steps)
: 1),
0 // Initial value
) * stepUpdate.total_steps // Scale to current number of steps.
const totalSteps = instancesWithProgressUpdates.reduce(
(sum, instance) => sum + (instance.total_steps || stepUpdate.total_steps),
stepUpdate.total_steps * (batchCount - task.batchesDone) // Initial value at (unstarted task count * Nbr of steps)
)
const percent = Math.min(100, 100 * (overallStepCount / totalSteps)).toFixed(0)
const timeTaken = stepUpdate.step_time // sec
const stepsRemaining = Math.max(0, totalSteps - overallStepCount)
const timeRemaining = timeTaken < 0 ? "" : millisecondsToStr(stepsRemaining * timeTaken * 1000)
outputMsg.innerHTML = `Batch ${task.batchesDone} of ${batchCount}. Generating image(s): ${percent}%. Time remaining (approx): ${timeRemaining}`
outputMsg.style.display = "block"
progressBarInner.style.width = `${percent}%`
if (stepUpdate.output) {
document.dispatchEvent(
new CustomEvent("on_task_step", {
detail: {
task: task,
reqBody: reqBody,
stepUpdate: stepUpdate,
outputContainer: outputContainer,
},
})
)
}
}
}
}
function onRenderTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) {
if (typeof stepUpdate === "object") {
if (stepUpdate.status === "succeeded") {
document.dispatchEvent(
new CustomEvent("on_render_task_success", {
detail: {
task: task,
reqBody: reqBody,
stepUpdate: stepUpdate,
outputContainer: outputContainer,
},
})
)
} else {
task.isProcessing = false
document.dispatchEvent(
new CustomEvent("on_render_task_fail", {
detail: {
task: task,
reqBody: reqBody,
stepUpdate: stepUpdate,
outputContainer: outputContainer,
},
})
)
}
}
if (task.isProcessing && task.batchesDone < task.batchCount) {
task["taskStatusLabel"].innerText = "Pending"
task["taskStatusLabel"].classList.add("waitingTaskLabel")
task["taskStatusLabel"].classList.remove("activeTaskLabel")
return
}
if ("instances" in task && task.instances.some((ins) => ins != instance && ins.isPending)) {
return
}
task.isProcessing = false
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
task["taskStatusLabel"].style.display = "none"
let time = millisecondsToStr(Date.now() - task.startTime)
if (task.batchesDone == task.batchCount) {
if (!task.outputMsg.innerText.toLowerCase().includes("error")) {
task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time}`
}
task.progressBar.style.height = "0px"
task.progressBar.style.border = "0px solid var(--background-color3)"
task.progressBar.classList.remove("active")
// setStatus("request", "done", "success")
} else {
task.outputMsg.innerText += `. Task ended after ${time}`
}
// if (randomSeedField.checked) { // we already update this before the task starts
// seedField.value = task.seed
// }
if (SD.activeTasks.size > 0) {
return
}
const uncompletedTasks = getUncompletedTaskEntries()
if (uncompletedTasks && uncompletedTasks.length > 0) {
return
}
if (pauseClient) {
resumeBtn.click()
}
document.dispatchEvent(
new CustomEvent("on_all_tasks_complete", {
detail: {},
})
)
}
function resumeClient() {
if (pauseClient) {
document.body.classList.remove("wait-pause")
document.body.classList.add("pause")
}
return new Promise((resolve) => {
let playbuttonclick = function() {
resumeBtn.removeEventListener("click", playbuttonclick)
resolve("resolved")
}
resumeBtn.addEventListener("click", playbuttonclick)
})
}
function abortTask(task) {
if (!task.isProcessing) {
return false
}
task.isProcessing = false
task.progressBar.classList.remove("active")
task["taskStatusLabel"].style.display = "none"
task["stopTask"].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
if (!task.instances?.some((r) => r.isPending)) {
return
}
task.instances.forEach((instance) => {
try {
instance.abort()
} catch (e) {
console.error(e)
}
})
}
async function stopAllTasks() {
getUncompletedTaskEntries().forEach((taskEntry) => {
const taskStatusLabel = taskEntry.querySelector(".taskStatusLabel")
if (taskStatusLabel) {
taskStatusLabel.style.display = "none"
}
const task = htmlTaskMap.get(taskEntry)
if (!task) {
return
}
abortTask(task)
})
}
function onTaskErrorHandler(task, reqBody, instance, reason) {
if (!task.isProcessing) {
return
}
console.log("Render request %o, Instance: %o, Error: %s", reqBody, instance, reason)
abortTask(task)
const outputMsg = task["outputMsg"]
logError(
"Stable Diffusion had an error. Please check the logs in the command-line window. <br/><br/>" +
reason +
"<br/><pre>" +
reason.stack +
"</pre>",
task,
outputMsg
)
// setStatus("request", "error", "error")
}
pauseBtn.addEventListener("click", function() {
pauseClient = true
pauseBtn.style.display = "none"
resumeBtn.style.display = "inline"
document.body.classList.add("wait-pause")
})
resumeBtn.addEventListener("click", function() {
pauseClient = false
resumeBtn.style.display = "none"
pauseBtn.style.display = "inline"
document.body.classList.remove("pause")
document.body.classList.remove("wait-pause")
})

View File

@ -1097,6 +1097,48 @@ async function deleteKeys(keyToDelete) {
}
}
/**
* @param {String} Data URL of the image
* @param {Integer} Top left X-coordinate of the crop area
* @param {Integer} Top left Y-coordinate of the crop area
* @param {Integer} Width of the crop area
* @param {Integer} Height of the crop area
* @return {String}
*/
function cropImageDataUrl(dataUrl, x, y, width, height) {
return new Promise((resolve, reject) => {
const image = new Image()
image.src = dataUrl
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(image, x, y, width, height, 0, 0, width, height)
const croppedDataUrl = canvas.toDataURL('image/png')
resolve(croppedDataUrl)
}
image.onerror = (error) => {
reject(error)
}
})
}
/**
* @param {String} HTML representing a single element
* @return {Element}
*/
function htmlToElement(html) {
var template = document.createElement('template');
html = html.trim(); // Never return a text node of whitespace as the result
template.innerHTML = html;
return template.content.firstChild;
}
function modalDialogCloseOnBackdropClick(dialog) {
dialog.addEventListener('mousedown', function (event) {
// Firefox creates an event with clientX|Y = 0|0 when choosing an <option>.
@ -1156,4 +1198,37 @@ function makeDialogDraggable(element) {
})() )
}
function logMsg(msg, level, outputMsg) {
if (outputMsg.hasChildNodes()) {
outputMsg.appendChild(document.createElement("br"))
}
if (level === "error") {
outputMsg.innerHTML += '<span style="color: red">Error: ' + msg + "</span>"
} else if (level === "warn") {
outputMsg.innerHTML += '<span style="color: orange">Warning: ' + msg + "</span>"
} else {
outputMsg.innerText += msg
}
console.log(level, msg)
}
function logError(msg, res, outputMsg) {
logMsg(msg, "error", outputMsg)
console.log("request error", res)
console.trace()
// setStatus("request", "error", "error")
}
function playSound() {
const audio = new Audio("/media/ding.mp3")
audio.volume = 0.2
var promise = audio.play()
if (promise !== undefined) {
promise
.then((_) => {})
.catch((error) => {
console.warn("browser blocked autoplay")
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,11 @@
"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));
@ -20,44 +25,17 @@
}
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 modelNames = LoRA.map(e => e.lora_model_0)
let modelWeights = LoRA.map(e => e.lora_alpha_0)
loraModelField.value = {modelNames: modelNames, modelWeights: modelWeights}
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);
showToast("Prompt successfully processed")
}
//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
@ -68,11 +46,13 @@
// Iterate over the string, finding matches
for (const match of prompt.matchAll(regex)) {
const modelFileName = isModelAvailable(modelsCache.options.lora, match[1].trim())
if (modelFileName !== "") {
const modelFileName = match[1].trim()
const loraPathes = getAllModelPathes("lora", modelFileName)
if (loraPathes.length > 0) {
const loraPath = loraPathes[0]
// Initialize an object to hold a match
let loraTag = {
lora_model_0: modelFileName,
lora_model_0: loraPath,
}
//console.log("Model:" + modelFileName);