forked from extern/easydiffusion
commit
c8de1cd49b
@ -712,3 +712,411 @@ FileSaver.js is licensed under the MIT license:
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
[1]: http://eligrey.com
|
[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.
|
||||||
|
39
CHANGES.md
39
CHANGES.md
@ -1,5 +1,44 @@
|
|||||||
# What's new?
|
# 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
|
## v2.5
|
||||||
### Major Changes
|
### 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
|
- **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
|
||||||
|
47
README.md
47
README.md
@ -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.
|
### 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.
|
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>
|
[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
|
# Installation
|
||||||
Click the download button for your operating system:
|
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.
|
- **UI Themes**: Customize the program to your liking.
|
||||||
- **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
|
- **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
|
||||||
|
|
||||||
### Image generation
|
### Powerful image generation
|
||||||
- **Supports**: "*Text to Image*" and "*Image to Image*".
|
- **Supports**: "*Text to Image*", "*Image to Image*" and "*InPainting*"
|
||||||
- **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`.
|
- **ControlNet**: For advanced control over the image, e.g. by setting the pose or drawing the outline for the AI to fill in.
|
||||||
- **In-Painting**: Specify areas of your image to paint into.
|
- **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.
|
- **Simple Drawing Tool**: Draw basic images to guide the AI, without needing an external drawing program.
|
||||||
- **Face Correction (GFPGAN)**
|
- **Face Correction (GFPGAN)**
|
||||||
- **Upscaling (RealESRGAN)**
|
- **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*.
|
- **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.
|
- **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`.
|
- **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 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}`
|
- **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.
|
- **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
|
### Advanced features
|
||||||
- **Custom Models**: Use your own `.ckpt` or `.safetensors` file, by placing it inside the `models/stable-diffusion` folder!
|
- **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**
|
- **Merge Models**
|
||||||
- **Use custom VAE models**
|
- **Use custom VAE models**
|
||||||
- **Use pre-trained Hypernetworks**
|
- **Textual Inversion Embeddings**
|
||||||
|
- **ControlNet**
|
||||||
- **Use custom GFPGAN models**
|
- **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!
|
- **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:
|
## Easy for new users, powerful features for advanced users:
|
||||||
![Screenshot of the initial UI](https://user-images.githubusercontent.com/844287/217043152-29454d15-0387-4228-b70d-9a4b84aeb8ba.png)
|
![image](https://github.com/easydiffusion/easydiffusion/assets/844287/efbbac9f-42ce-4aef-8625-fd23c74a8241)
|
||||||
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
## Task Queue
|
## Task Queue
|
||||||
![Screenshot of task queue](https://user-images.githubusercontent.com/844287/217043984-0b35f73b-1318-47cb-9eed-a2a91b430490.png)
|
![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
|
# 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).
|
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.
|
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
|
# Credits
|
||||||
|
BIN
patch.patch
Normal file
BIN
patch.patch
Normal file
Binary file not shown.
@ -18,13 +18,15 @@ os_name = platform.system()
|
|||||||
modules_to_check = {
|
modules_to_check = {
|
||||||
"torch": ("1.11.0", "1.13.1", "2.0.0"),
|
"torch": ("1.11.0", "1.13.1", "2.0.0"),
|
||||||
"torchvision": ("0.12.0", "0.14.1", "0.15.1"),
|
"torchvision": ("0.12.0", "0.14.1", "0.15.1"),
|
||||||
"sdkit": "1.0.165",
|
"sdkit": "2.0.3",
|
||||||
"stable-diffusion-sdkit": "2.1.4",
|
"stable-diffusion-sdkit": "2.1.4",
|
||||||
"rich": "12.6.0",
|
"rich": "12.6.0",
|
||||||
"uvicorn": "0.19.0",
|
"uvicorn": "0.19.0",
|
||||||
"fastapi": "0.85.1",
|
"fastapi": "0.85.1",
|
||||||
"pycloudflared": "0.2.0",
|
"pycloudflared": "0.2.0",
|
||||||
"ruamel.yaml": "0.17.21",
|
"ruamel.yaml": "0.17.21",
|
||||||
|
"sqlalchemy": "2.0.19",
|
||||||
|
"python-multipart": "0.0.6",
|
||||||
# "xformers": "0.0.16",
|
# "xformers": "0.0.16",
|
||||||
}
|
}
|
||||||
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]
|
modules_to_log = ["torch", "torchvision", "sdkit", "stable-diffusion-sdkit"]
|
||||||
|
@ -46,6 +46,8 @@ if "%update_branch%"=="" (
|
|||||||
|
|
||||||
@cd sd-ui-files
|
@cd sd-ui-files
|
||||||
|
|
||||||
|
@call git add -A .
|
||||||
|
@call git stash
|
||||||
@call git reset --hard
|
@call git reset --hard
|
||||||
@call git -c advice.detachedHead=false checkout "%update_branch%"
|
@call git -c advice.detachedHead=false checkout "%update_branch%"
|
||||||
@call git pull
|
@call git pull
|
||||||
|
@ -29,6 +29,8 @@ if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/in
|
|||||||
|
|
||||||
cd sd-ui-files
|
cd sd-ui-files
|
||||||
|
|
||||||
|
git add -A .
|
||||||
|
git stash
|
||||||
git reset --hard
|
git reset --hard
|
||||||
git -c advice.detachedHead=false checkout "$update_branch"
|
git -c advice.detachedHead=false checkout "$update_branch"
|
||||||
git pull
|
git pull
|
||||||
|
@ -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"))
|
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts"))
|
||||||
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models"))
|
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"))
|
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"))
|
CORE_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "plugins"))
|
||||||
@ -60,6 +61,7 @@ APP_CONFIG_DEFAULTS = {
|
|||||||
"ui": {
|
"ui": {
|
||||||
"open_browser_on_start": True,
|
"open_browser_on_start": True,
|
||||||
},
|
},
|
||||||
|
"test_diffusers": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
IMAGE_EXTENSIONS = [
|
IMAGE_EXTENSIONS = [
|
||||||
@ -115,7 +117,7 @@ def getConfig(default_val=APP_CONFIG_DEFAULTS):
|
|||||||
|
|
||||||
def set_config_on_startup(config: dict):
|
def set_config_on_startup(config: dict):
|
||||||
if getConfig.__test_diffusers_on_startup is None:
|
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}
|
config["config_on_startup"] = {"test_diffusers": getConfig.__test_diffusers_on_startup}
|
||||||
|
|
||||||
if os.path.isfile(config_yaml_path):
|
if os.path.isfile(config_yaml_path):
|
||||||
|
102
ui/easydiffusion/bucket_manager.py
Normal file
102
ui/easydiffusion/bucket_manager.py
Normal 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:]
|
24
ui/easydiffusion/easydb/crud.py
Normal file
24
ui/easydiffusion/easydb/crud.py
Normal 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
|
||||||
|
|
14
ui/easydiffusion/easydb/database.py
Normal file
14
ui/easydiffusion/easydb/database.py
Normal 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()
|
25
ui/easydiffusion/easydb/models.py
Normal file
25
ui/easydiffusion/easydb/models.py
Normal 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")
|
||||||
|
|
36
ui/easydiffusion/easydb/schemas.py
Normal file
36
ui/easydiffusion/easydb/schemas.py
Normal 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
|
||||||
|
|
@ -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 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.models.model_loader.controlnet_filters import filters as cn_filters
|
||||||
from sdkit.utils import hash_file_quick
|
from sdkit.utils import hash_file_quick
|
||||||
|
from sdkit.models.model_loader.embeddings import get_embedding_token
|
||||||
|
|
||||||
KNOWN_MODEL_TYPES = [
|
KNOWN_MODEL_TYPES = [
|
||||||
"stable-diffusion",
|
"stable-diffusion",
|
||||||
@ -29,7 +30,7 @@ MODEL_EXTENSIONS = {
|
|||||||
"hypernetwork": [".pt", ".safetensors"],
|
"hypernetwork": [".pt", ".safetensors"],
|
||||||
"gfpgan": [".pth"],
|
"gfpgan": [".pth"],
|
||||||
"realesrgan": [".pth"],
|
"realesrgan": [".pth"],
|
||||||
"lora": [".ckpt", ".safetensors"],
|
"lora": [".ckpt", ".safetensors", ".pt"],
|
||||||
"codeformer": [".pth"],
|
"codeformer": [".pth"],
|
||||||
"embeddings": [".pt", ".bin", ".safetensors"],
|
"embeddings": [".pt", ".bin", ".safetensors"],
|
||||||
"controlnet": [".pth", ".safetensors"],
|
"controlnet": [".pth", ".safetensors"],
|
||||||
@ -65,9 +66,6 @@ def load_default_models(context: Context):
|
|||||||
|
|
||||||
runtime.set_vram_optimizations(context)
|
runtime.set_vram_optimizations(context)
|
||||||
|
|
||||||
config = app.getConfig()
|
|
||||||
context.embeddings_path = os.path.join(app.MODELS_DIR, "embeddings")
|
|
||||||
|
|
||||||
# init default model paths
|
# init default model paths
|
||||||
for model_type in MODELS_TO_LOAD_ON_START:
|
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)
|
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):
|
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_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
|
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
|
return default_model_path
|
||||||
|
|
||||||
if model_name and fail_if_not_found:
|
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 = []):
|
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_mlsd": "Straight Lines"},
|
||||||
{"control_v11p_sd15_seg": "Segment"},
|
{"control_v11p_sd15_seg": "Segment"},
|
||||||
{"control_v11e_sd15_shuffle": "Shuffle"},
|
{"control_v11e_sd15_shuffle": "Shuffle"},
|
||||||
|
{"control_v11f1e_sd15_tile": "Tile"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -324,9 +334,11 @@ def getModels(scan_for_malicious: bool = True):
|
|||||||
class MaliciousModelException(Exception):
|
class MaliciousModelException(Exception):
|
||||||
"Raised when picklescan reports a problem with a model"
|
"Raised when picklescan reports a problem with a model"
|
||||||
|
|
||||||
def scan_directory(directory, suffixes, directoriesFirst: bool = True, default_entries=[]):
|
def scan_directory(directory, suffixes, directoriesFirst: bool = True, default_entries=[], nameFilter=None):
|
||||||
tree = list(default_entries)
|
|
||||||
nonlocal models_scanned
|
nonlocal models_scanned
|
||||||
|
|
||||||
|
tree = list(default_entries)
|
||||||
|
|
||||||
for entry in sorted(
|
for entry in sorted(
|
||||||
os.scandir(directory),
|
os.scandir(directory),
|
||||||
key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower()),
|
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)
|
raise MaliciousModelException(entry.path)
|
||||||
if scan_for_malicious:
|
if scan_for_malicious:
|
||||||
known_models[entry.path] = mtime
|
known_models[entry.path] = mtime
|
||||||
|
|
||||||
model_id = entry.name[: -len(matching_suffix)]
|
model_id = entry.name[: -len(matching_suffix)]
|
||||||
|
if callable(nameFilter):
|
||||||
|
model_id = nameFilter(model_id)
|
||||||
|
|
||||||
model_exists = False
|
model_exists = False
|
||||||
for m in tree: # allows default "named" models, like CodeFormer and known ControlNet models
|
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):
|
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
|
break
|
||||||
if not model_exists:
|
if not model_exists:
|
||||||
tree.append(model_id)
|
tree.append(model_id)
|
||||||
|
|
||||||
elif entry.is_dir():
|
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:
|
if len(scan) != 0:
|
||||||
tree.append((entry.name, scan))
|
tree.append((entry.name, scan))
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
def listModels(model_type):
|
def listModels(model_type, nameFilter=None):
|
||||||
nonlocal models_scanned
|
nonlocal models_scanned
|
||||||
|
|
||||||
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
|
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
|
||||||
@ -370,7 +387,9 @@ def getModels(scan_for_malicious: bool = True):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
default_tree = models["options"].get(model_type, [])
|
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:
|
except MaliciousModelException as e:
|
||||||
models["scan-error"] = str(e)
|
models["scan-error"] = str(e)
|
||||||
|
|
||||||
@ -382,7 +401,7 @@ def getModels(scan_for_malicious: bool = True):
|
|||||||
listModels(model_type="hypernetwork")
|
listModels(model_type="hypernetwork")
|
||||||
listModels(model_type="gfpgan")
|
listModels(model_type="gfpgan")
|
||||||
listModels(model_type="lora")
|
listModels(model_type="lora")
|
||||||
listModels(model_type="embeddings")
|
listModels(model_type="embeddings", nameFilter=get_embedding_token)
|
||||||
listModels(model_type="controlnet")
|
listModels(model_type="controlnet")
|
||||||
|
|
||||||
if scan_for_malicious and models_scanned > 0:
|
if scan_for_malicious and models_scanned > 0:
|
||||||
|
@ -12,9 +12,9 @@ from easydiffusion import app
|
|||||||
manifest = {
|
manifest = {
|
||||||
"tensorrt": {
|
"tensorrt": {
|
||||||
"install": [
|
"install": [
|
||||||
"nvidia-cudnn --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 --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com",
|
"tensorrt-libs --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
|
||||||
"tensorrt --extra-index-url=https://pypi.ngc.nvidia.com --trusted-host pypi.ngc.nvidia.com",
|
"tensorrt --pre --extra-index-url=https://pypi.nvidia.com --trusted-host pypi.nvidia.com",
|
||||||
],
|
],
|
||||||
"uninstall": ["tensorrt"],
|
"uninstall": ["tensorrt"],
|
||||||
# TODO also uninstall tensorrt-libs and nvidia-cudnn, but do it upon restarting (avoid 'file in use' error)
|
# 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
|
# remove this once TRT releases on pypi
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
trt_dir = os.path.join(app.ROOT_DIR, "tensorrt")
|
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)
|
files = os.listdir(trt_dir)
|
||||||
|
|
||||||
packages = manifest["tensorrt"]["install"]
|
packages = manifest["tensorrt"]["install"]
|
||||||
@ -61,6 +61,10 @@ def install(module_name):
|
|||||||
raise RuntimeError(f"Can't install unknown package: {module_name}!")
|
raise RuntimeError(f"Can't install unknown package: {module_name}!")
|
||||||
|
|
||||||
commands = manifest[module_name]["install"]
|
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]
|
commands = [f"python -m pip install --upgrade {cmd}" for cmd in commands]
|
||||||
|
|
||||||
installing.append(module_name)
|
installing.append(module_name)
|
||||||
|
@ -30,9 +30,7 @@ def init(device):
|
|||||||
from easydiffusion import app
|
from easydiffusion import app
|
||||||
|
|
||||||
app_config = app.getConfig()
|
app_config = app.getConfig()
|
||||||
context.test_diffusers = (
|
context.test_diffusers = app_config.get("test_diffusers", True)
|
||||||
app_config.get("test_diffusers", False) and app_config.get("update_branch", "main") != "main"
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info("Device usage during initialization:")
|
log.info("Device usage during initialization:")
|
||||||
get_device_usage(device, log_info=True, process_usage_only=False)
|
get_device_usage(device, log_info=True, process_usage_only=False)
|
||||||
|
@ -63,7 +63,7 @@ class SetAppConfigRequest(BaseModel, extra=Extra.allow):
|
|||||||
ui_open_browser_on_start: bool = None
|
ui_open_browser_on_start: bool = None
|
||||||
listen_to_network: bool = None
|
listen_to_network: bool = None
|
||||||
listen_port: int = None
|
listen_port: int = None
|
||||||
test_diffusers: bool = False
|
test_diffusers: bool = True
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
@ -139,6 +139,10 @@ def init():
|
|||||||
def modify_package(package_name: str, req: dict):
|
def modify_package(package_name: str, req: dict):
|
||||||
return modify_package_internal(package_name, req)
|
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("/")
|
@server_api.get("/")
|
||||||
def read_root():
|
def read_root():
|
||||||
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
|
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(str(e))
|
||||||
log.error(traceback.format_exc())
|
log.error(traceback.format_exc())
|
||||||
return HTTPException(status_code=500, detail=str(e))
|
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))
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import pprint
|
|||||||
|
|
||||||
from sdkit.filter import apply_filters
|
from sdkit.filter import apply_filters
|
||||||
from sdkit.models import load_model
|
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 import model_manager, runtime
|
||||||
from easydiffusion.types import FilterImageRequest, FilterImageResponse, ModelsData, OutputFormatData
|
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)
|
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
|
output_format = self.output_format
|
||||||
images = [
|
images = [
|
||||||
|
@ -15,6 +15,8 @@ from sdkit.utils import (
|
|||||||
img_to_base64_str,
|
img_to_base64_str,
|
||||||
img_to_buffer,
|
img_to_buffer,
|
||||||
latent_samples_to_images,
|
latent_samples_to_images,
|
||||||
|
resize_img,
|
||||||
|
get_image,
|
||||||
log,
|
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
|
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:
|
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]
|
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:
|
if context.test_diffusers:
|
||||||
pipe = context.models["stable-diffusion"]["default"]
|
pipe = context.models["stable-diffusion"]["default"]
|
||||||
if hasattr(pipe.unet, "_allocate_trt_buffers_backup"):
|
if hasattr(pipe.unet, "_allocate_trt_buffers_backup"):
|
||||||
|
@ -73,6 +73,7 @@ class TaskData(BaseModel):
|
|||||||
use_hypernetwork_model: Union[str, List[str]] = None
|
use_hypernetwork_model: Union[str, List[str]] = None
|
||||||
use_lora_model: Union[str, List[str]] = None
|
use_lora_model: Union[str, List[str]] = None
|
||||||
use_controlnet_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] = []
|
filters: List[str] = []
|
||||||
filter_params: Dict[str, Dict[str, Any]] = {}
|
filter_params: Dict[str, Dict[str, Any]] = {}
|
||||||
control_filter_to_apply: Union[str, List[str]] = None
|
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["hypernetwork"] = old_req.get("use_hypernetwork_model")
|
||||||
model_paths["lora"] = old_req.get("use_lora_model")
|
model_paths["lora"] = old_req.get("use_lora_model")
|
||||||
model_paths["controlnet"] = old_req.get("use_controlnet_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"] = old_req.get("use_face_correction", "")
|
||||||
model_paths["gfpgan"] = model_paths["gfpgan"] if "gfpgan" in model_paths["gfpgan"].lower() else None
|
model_paths["gfpgan"] = model_paths["gfpgan"] if "gfpgan" in model_paths["gfpgan"].lower() else None
|
||||||
|
@ -6,3 +6,15 @@ from .save_utils import (
|
|||||||
save_images_to_disk,
|
save_images_to_disk,
|
||||||
get_printable_request,
|
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()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from easydiffusion import app
|
|||||||
from easydiffusion.types import GenerateImageRequest, TaskData, OutputFormatData
|
from easydiffusion.types import GenerateImageRequest, TaskData, OutputFormatData
|
||||||
from numpy import base_repr
|
from numpy import base_repr
|
||||||
from sdkit.utils import save_dicts, save_images
|
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._-]")
|
filename_regex = re.compile("[^a-zA-Z0-9._-]")
|
||||||
img_number_regex = re.compile("([0-9]{5,})")
|
img_number_regex = re.compile("([0-9]{5,})")
|
||||||
@ -34,7 +35,7 @@ TASK_TEXT_MAPPING = {
|
|||||||
"lora_alpha": "LoRA Strength",
|
"lora_alpha": "LoRA Strength",
|
||||||
"use_hypernetwork_model": "Hypernetwork model",
|
"use_hypernetwork_model": "Hypernetwork model",
|
||||||
"hypernetwork_strength": "Hypernetwork Strength",
|
"hypernetwork_strength": "Hypernetwork Strength",
|
||||||
"use_embedding_models": "Embedding models",
|
"use_embeddings_model": "Embedding models",
|
||||||
"tiling": "Seamless Tiling",
|
"tiling": "Seamless Tiling",
|
||||||
"use_face_correction": "Use Face Correction",
|
"use_face_correction": "Use Face Correction",
|
||||||
"use_upscale": "Use Upscaling",
|
"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())
|
task_data_metadata.update(output_format.dict())
|
||||||
|
|
||||||
app_config = app.getConfig()
|
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
|
# Save the metadata in the order defined in TASK_TEXT_MAPPING
|
||||||
metadata = {}
|
metadata = {}
|
||||||
@ -228,20 +229,18 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
|
|||||||
metadata[key] = req_metadata[key]
|
metadata[key] = req_metadata[key]
|
||||||
elif key in task_data_metadata:
|
elif key in task_data_metadata:
|
||||||
metadata[key] = task_data_metadata[key]
|
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"}
|
embeddings_extensions = {".pt", ".bin", ".safetensors"}
|
||||||
|
|
||||||
def scan_directory(directory_path: str):
|
def scan_directory(directory_path: str):
|
||||||
used_embeddings = []
|
used_embeddings = []
|
||||||
for entry in os.scandir(directory_path):
|
for entry in os.scandir(directory_path):
|
||||||
if entry.is_file():
|
if entry.is_file():
|
||||||
entry_extension = os.path.splitext(entry.name)[1]
|
# Check if the filename has the right extension
|
||||||
if entry_extension not in embeddings_extensions:
|
if not any(map(lambda ext: entry.name.endswith(ext), embeddings_extensions)):
|
||||||
continue
|
continue
|
||||||
|
embedding_name_regex = regex.compile(r"(^|[\s,])" + regex.escape(get_embedding_token(entry.name)) + r"([+-]*$|[\s,]|[+-]+[\s,])")
|
||||||
embedding_name_regex = regex.compile(
|
|
||||||
r"(^|[\s,])" + regex.escape(os.path.splitext(entry.name)[0]) + r"([+-]*$|[\s,]|[+-]+[\s,])"
|
|
||||||
)
|
|
||||||
if embedding_name_regex.search(req.prompt) or embedding_name_regex.search(req.negative_prompt):
|
if embedding_name_regex.search(req.prompt) or embedding_name_regex.search(req.negative_prompt):
|
||||||
used_embeddings.append(entry.path)
|
used_embeddings.append(entry.path)
|
||||||
elif entry.is_dir():
|
elif entry.is_dir():
|
||||||
@ -249,7 +248,7 @@ def get_printable_request(req: GenerateImageRequest, task_data: TaskData, output
|
|||||||
return used_embeddings
|
return used_embeddings
|
||||||
|
|
||||||
used_embeddings = scan_directory(os.path.join(app.MODELS_DIR, "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
|
# Clean up the metadata
|
||||||
if req.init_image is None and "prompt_strength" in 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:
|
if task_data.use_controlnet_model is None and "control_filter_to_apply" in metadata:
|
||||||
del metadata["control_filter_to_apply"]
|
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 (
|
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
|
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
|
||||||
):
|
):
|
||||||
|
104
ui/index.html
104
ui/index.html
@ -18,12 +18,15 @@
|
|||||||
<link rel="stylesheet" href="/media/css/image-modal.css">
|
<link rel="stylesheet" href="/media/css/image-modal.css">
|
||||||
<link rel="stylesheet" href="/media/css/plugins.css">
|
<link rel="stylesheet" href="/media/css/plugins.css">
|
||||||
<link rel="stylesheet" href="/media/css/animations.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">
|
<link rel="manifest" href="/media/manifest.webmanifest">
|
||||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||||
<script src="/media/js/jquery-confirm.min.js"></script>
|
<script src="/media/js/jquery-confirm.min.js"></script>
|
||||||
<script src="/media/js/jszip.min.js"></script>
|
<script src="/media/js/jszip.min.js"></script>
|
||||||
<script src="/media/js/FileSaver.min.js"></script>
|
<script src="/media/js/FileSaver.min.js"></script>
|
||||||
<script src="/media/js/marked.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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
@ -32,7 +35,7 @@
|
|||||||
<h1>
|
<h1>
|
||||||
<img id="logo_img" src="/media/images/icon-512x512.png" >
|
<img id="logo_img" src="/media/images/icon-512x512.png" >
|
||||||
Easy Diffusion
|
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>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div id="server-status">
|
<div id="server-status">
|
||||||
@ -59,7 +62,14 @@
|
|||||||
<div id="editor-inputs-prompt" class="row">
|
<div id="editor-inputs-prompt" class="row">
|
||||||
<div id="prompt-toolbar" class="split-toolbar">
|
<div id="prompt-toolbar" class="split-toolbar">
|
||||||
<div id="prompt-toolbar-left" class="toolbar-left">
|
<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>
|
||||||
<div id="prompt-toolbar-right" class="toolbar-right">
|
<div id="prompt-toolbar-right" class="toolbar-right">
|
||||||
<button id="image-modifier-dropdown" class="tertiaryButton smallButton">+ Image Modifiers</button>
|
<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>
|
<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>
|
<small>(optional)</small>
|
||||||
</label>
|
</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">
|
<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>
|
<textarea id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
@ -81,6 +91,11 @@
|
|||||||
|
|
||||||
<div id="editor-inputs-init-image" class="row">
|
<div id="editor-inputs-init-image" class="row">
|
||||||
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label>
|
<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_preview_container" class="image_preview_container">
|
||||||
<div id="init_image_wrapper" class="preview_image_wrapper">
|
<div id="init_image_wrapper" class="preview_image_wrapper">
|
||||||
@ -141,7 +156,12 @@
|
|||||||
<div><table>
|
<div><table>
|
||||||
<tr><b class="settings-subheader">Image Settings</b></tr>
|
<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="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">
|
<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="" />
|
<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>
|
<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="1792">1792</option>
|
||||||
<option value="2048">2048</option>
|
<option value="2048">2048</option>
|
||||||
</select>
|
</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>
|
<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">
|
<select id="height" name="height" value="512">
|
||||||
<option value="128">128</option>
|
<option value="128">128</option>
|
||||||
@ -293,7 +313,7 @@
|
|||||||
<option value="1792">1792</option>
|
<option value="1792">1792</option>
|
||||||
<option value="2048">2048</option>
|
<option value="2048">2048</option>
|
||||||
</select>
|
</select>
|
||||||
<label for="height"><small>(height)</small></label>
|
<label id="heightLabel" for="height"><small><span>(height)</span></small></label>
|
||||||
<div id="recent-resolutions-container">
|
<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>
|
<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">
|
<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)">
|
<input id="custom-width" name="custom-width" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)">
|
||||||
×
|
×
|
||||||
<input id="custom-height" name="custom-height" type="number" min="128" value="512" onkeypress="preventNonNumericalInput(event)"><br>
|
<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> <button data-factor="1.2" class="tertiaryButton smallButton">×1.2</button> <button data-factor="1.5" class="tertiaryButton smallButton">×1.5</button> <button data-factor="2" class="tertiaryButton smallButton">×2</button> <button data-factor="3" class="tertiaryButton smallButton">×3</button></div>
|
||||||
|
|
||||||
<small>Enlarge:</small><br>
|
<div class="two-column">
|
||||||
<div id="enlarge-buttons"><button id="enlarge15" class="tertiaryButton smallButton">×1.5</button> <button id="enlarge2" class="tertiaryButton smallButton">×2</button> <button id="enlarge3" class="tertiaryButton smallButton">×3</button></div>
|
<div class="left-column">
|
||||||
|
<small>Recently used:</small><br>
|
||||||
|
<div id="recent-resolution-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right-column">
|
||||||
|
<small>Common sizes:</small><br>
|
||||||
|
<div id="common-resolution-list">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<small>Recently used:</small><br>
|
|
||||||
<div id="recent-resolution-list">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -320,11 +350,10 @@
|
|||||||
<label for="lora_model">LoRA:</label>
|
<label for="lora_model">LoRA:</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="diffusers-restart-needed">
|
<td class="diffusers-restart-needed">
|
||||||
<div class="model_entries"></div>
|
<div id="lora_model" data-path=""></div>
|
||||||
<button class="add_model_entry"><i class="fa-solid fa-plus"></i> add another LoRA</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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="" />
|
<input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr id="hypernetwork_strength_container" class="pl-5">
|
<tr id="hypernetwork_strength_container" class="pl-5">
|
||||||
@ -389,7 +418,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div id="preview" class="col-free">
|
<div id="preview" class="col-free">
|
||||||
@ -403,7 +432,7 @@
|
|||||||
<div id="preview-content">
|
<div id="preview-content">
|
||||||
<div id="preview-tools" class="displayNone">
|
<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 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">
|
<div class="display-settings">
|
||||||
<button id="undo" class="displayNone primaryButton">
|
<button id="undo" class="displayNone primaryButton">
|
||||||
Undo <i class="fa-solid fa-rotate-left icon"></i>
|
Undo <i class="fa-solid fa-rotate-left icon"></i>
|
||||||
@ -432,6 +461,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="clearfix" style="clear: both;"></div>
|
<div class="clearfix" style="clear: both;"></div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -669,6 +701,8 @@
|
|||||||
<span>
|
<span>
|
||||||
</div>
|
</div>
|
||||||
<div id="embeddings-dialog-header-right">
|
<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>
|
<i id="embeddings-dialog-close-button" class="fa-solid fa-xmark fa-lg"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -679,6 +713,15 @@
|
|||||||
</button>
|
</button>
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
<input id="embeddings-search-box" type="text" spellcheck="false" autocomplete="off" placeholder="Search...">
|
<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> <select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
|
<span style="float:right;"><label>Mode:</label> <select id="embeddings-mode"><option value="insert">Insert at cursor position</option><option value="append">Append at the end</option></select>
|
||||||
</div>
|
</div>
|
||||||
<div id="embeddings-list">
|
<div id="embeddings-list">
|
||||||
@ -686,6 +729,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</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 id="image-editor" class="popup image-editor-popup">
|
||||||
<div>
|
<div>
|
||||||
<i class="close-button fa-solid fa-xmark"></i>
|
<i class="close-button fa-solid fa-xmark"></i>
|
||||||
@ -721,7 +792,6 @@
|
|||||||
<div id="footer-spacer"></div>
|
<div id="footer-spacer"></div>
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div class="line-separator"> </div>
|
<div class="line-separator"> </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>
|
<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">
|
<div id="footer-legal">
|
||||||
<p><b>Disclaimer:</b> The authors of this project are not responsible for any content generated using this interface.</p>
|
<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/auto-save.js"></script>
|
||||||
|
|
||||||
<script src="media/js/searchable-models.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/main.js"></script>
|
||||||
<script src="media/js/plugins.js"></script>
|
<script src="media/js/plugins.js"></script>
|
||||||
<script src="media/js/themes.js"></script>
|
<script src="media/js/themes.js"></script>
|
||||||
|
@ -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
|
from easydiffusion.server import server_api # required for uvicorn
|
||||||
|
|
||||||
app.init()
|
app.init()
|
||||||
@ -8,6 +8,7 @@ server.init()
|
|||||||
# Init the app
|
# Init the app
|
||||||
model_manager.init()
|
model_manager.init()
|
||||||
app.init_render_threads()
|
app.init_render_threads()
|
||||||
|
bucket_manager.init()
|
||||||
|
|
||||||
# start the browser ui
|
# start the browser ui
|
||||||
app.open_browser()
|
app.open_browser()
|
||||||
|
58
ui/media/css/croppr.css
Normal file
58
ui/media/css/croppr.css
Normal 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;
|
||||||
|
}
|
@ -229,4 +229,27 @@
|
|||||||
}
|
}
|
||||||
.inpainter .load_mask {
|
.inpainter .load_mask {
|
||||||
display: flex;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ code {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
transform: translateY(4px);
|
transform: translateY(4px);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#prompt {
|
#prompt {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -476,6 +477,7 @@ dialog {
|
|||||||
background: var(--background-color2);
|
background: var(--background-color2);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
box-shadow: 0px 0px 30px black;
|
||||||
border: 2px solid rgb(255 255 255 / 10%);
|
border: 2px solid rgb(255 255 255 / 10%);
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
@ -1088,7 +1090,7 @@ input::file-selector-button {
|
|||||||
.tab-content-inner {
|
.tab-content-inner {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
.tab {
|
#top-nav .tab {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
.tab .icon {
|
.tab .icon {
|
||||||
@ -1114,6 +1116,9 @@ input::file-selector-button {
|
|||||||
#preview-tools button .icon {
|
#preview-tools button .icon {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
}
|
}
|
||||||
|
#show-download-popup .fa-solid {
|
||||||
|
font-size: 12pt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 500px) {
|
@media screen and (max-width: 500px) {
|
||||||
@ -1418,6 +1423,10 @@ div.task-fs-initimage {
|
|||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
div.task-fs-initimage img {
|
||||||
|
max-height: 70vH;
|
||||||
|
max-width: 70vW;
|
||||||
|
}
|
||||||
div.task-initimg:hover div.task-fs-initimage {
|
div.task-initimg:hover div.task-fs-initimage {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -1433,9 +1442,13 @@ div.top-right {
|
|||||||
right: 8px;
|
right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-fs-initimage .top-right button {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
#small_image_warning {
|
#small_image_warning {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: var(--status-orange);
|
color: var(--status-orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
button#save-system-settings-btn {
|
button#save-system-settings-btn {
|
||||||
@ -1460,6 +1473,9 @@ button#save-system-settings-btn {
|
|||||||
cursor: pointer;;
|
cursor: pointer;;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.validation-failed {
|
||||||
|
border: solid 2px red;
|
||||||
|
}
|
||||||
/* SCROLLBARS */
|
/* SCROLLBARS */
|
||||||
:root {
|
:root {
|
||||||
--scrollbar-width: 14px;
|
--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 {
|
#embeddings-dialog {
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
}
|
}
|
||||||
@ -1664,6 +1709,12 @@ body.wait-pause {
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1400px) {
|
||||||
|
#embeddings-list {
|
||||||
|
width: 80vW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#embeddings-list button {
|
#embeddings-list button {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
color: var(--button-color);
|
color: var(--button-color);
|
||||||
@ -1741,6 +1792,32 @@ body.wait-pause {
|
|||||||
float: right;
|
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 {
|
.diffusers-disabled-on-startup .diffusers-restart-needed {
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
}
|
}
|
||||||
@ -1778,6 +1855,10 @@ div#recent-resolutions-popup small {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#common-resolution-list button {
|
||||||
|
background: var(--background-color1);
|
||||||
|
}
|
||||||
|
|
||||||
td#image-size-options small {
|
td#image-size-options small {
|
||||||
margin-right: 0px !important;
|
margin-right: 0px !important;
|
||||||
}
|
}
|
||||||
@ -1794,6 +1875,27 @@ div#enlarge-buttons {
|
|||||||
text-align: center;
|
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 {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -1828,7 +1930,100 @@ div#enlarge-buttons {
|
|||||||
width: 77%;
|
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 */
|
/* hack for fixing Image Modifier Improvements plugin */
|
||||||
#imageTagPopupContainer {
|
#imageTagPopupContainer {
|
||||||
position: absolute;
|
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
BIN
ui/media/images/noimg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
@ -15,14 +15,12 @@ const SETTINGS_IDS_LIST = [
|
|||||||
"stable_diffusion_model",
|
"stable_diffusion_model",
|
||||||
"clip_skip",
|
"clip_skip",
|
||||||
"vae_model",
|
"vae_model",
|
||||||
"hypernetwork_model",
|
|
||||||
"sampler_name",
|
"sampler_name",
|
||||||
"width",
|
"width",
|
||||||
"height",
|
"height",
|
||||||
"num_inference_steps",
|
"num_inference_steps",
|
||||||
"guidance_scale",
|
"guidance_scale",
|
||||||
"prompt_strength",
|
"prompt_strength",
|
||||||
"hypernetwork_strength",
|
|
||||||
"tiling",
|
"tiling",
|
||||||
"output_format",
|
"output_format",
|
||||||
"output_quality",
|
"output_quality",
|
||||||
@ -45,6 +43,7 @@ const SETTINGS_IDS_LIST = [
|
|||||||
"sound_toggle",
|
"sound_toggle",
|
||||||
"vram_usage_level",
|
"vram_usage_level",
|
||||||
"confirm_dangerous_actions",
|
"confirm_dangerous_actions",
|
||||||
|
"profileName",
|
||||||
"metadata_output_format",
|
"metadata_output_format",
|
||||||
"auto_save_settings",
|
"auto_save_settings",
|
||||||
"apply_color_correction",
|
"apply_color_correction",
|
||||||
@ -54,10 +53,18 @@ const SETTINGS_IDS_LIST = [
|
|||||||
"zip_toggle",
|
"zip_toggle",
|
||||||
"tree_toggle",
|
"tree_toggle",
|
||||||
"json_toggle",
|
"json_toggle",
|
||||||
|
"extract_lora_from_prompt",
|
||||||
|
"embedding-card-size-selector",
|
||||||
|
"lora_model",
|
||||||
]
|
]
|
||||||
|
|
||||||
const IGNORE_BY_DEFAULT = ["prompt"]
|
const IGNORE_BY_DEFAULT = ["prompt"]
|
||||||
|
|
||||||
|
if (!testDiffusers.checked) {
|
||||||
|
SETTINGS_IDS_LIST.push("hypernetwork_model")
|
||||||
|
SETTINGS_IDS_LIST.push("hypernetwork_strength")
|
||||||
|
}
|
||||||
|
|
||||||
const SETTINGS_SECTIONS = [
|
const SETTINGS_SECTIONS = [
|
||||||
// gets the "keys" property filled in with an ordered list of settings in this section via initSettings
|
// gets the "keys" property filled in with an ordered list of settings in this section via initSettings
|
||||||
{ id: "editor-inputs", name: "Prompt" },
|
{ id: "editor-inputs", name: "Prompt" },
|
||||||
|
1189
ui/media/js/croppr.js
Executable file
1189
ui/media/js/croppr.js
Executable file
File diff suppressed because it is too large
Load Diff
@ -289,42 +289,56 @@ const TASK_MAPPING = {
|
|||||||
readUI: () => vaeModelField.value,
|
readUI: () => vaeModelField.value,
|
||||||
parse: (val) => val,
|
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: {
|
use_lora_model: {
|
||||||
name: "LoRA model",
|
name: "LoRA model",
|
||||||
setUI: (use_lora_model) => {
|
setUI: (use_lora_model) => {
|
||||||
// create rows
|
let modelPaths = []
|
||||||
for (let i = loraModels.length; i < use_lora_model.length; i++) {
|
use_lora_model = Array.isArray(use_lora_model) ? use_lora_model : [use_lora_model]
|
||||||
createLoraEntry()
|
use_lora_model.forEach((m) => {
|
||||||
}
|
if (m.includes("models\\lora\\")) {
|
||||||
|
m = m.split("models\\lora\\")[1]
|
||||||
use_lora_model.forEach((model_name, i) => {
|
} else if (m.includes("models\\\\lora\\\\")) {
|
||||||
let field = loraModels[i][0]
|
m = m.split("models\\\\lora\\\\")[1]
|
||||||
const oldVal = field.value
|
} else if (m.includes("models/lora/")) {
|
||||||
|
m = m.split("models/lora/")[1]
|
||||||
if (model_name !== "") {
|
|
||||||
model_name = getModelPath(model_name, [".ckpt", ".safetensors"])
|
|
||||||
model_name = model_name !== "" ? model_name : oldVal
|
|
||||||
}
|
}
|
||||||
field.value = model_name
|
m = m.replaceAll("\\\\", "/")
|
||||||
|
m = getModelPath(m, [".ckpt", ".safetensors"])
|
||||||
|
modelPaths.push(m)
|
||||||
})
|
})
|
||||||
|
loraModelField.modelNames = modelPaths
|
||||||
// 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)
|
|
||||||
},
|
},
|
||||||
readUI: () => {
|
readUI: () => {
|
||||||
let values = loraModels.map((e) => e[0].value)
|
return loraModelField.modelNames
|
||||||
values = values.filter((e) => e.trim() !== "")
|
|
||||||
values = values.length > 0 ? values : "None"
|
|
||||||
return values
|
|
||||||
},
|
},
|
||||||
parse: (val) => {
|
parse: (val) => {
|
||||||
val = !val || val === "None" ? "" : 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]
|
val = Array.isArray(val) ? val : [val]
|
||||||
return val
|
return val
|
||||||
},
|
},
|
||||||
@ -332,31 +346,17 @@ const TASK_MAPPING = {
|
|||||||
lora_alpha: {
|
lora_alpha: {
|
||||||
name: "LoRA Strength",
|
name: "LoRA Strength",
|
||||||
setUI: (lora_alpha) => {
|
setUI: (lora_alpha) => {
|
||||||
for (let i = loraModels.length; i < lora_alpha.length; i++) {
|
lora_alpha = Array.isArray(lora_alpha) ? lora_alpha : [lora_alpha]
|
||||||
createLoraEntry()
|
loraModelField.modelWeights = lora_alpha
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
},
|
},
|
||||||
readUI: () => {
|
readUI: () => {
|
||||||
let models = loraModels.filter((e) => e[0].value.trim() !== "")
|
return loraModelField.modelWeights
|
||||||
let values = models.map((e) => e[1].value)
|
|
||||||
values = values.length > 0 ? values : 0
|
|
||||||
return values
|
|
||||||
},
|
},
|
||||||
parse: (val) => {
|
parse: (val) => {
|
||||||
|
if (typeof val === "string" && val.includes(",")) {
|
||||||
|
val = "[" + val.replaceAll("'", '"') + "]"
|
||||||
|
val = JSON.parse(val)
|
||||||
|
}
|
||||||
val = Array.isArray(val) ? val : [val]
|
val = Array.isArray(val) ? val : [val]
|
||||||
val = val.map((e) => parseFloat(e))
|
val = val.map((e) => parseFloat(e))
|
||||||
return val
|
return val
|
||||||
@ -472,11 +472,8 @@ function restoreTaskToUI(task, fieldsToSkip) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!("use_lora_model" in task.reqBody)) {
|
if (!("use_lora_model" in task.reqBody)) {
|
||||||
loraModels.forEach((e) => {
|
loraModelField.modelNames = []
|
||||||
e[0].value = ""
|
loraModelField.modelWeights = []
|
||||||
e[1].value = 0
|
|
||||||
e[0].dispatchEvent(new Event("change"))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
|
// 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
|
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() {
|
function readUI() {
|
||||||
const reqBody = {}
|
const reqBody = {}
|
||||||
for (const key in TASK_MAPPING) {
|
for (const key in TASK_MAPPING) {
|
||||||
|
if (testDiffusers.checked && (key === "use_hypernetwork_model" || key === "hypernetwork_strength")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
reqBody[key] = TASK_MAPPING[key].readUI()
|
reqBody[key] = TASK_MAPPING[key].readUI()
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@ -569,6 +579,10 @@ const TASK_TEXT_MAPPING = {
|
|||||||
use_stable_diffusion_model: "Stable Diffusion model",
|
use_stable_diffusion_model: "Stable Diffusion model",
|
||||||
use_hypernetwork_model: "Hypernetwork model",
|
use_hypernetwork_model: "Hypernetwork model",
|
||||||
hypernetwork_strength: "Hypernetwork Strength",
|
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) {
|
function parseTaskFromText(str) {
|
||||||
const taskReqBody = {}
|
const taskReqBody = {}
|
||||||
|
2
ui/media/js/exif-reader.js
Normal file
2
ui/media/js/exif-reader.js
Normal file
File diff suppressed because one or more lines are too long
1171
ui/media/js/main.js
1171
ui/media/js/main.js
File diff suppressed because it is too large
Load Diff
256
ui/media/js/multi-model-selector.js
Normal file
256
ui/media/js/multi-model-selector.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -121,6 +121,15 @@ var PARAMETERS = [
|
|||||||
icon: "fa-arrow-down-short-wide",
|
icon: "fa-arrow-down-short-wide",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "extract_lora_from_prompt",
|
||||||
|
type: ParameterType.checkbox,
|
||||||
|
label: "Extract LoRA tags from the prompt",
|
||||||
|
note:
|
||||||
|
"Automatically extract lora tags like <lora:name:0.4> from the prompt, and apply the correct LoRA (if present)",
|
||||||
|
icon: "fa-code",
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "ui_open_browser_on_start",
|
id: "ui_open_browser_on_start",
|
||||||
type: ParameterType.checkbox,
|
type: ParameterType.checkbox,
|
||||||
@ -185,6 +194,17 @@ var PARAMETERS = [
|
|||||||
icon: "fa-check-double",
|
icon: "fa-check-double",
|
||||||
default: true,
|
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",
|
id: "listen_to_network",
|
||||||
type: ParameterType.checkbox,
|
type: ParameterType.checkbox,
|
||||||
@ -220,11 +240,11 @@ var PARAMETERS = [
|
|||||||
{
|
{
|
||||||
id: "test_diffusers",
|
id: "test_diffusers",
|
||||||
type: ParameterType.checkbox,
|
type: ParameterType.checkbox,
|
||||||
label: "Test Diffusers",
|
label: "Use the new v3 engine (diffusers)",
|
||||||
note:
|
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",
|
icon: "fa-bolt",
|
||||||
default: false,
|
default: true,
|
||||||
saveInAppConfig: true,
|
saveInAppConfig: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -401,6 +421,7 @@ let useBetaChannelField = document.querySelector("#use_beta_channel")
|
|||||||
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
|
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
|
||||||
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
|
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
|
||||||
let testDiffusers = document.querySelector("#test_diffusers")
|
let testDiffusers = document.querySelector("#test_diffusers")
|
||||||
|
let profileNameField = document.querySelector("#profileName")
|
||||||
|
|
||||||
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
|
let saveSettingsBtn = document.querySelector("#save-system-settings-btn")
|
||||||
|
|
||||||
@ -432,8 +453,6 @@ async function getAppConfig() {
|
|||||||
if (config.update_branch === "beta") {
|
if (config.update_branch === "beta") {
|
||||||
useBetaChannelField.checked = true
|
useBetaChannelField.checked = true
|
||||||
document.querySelector("#updateBranchLabel").innerText = "(beta)"
|
document.querySelector("#updateBranchLabel").innerText = "(beta)"
|
||||||
} else {
|
|
||||||
getParameterSettingsEntry("test_diffusers").classList.add("displayNone")
|
|
||||||
}
|
}
|
||||||
if (config.ui && config.ui.open_browser_on_start === false) {
|
if (config.ui && config.ui.open_browser_on_start === false) {
|
||||||
uiOpenBrowserOnStartField.checked = false
|
uiOpenBrowserOnStartField.checked = false
|
||||||
@ -445,11 +464,14 @@ async function getAppConfig() {
|
|||||||
listenPortField.value = config.net.listen_port
|
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
|
testDiffusers.checked = testDiffusersEnabled
|
||||||
|
|
||||||
if (config.config_on_startup) {
|
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.add("diffusers-enabled-on-startup")
|
||||||
document.body.classList.remove("diffusers-disabled-on-startup")
|
document.body.classList.remove("diffusers-disabled-on-startup")
|
||||||
} else {
|
} else {
|
||||||
@ -462,7 +484,9 @@ async function getAppConfig() {
|
|||||||
document.querySelector("#lora_model_container").style.display = "none"
|
document.querySelector("#lora_model_container").style.display = "none"
|
||||||
document.querySelector("#tiling_container").style.display = "none"
|
document.querySelector("#tiling_container").style.display = "none"
|
||||||
document.querySelector("#controlnet_model_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("#hypernetwork_strength_container").style.display = ""
|
||||||
|
document.querySelector("#negative-embeddings-button").style.display = "none"
|
||||||
|
|
||||||
document.querySelectorAll("#sampler_name option.diffusers-only").forEach((option) => {
|
document.querySelectorAll("#sampler_name option.diffusers-only").forEach((option) => {
|
||||||
option.style.display = "none"
|
option.style.display = "none"
|
||||||
@ -474,6 +498,7 @@ async function getAppConfig() {
|
|||||||
document.querySelector("#lora_model_container").style.display = ""
|
document.querySelector("#lora_model_container").style.display = ""
|
||||||
document.querySelector("#tiling_container").style.display = ""
|
document.querySelector("#tiling_container").style.display = ""
|
||||||
document.querySelector("#controlnet_model_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.querySelector("#hypernetwork_strength_container").style.display = "none"
|
||||||
|
|
||||||
document.querySelectorAll("#sampler_name option.k_diffusion-only").forEach((option) => {
|
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("#clip_skip_config").classList.remove("displayNone")
|
||||||
document.querySelector("#embeddings-button").classList.remove("displayNone")
|
document.querySelector("#embeddings-button").classList.remove("displayNone")
|
||||||
document.querySelector("#negative-embeddings-button").classList.remove("displayNone")
|
|
||||||
IMAGE_STEP_SIZE = 8
|
IMAGE_STEP_SIZE = 8
|
||||||
customWidthField.step = IMAGE_STEP_SIZE
|
customWidthField.step = IMAGE_STEP_SIZE
|
||||||
customHeightField.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))
|
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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
@ -118,13 +118,16 @@ class ModelDropdown {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
saveCurrentSelection(elem, value, path) {
|
saveCurrentSelection(elem, value, path, dispatchEvent = true) {
|
||||||
this.currentSelection.elem = elem
|
this.currentSelection.elem = elem
|
||||||
this.currentSelection.value = value
|
this.currentSelection.value = value
|
||||||
this.currentSelection.path = path
|
this.currentSelection.path = path
|
||||||
this.modelFilter.dataset.path = path
|
this.modelFilter.dataset.path = path
|
||||||
this.modelFilter.value = value
|
this.modelFilter.value = value
|
||||||
this.modelFilter.dispatchEvent(new Event("change"))
|
|
||||||
|
if (dispatchEvent) {
|
||||||
|
this.modelFilter.dispatchEvent(new Event("change"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processClick(e) {
|
processClick(e) {
|
||||||
@ -348,13 +351,13 @@ class ModelDropdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectEntry(path) {
|
selectEntry(path, dispatchEvent = true) {
|
||||||
if (path !== undefined) {
|
if (path !== undefined) {
|
||||||
const entries = this.modelElements
|
const entries = this.modelElements
|
||||||
|
|
||||||
for (const elem of entries) {
|
for (const elem of entries) {
|
||||||
if (elem.dataset.path == path) {
|
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
|
this.highlightedModelEntry = elem
|
||||||
elem.scrollIntoView({ block: "nearest" })
|
elem.scrollIntoView({ block: "nearest" })
|
||||||
break
|
break
|
||||||
@ -529,7 +532,7 @@ class ModelDropdown {
|
|||||||
rootModelList.style.minWidth = modelFilterStyle.width
|
rootModelList.style.minWidth = modelFilterStyle.width
|
||||||
})
|
})
|
||||||
|
|
||||||
this.selectEntry(this.activeModel)
|
this.selectEntry(this.activeModel, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
409
ui/media/js/task-manager.js
Normal file
409
ui/media/js/task-manager.js
Normal 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")
|
||||||
|
})
|
@ -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) {
|
function modalDialogCloseOnBackdropClick(dialog) {
|
||||||
dialog.addEventListener('mousedown', function (event) {
|
dialog.addEventListener('mousedown', function (event) {
|
||||||
// Firefox creates an event with clientX|Y = 0|0 when choosing an <option>.
|
// 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
@ -8,6 +8,11 @@
|
|||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
promptField.addEventListener('input', function(e) {
|
promptField.addEventListener('input', function(e) {
|
||||||
|
let loraExtractSetting = document.getElementById("extract_lora_from_prompt")
|
||||||
|
if (!loraExtractSetting.checked) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { LoRA, prompt } = extractLoraTags(e.target.value);
|
const { LoRA, prompt } = extractLoraTags(e.target.value);
|
||||||
//console.log('e.target: ' + JSON.stringify(LoRA));
|
//console.log('e.target: ' + JSON.stringify(LoRA));
|
||||||
|
|
||||||
@ -20,44 +25,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (LoRA !== null && LoRA.length > 0 && testDiffusers?.checked) {
|
if (LoRA !== null && LoRA.length > 0 && testDiffusers?.checked) {
|
||||||
for (let i = 0; i < LoRA.length; i++) {
|
let modelNames = LoRA.map(e => e.lora_model_0)
|
||||||
//if (loraModelField.value !== LoRA[0].lora_model) {
|
let modelWeights = LoRA.map(e => e.lora_alpha_0)
|
||||||
// Set the new LoRA value
|
loraModelField.value = {modelNames: modelNames, modelWeights: modelWeights}
|
||||||
//console.log("Loading info");
|
|
||||||
//console.log(LoRA[0].lora_model_0);
|
showToast("Prompt successfully processed")
|
||||||
//console.log(JSON.stringify(LoRa));
|
|
||||||
|
|
||||||
let lora = `lora_model_${i}`;
|
|
||||||
let alpha = `lora_alpha_${i}`;
|
|
||||||
let loramodel = document.getElementById(lora);
|
|
||||||
let alphavalue = document.getElementById(alpha);
|
|
||||||
loramodel.setAttribute("data-path", LoRA[i].lora_model_0);
|
|
||||||
loramodel.value = LoRA[i].lora_model_0;
|
|
||||||
alphavalue.value = LoRA[i].lora_alpha_0;
|
|
||||||
if (i != LoRA.length - 1)
|
|
||||||
createLoraEntry();
|
|
||||||
}
|
|
||||||
//loraAlphaSlider.value = loraAlphaField.value * 100;
|
|
||||||
//TBD.value = LoRA[0].blockweights; // block weights not supported by ED at this time
|
|
||||||
//}
|
|
||||||
showToast("Prompt successfully processed", LoRA[0].lora_model_0);
|
|
||||||
//console.log('LoRa: ' + LoRA[0].lora_model_0);
|
|
||||||
//showToast("Prompt successfully processed", lora_model_0.value);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//promptField.dispatchEvent(new Event('change'));
|
//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
|
// extract LoRA tags from strings
|
||||||
function extractLoraTags(prompt) {
|
function extractLoraTags(prompt) {
|
||||||
// Define the regular expression for the tags
|
// Define the regular expression for the tags
|
||||||
@ -68,11 +46,13 @@
|
|||||||
|
|
||||||
// Iterate over the string, finding matches
|
// Iterate over the string, finding matches
|
||||||
for (const match of prompt.matchAll(regex)) {
|
for (const match of prompt.matchAll(regex)) {
|
||||||
const modelFileName = isModelAvailable(modelsCache.options.lora, match[1].trim())
|
const modelFileName = match[1].trim()
|
||||||
if (modelFileName !== "") {
|
const loraPathes = getAllModelPathes("lora", modelFileName)
|
||||||
|
if (loraPathes.length > 0) {
|
||||||
|
const loraPath = loraPathes[0]
|
||||||
// Initialize an object to hold a match
|
// Initialize an object to hold a match
|
||||||
let loraTag = {
|
let loraTag = {
|
||||||
lora_model_0: modelFileName,
|
lora_model_0: loraPath,
|
||||||
}
|
}
|
||||||
//console.log("Model:" + modelFileName);
|
//console.log("Model:" + modelFileName);
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user