Compare commits

...

211 Commits

Author SHA1 Message Date
e706fae648 Update README.md 2023-03-02 10:31:23 +05:30
118a4862ab Merge pull request #954 from cmdr2/beta
Beta
2023-03-02 10:26:25 +05:30
5e2f31e3bf Merge branch 'main' into beta 2023-03-02 10:15:51 +05:30
f78b31b1bc Less jittery dropdown 2023-03-01 18:41:21 +05:30
8d698cb997 reword 2023-03-01 18:35:41 +05:30
8945aac319 Merge models is no longer in beta 2023-03-01 18:28:58 +05:30
f2a960136e Move zoom and 'scroll to generated image' into a flat icon strip, with a press-toggle button for the 'scroll to' button; Tweaked the behavior of the on-scroll dropdown CSS class 2023-03-01 18:27:48 +05:30
7a1170f1dd Use naturalWidth, to show the actual image width (especially for upscaled images) 2023-03-01 15:22:38 +05:30
24cce08580 Show the image dimensions on mouse over 2023-03-01 15:17:27 +05:30
b425b43d3e changelog 2023-02-28 15:38:53 +05:30
353fe88226 Set tertiary colors on buttons that don't need to be visible in a very dominant manner 2023-02-28 15:37:38 +05:30
1a3086230e Set the dropdown width only when the dropdown is opened, to fix a bug where it would get set before the DOM element actually rendered. The settings field is collapsed by default (on new installations), so the computed DOM width would be invalid 2023-02-28 15:37:06 +05:30
0e57487774 Preserve full names for shortened modifiers (#945)
* Preserve full names for shortened modifiers

The PR https://github.com/cmdr2/stable-diffusion-ui/pull/779/files added code to preserve the full names of truncated image modifiers, but only in the "short image modifiers" code path. This PR fixes that by preserving the full car name for truncated modifier names too.

* Pick the full modifier name

The previous code selected the entire innerText from the modifier-car-label element, but for truncated modifiers this would also include the tooltip text. This modification fixes that by only picking specifically the full modifier name.

* Only pick the full modifier name

Previous code would pick up the tooltip text too, causing a mismatch of strings in the comparison.

* Display the truncated image modifier names

What we process and compare is always the full image modifier string, but we still want to display a shortened string when applicable.
2023-02-28 14:42:24 +05:30
3024465086 Merge pull request #932 from patriceac/patch-42
DOM tweaking to identify modifier categories
2023-02-28 14:41:46 +05:30
c95b43253a Merge pull request #933 from patriceac/patch-43
Cleanup logging
2023-02-28 14:41:04 +05:30
aedf7856e5 Merge pull request #940 from patriceac/patch-47
Select model by clicking on the file icon itself
2023-02-28 11:59:32 +05:30
d83e034d5e Select model by clicking on the file icon itself
Currently one has to click on the model name to select a model. Clicking on the file icon won't work and doesn't do anything. This change fixes that behavior by allowing the user to click on either the model name or the file icon to select a model.
2023-02-26 17:26:22 -08:00
b9676b51cb Cleanup logging
Is it okay for you if I comment this in Beta?
2023-02-24 17:46:53 -08:00
5698473891 Renaming custom-modifier-category to modifier-category 2023-02-24 13:55:18 -08:00
de1d1ad961 Shrink the preview tools buttons to icons-only on a small screen 2023-02-24 21:50:21 +05:30
bd82480fa3 Keep the min-width of a dropdown equal to the width of the input element 2023-02-24 20:08:19 +05:30
fce8b96d3b Tweaks to the styling of the models dropdown 2023-02-24 19:29:25 +05:30
37b47e7f05 Show root-level models at the top 2023-02-24 18:55:57 +05:30
a6f94959fe DOM tweaking to identify custom categories
This is purely a DOM update to be able to identify the custom category a given custom image modifier is part of, e.g. using .closest() from a custom modifier plugin. No UI change.
2023-02-24 01:16:14 -08:00
45a2c9f7ef Show icons next to the model folder and files in the dropdown, styling tweaks to increase padding 2023-02-23 22:11:53 +05:30
c49ac6880d Merge pull request #931 from patriceac/patch-41
Fix the restoration of the inpainting toggle
2023-02-23 19:29:40 +05:30
e0258d9e7b Merge pull request #930 from patriceac/patch-40
Fix the display of the Preview options button
2023-02-23 19:29:00 +05:30
e3ff6f183b Merge pull request #928 from ogmaresca/fix-models-up-down-arrow-keys
Fix up/down arrow keys on model selects
2023-02-23 19:28:16 +05:30
e6ec7393c6 Fix the restoration of the inpainting toggle
The Inpainting toggle doesn't get restored at the very first attempt.

Repro steps:
1. Create a task with a source image and enable the inpainting toggle.
2. Copy the task to the clipboard
3. Refresh the page (F5)
4. Paste the task from the clipboard

Expected result:
The task gets restored and the toggle is ON.

Actual result:
The task is restored, but the toggle is OFF.

To fix that, we have to restore the toggle's state after loading the source image.
2023-02-22 23:46:07 -08:00
f733b53c25 Merge pull request #927 from rbertus2000/fixthumbnailsize
fix for thumbnail slider to support seamless resize
2023-02-23 09:19:07 +05:30
204a68b17d Fix the display of the Preview options button
The preview options button overlaps the image task container when the window is reduced because of the float:right property of the button. This technique makes the parent div grow as needed when it contains a floated element, resulting in cleaner display.
2023-02-22 19:11:18 -08:00
1379dde1a7 Fix up/down arrow keys on model selects 2023-02-22 18:18:06 -05:00
79eee62d42 fix for thumbnail slider 2023-02-22 22:05:16 +01:00
7c1f18b6cd reword test 2 2023-02-22 20:16:52 +05:30
b59371988d Minor styling tweak 2023-02-22 20:15:33 +05:30
30dbadb2ab Focus the prompt textbox on start 2023-02-22 20:11:19 +05:30
a342de0207 Rename dropdown to 'View options' 2023-02-22 20:08:17 +05:30
6e6d236819 Change the image zoom size if the textbox is edited next to the slider 2023-02-22 19:56:45 +05:30
0e41483564 version 2023-02-22 19:35:13 +05:30
1023f5f7cc Slider for preview image size (#767)
* Slider for preview image size
Add a slider to the system settings so that users can configure the max size of thumbnails

* Remove debug output

* Fix var definition

* Move slider to 'display settings' menu

* thumbnail slider CSS
2023-02-22 19:32:00 +05:30
4bc7bca60d Merge pull request #924 from patriceac/patch-37
Allow modifier images to be passed as base64 images
2023-02-22 19:28:47 +05:30
de7dbd27c0 Merge pull request #925 from patriceac/patch-39
Fix reloading of image tags with weight modifiers
2023-02-22 19:26:19 +05:30
14118f142c Update image-modifiers.js
Reloading of image tags with ((weight modifiers)) doesn't reuse the modifier card even if it exists, which means images are not restored either. This change fixes that behavior by ensuring proper matching of the tags with existing modifiers.
2023-02-22 00:57:03 -08:00
9b99be4c1d Allow modifiers to be passed as base64 images
No change in existing UI behavior, this change allows image modifier plugins to (optionally) pass the card image as a base64-encoded image rather than a source file.
2023-02-22 00:06:07 -08:00
91c4b5865c Pin the sdkit version during fresh installs 2023-02-21 10:40:17 +05:30
1b4c14af71 Merge pull request #920 from patriceac/patch-36
Fix the active selection's display
2023-02-21 08:42:10 +05:30
7b85e50604 Merge pull request #918 from JeLuF/downloadall
Download all: Fix to download more than 10 images
2023-02-21 08:41:50 +05:30
d64b2d8fbe Merge pull request #917 from JeLuF/NoSSLmodule
Copy SSL DLLs
2023-02-21 08:41:09 +05:30
f1a7aed1b6 Merge pull request #916 from JeLuF/skipst
Don't scan safetensors files
2023-02-21 08:39:52 +05:30
75f758e792 Bugfix for enforce autosave (#909)
* show saveToDisk as checked and enable metadata format selection

* spaces instead of tabs

* check if force = true
2023-02-21 08:39:16 +05:30
e25e1bfe10 Make stream_image_progress accept an integer for the rate the progress frames should be generated. (#889)
* Make stream_image_progress accept an integer

for the rate the progress frames should be generated.

* Use a different field for the progress interval.
2023-02-21 08:38:21 +05:30
09deaefab0 Fix the active selection's display
Yesterday's PR caused a regression on the active brush display, specifically for Sharpness, which is treated differently from the other brushes in the code. This is the fix.
2023-02-20 18:25:57 -08:00
f80ecbde71 Download all: Fix to download more than 10 images
https://discord.com/channels/1014774730907209781/1021695193499582494/1077218966205902860
2023-02-21 00:49:04 +01:00
5e1e198a1f Copy SSL DLLs
Prevent the 'SSL module is not available' error message
2023-02-20 23:02:27 +01:00
bdbb741716 Don't scan safetensors files
In newer versions of the picklescanner, scanning of .safetensors files creates an error:

21:28:01.067 ERROR MainThread ERROR: parsing pickle in D:\2.35\dev\models\stable-diffusion\dantionrealmix_10.safetensors: at position 1, opcode b'\xce' unknown

To avoid these entries in the logs, skip scanning of safetensors files.
2023-02-20 22:44:10 +01:00
2f0e8a8a4a Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-20 19:31:57 +05:30
4f8424c544 sdkit 1.0.43 - unipc samplers on any device, not just cuda 2023-02-20 19:31:47 +05:30
ce3355d6aa Merge pull request #902 from patriceac/patch-32
Select but don't empty the search box upon selection
2023-02-20 19:14:02 +05:30
fb67ef2df0 Merge pull request #908 from JeLuF/png
Download all: Fix file name suffix
2023-02-20 18:57:24 +05:30
380e9aaf13 Merge pull request #903 from patriceac/patch-33
Fix autoscroll behavior for the first image
2023-02-20 18:56:52 +05:30
255e90d125 Merge pull request #910 from patriceac/patch-34
Fix the centering of sharpness brushes
2023-02-20 18:54:22 +05:30
504f7f3799 Merge pull request #913 from patriceac/patch-35
Fix the toggling of image modifiers
2023-02-20 18:49:52 +05:30
9970e505de sdkit version 1.0.42 - WEBP format support 2023-02-20 18:43:39 +05:30
0ccacd5eca 🔥 Installer: Fix ESRGAN anime model's path
The size check fails on every installation. The path name of the check wasn't changed when the model was moved to the models directory.
2023-02-20 18:40:13 +05:30
50e4683492 Merge pull request #907 from ogmaresca/webp-support
Support WEBP image formats
2023-02-20 18:36:40 +05:30
bc14bdc010 Merge pull request #900 from patriceac/patch-31
Fix filename parsing issue
2023-02-20 18:30:45 +05:30
14b0dabfdf Merge pull request #904 from JeLuF/patch-16
🔥 Installer: Fix ESRGAN anime model's path
2023-02-20 18:28:31 +05:30
e140acd2a4 Merge branch 'beta' into webp-support 2023-02-19 23:30:38 -05:00
facfed07fe sdkit 1.0.41 - NSFW filter 2023-02-20 08:44:26 +05:30
41a3309cbe Fix the toggling of image modifiers
The toggling of image modifiers doesn't get properly applied if weights are changed after restoring the image modifiers.
2023-02-19 15:59:20 -08:00
4df9a22dd6 Fix the centering of sharpness brushes
Fixing a visual glitch that becomes visible when a plugin adds borders to the brushes to make them more visible.

See this for context; https://discord.com/channels/1014774730907209781/1058857864954904607/1076694770845487155
2023-02-19 12:08:06 -08:00
31a1c4b2b2 Download all: Fix file name suffix 2023-02-19 12:44:10 +01:00
c2c33b7df1 Support WEBP outputs 2023-02-18 22:37:34 -05:00
6a2c2152e2 🔥 Installer: Fix ESRGAN anime model's path
The size check fails on every installation. The path name of the check wasn't changed when the model was moved to the models directory.
2023-02-18 14:05:55 +01:00
37f2755611 changelog 2023-02-18 15:01:52 +05:30
aa70f2849b NSFW filter setting 2023-02-18 15:01:13 +05:30
e7a2dfa57f changelog 2023-02-18 14:31:39 +05:30
b43f9fc4ee Upgrade stable-diffusion-sdkit to 2.1.3, to use transformers 4.26.1 2023-02-18 14:30:37 +05:30
51b6a2fd2a Pin the version of stable-diffusion-sdkit used, to avoid untested releases from getting used 2023-02-18 14:21:24 +05:30
5fffb82b16 Pin the version of stable-diffusion-sdkit used, to avoid untested releases from getting used 2023-02-18 14:17:28 +05:30
e051dbc2c7 Fix autoscroll behavior for the first image
When the first image is generated, the autoscroll triggers before the image is fully displayed by the browser. This causes it to not be positioned properly.
The fix is to listen for the "load" event on the IMG element before triggering the scrolling event. Once the image fully loaded and rendered, the browser correctly detects the size of the viewport and renders properly.
2023-02-18 00:00:00 -08:00
c2fba39cc7 Select but don't empty the search box upon selection
As per your request.
2023-02-17 23:26:17 -08:00
1050b13bbb Merge pull request #899 from patriceac/patch-30
Fix the chevron enabled state upon refresh
2023-02-18 12:30:54 +05:30
92d3d9cd33 Fix filename parsing issue
Here is a more robust fix for task restoration in dnd.js. Task restoration will fail if the JSON contains "use_face_correction": false, which can happen under some circumstances.

The fix checks if the filename passed to getModelPath is actually a string, which covers both the previous scenario (filename === null) and this new one (filename === false).
2023-02-17 18:49:51 -08:00
d8dec3e56a Fix the chevron enabled state upon refresh
Fix for my previous PR. Apologies for this silly copy/paste mistake.

https://discord.com/channels/1014774730907209781/1014780368890630164/1075782233970970704
2023-02-17 16:40:16 -08:00
130f9678b2 Merge pull request #896 from JeLuF/0x0a0d
Remove superfluous CarriageReturn
2023-02-17 16:06:29 +05:30
29d13cb06d Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-17 15:29:43 +05:30
620f521e0c changelog 2023-02-17 15:25:49 +05:30
a36fb55b05 Remove superfluous CarriageReturn
\r\n creates CR CR LF in python, which confuses the Windows batch processor.
With only \n, adding the config line for FP32 works as expected:

10:50:43.659 WARNING cuda:0 forcing full precision on this GPU, to avoid green images. GPU detected: NVIDIA GeForce GTX 1060 6GB
2023-02-17 10:53:51 +01:00
23f9bcb38b Upgrade sdkit, moving the experimental parser into a plugin 2023-02-17 15:22:59 +05:30
e73e820237 Support server-side plugins. Currently supports overriding the get_cond_and_uncond function 2023-02-17 15:22:42 +05:30
7e4735ae0f Merge pull request #893 from JeLuF/oneclick
Only confirm image deletion once
2023-02-17 11:17:53 +05:30
66ffcbbee6 Merge pull request #894 from ogmaresca/fix-model-folders-broken-up
Fix model folders being split up by child folders
2023-02-17 11:08:16 +05:30
4754743c84 5 new samplers - UniPC 2023-02-17 10:24:59 +05:30
09c1dfd92b Remove leading slash from data-path attributes 2023-02-16 23:29:32 -05:00
7fc46f3672 sdkit 1.0.39 - unipc samplers 2023-02-17 09:36:10 +05:30
df93fee034 Fix model dropdown icon 2023-02-16 21:16:19 -05:00
fc2cf742c8 Remove trailing slash 2023-02-16 21:08:41 -05:00
9bec441e94 Fix model folders being split up by child folders 2023-02-16 21:03:02 -05:00
1caab1da85 Only confirm image deletion once
The previous code added an event listener per preview image (if live preview is enabled), so
that multiple confirmations were required.
2023-02-17 00:54:41 +01:00
d612d7ab53 changelog 2023-02-16 21:10:51 +05:30
3d3994bbad sdkit 1.0.38 - experimental parser, requires the prompt to start with an exclamation mark 2023-02-16 21:01:33 +05:30
d643ae0299 temp fix for broken dropdowns 2023-02-16 19:46:06 +05:30
0a099434a3 Merge pull request #885 from patriceac/patch-28
Cleaning up event listener that's no longer needed
2023-02-16 19:31:19 +05:30
16905a8999 Merge pull request #888 from JeLuF/macos2
No /proc/cpuinfo on MacOS
2023-02-16 19:29:42 +05:30
282c4cca82 Add support for disabled state to model dropdown (#886)
* Add support for disabled state to model dropdown

As per https://discord.com/channels/1014774730907209781/1021695193499582494/1075068193753804831

The only limitation is that we cannot visually gray out the chevron itself because the corresponding font-awesome icon is a Pro icon (https://fontawesome.com/icons/angle-down?s=duotone&f=classic).

* Gray out the chevron when the control is disabled

* Remove empty line

* Disable the transition on the chevron

Apply effect immediately when the dropdown is enabled/disabled.
2023-02-16 19:29:08 +05:30
f36b7ce016 Merge branch 'main' of github.com:cmdr2/stable-diffusion-ui 2023-02-16 19:22:04 +05:30
9fb5cac5d4 Bypass incorrect ERRORLEVEL values in nested code blocks by using something called delayedexpansion. Ugh 2023-02-16 19:21:51 +05:30
9f5f213cd3 Fix for dropdown widths (#883)
* Fix dropdown location

* change width
2023-02-16 10:35:46 +05:30
5d3b59b94e No /proc/cpuinfo on MacOS
Check whether /proc/cpuinfo exists before checking for AVX support
2023-02-15 21:15:55 +01:00
744c6e4725 sdkit 1.0.37 2023-02-15 21:40:02 +05:30
c59745d346 Cleaning up event listener that's no longer needed
The event listener instantiates two objects every time the user clicks on the Merge tab. This is no longer needed after AssassinJN's CSS fixes from yesterday.
2023-02-15 00:10:02 -08:00
9d1dd09a07 'Download all images' button (#765)
* Use standard DOM function

* Add 'download all images' button

---------

Co-authored-by: cmdr2 <secondary.cmdr2@gmail.com>
2023-02-14 19:33:25 +05:30
2eb317c6b6 Format code, PEP8 using Black 2023-02-14 18:47:50 +05:30
0ad08c609d Merge pull request #878 from patriceac/patch-26
Removing the 'None' option for face correction
2023-02-14 16:44:39 +05:30
85f6f8b31d Merge pull request #881 from patriceac/patch-27
Fix reloading of tasks with no file path
2023-02-14 16:44:01 +05:30
9799309db9 Fix reloading of tasks with no file path
In some conditions tasks may be reloaded with an empty file path (e.g. no face correction)
2023-02-14 02:31:13 -08:00
fa205f483a Merge pull request #880 from JeLuF/instfix
Fix functions.sh upgrade, change messages to Easy Diffusion
2023-02-14 15:08:06 +05:30
2df4286256 Change SDUI to Easy Diffusion 2023-02-14 09:05:23 +01:00
b89f689ea3 Fix functions.sh upgrade, change messages to Easy Diffusion 2023-02-14 09:00:02 +01:00
f58b21746e Removing the 'None' option for face correction
As per conversation : https://discord.com/channels/1014774730907209781/1014780368890630164/1074802779471757405
2023-02-13 17:42:36 -08:00
6971f9dcf1 Merge pull request #873 from AssassinJN/patch-1
remove js based sizing
2023-02-13 21:12:45 +05:30
3454a47f67 Merge pull request #872 from AssassinJN/patch-2
Fix for searchable model width
2023-02-13 21:12:15 +05:30
5922fd39c5 Fix for searchable model width 2023-02-13 09:31:53 -05:00
cdbddbae3b remove js based sizing 2023-02-13 09:20:11 -05:00
af4a26c1d0 Merge pull request #871 from JeLuF/patch-15
Fix typo in `cp functions.sh`
2023-02-13 19:12:18 +05:30
d3f42e47a7 Fix typo in cp functions.sh 2023-02-13 14:33:34 +01:00
8821e471b5 Merge pull request #870 from patriceac/Fix-autocomplete
Fix autocomplete in GFPGAN and Merge
2023-02-13 15:26:51 +05:30
d34aed0b14 Fix autocomplete in GFPGAN and Merge 2023-02-13 01:54:55 -08:00
b7391652ca Merge pull request #869 from patriceac/searchable-models-fixes
Model search bug fixes
2023-02-13 15:16:22 +05:30
074a14f056 Second batch of fixes for search models
Addresses the issues reported by JeLuf:
- - gfpgan: the list with models doesn't appear under the <input> box
- merge models: As long as no models are selected, the <input> box is very short.
- When searching for models by name, the width of the model list shrinks and is smaller than the <input> element.
2023-02-13 01:37:00 -08:00
b1db708af1 Model search bug fixes
First batch of bug fixes for model search:
- fix navigation issues with arrow keys when filtering models
- fix the issue with arrow keys jumping several entries after model reloading
- disable autocomplete in search box
2023-02-12 23:23:26 -08:00
b2a66709b0 Merge pull request #868 from JeLuF/modelselect
Use new model selector for merging and gfpgan
2023-02-13 09:03:13 +05:30
e3e43913ab Linux Installer fixes (#867)
Copy functions.sh from git repo as well
Remove duplicate 'Press any key' call
2023-02-13 09:02:31 +05:30
c7fed0a42a typo 2023-02-13 08:56:43 +05:30
c6c5e0734a typo 2023-02-13 08:55:35 +05:30
73cbc58a50 typo 2023-02-13 08:54:29 +05:30
8431395326 Fix model merger tab initialization 2023-02-13 00:30:06 +01:00
dd21c07d4a Use new model selector for merging and gfpgan 2023-02-13 00:13:13 +01:00
ce9591428e hotfix for broken model dropdown. thanks @patrice 2023-02-12 18:46:09 +05:30
a801a5d8b6 Searchable models (#786)
* Searchable models

Creates searchable dropdowns for SD, VAE, or HN models. Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models).

* Fixing the editor pane display

* Revert "Fixing the editor pane display"

This reverts commit de902a6340.

* Move formatting to the CSS file

* Rewritten the siblings functions

I like these much better, and I imagine you will too. :)

* Code cleanup

* Minor tweak in list ordering

Minor tweak to move the root folder's content at the end of the list (similar to the current version).
2023-02-12 14:48:09 +05:30
04e8458ce2 Merge pull request #862 from JeLuF/dragfix
Fix task reordering
2023-02-12 14:40:21 +05:30
4b4fa84879 Merge pull request #856 from JeLuF/mac
Changes to make things work on MacOS/x64
2023-02-12 14:39:32 +05:30
1b3df8c4de Merge pull request #864 from patriceac/patch-25
Fix the inpainter and image editor display
2023-02-12 14:38:34 +05:30
7ce223771d Add k40m to list of FP32 cards (#863)
https://discord.com/channels/1014774730907209781/1073819636329631754
2023-02-12 14:37:53 +05:30
ccf71ed445 Fix the inpainter and image editor display 2023-02-11 21:57:56 -08:00
aa7c031e8a Fix task reordering 2023-02-12 01:02:27 +01:00
8465bc1bc9 Changes to make things work on MacOS/x64 2023-02-10 22:34:52 +01:00
f2f3ed71d4 Hide Image buttons hides task (#854)
* hide task when all images are hidden

* Update main.css

* remove console logs

* remove another console log

* Update main.js

* Update main.js
2023-02-10 22:58:07 +05:30
ab7ba35639 Revert "sdkit message"
This reverts commit 6ab3133b33.
2023-02-10 22:54:45 +05:30
1cc09cbe5f Revert "sdkit message"
This reverts commit 6ab3133b33.
2023-02-10 22:54:02 +05:30
fe7e398eb4 sdkit message 2023-02-10 19:01:27 +05:30
6ab3133b33 sdkit message 2023-02-10 19:01:03 +05:30
ef77c37a7e changelog 2023-02-10 18:43:06 +05:30
1dd165a9c9 Keep txt as the default metadata format, and write metadata files by default, if saving to disk 2023-02-10 18:13:08 +05:30
3c74540615 Merge pull request #794 from patriceac/Embed-Metadata
Embed metadata
2023-02-10 18:11:56 +05:30
ad249c4651 sdkit 1.0.36, for the image metadata embedding change 2023-02-10 18:09:23 +05:30
071a4d6f37 Use a fixed sdkit version, to avoid bumping to the latest sdkit version in the main branch 2023-02-10 18:05:17 +05:30
5f2fb19d71 Use a fixed sdkit version, to avoid bumping to the latest sdkit version in the main branch 2023-02-10 18:04:43 +05:30
ce61657f7a typo 2023-02-10 17:46:50 +05:30
dc54e5bdce version 2023-02-10 17:44:16 +05:30
f7b8e000c5 Merge pull request #830 from ogmaresca/sort-models
Sort models alphabetically
2023-02-10 17:42:24 +05:30
73abf131a6 Merge pull request #771 from patriceac/patch-19
Fix restoration of models with subfolders
2023-02-10 17:32:56 +05:30
5741af2aba Merge pull request #777 from patriceac/preview-content-container
Group image containers (DOM tweak)
2023-02-10 17:28:08 +05:30
159af669f6 Merge pull request #769 from JeLuF/counter
Fix number on the "Make X images" button
2023-02-10 17:22:24 +05:30
a517255653 Merge pull request #784 from JeLuF/no-src
Warn when running installer from git checkout
2023-02-10 17:20:10 +05:30
573154633b Merge pull request #793 from patriceac/patch-20
Fix the tooltip display over image modifier cards
2023-02-10 17:18:46 +05:30
baa8afd9eb Merge pull request #843 from JeLuF/prompthook
Add hook to implement custom prompt preprocessors
2023-02-10 17:18:06 +05:30
9e718da70e Merge pull request #789 from JeLuF/gfpgan-chooser
Support multiple GFPGAN models
2023-02-10 17:16:49 +05:30
4df442f169 Merge pull request #736 from JeLuF/enfdir
Enforce an autosave directory
2023-02-10 16:55:23 +05:30
1dc93c7a39 Merge pull request #829 from Schorny/beta
add random_seed flag to reqBody
2023-02-10 16:52:41 +05:30
3d124986d3 renamed random_seed to used_random_seed 2023-02-10 11:59:34 +01:00
a589d98cd4 Merge pull request #850 from JeLuF/patch-13
Link to LINUX.zip
2023-02-10 10:51:12 +05:30
ed9f18e22c Trim lines 2023-02-09 17:56:54 -05:00
14fb115fc8 Link to LINUX.zip 2023-02-09 20:25:27 +01:00
c35a731a60 Update README.md 2023-02-09 19:56:18 +05:30
4f3d2bd120 Merge pull request #779 from patriceac/Fix-card-names-and-toggling
Fix card names and toggling
2023-02-09 19:43:57 +05:30
69c8fc3236 Merge pull request #811 from patriceac/patch-23
Removing the ':' after the tooltip icon
2023-02-09 19:31:10 +05:30
840ff5c363 Merge branch 'main' into patch-23 2023-02-09 19:30:55 +05:30
8386cd5cf7 Merge pull request #817 from fernandoisnaldo/patch-1
Fix aarch64 (arm64) verification
2023-02-09 19:24:13 +05:30
666c2f8771 Merge pull request #831 from ogmaresca/remove-promt-strength-txt2img
Restore VAE model in metadata files and remove prompt strength in txt2img generations
2023-02-09 19:23:54 +05:30
b342fa9661 Merge pull request #837 from patriceac/patch-24
Fix the behavior of the use as input button
2023-02-09 19:09:09 +05:30
63bf84fdd5 Merge pull request #845 from cmdr2/beta
Changelog
2023-02-09 19:01:53 +05:30
50fd64150e Merge pull request #844 from JeLuF/patch-12
Add T500 to list of full precision cards
2023-02-09 09:56:51 +05:30
63c5de2612 Add T500 to list of full precision cards 2023-02-09 01:46:32 +01:00
c576d582e2 Add hook to implement custom prompt preprocessors 2023-02-08 17:26:55 +01:00
026a4b6c76 Merge pull request #842 from cmdr2/beta
Don't force a user to 'low' VRAM usage, if their GPU has 4 GB or less VRAM
2023-02-08 19:46:57 +05:30
e7bf2ee58b Show models above folders in child folders to avoid models from appearing to belong the grandchild folder, prevent creating empty <optgroup />s 2023-02-07 21:13:06 -05:00
a931aa59a3 Fix the behavior of the use as input button
Clicking the button toggles the task container behind it.
2023-02-07 18:02:42 -08:00
a0178e15b3 More robust relative path calculation 2023-02-06 22:19:57 -08:00
f07d05a490 Also remove Hypernetwork Strength if not using a hypernetwork 2023-02-06 23:35:23 -05:00
b3a988bc0b Restore VAE model in metadata files and remove prompt strength in txt2img generations 2023-02-06 23:07:23 -05:00
e0f22d29e8 Sort models alphabetically 2023-02-06 19:03:03 -05:00
07ee97b862 add random_seed flag to reqBody (#1)
expose if the user requested a random seed or used a fixed seed
2023-02-06 23:13:00 +01:00
b99d9db8f9 Create exactly 'total' images even if 'in parallel' is no factor of 'total' 2023-02-05 17:09:56 +01:00
b7047dafb2 Fix aarch64 (arm64) verification 2023-02-03 16:36:49 -03:00
6bff97d6fa Removing the ':' after the tooltip icon
This colon after the tooltip icon just feels out of place.
2023-01-30 23:09:36 -08:00
01368ac496 Add support for Windows path names 2023-01-25 02:47:50 -08:00
200f8fd245 Code cleanup 2023-01-24 01:53:22 -08:00
64bf4356b4 Update save_utils.py 2023-01-24 01:48:16 -08:00
8d4d409cd6 Add 'embed' and 'none' to metadata saving options
*** Please merge https://github.com/easydiffusion/sdkit/pull/9 before merging this one. ***

This is the ED client part of metadata embedding. It adds 'embed' and 'none' options to the metadata setting and makes none the default (if never set before) because (1) it feels weird to create metadata files by default and (2) embedding by default could cause be problematic if users don't realize it's happening.

Also fixes the disabling of the dropdown in the settings when Save images to disk is toggled off.
2023-01-24 01:47:48 -08:00
dd4937178f Fix the tooltip display over image modifier cards 2023-01-24 01:37:37 -08:00
b044bc1791 Support multiple GFPGAN models
Add scanning for models and a dropdown to choose different models from
2023-01-19 20:49:54 +01:00
409ec61be2 Fail fatally, add same check on Linux, add some extra checks on Linux
Linux: Check that curl, bzip2, tar are available, check whether there's a space character in the install path, check whether the CPU supports AVX.
2023-01-19 00:40:20 +01:00
79d112ca7b Warn when running installer from git checkout 2023-01-18 00:11:23 +01:00
66d311258a Fix card names and toggling
Fix names and toggling for cards starting  with "By ", e.g. "By the ocean".
2023-01-16 23:56:44 -08:00
3d5133209b Group image containers (DOM tweak)
Move image containers in their own container to create a clear delineation in the DOM. Purely DOM structural adjustment meant to make a sticky footer possible in the preview pane (for plugins).
2023-01-15 23:34:56 -08:00
a8fba8f3fb Fix restoration of models with subfolders
In dnd.js, when models are restored in the UI, there is code that strips the path from the model file name. Now that we allow models to be hosted in subfolders, this code break the task restoration (e.g. use settings, D&D, copy/paste) because "/my models/model.ckpt" becomes "model.ckpt", which won't be found.

https://discord.com/channels/1014774730907209781/1014780368890630164/1063726724573052948
2023-01-14 23:54:09 -08:00
9d9fc1683a Fix number on the "Make X images" button
With this change, the number of prompt variants is taken into account when computing the number of images that will be generated.
X = getPrompts().length * numOutputsTotalField.value
2023-01-13 22:05:25 +01:00
192fd223b4 use config.json instead of config.bat 2023-01-10 23:40:35 +01:00
d5e76e662f Enforce a autosave directory 2022-12-30 21:05:25 +01:00
38 changed files with 2130 additions and 738 deletions

View File

@ -5,7 +5,7 @@
- **Nearly twice as fast** - significantly faster speed of image generation. We're now pretty close to automatic1111's speed. 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. We're now pretty close to automatic1111's speed. Code contributions are welcome to make our project even faster: https://github.com/easydiffusion/sdkit/#is-it-fast
- **Full support for Stable Diffusion 2.1 (including CPU)** - supports loading v1.4 or v2.0 or v2.1 models seamlessly. No need to enable "Test SD2", and no need to add `sd2_` to your SD 2.0 model file names. Works on CPU as well. - **Full support for Stable Diffusion 2.1 (including CPU)** - supports loading v1.4 or v2.0 or v2.1 models seamlessly. No need to enable "Test SD2", and no need to add `sd2_` to your SD 2.0 model file names. Works on CPU as well.
- **Memory optimized Stable Diffusion 2.1** - you can now use Stable Diffusion 2.1 models, with the same low VRAM optimizations that we've always had for SD 1.4. Please note, the SD 2.0 and 2.1 models require more GPU and System RAM, as compared to the SD 1.4 and 1.5 models. - **Memory optimized Stable Diffusion 2.1** - you can now use Stable Diffusion 2.1 models, with the same low VRAM optimizations that we've always had for SD 1.4. Please note, the SD 2.0 and 2.1 models require more GPU and System RAM, as compared to the SD 1.4 and 1.5 models.
- **6 new samplers!** - explore the new samplers, some of which can generate great images in less than 10 inference steps! - **11 new samplers!** - explore the new samplers, some of which can generate great images in less than 10 inference steps! We've added the Karras and UniPC samplers.
- **Model Merging** - You can now merge two models (`.ckpt` or `.safetensors`) and output `.ckpt` or `.safetensors` models, optionally in `fp16` precision. Details: https://github.com/cmdr2/stable-diffusion-ui/wiki/Model-Merging - **Model Merging** - You can now merge two models (`.ckpt` or `.safetensors`) and output `.ckpt` or `.safetensors` models, optionally in `fp16` precision. Details: https://github.com/cmdr2/stable-diffusion-ui/wiki/Model-Merging
- **Fast loading/unloading of VAEs** - No longer needs to reload the entire Stable Diffusion model, each time you change the VAE - **Fast loading/unloading of VAEs** - No longer needs to reload the entire Stable Diffusion model, each time you change the VAE
- **Database of known models** - automatically picks the right configuration for known models. E.g. we automatically detect and apply "v" parameterization (required for some SD 2.0 models), and "fp32" attention precision (required for some SD 2.1 models). - **Database of known models** - automatically picks the right configuration for known models. E.g. we automatically detect and apply "v" parameterization (required for some SD 2.0 models), and "fp32" attention precision (required for some SD 2.1 models).
@ -19,6 +19,24 @@
Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed. Our focus continues to remain on an easy installation experience, and an easy user-interface. While still remaining pretty powerful, in terms of features and speed.
### Detailed changelog ### Detailed changelog
* 2.5.22 - 28 Feb 2023 - Minor styling changes to UI buttons, and the models dropdown.
* 2.5.22 - 28 Feb 2023 - Lots of UI-related bug fixes. Thanks @patriceac.
* 2.5.21 - 22 Feb 2023 - An option to control the size of the image thumbnails. You can use the `Display options` in the top-right corner to change this. Thanks @JeLuf.
* 2.5.20 - 20 Feb 2023 - Support saving images in WEBP format (which consumes less disk space, with similar quality). Thanks @ogmaresca.
* 2.5.20 - 18 Feb 2023 - A setting to block NSFW images from being generated. You can enable this setting in the Settings tab.
* 2.5.19 - 17 Feb 2023 - Initial support for server-side plugins. Currently supports overriding the `get_cond_and_uncond()` function.
* 2.5.18 - 17 Feb 2023 - 5 new samplers! UniPC samplers, some of which produce images in less than 15 steps. Thanks @Schorny.
* 2.5.16 - 13 Feb 2023 - Searchable dropdown for models. This is useful if you have a LOT of models. You can type part of the model name, to auto-search through your models. Thanks @patriceac for the feature, and @AssassinJN for help in UI tweaks!
* 2.5.16 - 13 Feb 2023 - Lots of fixes and improvements to the installer. First round of changes to add Mac support. Thanks @JeLuf.
* 2.5.16 - 13 Feb 2023 - UI bug fixes for the inpainter editor. Thanks @patriceac.
* 2.5.16 - 13 Feb 2023 - Fix broken task reorder. Thanks @JeLuf.
* 2.5.16 - 13 Feb 2023 - Remove a task if all the images inside it have been removed. Thanks @AssassinJN.
* 2.5.16 - 10 Feb 2023 - Embed metadata into the JPG/PNG images, if selected in the "Settings" tab (under "Metadata format"). Thanks @patriceac.
* 2.5.16 - 10 Feb 2023 - Sort models alphabetically in the models dropdown. Thanks @ogmaresca.
* 2.5.16 - 10 Feb 2023 - Support multiple GFPGAN models. Download new GFPGAN models into the `models/gfpgan` folder, and refresh the UI to use it. Thanks @JeLuf.
* 2.5.16 - 10 Feb 2023 - Allow a server to enforce a fixed directory path to save images. This is useful if the server is exposed to a lot of users. This can be set in the `config.json` file as `force_save_path: "/path/to/fixed/save/dir"`. E.g. `force_save_path: "D:/user_images"`. Thanks @JeLuf.
* 2.5.16 - 10 Feb 2023 - The "Make Images" button now shows the correct amount of images it'll create when using operators like `{}` or `|`. For e.g. if the prompt is `Photo of a {woman, man}`, then the button will say `Make 2 Images`. Thanks @JeLuf.
* 2.5.16 - 10 Feb 2023 - A bunch of UI-related bug fixes. Thanks @patriceac.
* 2.5.15 - 8 Feb 2023 - Allow using 'balanced' VRAM usage mode on GPUs with 4 GB or less of VRAM. This mode used to be called 'Turbo' in the previous version. * 2.5.15 - 8 Feb 2023 - Allow using 'balanced' VRAM usage mode on GPUs with 4 GB or less of VRAM. This mode used to be called 'Turbo' in the previous version.
* 2.5.14 - 8 Feb 2023 - Fix broken auto-save settings. We renamed `sampler` to `sampler_name`, which caused old settings to fail. * 2.5.14 - 8 Feb 2023 - Fix broken auto-save settings. We renamed `sampler` to `sampler_name`, which caused old settings to fail.
* 2.5.14 - 6 Feb 2023 - Simplify the UI for merging models, and some other minor UI tweaks. Better error reporting if a model failed to load. * 2.5.14 - 6 Feb 2023 - Simplify the UI for merging models, and some other minor UI tweaks. Better error reporting if a model failed to load.

View File

@ -11,8 +11,8 @@ Does not require technical knowledge, does not require pre-installed software. 1
Click the download button for your operating system: Click the download button for your operating system:
<p float="left"> <p float="left">
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.4.13/stable-diffusion-ui-windows.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-win.png" width="200" /></a> <a href="https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.5.15/stable-diffusion-ui-windows.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-win.png" width="200" /></a>
<a href="https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.4.13/stable-diffusion-ui-linux.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-linux.png" width="200" /></a> <a href="https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.5.15/stable-diffusion-ui-linux.zip"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/main/media/download-linux.png" width="200" /></a>
</p> </p>
## On Windows: ## On Windows:
@ -50,11 +50,11 @@ The installer will take care of whatever is needed. If you face any problems, yo
- **Multiple Prompts File**: Queue multiple prompts by entering one prompt per line, or by running a text file. - **Multiple Prompts File**: Queue multiple prompts by entering one prompt per line, or by running a text file.
- **Save generated images to disk**: Save your images to your PC! - **Save generated images to disk**: Save your images to your PC!
- **UI Themes**: Customize the program to your liking. - **UI Themes**: Customize the program to your liking.
- **Organize your models into sub-folders** - **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
### Image generation ### Image generation
- **Supports**: "*Text to Image*" and "*Image to Image*". - **Supports**: "*Text to Image*" and "*Image to Image*".
- **14 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` - **19 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`, `unipc_snr`, `unipc_tu`, `unipc_tq`, `unipc_snr_2`, `unipc_tu_2`.
- **In-Painting**: Specify areas of your image to paint into. - **In-Painting**: Specify areas of your image to paint into.
- **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)**
@ -67,7 +67,7 @@ The installer will take care of whatever is needed. If you face any problems, yo
- **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.
- **Make Similar Images**: Click to generate multiple variations of a generated image. - **Make Similar Images**: Click to generate multiple variations of a generated image.
- **NSFW Setting**: A setting in the UI to control *NSFW content*. - **NSFW Setting**: A setting in the UI to control *NSFW content*.
- **JPEG/PNG output**: Multiple file formats. - **JPEG/PNG/WEBP output**: Multiple file formats.
### 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!
@ -75,6 +75,7 @@ The installer will take care of whatever is needed. If you face any problems, yo
- **Merge Models** - **Merge Models**
- **Use custom VAE models** - **Use custom VAE models**
- **Use pre-trained Hypernetworks** - **Use pre-trained Hypernetworks**
- **Use custom GFPGAN models**
- **UI Plugins**: Choose from a growing list of [community-generated UI plugins](https://github.com/cmdr2/stable-diffusion-ui/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/cmdr2/stable-diffusion-ui/wiki/UI-Plugins), or write your own plugin to add features to the project!
### Performance and security ### Performance and security

View File

@ -1,8 +1,27 @@
@echo off @echo off
cd /d %~dp0 cd /d %~dp0
echo Install dir: %~dp0
set PATH=C:\Windows\System32;%PATH% set PATH=C:\Windows\System32;%PATH%
if exist "on_sd_start.bat" (
echo ================================================================================
echo.
echo !!!! WARNING !!!!
echo.
echo It looks like you're trying to run the installation script from a source code
echo download. This will not work.
echo.
echo Recommended: Please close this window and download the installer from
echo https://stable-diffusion-ui.github.io/docs/installation/
echo.
echo ================================================================================
echo.
pause
exit /b
)
@rem set legacy installer's PATH, if it exists @rem set legacy installer's PATH, if it exists
if exist "installer" set PATH=%cd%\installer;%cd%\installer\Library\bin;%cd%\installer\Scripts;%cd%\installer\Library\usr\bin;%PATH% if exist "installer" set PATH=%cd%\installer;%cd%\installer\Library\bin;%cd%\installer\Scripts;%cd%\installer\Library\usr\bin;%PATH%

View File

@ -1,4 +1,5 @@
@echo off @echo off
setlocal enabledelayedexpansion
@rem This script will install git and conda (if not found on the PATH variable) @rem This script will install git and conda (if not found on the PATH variable)
@rem using micromamba (an 8mb static-linked single-file binary, conda replacement). @rem using micromamba (an 8mb static-linked single-file binary, conda replacement).
@ -28,10 +29,10 @@ if not exist "%LEGACY_INSTALL_ENV_DIR%\etc\profile.d\conda.sh" (
) )
call git --version >.tmp1 2>.tmp2 call git --version >.tmp1 2>.tmp2
if "%ERRORLEVEL%" NEQ "0" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% git if "!ERRORLEVEL!" NEQ "0" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% git
call "%MAMBA_ROOT_PREFIX%\micromamba.exe" --version >.tmp1 2>.tmp2 call "%MAMBA_ROOT_PREFIX%\micromamba.exe" --version >.tmp1 2>.tmp2
if "%ERRORLEVEL%" EQU "0" set umamba_exists=T if "!ERRORLEVEL!" EQU "0" set umamba_exists=T
@rem (if necessary) install git and conda into a contained environment @rem (if necessary) install git and conda into a contained environment
if "%PACKAGES_TO_INSTALL%" NEQ "" ( if "%PACKAGES_TO_INSTALL%" NEQ "" (
@ -42,7 +43,7 @@ if "%PACKAGES_TO_INSTALL%" NEQ "" (
mkdir "%MAMBA_ROOT_PREFIX%" mkdir "%MAMBA_ROOT_PREFIX%"
call curl -Lk "%MICROMAMBA_DOWNLOAD_URL%" > "%MAMBA_ROOT_PREFIX%\micromamba.exe" call curl -Lk "%MICROMAMBA_DOWNLOAD_URL%" > "%MAMBA_ROOT_PREFIX%\micromamba.exe"
if "%ERRORLEVEL%" NEQ "0" ( if "!ERRORLEVEL!" NEQ "0" (
echo "There was a problem downloading micromamba. Cannot continue." echo "There was a problem downloading micromamba. Cannot continue."
pause pause
exit /b exit /b

View File

@ -21,9 +21,19 @@ OS_ARCH=$(uname -m)
case "${OS_ARCH}" in case "${OS_ARCH}" in
x86_64*) OS_ARCH="64";; x86_64*) OS_ARCH="64";;
arm64*) OS_ARCH="arm64";; arm64*) OS_ARCH="arm64";;
aarch64*) OS_ARCH="arm64";;
*) echo "Unknown system architecture: $OS_ARCH! This script runs only on x86_64 or arm64" && exit *) echo "Unknown system architecture: $OS_ARCH! This script runs only on x86_64 or arm64" && exit
esac esac
if ! which curl; then fail "'curl' not found. Please install curl."; fi
if ! which tar; then fail "'tar' not found. Please install tar."; fi
if ! which bzip2; then fail "'bzip2' not found. Please install bzip2."; fi
if pwd | grep ' '; then fail "The installation directory's path contains a space character. Conda will fail to install. Please change the directory."; fi
if [ -f /proc/cpuinfo ]; then
if ! cat /proc/cpuinfo | grep avx | uniq; then fail "Your CPU doesn't support AVX."; fi
fi
# https://mamba.readthedocs.io/en/latest/installation.html # https://mamba.readthedocs.io/en/latest/installation.html
if [ "$OS_NAME" == "linux" ] && [ "$OS_ARCH" == "arm64" ]; then OS_ARCH="aarch64"; fi if [ "$OS_NAME" == "linux" ] && [ "$OS_ARCH" == "arm64" ]; then OS_ARCH="aarch64"; fi
@ -51,7 +61,7 @@ if [ "$PACKAGES_TO_INSTALL" != "" ]; then
echo "Downloading micromamba from $MICROMAMBA_DOWNLOAD_URL to $MAMBA_ROOT_PREFIX/micromamba" echo "Downloading micromamba from $MICROMAMBA_DOWNLOAD_URL to $MAMBA_ROOT_PREFIX/micromamba"
mkdir -p "$MAMBA_ROOT_PREFIX" mkdir -p "$MAMBA_ROOT_PREFIX"
curl -L "$MICROMAMBA_DOWNLOAD_URL" | tar -xvj bin/micromamba -O > "$MAMBA_ROOT_PREFIX/micromamba" curl -L "$MICROMAMBA_DOWNLOAD_URL" | tar -xvj -O bin/micromamba > "$MAMBA_ROOT_PREFIX/micromamba"
if [ "$?" != "0" ]; then if [ "$?" != "0" ]; then
echo echo

View File

@ -28,5 +28,12 @@ EOF
} }
filesize() {
case "$(uname -s)" in
Linux*) stat -c "%s" $1;;
Darwin*) stat -f "%z" $1;;
*) echo "Unknown OS: $OS_NAME! This script runs only on Linux or Mac" && exit
esac
}

View File

@ -1,6 +1,6 @@
@echo off @echo off
@echo. & echo "Stable Diffusion UI - v2" & echo. @echo. & echo "Easy Diffusion - v2" & echo.
set PATH=C:\Windows\System32;%PATH% set PATH=C:\Windows\System32;%PATH%
@ -28,7 +28,7 @@ if "%update_branch%"=="" (
@>nul findstr /m "sd_ui_git_cloned" scripts\install_status.txt @>nul findstr /m "sd_ui_git_cloned" scripts\install_status.txt
@if "%ERRORLEVEL%" EQU "0" ( @if "%ERRORLEVEL%" EQU "0" (
@echo "Stable Diffusion UI's git repository was already installed. Updating from %update_branch%.." @echo "Easy Diffusion's git repository was already installed. Updating from %update_branch%.."
@cd sd-ui-files @cd sd-ui-files
@ -38,13 +38,13 @@ if "%update_branch%"=="" (
@cd .. @cd ..
) else ( ) else (
@echo. & echo "Downloading Stable Diffusion UI.." & echo. @echo. & echo "Downloading Easy Diffusion..." & echo.
@echo "Using the %update_branch% channel" & echo. @echo "Using the %update_branch% channel" & echo.
@call git clone -b "%update_branch%" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files && ( @call git clone -b "%update_branch%" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files && (
@echo sd_ui_git_cloned >> scripts\install_status.txt @echo sd_ui_git_cloned >> scripts\install_status.txt
) || ( ) || (
@echo "Error downloading Stable Diffusion UI. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" @echo "Error downloading Easy Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
pause pause
@exit /b @exit /b
) )

View File

@ -2,7 +2,7 @@
source ./scripts/functions.sh source ./scripts/functions.sh
printf "\n\nStable Diffusion UI\n\n" printf "\n\nEasy Diffusion\n\n"
if [ -f "scripts/config.sh" ]; then if [ -f "scripts/config.sh" ]; then
source scripts/config.sh source scripts/config.sh
@ -13,7 +13,7 @@ if [ "$update_branch" == "" ]; then
fi fi
if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/install_status.txt` -gt "0" ]; then if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/install_status.txt` -gt "0" ]; then
echo "Stable Diffusion UI's git repository was already installed. Updating from $update_branch.." echo "Easy Diffusion's git repository was already installed. Updating from $update_branch.."
cd sd-ui-files cd sd-ui-files
@ -23,7 +23,7 @@ if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/in
cd .. cd ..
else else
printf "\n\nDownloading Stable Diffusion UI..\n\n" printf "\n\nDownloading Easy Diffusion..\n\n"
printf "Using the $update_branch channel\n\n" printf "Using the $update_branch channel\n\n"
if git clone -b "$update_branch" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files ; then if git clone -b "$update_branch" https://github.com/cmdr2/stable-diffusion-ui.git sd-ui-files ; then
@ -40,7 +40,6 @@ cp sd-ui-files/scripts/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/ cp sd-ui-files/scripts/check_modules.py scripts/
cp sd-ui-files/scripts/start.sh . cp sd-ui-files/scripts/start.sh .
cp sd-ui-files/scripts/developer_console.sh . cp sd-ui-files/scripts/developer_console.sh .
cp sd-ui-files/scripts/functions.sh scripts/
./scripts/on_sd_start.sh exec ./scripts/on_sd_start.sh
read -p "Press any key to continue"

View File

@ -26,7 +26,7 @@ if exist "%cd%\stable-diffusion\env" (
@rem activate the installer env @rem activate the installer env
call conda activate call conda activate
@if "%ERRORLEVEL%" NEQ "0" ( @if "%ERRORLEVEL%" NEQ "0" (
@echo. & echo "Error activating conda for Stable Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo. @echo. & echo "Error activating conda for Easy Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
pause pause
exit /b exit /b
) )
@ -61,6 +61,9 @@ if exist "GFPGANv1.3.pth" move GFPGANv1.3.pth ..\models\gfpgan\
if exist "RealESRGAN_x4plus.pth" move RealESRGAN_x4plus.pth ..\models\realesrgan\ if exist "RealESRGAN_x4plus.pth" move RealESRGAN_x4plus.pth ..\models\realesrgan\
if exist "RealESRGAN_x4plus_anime_6B.pth" move RealESRGAN_x4plus_anime_6B.pth ..\models\realesrgan\ if exist "RealESRGAN_x4plus_anime_6B.pth" move RealESRGAN_x4plus_anime_6B.pth ..\models\realesrgan\
if not exist "%INSTALL_ENV_DIR%\DLLs\libssl-1_1-x64.dll" copy "%INSTALL_ENV_DIR%\Library\bin\libssl-1_1-x64.dll" "%INSTALL_ENV_DIR%\DLLs\"
if not exist "%INSTALL_ENV_DIR%\DLLs\libcrypto-1_1-x64.dll" copy "%INSTALL_ENV_DIR%\Library\bin\libcrypto-1_1-x64.dll" "%INSTALL_ENV_DIR%\DLLs\"
@rem install torch and torchvision @rem install torch and torchvision
call python ..\scripts\check_modules.py torch torchvision call python ..\scripts\check_modules.py torch torchvision
if "%ERRORLEVEL%" EQU "0" ( if "%ERRORLEVEL%" EQU "0" (
@ -92,7 +95,7 @@ if "%ERRORLEVEL%" EQU "0" (
set PYTHONNOUSERSITE=1 set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
call python -m pip install --upgrade sdkit -q || ( call python -m pip install --upgrade sdkit==1.0.43 -q || (
echo "Error updating sdkit" echo "Error updating sdkit"
) )
) )
@ -103,7 +106,7 @@ if "%ERRORLEVEL%" EQU "0" (
set PYTHONNOUSERSITE=1 set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
call python -m pip install sdkit || ( call python -m pip install sdkit==1.0.43 || (
echo "Error installing sdkit. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" echo "Error installing sdkit. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
pause pause
exit /b exit /b
@ -113,7 +116,7 @@ if "%ERRORLEVEL%" EQU "0" (
call python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))" call python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))"
@rem upgrade stable-diffusion-sdkit @rem upgrade stable-diffusion-sdkit
call python -m pip install --upgrade stable-diffusion-sdkit -q || ( call python -m pip install --upgrade stable-diffusion-sdkit==2.1.3 -q || (
echo "Error updating stable-diffusion-sdkit" echo "Error updating stable-diffusion-sdkit"
) )
call python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))" call python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))"
@ -139,15 +142,15 @@ set PATH=C:\Windows\System32;%PATH%
call python ..\scripts\check_modules.py uvicorn fastapi call python ..\scripts\check_modules.py uvicorn fastapi
@if "%ERRORLEVEL%" EQU "0" ( @if "%ERRORLEVEL%" EQU "0" (
echo "Packages necessary for Stable Diffusion UI were already installed" echo "Packages necessary for Easy Diffusion were already installed"
) else ( ) else (
@echo. & echo "Downloading packages necessary for Stable Diffusion UI.." & echo. @echo. & echo "Downloading packages necessary for Easy Diffusion..." & echo.
set PYTHONNOUSERSITE=1 set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
@call conda install -c conda-forge -y uvicorn fastapi || ( @call conda install -c conda-forge -y uvicorn fastapi || (
echo "Error installing the packages necessary for Stable Diffusion UI. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" echo "Error installing the packages necessary for Easy Diffusion. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!"
pause pause
exit /b exit /b
) )
@ -279,7 +282,7 @@ call WHERE uvicorn > .tmp
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > ..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth @call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > ..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth
@if exist "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" ( @if exist "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" (
for %%I in ("RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" NEQ "17938799" ( for %%I in ("..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" NEQ "17938799" (
echo. & echo "Error: The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: %%~zI" & echo. echo. & echo "Error: The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: %%~zI" & echo.
echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo. echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime. Sorry about that, please try to:" & echo " 1. Run this installer again." & echo " 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" & echo " 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB" & echo " 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues" & echo "Thanks!" & echo.
pause pause
@ -328,7 +331,7 @@ call WHERE uvicorn > .tmp
@echo sd_install_complete >> ..\scripts\install_status.txt @echo sd_install_complete >> ..\scripts\install_status.txt
) )
@echo. & echo "Stable Diffusion is ready!" & echo. @echo. & echo "Easy Diffusion installation complete! Starting the server!" & echo.
@set SD_DIR=%cd% @set SD_DIR=%cd%

View File

@ -1,11 +1,12 @@
#!/bin/bash #!/bin/bash
source ./scripts/functions.sh cp sd-ui-files/scripts/functions.sh scripts/
cp sd-ui-files/scripts/on_env_start.sh scripts/ cp sd-ui-files/scripts/on_env_start.sh scripts/
cp sd-ui-files/scripts/bootstrap.sh scripts/ cp sd-ui-files/scripts/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/ cp sd-ui-files/scripts/check_modules.py scripts/
source ./scripts/functions.sh
# activate the installer env # activate the installer env
CONDA_BASEPATH=$(conda info --base) CONDA_BASEPATH=$(conda info --base)
source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # avoids the 'shell not initialized' error source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # avoids the 'shell not initialized' error
@ -80,7 +81,7 @@ if python ../scripts/check_modules.py sdkit sdkit.models ldm transformers numpy
export PYTHONNOUSERSITE=1 export PYTHONNOUSERSITE=1
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
python -m pip install --upgrade sdkit -q python -m pip install --upgrade sdkit==1.0.43 -q
fi fi
else else
echo "Installing sdkit: https://pypi.org/project/sdkit/" echo "Installing sdkit: https://pypi.org/project/sdkit/"
@ -88,7 +89,7 @@ else
export PYTHONNOUSERSITE=1 export PYTHONNOUSERSITE=1
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
if python -m pip install sdkit ; then if python -m pip install sdkit==1.0.43 ; then
echo "Installed." echo "Installed."
else else
fail "sdkit install failed" fail "sdkit install failed"
@ -98,7 +99,7 @@ fi
python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))" python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))"
# upgrade stable-diffusion-sdkit # upgrade stable-diffusion-sdkit
python -m pip install --upgrade stable-diffusion-sdkit -q python -m pip install --upgrade stable-diffusion-sdkit==2.1.3 -q
python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))" python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))"
# install rich # install rich
@ -118,9 +119,9 @@ else
fi fi
if python ../scripts/check_modules.py uvicorn fastapi ; then if python ../scripts/check_modules.py uvicorn fastapi ; then
echo "Packages necessary for Stable Diffusion UI were already installed" echo "Packages necessary for Easy Diffusion were already installed"
else else
printf "\n\nDownloading packages necessary for Stable Diffusion UI..\n\n" printf "\n\nDownloading packages necessary for Easy Diffusion..\n\n"
export PYTHONNOUSERSITE=1 export PYTHONNOUSERSITE=1
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages" export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
@ -137,7 +138,7 @@ else
fi fi
if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
model_size=`find "../models/stable-diffusion/sd-v1-4.ckpt" -printf "%s"` model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"`
if [ "$model_size" -eq "4265380512" ] || [ "$model_size" -eq "7703807346" ] || [ "$model_size" -eq "7703810927" ]; then if [ "$model_size" -eq "4265380512" ] || [ "$model_size" -eq "7703807346" ] || [ "$model_size" -eq "7703810927" ]; then
echo "Data files (weights) necessary for Stable Diffusion were already downloaded" echo "Data files (weights) necessary for Stable Diffusion were already downloaded"
@ -153,7 +154,7 @@ if [ ! -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
curl -L -k https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt > ../models/stable-diffusion/sd-v1-4.ckpt curl -L -k https://huggingface.co/CompVis/stable-diffusion-v-1-4-original/resolve/main/sd-v1-4.ckpt > ../models/stable-diffusion/sd-v1-4.ckpt
if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
model_size=`find "../models/stable-diffusion/sd-v1-4.ckpt" -printf "%s"` model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"`
if [ ! "$model_size" == "4265380512" ]; then if [ ! "$model_size" == "4265380512" ]; then
fail "The downloaded model file was invalid! Bytes downloaded: $model_size" fail "The downloaded model file was invalid! Bytes downloaded: $model_size"
fi fi
@ -164,7 +165,7 @@ fi
if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
model_size=`find "../models/gfpgan/GFPGANv1.3.pth" -printf "%s"` model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"`
if [ "$model_size" -eq "348632874" ]; then if [ "$model_size" -eq "348632874" ]; then
echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded" echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded"
@ -180,7 +181,7 @@ if [ ! -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > ../models/gfpgan/GFPGANv1.3.pth curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > ../models/gfpgan/GFPGANv1.3.pth
if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
model_size=`find "../models/gfpgan/GFPGANv1.3.pth" -printf "%s"` model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"`
if [ ! "$model_size" -eq "348632874" ]; then if [ ! "$model_size" -eq "348632874" ]; then
fail "The downloaded GFPGAN model file was invalid! Bytes downloaded: $model_size" fail "The downloaded GFPGAN model file was invalid! Bytes downloaded: $model_size"
fi fi
@ -191,7 +192,7 @@ fi
if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
model_size=`find "../models/realesrgan/RealESRGAN_x4plus.pth" -printf "%s"` model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"`
if [ "$model_size" -eq "67040989" ]; then if [ "$model_size" -eq "67040989" ]; then
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded" echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded"
@ -207,7 +208,7 @@ if [ ! -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > ../models/realesrgan/RealESRGAN_x4plus.pth curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > ../models/realesrgan/RealESRGAN_x4plus.pth
if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
model_size=`find "../models/realesrgan/RealESRGAN_x4plus.pth" -printf "%s"` model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"`
if [ ! "$model_size" -eq "67040989" ]; then if [ ! "$model_size" -eq "67040989" ]; then
fail "The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: $model_size" fail "The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: $model_size"
fi fi
@ -218,7 +219,7 @@ fi
if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`find "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" -printf "%s"` model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"`
if [ "$model_size" -eq "17938799" ]; then if [ "$model_size" -eq "17938799" ]; then
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded" echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded"
@ -234,7 +235,7 @@ if [ ! -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > ../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > ../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth
if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`find "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" -printf "%s"` model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"`
if [ ! "$model_size" -eq "17938799" ]; then if [ ! "$model_size" -eq "17938799" ]; then
fail "The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: $model_size" fail "The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: $model_size"
fi fi
@ -245,7 +246,7 @@ fi
if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
model_size=`find ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt -printf "%s"` model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"`
if [ "$model_size" -eq "334695179" ]; then if [ "$model_size" -eq "334695179" ]; then
echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded" echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded"
@ -261,7 +262,7 @@ if [ ! -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
curl -L -k https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt > ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt curl -L -k https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.ckpt > ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt
if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
model_size=`find ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt -printf "%s"` model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"`
if [ ! "$model_size" -eq "334695179" ]; then if [ ! "$model_size" -eq "334695179" ]; then
printf "\n\nError: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: $model_size\n\n" printf "\n\nError: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: $model_size\n\n"
printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n" printf "\n\nError downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). Sorry about that, please try to:\n 1. Run this installer again.\n 2. If that doesn't fix it, please try the common troubleshooting steps at https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting\n 3. If those steps don't help, please copy *all* the error messages in this window, and ask the community at https://discord.com/invite/u9yhsFmEkB\n 4. If that doesn't solve the problem, please file an issue at https://github.com/cmdr2/stable-diffusion-ui/issues\nThanks!\n\n"
@ -280,7 +281,7 @@ if [ `grep -c sd_install_complete ../scripts/install_status.txt` -gt "0" ]; then
echo sd_install_complete >> ../scripts/install_status.txt echo sd_install_complete >> ../scripts/install_status.txt
fi fi
printf "\n\nStable Diffusion is ready!\n\n" printf "\n\nEasy Diffusion installation complete, starting the server!\n\n"
SD_PATH=`pwd` SD_PATH=`pwd`

View File

@ -2,6 +2,24 @@
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
if [ -f "on_sd_start.bat" ]; then
echo ================================================================================
echo
echo !!!! WARNING !!!!
echo
echo It looks like you\'re trying to run the installation script from a source code
echo download. This will not work.
echo
echo Recommended: Please close this window and download the installer from
echo https://stable-diffusion-ui.github.io/docs/installation/
echo
echo ================================================================================
echo
read
exit 1
fi
# set legacy installer's PATH, if it exists # set legacy installer's PATH, if it exists
if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi

View File

@ -4,9 +4,10 @@ import sys
import json import json
import traceback import traceback
import logging import logging
import shlex
from rich.logging import RichHandler from rich.logging import RichHandler
from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config
from easydiffusion import task_manager from easydiffusion import task_manager
from easydiffusion.utils import log from easydiffusion.utils import log
@ -15,138 +16,205 @@ from easydiffusion.utils import log
for handler in logging.root.handlers[:]: for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler) logging.root.removeHandler(handler)
LOG_FORMAT = '%(asctime)s.%(msecs)03d %(levelname)s %(threadName)s %(message)s' LOG_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s %(threadName)s %(message)s"
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format=LOG_FORMAT, format=LOG_FORMAT,
datefmt="%X", datefmt="%X",
handlers=[RichHandler(markup=True, rich_tracebacks=False, show_time=False, show_level=False)], handlers=[RichHandler(markup=True, rich_tracebacks=False, show_time=False, show_level=False)],
) )
SD_DIR = os.getcwd() SD_DIR = os.getcwd()
SD_UI_DIR = os.getenv('SD_UI_PATH', None) SD_UI_DIR = os.getenv("SD_UI_PATH", None)
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, "..", "scripts"))
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, "..", "models"))
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"))
USER_UI_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "ui")
CORE_UI_PLUGINS_DIR = os.path.join(CORE_PLUGINS_DIR, "ui")
USER_SERVER_PLUGINS_DIR = os.path.join(USER_PLUGINS_DIR, "server")
UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, "core"), (USER_UI_PLUGINS_DIR, "user"))
sys.path.append(os.path.dirname(SD_UI_DIR)) sys.path.append(os.path.dirname(SD_UI_DIR))
sys.path.append(USER_SERVER_PLUGINS_DIR)
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, '..', 'scripts')) OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'models')) PRESERVE_CONFIG_VARS = ["FORCE_FULL_PRECISION"]
TASK_TTL = 15 * 60 # Discard last session's task timeout
USER_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'plugins', 'ui'))
CORE_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, 'plugins', 'ui'))
UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, 'core'), (USER_UI_PLUGINS_DIR, 'user'))
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
TASK_TTL = 15 * 60 # Discard last session's task timeout
APP_CONFIG_DEFAULTS = { APP_CONFIG_DEFAULTS = {
# auto: selects the cuda device with the most free memory, cuda: use the currently active cuda device. # auto: selects the cuda device with the most free memory, cuda: use the currently active cuda device.
'render_devices': 'auto', # valid entries: 'auto', 'cpu' or 'cuda:N' (where N is a GPU index) "render_devices": "auto", # valid entries: 'auto', 'cpu' or 'cuda:N' (where N is a GPU index)
'update_branch': 'main', "update_branch": "main",
'ui': { "ui": {
'open_browser_on_start': True, "open_browser_on_start": True,
}, },
} }
def init(): def init():
os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True) os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True)
os.makedirs(USER_SERVER_PLUGINS_DIR, exist_ok=True)
load_server_plugins()
update_render_threads() update_render_threads()
def getConfig(default_val=APP_CONFIG_DEFAULTS): def getConfig(default_val=APP_CONFIG_DEFAULTS):
try: try:
config_json_path = os.path.join(CONFIG_DIR, 'config.json') config_json_path = os.path.join(CONFIG_DIR, "config.json")
if not os.path.exists(config_json_path): if not os.path.exists(config_json_path):
return default_val config = default_val
with open(config_json_path, 'r', encoding='utf-8') as f: else:
config = json.load(f) with open(config_json_path, "r", encoding="utf-8") as f:
if 'net' not in config: config = json.load(f)
config['net'] = {} if "net" not in config:
if os.getenv('SD_UI_BIND_PORT') is not None: config["net"] = {}
config['net']['listen_port'] = int(os.getenv('SD_UI_BIND_PORT')) if os.getenv("SD_UI_BIND_PORT") is not None:
if os.getenv('SD_UI_BIND_IP') is not None: config["net"]["listen_port"] = int(os.getenv("SD_UI_BIND_PORT"))
config['net']['listen_to_network'] = (os.getenv('SD_UI_BIND_IP') == '0.0.0.0') else:
return config config["net"]["listen_port"] = 9000
if os.getenv("SD_UI_BIND_IP") is not None:
config["net"]["listen_to_network"] = os.getenv("SD_UI_BIND_IP") == "0.0.0.0"
else:
config["net"]["listen_to_network"] = True
return config
except Exception as e: except Exception as e:
log.warn(traceback.format_exc()) log.warn(traceback.format_exc())
return default_val return default_val
def setConfig(config): def setConfig(config):
try: # config.json try: # config.json
config_json_path = os.path.join(CONFIG_DIR, 'config.json') config_json_path = os.path.join(CONFIG_DIR, "config.json")
with open(config_json_path, 'w', encoding='utf-8') as f: with open(config_json_path, "w", encoding="utf-8") as f:
json.dump(config, f) json.dump(config, f)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
try: # config.bat try: # config.bat
config_bat_path = os.path.join(CONFIG_DIR, 'config.bat') config_bat_path = os.path.join(CONFIG_DIR, "config.bat")
config_bat = [] config_bat = []
if 'update_branch' in config: if "update_branch" in config:
config_bat.append(f"@set update_branch={config['update_branch']}") config_bat.append(f"@set update_branch={config['update_branch']}")
config_bat.append(f"@set SD_UI_BIND_PORT={config['net']['listen_port']}") config_bat.append(f"@set SD_UI_BIND_PORT={config['net']['listen_port']}")
bind_ip = '0.0.0.0' if config['net']['listen_to_network'] else '127.0.0.1' bind_ip = "0.0.0.0" if config["net"]["listen_to_network"] else "127.0.0.1"
config_bat.append(f"@set SD_UI_BIND_IP={bind_ip}") config_bat.append(f"@set SD_UI_BIND_IP={bind_ip}")
# Preserve these variables if they are set
for var in PRESERVE_CONFIG_VARS:
if os.getenv(var) is not None:
config_bat.append(f"@set {var}={os.getenv(var)}")
if len(config_bat) > 0: if len(config_bat) > 0:
with open(config_bat_path, 'w', encoding='utf-8') as f: with open(config_bat_path, "w", encoding="utf-8") as f:
f.write('\r\n'.join(config_bat)) f.write("\n".join(config_bat))
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
try: # config.sh try: # config.sh
config_sh_path = os.path.join(CONFIG_DIR, 'config.sh') config_sh_path = os.path.join(CONFIG_DIR, "config.sh")
config_sh = ['#!/bin/bash'] config_sh = ["#!/bin/bash"]
if 'update_branch' in config: if "update_branch" in config:
config_sh.append(f"export update_branch={config['update_branch']}") config_sh.append(f"export update_branch={config['update_branch']}")
config_sh.append(f"export SD_UI_BIND_PORT={config['net']['listen_port']}") config_sh.append(f"export SD_UI_BIND_PORT={config['net']['listen_port']}")
bind_ip = '0.0.0.0' if config['net']['listen_to_network'] else '127.0.0.1' bind_ip = "0.0.0.0" if config["net"]["listen_to_network"] else "127.0.0.1"
config_sh.append(f"export SD_UI_BIND_IP={bind_ip}") config_sh.append(f"export SD_UI_BIND_IP={bind_ip}")
# Preserve these variables if they are set
for var in PRESERVE_CONFIG_VARS:
if os.getenv(var) is not None:
config_bat.append(f'export {var}="{shlex.quote(os.getenv(var))}"')
if len(config_sh) > 1: if len(config_sh) > 1:
with open(config_sh_path, 'w', encoding='utf-8') as f: with open(config_sh_path, "w", encoding="utf-8") as f:
f.write('\n'.join(config_sh)) f.write("\n".join(config_sh))
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
def save_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name, vram_usage_level): def save_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name, vram_usage_level):
config = getConfig() config = getConfig()
if 'model' not in config: if "model" not in config:
config['model'] = {} config["model"] = {}
config['model']['stable-diffusion'] = ckpt_model_name config["model"]["stable-diffusion"] = ckpt_model_name
config['model']['vae'] = vae_model_name config["model"]["vae"] = vae_model_name
config['model']['hypernetwork'] = hypernetwork_model_name config["model"]["hypernetwork"] = hypernetwork_model_name
if vae_model_name is None or vae_model_name == "": if vae_model_name is None or vae_model_name == "":
del config['model']['vae'] del config["model"]["vae"]
if hypernetwork_model_name is None or hypernetwork_model_name == "": if hypernetwork_model_name is None or hypernetwork_model_name == "":
del config['model']['hypernetwork'] del config["model"]["hypernetwork"]
config['vram_usage_level'] = vram_usage_level config["vram_usage_level"] = vram_usage_level
setConfig(config) setConfig(config)
def update_render_threads(): def update_render_threads():
config = getConfig() config = getConfig()
render_devices = config.get('render_devices', 'auto') render_devices = config.get("render_devices", "auto")
active_devices = task_manager.get_devices()['active'].keys() active_devices = task_manager.get_devices()["active"].keys()
log.debug(f'requesting for render_devices: {render_devices}') log.debug(f"requesting for render_devices: {render_devices}")
task_manager.update_render_threads(render_devices, active_devices) task_manager.update_render_threads(render_devices, active_devices)
def getUIPlugins(): def getUIPlugins():
plugins = [] plugins = []
for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES: for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES:
for file in os.listdir(plugins_dir): for file in os.listdir(plugins_dir):
if file.endswith('.plugin.js'): if file.endswith(".plugin.js"):
plugins.append(f'/plugins/{dir_prefix}/{file}') plugins.append(f"/plugins/{dir_prefix}/{file}")
return plugins return plugins
def load_server_plugins():
if not os.path.exists(USER_SERVER_PLUGINS_DIR):
return
import importlib
def load_plugin(file):
mod_path = file.replace(".py", "")
return importlib.import_module(mod_path)
def apply_plugin(file, plugin):
if hasattr(plugin, "get_cond_and_uncond"):
import sdkit.generate.image_generator
sdkit.generate.image_generator.get_cond_and_uncond = plugin.get_cond_and_uncond
log.info(f"Overridden get_cond_and_uncond with the one in the server plugin: {file}")
for file in os.listdir(USER_SERVER_PLUGINS_DIR):
file_path = os.path.join(USER_SERVER_PLUGINS_DIR, file)
if (not os.path.isdir(file_path) and not file_path.endswith("_plugin.py")) or (
os.path.isdir(file_path) and not file_path.endswith("_plugin")
):
continue
try:
log.info(f"Loading server plugin: {file}")
mod = load_plugin(file)
log.info(f"Applying server plugin: {file}")
apply_plugin(file, mod)
except:
log.warn(f"Error while loading a server plugin")
log.warn(traceback.format_exc())
def getIPConfig(): def getIPConfig():
try: try:
ips = socket.gethostbyname_ex(socket.gethostname()) ips = socket.gethostbyname_ex(socket.gethostname())
@ -156,10 +224,13 @@ def getIPConfig():
log.exception(e) log.exception(e)
return [] return []
def open_browser(): def open_browser():
config = getConfig() config = getConfig()
ui = config.get('ui', {}) ui = config.get("ui", {})
net = config.get('net', {'listen_port':9000}) net = config.get("net", {"listen_port": 9000})
port = net.get('listen_port', 9000) port = net.get("listen_port", 9000)
if ui.get('open_browser_on_start', True): if ui.get("open_browser_on_start", True):
import webbrowser; webbrowser.open(f"http://localhost:{port}") import webbrowser
webbrowser.open(f"http://localhost:{port}")

View File

@ -5,45 +5,54 @@ import re
from easydiffusion.utils import log from easydiffusion.utils import log
''' """
Set `FORCE_FULL_PRECISION` in the environment variables, or in `config.bat`/`config.sh` to set full precision (i.e. float32). Set `FORCE_FULL_PRECISION` in the environment variables, or in `config.bat`/`config.sh` to set full precision (i.e. float32).
Otherwise the models will load at half-precision (i.e. float16). Otherwise the models will load at half-precision (i.e. float16).
Half-precision is fine most of the time. Full precision is only needed for working around GPU bugs (like NVIDIA 16xx GPUs). Half-precision is fine most of the time. Full precision is only needed for working around GPU bugs (like NVIDIA 16xx GPUs).
''' """
COMPARABLE_GPU_PERCENTILE = 0.65 # if a GPU's free_mem is within this % of the GPU with the most free_mem, it will be picked COMPARABLE_GPU_PERCENTILE = (
0.65 # if a GPU's free_mem is within this % of the GPU with the most free_mem, it will be picked
)
mem_free_threshold = 0 mem_free_threshold = 0
def get_device_delta(render_devices, active_devices): def get_device_delta(render_devices, active_devices):
''' """
render_devices: 'cpu', or 'auto' or ['cuda:N'...] render_devices: 'cpu', or 'auto' or ['cuda:N'...]
active_devices: ['cpu', 'cuda:N'...] active_devices: ['cpu', 'cuda:N'...]
''' """
if render_devices in ('cpu', 'auto'): if render_devices in ("cpu", "auto"):
render_devices = [render_devices] render_devices = [render_devices]
elif render_devices is not None: elif render_devices is not None:
if isinstance(render_devices, str): if isinstance(render_devices, str):
render_devices = [render_devices] render_devices = [render_devices]
if isinstance(render_devices, list) and len(render_devices) > 0: if isinstance(render_devices, list) and len(render_devices) > 0:
render_devices = list(filter(lambda x: x.startswith('cuda:'), render_devices)) render_devices = list(filter(lambda x: x.startswith("cuda:"), render_devices))
if len(render_devices) == 0: if len(render_devices) == 0:
raise Exception('Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}') raise Exception(
'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}'
)
render_devices = list(filter(lambda x: is_device_compatible(x), render_devices)) render_devices = list(filter(lambda x: is_device_compatible(x), render_devices))
if len(render_devices) == 0: if len(render_devices) == 0:
raise Exception('Sorry, none of the render_devices configured in config.json are compatible with Stable Diffusion') raise Exception(
"Sorry, none of the render_devices configured in config.json are compatible with Stable Diffusion"
)
else: else:
raise Exception('Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}') raise Exception(
'Invalid render_devices value in config.json. Valid: {"render_devices": ["cuda:0", "cuda:1"...]}, or {"render_devices": "cpu"} or {"render_devices": "auto"}'
)
else: else:
render_devices = ['auto'] render_devices = ["auto"]
if 'auto' in render_devices: if "auto" in render_devices:
render_devices = auto_pick_devices(active_devices) render_devices = auto_pick_devices(active_devices)
if 'cpu' in render_devices: if "cpu" in render_devices:
log.warn('WARNING: Could not find a compatible GPU. Using the CPU, but this will be very slow!') log.warn("WARNING: Could not find a compatible GPU. Using the CPU, but this will be very slow!")
active_devices = set(active_devices) active_devices = set(active_devices)
render_devices = set(render_devices) render_devices = set(render_devices)
@ -53,19 +62,21 @@ def get_device_delta(render_devices, active_devices):
return devices_to_start, devices_to_stop return devices_to_start, devices_to_stop
def auto_pick_devices(currently_active_devices): def auto_pick_devices(currently_active_devices):
global mem_free_threshold global mem_free_threshold
if not torch.cuda.is_available(): return ['cpu'] if not torch.cuda.is_available():
return ["cpu"]
device_count = torch.cuda.device_count() device_count = torch.cuda.device_count()
if device_count == 1: if device_count == 1:
return ['cuda:0'] if is_device_compatible('cuda:0') else ['cpu'] return ["cuda:0"] if is_device_compatible("cuda:0") else ["cpu"]
log.debug('Autoselecting GPU. Using most free memory.') log.debug("Autoselecting GPU. Using most free memory.")
devices = [] devices = []
for device in range(device_count): for device in range(device_count):
device = f'cuda:{device}' device = f"cuda:{device}"
if not is_device_compatible(device): if not is_device_compatible(device):
continue continue
@ -73,11 +84,13 @@ def auto_pick_devices(currently_active_devices):
mem_free /= float(10**9) mem_free /= float(10**9)
mem_total /= float(10**9) mem_total /= float(10**9)
device_name = torch.cuda.get_device_name(device) device_name = torch.cuda.get_device_name(device)
log.debug(f'{device} detected: {device_name} - Memory (free/total): {round(mem_free, 2)}Gb / {round(mem_total, 2)}Gb') log.debug(
devices.append({'device': device, 'device_name': device_name, 'mem_free': mem_free}) f"{device} detected: {device_name} - Memory (free/total): {round(mem_free, 2)}Gb / {round(mem_total, 2)}Gb"
)
devices.append({"device": device, "device_name": device_name, "mem_free": mem_free})
devices.sort(key=lambda x:x['mem_free'], reverse=True) devices.sort(key=lambda x: x["mem_free"], reverse=True)
max_mem_free = devices[0]['mem_free'] max_mem_free = devices[0]["mem_free"]
curr_mem_free_threshold = COMPARABLE_GPU_PERCENTILE * max_mem_free curr_mem_free_threshold = COMPARABLE_GPU_PERCENTILE * max_mem_free
mem_free_threshold = max(curr_mem_free_threshold, mem_free_threshold) mem_free_threshold = max(curr_mem_free_threshold, mem_free_threshold)
@ -87,23 +100,26 @@ def auto_pick_devices(currently_active_devices):
# always be very low (since their VRAM contains the model). # always be very low (since their VRAM contains the model).
# These already-running devices probably aren't terrible, since they were picked in the past. # These already-running devices probably aren't terrible, since they were picked in the past.
# Worst case, the user can restart the program and that'll get rid of them. # Worst case, the user can restart the program and that'll get rid of them.
devices = list(filter((lambda x: x['mem_free'] > mem_free_threshold or x['device'] in currently_active_devices), devices)) devices = list(
devices = list(map(lambda x: x['device'], devices)) filter((lambda x: x["mem_free"] > mem_free_threshold or x["device"] in currently_active_devices), devices)
)
devices = list(map(lambda x: x["device"], devices))
return devices return devices
def device_init(context, device): def device_init(context, device):
''' """
This function assumes the 'device' has already been verified to be compatible. This function assumes the 'device' has already been verified to be compatible.
`get_device_delta()` has already filtered out incompatible devices. `get_device_delta()` has already filtered out incompatible devices.
''' """
validate_device_id(device, log_prefix='device_init') validate_device_id(device, log_prefix="device_init")
if device == 'cpu': if device == "cpu":
context.device = 'cpu' context.device = "cpu"
context.device_name = get_processor_name() context.device_name = get_processor_name()
context.half_precision = False context.half_precision = False
log.debug(f'Render device CPU available as {context.device_name}') log.debug(f"Render device CPU available as {context.device_name}")
return return
context.device_name = torch.cuda.get_device_name(device) context.device_name = torch.cuda.get_device_name(device)
@ -111,7 +127,7 @@ def device_init(context, device):
# Force full precision on 1660 and 1650 NVIDIA cards to avoid creating green images # Force full precision on 1660 and 1650 NVIDIA cards to avoid creating green images
if needs_to_force_full_precision(context): if needs_to_force_full_precision(context):
log.warn(f'forcing full precision on this GPU, to avoid green images. GPU detected: {context.device_name}') log.warn(f"forcing full precision on this GPU, to avoid green images. GPU detected: {context.device_name}")
# Apply force_full_precision now before models are loaded. # Apply force_full_precision now before models are loaded.
context.half_precision = False context.half_precision = False
@ -120,72 +136,93 @@ def device_init(context, device):
return return
def needs_to_force_full_precision(context): def needs_to_force_full_precision(context):
if 'FORCE_FULL_PRECISION' in os.environ: if "FORCE_FULL_PRECISION" in os.environ:
return True return True
device_name = context.device_name.lower() device_name = context.device_name.lower()
return (('nvidia' in device_name or 'geforce' in device_name or 'quadro' in device_name) and (' 1660' in device_name or ' 1650' in device_name or ' t400' in device_name or ' t550' in device_name or ' t600' in device_name or ' t1000' in device_name or ' t1200' in device_name or ' t2000' in device_name)) return (
("nvidia" in device_name or "geforce" in device_name or "quadro" in device_name)
and (
" 1660" in device_name
or " 1650" in device_name
or " t400" in device_name
or " t550" in device_name
or " t600" in device_name
or " t1000" in device_name
or " t1200" in device_name
or " t2000" in device_name
)
) or ("tesla k40m" in device_name)
def get_max_vram_usage_level(device): def get_max_vram_usage_level(device):
if device != 'cpu': if device != "cpu":
_, mem_total = torch.cuda.mem_get_info(device) _, mem_total = torch.cuda.mem_get_info(device)
mem_total /= float(10**9) mem_total /= float(10**9)
if mem_total < 4.5: if mem_total < 4.5:
return 'low' return "low"
elif mem_total < 6.5: elif mem_total < 6.5:
return 'balanced' return "balanced"
return 'high' return "high"
def validate_device_id(device, log_prefix=''):
def validate_device_id(device, log_prefix=""):
def is_valid(): def is_valid():
if not isinstance(device, str): if not isinstance(device, str):
return False return False
if device == 'cpu': if device == "cpu":
return True return True
if not device.startswith('cuda:') or not device[5:].isnumeric(): if not device.startswith("cuda:") or not device[5:].isnumeric():
return False return False
return True return True
if not is_valid(): if not is_valid():
raise EnvironmentError(f"{log_prefix}: device id should be 'cpu', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}") raise EnvironmentError(
f"{log_prefix}: device id should be 'cpu', or 'cuda:N' (where N is an integer index for the GPU). Got: {device}"
)
def is_device_compatible(device): def is_device_compatible(device):
''' """
Returns True/False, and prints any compatibility errors Returns True/False, and prints any compatibility errors
''' """
# static variable "history". # static variable "history".
is_device_compatible.history = getattr(is_device_compatible, 'history', {}) is_device_compatible.history = getattr(is_device_compatible, "history", {})
try: try:
validate_device_id(device, log_prefix='is_device_compatible') validate_device_id(device, log_prefix="is_device_compatible")
except: except:
log.error(str(e)) log.error(str(e))
return False return False
if device == 'cpu': return True if device == "cpu":
return True
# Memory check # Memory check
try: try:
_, mem_total = torch.cuda.mem_get_info(device) _, mem_total = torch.cuda.mem_get_info(device)
mem_total /= float(10**9) mem_total /= float(10**9)
if mem_total < 3.0: if mem_total < 3.0:
if is_device_compatible.history.get(device) == None: if is_device_compatible.history.get(device) == None:
log.warn(f'GPU {device} with less than 3 GB of VRAM is not compatible with Stable Diffusion') log.warn(f"GPU {device} with less than 3 GB of VRAM is not compatible with Stable Diffusion")
is_device_compatible.history[device] = 1 is_device_compatible.history[device] = 1
return False return False
except RuntimeError as e: except RuntimeError as e:
log.error(str(e)) log.error(str(e))
return False return False
return True return True
def get_processor_name(): def get_processor_name():
try: try:
import platform, subprocess import platform, subprocess
if platform.system() == "Windows": if platform.system() == "Windows":
return platform.processor() return platform.processor()
elif platform.system() == "Darwin": elif platform.system() == "Darwin":
os.environ['PATH'] = os.environ['PATH'] + os.pathsep + '/usr/sbin' os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin"
command = "sysctl -n machdep.cpu.brand_string" command = "sysctl -n machdep.cpu.brand_string"
return subprocess.check_output(command).strip() return subprocess.check_output(command).strip()
elif platform.system() == "Linux": elif platform.system() == "Linux":

View File

@ -1,36 +1,37 @@
import os import os
from easydiffusion import app, device_manager from easydiffusion import app
from easydiffusion.types import TaskData from easydiffusion.types import TaskData
from easydiffusion.utils import log from easydiffusion.utils import log
from sdkit import Context from sdkit import Context
from sdkit.models import load_model, unload_model, get_model_info_from_db, scan_model from sdkit.models import load_model, unload_model, scan_model
from sdkit.utils import hash_file_quick
KNOWN_MODEL_TYPES = ['stable-diffusion', 'vae', 'hypernetwork', 'gfpgan', 'realesrgan'] KNOWN_MODEL_TYPES = ["stable-diffusion", "vae", "hypernetwork", "gfpgan", "realesrgan"]
MODEL_EXTENSIONS = { MODEL_EXTENSIONS = {
'stable-diffusion': ['.ckpt', '.safetensors'], "stable-diffusion": [".ckpt", ".safetensors"],
'vae': ['.vae.pt', '.ckpt', '.safetensors'], "vae": [".vae.pt", ".ckpt", ".safetensors"],
'hypernetwork': ['.pt', '.safetensors'], "hypernetwork": [".pt", ".safetensors"],
'gfpgan': ['.pth'], "gfpgan": [".pth"],
'realesrgan': ['.pth'], "realesrgan": [".pth"],
} }
DEFAULT_MODELS = { DEFAULT_MODELS = {
'stable-diffusion': [ # needed to support the legacy installations "stable-diffusion": [ # needed to support the legacy installations
'custom-model', # only one custom model file was supported initially, creatively named 'custom-model' "custom-model", # only one custom model file was supported initially, creatively named 'custom-model'
'sd-v1-4', # Default fallback. "sd-v1-4", # Default fallback.
], ],
'gfpgan': ['GFPGANv1.3'], "gfpgan": ["GFPGANv1.3"],
'realesrgan': ['RealESRGAN_x4plus'], "realesrgan": ["RealESRGAN_x4plus"],
} }
MODELS_TO_LOAD_ON_START = ['stable-diffusion', 'vae', 'hypernetwork'] MODELS_TO_LOAD_ON_START = ["stable-diffusion", "vae", "hypernetwork"]
known_models = {} known_models = {}
def init(): def init():
make_model_folders() make_model_folders()
getModels() # run this once, to cache the picklescan results getModels() # run this once, to cache the picklescan results
def load_default_models(context: Context): def load_default_models(context: Context):
set_vram_optimizations(context) set_vram_optimizations(context)
@ -39,27 +40,28 @@ def load_default_models(context: Context):
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) context.model_paths[model_type] = resolve_model_to_use(model_type=model_type)
try: try:
load_model(context, model_type) load_model(context, model_type)
except Exception as e: except Exception as e:
log.error(f'[red]Error while loading {model_type} model: {context.model_paths[model_type]}[/red]') log.error(f"[red]Error while loading {model_type} model: {context.model_paths[model_type]}[/red]")
log.error(f'[red]Error: {e}[/red]') log.error(f"[red]Error: {e}[/red]")
log.error(f'[red]Consider removing the model from the model folder.[red]') log.error(f"[red]Consider removing the model from the model folder.[red]")
def unload_all(context: Context): def unload_all(context: Context):
for model_type in KNOWN_MODEL_TYPES: for model_type in KNOWN_MODEL_TYPES:
unload_model(context, model_type) unload_model(context, model_type)
def resolve_model_to_use(model_name:str=None, model_type:str=None):
def resolve_model_to_use(model_name: str = None, model_type: str = None):
model_extensions = MODEL_EXTENSIONS.get(model_type, []) model_extensions = MODEL_EXTENSIONS.get(model_type, [])
default_models = DEFAULT_MODELS.get(model_type, []) default_models = DEFAULT_MODELS.get(model_type, [])
config = app.getConfig() config = app.getConfig()
model_dirs = [os.path.join(app.MODELS_DIR, model_type), app.SD_DIR] model_dirs = [os.path.join(app.MODELS_DIR, model_type), app.SD_DIR]
if not model_name: # When None try user configured model. if not model_name: # When None try user configured model.
# config = getConfig() # config = getConfig()
if 'model' in config and model_type in config['model']: if "model" in config and model_type in config["model"]:
model_name = config['model'][model_type] model_name = config["model"][model_type]
if model_name: if model_name:
# Check models directory # Check models directory
@ -84,41 +86,55 @@ def resolve_model_to_use(model_name:str=None, model_type:str=None):
for model_extension in model_extensions: for model_extension in model_extensions:
if os.path.exists(default_model_path + model_extension): if os.path.exists(default_model_path + model_extension):
if model_name is not None: if model_name is not None:
log.warn(f'Could not find the configured custom model {model_name}{model_extension}. Using the default one: {default_model_path}{model_extension}') log.warn(
f"Could not find the configured custom model {model_name}{model_extension}. Using the default one: {default_model_path}{model_extension}"
)
return default_model_path + model_extension return default_model_path + model_extension
return None return None
def reload_models_if_necessary(context: Context, task_data: TaskData): def reload_models_if_necessary(context: Context, task_data: TaskData):
model_paths_in_req = { model_paths_in_req = {
'stable-diffusion': task_data.use_stable_diffusion_model, "stable-diffusion": task_data.use_stable_diffusion_model,
'vae': task_data.use_vae_model, "vae": task_data.use_vae_model,
'hypernetwork': task_data.use_hypernetwork_model, "hypernetwork": task_data.use_hypernetwork_model,
'gfpgan': task_data.use_face_correction, "gfpgan": task_data.use_face_correction,
'realesrgan': task_data.use_upscale, "realesrgan": task_data.use_upscale,
"nsfw_checker": True if task_data.block_nsfw else None,
}
models_to_reload = {
model_type: path
for model_type, path in model_paths_in_req.items()
if context.model_paths.get(model_type) != path
} }
models_to_reload = {model_type: path for model_type, path in model_paths_in_req.items() if context.model_paths.get(model_type) != path}
if set_vram_optimizations(context): # reload SD if set_vram_optimizations(context): # reload SD
models_to_reload['stable-diffusion'] = model_paths_in_req['stable-diffusion'] models_to_reload["stable-diffusion"] = model_paths_in_req["stable-diffusion"]
for model_type, model_path_in_req in models_to_reload.items(): for model_type, model_path_in_req in models_to_reload.items():
context.model_paths[model_type] = model_path_in_req context.model_paths[model_type] = model_path_in_req
action_fn = unload_model if context.model_paths[model_type] is None else load_model action_fn = unload_model if context.model_paths[model_type] is None else load_model
action_fn(context, model_type, scan_model=False) # we've scanned them already action_fn(context, model_type, scan_model=False) # we've scanned them already
def resolve_model_paths(task_data: TaskData): def resolve_model_paths(task_data: TaskData):
task_data.use_stable_diffusion_model = resolve_model_to_use(task_data.use_stable_diffusion_model, model_type='stable-diffusion') task_data.use_stable_diffusion_model = resolve_model_to_use(
task_data.use_vae_model = resolve_model_to_use(task_data.use_vae_model, model_type='vae') task_data.use_stable_diffusion_model, model_type="stable-diffusion"
task_data.use_hypernetwork_model = resolve_model_to_use(task_data.use_hypernetwork_model, model_type='hypernetwork') )
task_data.use_vae_model = resolve_model_to_use(task_data.use_vae_model, model_type="vae")
task_data.use_hypernetwork_model = resolve_model_to_use(task_data.use_hypernetwork_model, model_type="hypernetwork")
if task_data.use_face_correction:
task_data.use_face_correction = resolve_model_to_use(task_data.use_face_correction, "gfpgan")
if task_data.use_upscale:
task_data.use_upscale = resolve_model_to_use(task_data.use_upscale, "realesrgan")
if task_data.use_face_correction: task_data.use_face_correction = resolve_model_to_use(task_data.use_face_correction, 'gfpgan')
if task_data.use_upscale: task_data.use_upscale = resolve_model_to_use(task_data.use_upscale, 'realesrgan')
def set_vram_optimizations(context: Context): def set_vram_optimizations(context: Context):
config = app.getConfig() config = app.getConfig()
vram_usage_level = config.get('vram_usage_level', 'balanced') vram_usage_level = config.get("vram_usage_level", "balanced")
if vram_usage_level != context.vram_usage_level: if vram_usage_level != context.vram_usage_level:
context.vram_usage_level = vram_usage_level context.vram_usage_level = vram_usage_level
@ -126,42 +142,53 @@ def set_vram_optimizations(context: Context):
return False return False
def make_model_folders(): def make_model_folders():
for model_type in KNOWN_MODEL_TYPES: for model_type in KNOWN_MODEL_TYPES:
model_dir_path = os.path.join(app.MODELS_DIR, model_type) model_dir_path = os.path.join(app.MODELS_DIR, model_type)
os.makedirs(model_dir_path, exist_ok=True) os.makedirs(model_dir_path, exist_ok=True)
help_file_name = f'Place your {model_type} model files here.txt' help_file_name = f"Place your {model_type} model files here.txt"
help_file_contents = f'Supported extensions: {" or ".join(MODEL_EXTENSIONS.get(model_type))}' help_file_contents = f'Supported extensions: {" or ".join(MODEL_EXTENSIONS.get(model_type))}'
with open(os.path.join(model_dir_path, help_file_name), 'w', encoding='utf-8') as f: with open(os.path.join(model_dir_path, help_file_name), "w", encoding="utf-8") as f:
f.write(help_file_contents) f.write(help_file_contents)
def is_malicious_model(file_path): def is_malicious_model(file_path):
try: try:
if file_path.endswith(".safetensors"):
return False
scan_result = scan_model(file_path) scan_result = scan_model(file_path)
if scan_result.issues_count > 0 or scan_result.infected_files > 0: if scan_result.issues_count > 0 or scan_result.infected_files > 0:
log.warn(":warning: [bold red]Scan %s: %d scanned, %d issue, %d infected.[/bold red]" % (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files)) log.warn(
":warning: [bold red]Scan %s: %d scanned, %d issue, %d infected.[/bold red]"
% (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files)
)
return True return True
else: else:
log.debug("Scan %s: [green]%d scanned, %d issue, %d infected.[/green]" % (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files)) log.debug(
"Scan %s: [green]%d scanned, %d issue, %d infected.[/green]"
% (file_path, scan_result.scanned_files, scan_result.issues_count, scan_result.infected_files)
)
return False return False
except Exception as e: except Exception as e:
log.error(f'error while scanning: {file_path}, error: {e}') log.error(f"error while scanning: {file_path}, error: {e}")
return False return False
def getModels(): def getModels():
models = { models = {
'active': { "active": {
'stable-diffusion': 'sd-v1-4', "stable-diffusion": "sd-v1-4",
'vae': '', "vae": "",
'hypernetwork': '', "hypernetwork": "",
}, },
'options': { "options": {
'stable-diffusion': ['sd-v1-4'], "stable-diffusion": ["sd-v1-4"],
'vae': [], "vae": [],
'hypernetwork': [], "hypernetwork": [],
}, },
} }
@ -171,13 +198,16 @@ def getModels():
"Raised when picklescan reports a problem with a model" "Raised when picklescan reports a problem with a model"
pass pass
def scan_directory(directory, suffixes): def scan_directory(directory, suffixes, directoriesFirst: bool = True):
nonlocal models_scanned nonlocal models_scanned
tree = [] tree = []
for entry in os.scandir(directory): for entry in sorted(
os.scandir(directory), key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower())
):
if entry.is_file(): if entry.is_file():
matching_suffix = list(filter(lambda s: entry.name.endswith(s), suffixes)) matching_suffix = list(filter(lambda s: entry.name.endswith(s), suffixes))
if len(matching_suffix) == 0: continue if len(matching_suffix) == 0:
continue
matching_suffix = matching_suffix[0] matching_suffix = matching_suffix[0]
mtime = entry.stat().st_mtime mtime = entry.stat().st_mtime
@ -187,11 +217,12 @@ def getModels():
if is_malicious_model(entry.path): if is_malicious_model(entry.path):
raise MaliciousModelException(entry.path) raise MaliciousModelException(entry.path)
known_models[entry.path] = mtime known_models[entry.path] = mtime
tree.append(entry.name[:-len(matching_suffix)]) tree.append(entry.name[: -len(matching_suffix)])
elif entry.is_dir(): elif entry.is_dir():
scan=scan_directory(entry.path, suffixes) scan = scan_directory(entry.path, suffixes, directoriesFirst=False)
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):
@ -203,20 +234,22 @@ def getModels():
os.makedirs(models_dir) os.makedirs(models_dir)
try: try:
models['options'][model_type] = scan_directory(models_dir, model_extensions) models["options"][model_type] = scan_directory(models_dir, model_extensions)
except MaliciousModelException as e: except MaliciousModelException as e:
models['scan-error'] = e models["scan-error"] = e
# custom models # custom models
listModels(model_type='stable-diffusion') listModels(model_type="stable-diffusion")
listModels(model_type='vae') listModels(model_type="vae")
listModels(model_type='hypernetwork') listModels(model_type="hypernetwork")
listModels(model_type="gfpgan")
if models_scanned > 0: log.info(f'[green]Scanned {models_scanned} models. Nothing infected[/]') if models_scanned > 0:
log.info(f"[green]Scanned {models_scanned} models. Nothing infected[/]")
# legacy # legacy
custom_weight_path = os.path.join(app.SD_DIR, 'custom-model.ckpt') custom_weight_path = os.path.join(app.SD_DIR, "custom-model.ckpt")
if os.path.exists(custom_weight_path): if os.path.exists(custom_weight_path):
models['options']['stable-diffusion'].append('custom-model') models["options"]["stable-diffusion"].append("custom-model")
return models return models

View File

@ -12,22 +12,26 @@ from sdkit.generate import generate_images
from sdkit.filter import apply_filters from sdkit.filter import apply_filters
from sdkit.utils import img_to_buffer, img_to_base64_str, latent_samples_to_images, gc from sdkit.utils import img_to_buffer, img_to_base64_str, latent_samples_to_images, gc
context = Context() # thread-local context = Context() # thread-local
''' """
runtime data (bound locally to this thread), for e.g. device, references to loaded models, optimization flags etc runtime data (bound locally to this thread), for e.g. device, references to loaded models, optimization flags etc
''' """
def init(device): def init(device):
''' """
Initializes the fields that will be bound to this runtime's context, and sets the current torch device Initializes the fields that will be bound to this runtime's context, and sets the current torch device
''' """
context.stop_processing = False context.stop_processing = False
context.temp_images = {} context.temp_images = {}
context.partial_x_samples = None context.partial_x_samples = None
device_manager.device_init(context, device) device_manager.device_init(context, device)
def make_images(req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback):
def make_images(
req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback
):
context.stop_processing = False context.stop_processing = False
print_task_info(req, task_data) print_task_info(req, task_data)
@ -36,18 +40,25 @@ def make_images(req: GenerateImageRequest, task_data: TaskData, data_queue: queu
res = Response(req, task_data, images=construct_response(images, seeds, task_data, base_seed=req.seed)) res = Response(req, task_data, images=construct_response(images, seeds, task_data, base_seed=req.seed))
res = res.json() res = res.json()
data_queue.put(json.dumps(res)) data_queue.put(json.dumps(res))
log.info('Task completed') log.info("Task completed")
return res return res
def print_task_info(req: GenerateImageRequest, task_data: TaskData):
req_str = pprint.pformat(get_printable_request(req)).replace("[","\[")
task_str = pprint.pformat(task_data.dict()).replace("[","\[")
log.info(f'request: {req_str}')
log.info(f'task data: {task_str}')
def make_images_internal(req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback): def print_task_info(req: GenerateImageRequest, task_data: TaskData):
images, user_stopped = generate_images_internal(req, task_data, data_queue, task_temp_images, step_callback, task_data.stream_image_progress) req_str = pprint.pformat(get_printable_request(req)).replace("[", "\[")
task_str = pprint.pformat(task_data.dict()).replace("[", "\[")
log.info(f"request: {req_str}")
log.info(f"task data: {task_str}")
def make_images_internal(
req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback
):
images, user_stopped = generate_images_internal(
req, task_data, data_queue, task_temp_images, step_callback, task_data.stream_image_progress, task_data.stream_image_progress_interval
)
filtered_images = filter_images(task_data, images, user_stopped) filtered_images = filter_images(task_data, images, user_stopped)
if task_data.save_to_disk_path is not None: if task_data.save_to_disk_path is not None:
@ -59,13 +70,23 @@ def make_images_internal(req: GenerateImageRequest, task_data: TaskData, data_qu
else: else:
return images + filtered_images, seeds + seeds return images + filtered_images, seeds + seeds
def generate_images_internal(req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback, stream_image_progress: bool):
def generate_images_internal(
req: GenerateImageRequest,
task_data: TaskData,
data_queue: queue.Queue,
task_temp_images: list,
step_callback,
stream_image_progress: bool,
stream_image_progress_interval: int,
):
context.temp_images.clear() context.temp_images.clear()
callback = make_step_callback(req, task_data, data_queue, task_temp_images, step_callback, stream_image_progress) callback = make_step_callback(req, task_data, data_queue, task_temp_images, step_callback, stream_image_progress, stream_image_progress_interval)
try: try:
if req.init_image is not None: req.sampler_name = 'ddim' if req.init_image is not None:
req.sampler_name = "ddim"
images = generate_images(context, callback=callback, **req.dict()) images = generate_images(context, callback=callback, **req.dict())
user_stopped = False user_stopped = False
@ -75,31 +96,50 @@ def generate_images_internal(req: GenerateImageRequest, task_data: TaskData, dat
if context.partial_x_samples is not None: if context.partial_x_samples is not None:
images = latent_samples_to_images(context, context.partial_x_samples) images = latent_samples_to_images(context, context.partial_x_samples)
finally: finally:
if hasattr(context, 'partial_x_samples') and context.partial_x_samples is not None: if hasattr(context, "partial_x_samples") and context.partial_x_samples is not None:
del context.partial_x_samples del context.partial_x_samples
context.partial_x_samples = None context.partial_x_samples = None
return images, user_stopped return images, user_stopped
def filter_images(task_data: TaskData, images: list, user_stopped): def filter_images(task_data: TaskData, images: list, user_stopped):
if user_stopped or (task_data.use_face_correction is None and task_data.use_upscale is None): if user_stopped:
return images return images
filters_to_apply = [] filters_to_apply = []
if task_data.use_face_correction and 'gfpgan' in task_data.use_face_correction.lower(): filters_to_apply.append('gfpgan') if task_data.block_nsfw:
if task_data.use_upscale and 'realesrgan' in task_data.use_upscale.lower(): filters_to_apply.append('realesrgan') filters_to_apply.append("nsfw_checker")
if task_data.use_face_correction and "gfpgan" in task_data.use_face_correction.lower():
filters_to_apply.append("gfpgan")
if task_data.use_upscale and "realesrgan" in task_data.use_upscale.lower():
filters_to_apply.append("realesrgan")
if len(filters_to_apply) == 0:
return images
return apply_filters(context, filters_to_apply, images, scale=task_data.upscale_amount) return apply_filters(context, filters_to_apply, images, scale=task_data.upscale_amount)
def construct_response(images: list, seeds: list, task_data: TaskData, base_seed: int): def construct_response(images: list, seeds: list, task_data: TaskData, base_seed: int):
return [ return [
ResponseImage( ResponseImage(
data=img_to_base64_str(img, task_data.output_format, task_data.output_quality), data=img_to_base64_str(img, task_data.output_format, task_data.output_quality),
seed=seed, seed=seed,
) for img, seed in zip(images, seeds) )
for img, seed in zip(images, seeds)
] ]
def make_step_callback(req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback, stream_image_progress: bool):
def make_step_callback(
req: GenerateImageRequest,
task_data: TaskData,
data_queue: queue.Queue,
task_temp_images: list,
step_callback,
stream_image_progress: bool,
stream_image_progress_interval: int,
):
n_steps = req.num_inference_steps if req.init_image is None else int(req.num_inference_steps * req.prompt_strength) n_steps = req.num_inference_steps if req.init_image is None else int(req.num_inference_steps * req.prompt_strength)
last_callback_time = -1 last_callback_time = -1
@ -107,11 +147,11 @@ def make_step_callback(req: GenerateImageRequest, task_data: TaskData, data_queu
partial_images = [] partial_images = []
images = latent_samples_to_images(context, x_samples) images = latent_samples_to_images(context, x_samples)
for i, img in enumerate(images): for i, img in enumerate(images):
buf = img_to_buffer(img, output_format='JPEG') buf = img_to_buffer(img, output_format="JPEG")
context.temp_images[f"{task_data.request_id}/{i}"] = buf context.temp_images[f"{task_data.request_id}/{i}"] = buf
task_temp_images[i] = buf task_temp_images[i] = buf
partial_images.append({'path': f"/image/tmp/{task_data.request_id}/{i}"}) partial_images.append({"path": f"/image/tmp/{task_data.request_id}/{i}"})
del images del images
return partial_images return partial_images
@ -124,8 +164,8 @@ def make_step_callback(req: GenerateImageRequest, task_data: TaskData, data_queu
progress = {"step": i, "step_time": step_time, "total_steps": n_steps} progress = {"step": i, "step_time": step_time, "total_steps": n_steps}
if stream_image_progress and i % 5 == 0: if stream_image_progress and stream_image_progress_interval > 0 and i % stream_image_progress_interval == 0:
progress['output'] = update_temp_img(x_samples, task_temp_images) progress["output"] = update_temp_img(x_samples, task_temp_images)
data_queue.put(json.dumps(progress)) data_queue.put(json.dumps(progress))

View File

@ -16,21 +16,25 @@ from easydiffusion import app, model_manager, task_manager
from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest
from easydiffusion.utils import log from easydiffusion.utils import log
log.info(f'started in {app.SD_DIR}') log.info(f"started in {app.SD_DIR}")
log.info(f'started at {datetime.datetime.now():%x %X}') log.info(f"started at {datetime.datetime.now():%x %X}")
server_api = FastAPI() server_api = FastAPI()
NOCACHE_HEADERS={"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"} NOCACHE_HEADERS = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
class NoCacheStaticFiles(StaticFiles): class NoCacheStaticFiles(StaticFiles):
def is_not_modified(self, response_headers, request_headers) -> bool: def is_not_modified(self, response_headers, request_headers) -> bool:
if 'content-type' in response_headers and ('javascript' in response_headers['content-type'] or 'css' in response_headers['content-type']): if "content-type" in response_headers and (
"javascript" in response_headers["content-type"] or "css" in response_headers["content-type"]
):
response_headers.update(NOCACHE_HEADERS) response_headers.update(NOCACHE_HEADERS)
return False return False
return super().is_not_modified(response_headers, request_headers) return super().is_not_modified(response_headers, request_headers)
class SetAppConfigRequest(BaseModel): class SetAppConfigRequest(BaseModel):
update_branch: str = None update_branch: str = None
render_devices: Union[List[str], List[int], str, int] = None render_devices: Union[List[str], List[int], str, int] = None
@ -39,203 +43,243 @@ class SetAppConfigRequest(BaseModel):
listen_to_network: bool = None listen_to_network: bool = None
listen_port: int = None listen_port: int = None
def init(): def init():
server_api.mount('/media', NoCacheStaticFiles(directory=os.path.join(app.SD_UI_DIR, 'media')), name="media") server_api.mount("/media", NoCacheStaticFiles(directory=os.path.join(app.SD_UI_DIR, "media")), name="media")
for plugins_dir, dir_prefix in app.UI_PLUGINS_SOURCES: for plugins_dir, dir_prefix in app.UI_PLUGINS_SOURCES:
server_api.mount(f'/plugins/{dir_prefix}', NoCacheStaticFiles(directory=plugins_dir), name=f"plugins-{dir_prefix}") server_api.mount(
f"/plugins/{dir_prefix}", NoCacheStaticFiles(directory=plugins_dir), name=f"plugins-{dir_prefix}"
)
@server_api.post('/app_config') @server_api.post("/app_config")
async def set_app_config(req : SetAppConfigRequest): async def set_app_config(req: SetAppConfigRequest):
return set_app_config_internal(req) return set_app_config_internal(req)
@server_api.get('/get/{key:path}') @server_api.get("/get/{key:path}")
def read_web_data(key:str=None): def read_web_data(key: str = None):
return read_web_data_internal(key) return read_web_data_internal(key)
@server_api.get('/ping') # Get server and optionally session status. @server_api.get("/ping") # Get server and optionally session status.
def ping(session_id:str=None): def ping(session_id: str = None):
return ping_internal(session_id) return ping_internal(session_id)
@server_api.post('/render') @server_api.post("/render")
def render(req: dict): def render(req: dict):
return render_internal(req) return render_internal(req)
@server_api.post('/model/merge') @server_api.post("/model/merge")
def model_merge(req: dict): def model_merge(req: dict):
print(req) print(req)
return model_merge_internal(req) return model_merge_internal(req)
@server_api.get('/image/stream/{task_id:int}') @server_api.get("/image/stream/{task_id:int}")
def stream(task_id:int): def stream(task_id: int):
return stream_internal(task_id) return stream_internal(task_id)
@server_api.get('/image/stop') @server_api.get("/image/stop")
def stop(task: int): def stop(task: int):
return stop_internal(task) return stop_internal(task)
@server_api.get('/image/tmp/{task_id:int}/{img_id:int}') @server_api.get("/image/tmp/{task_id:int}/{img_id:int}")
def get_image(task_id: int, img_id: int): def get_image(task_id: int, img_id: int):
return get_image_internal(task_id, img_id) return get_image_internal(task_id, img_id)
@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)
@server_api.on_event("shutdown") @server_api.on_event("shutdown")
def shutdown_event(): # Signal render thread to close on shutdown def shutdown_event(): # Signal render thread to close on shutdown
task_manager.current_state_error = SystemExit('Application shutting down.') task_manager.current_state_error = SystemExit("Application shutting down.")
# API implementations # API implementations
def set_app_config_internal(req : SetAppConfigRequest): def set_app_config_internal(req: SetAppConfigRequest):
config = app.getConfig() config = app.getConfig()
if req.update_branch is not None: if req.update_branch is not None:
config['update_branch'] = req.update_branch config["update_branch"] = req.update_branch
if req.render_devices is not None: if req.render_devices is not None:
update_render_devices_in_config(config, req.render_devices) update_render_devices_in_config(config, req.render_devices)
if req.ui_open_browser_on_start is not None: if req.ui_open_browser_on_start is not None:
if 'ui' not in config: if "ui" not in config:
config['ui'] = {} config["ui"] = {}
config['ui']['open_browser_on_start'] = req.ui_open_browser_on_start config["ui"]["open_browser_on_start"] = req.ui_open_browser_on_start
if req.listen_to_network is not None: if req.listen_to_network is not None:
if 'net' not in config: if "net" not in config:
config['net'] = {} config["net"] = {}
config['net']['listen_to_network'] = bool(req.listen_to_network) config["net"]["listen_to_network"] = bool(req.listen_to_network)
if req.listen_port is not None: if req.listen_port is not None:
if 'net' not in config: if "net" not in config:
config['net'] = {} config["net"] = {}
config['net']['listen_port'] = int(req.listen_port) config["net"]["listen_port"] = int(req.listen_port)
try: try:
app.setConfig(config) app.setConfig(config)
if req.render_devices: if req.render_devices:
app.update_render_threads() app.update_render_threads()
return JSONResponse({'status': 'OK'}, headers=NOCACHE_HEADERS) return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def update_render_devices_in_config(config, render_devices): def update_render_devices_in_config(config, render_devices):
if render_devices not in ('cpu', 'auto') and not render_devices.startswith('cuda:'): if render_devices not in ("cpu", "auto") and not render_devices.startswith("cuda:"):
raise HTTPException(status_code=400, detail=f'Invalid render device requested: {render_devices}') raise HTTPException(status_code=400, detail=f"Invalid render device requested: {render_devices}")
if render_devices.startswith('cuda:'): if render_devices.startswith("cuda:"):
render_devices = render_devices.split(',') render_devices = render_devices.split(",")
config['render_devices'] = render_devices config["render_devices"] = render_devices
def read_web_data_internal(key:str=None):
if not key: # /get without parameters, stable-diffusion easter egg. def read_web_data_internal(key: str = None):
raise HTTPException(status_code=418, detail="StableDiffusion is drawing a teapot!") # HTTP418 I'm a teapot if not key: # /get without parameters, stable-diffusion easter egg.
elif key == 'app_config': raise HTTPException(status_code=418, detail="StableDiffusion is drawing a teapot!") # HTTP418 I'm a teapot
elif key == "app_config":
return JSONResponse(app.getConfig(), headers=NOCACHE_HEADERS) return JSONResponse(app.getConfig(), headers=NOCACHE_HEADERS)
elif key == 'system_info': elif key == "system_info":
config = app.getConfig() config = app.getConfig()
system_info = {
'devices': task_manager.get_devices(),
'hosts': app.getIPConfig(),
'default_output_dir': os.path.join(os.path.expanduser("~"), app.OUTPUT_DIRNAME),
}
system_info['devices']['config'] = config.get('render_devices', "auto")
return JSONResponse(system_info, headers=NOCACHE_HEADERS)
elif key == 'models':
return JSONResponse(model_manager.getModels(), headers=NOCACHE_HEADERS)
elif key == 'modifiers': return FileResponse(os.path.join(app.SD_UI_DIR, 'modifiers.json'), headers=NOCACHE_HEADERS)
elif key == 'ui_plugins': return JSONResponse(app.getUIPlugins(), headers=NOCACHE_HEADERS)
else:
raise HTTPException(status_code=404, detail=f'Request for unknown {key}') # HTTP404 Not Found
def ping_internal(session_id:str=None): output_dir = config.get("force_save_path", os.path.join(os.path.expanduser("~"), app.OUTPUT_DIRNAME))
if task_manager.is_alive() <= 0: # Check that render threads are alive.
if task_manager.current_state_error: raise HTTPException(status_code=500, detail=str(task_manager.current_state_error)) system_info = {
raise HTTPException(status_code=500, detail='Render thread is dead.') "devices": task_manager.get_devices(),
if task_manager.current_state_error and not isinstance(task_manager.current_state_error, StopAsyncIteration): raise HTTPException(status_code=500, detail=str(task_manager.current_state_error)) "hosts": app.getIPConfig(),
"default_output_dir": output_dir,
"enforce_output_dir": ("force_save_path" in config),
}
system_info["devices"]["config"] = config.get("render_devices", "auto")
return JSONResponse(system_info, headers=NOCACHE_HEADERS)
elif key == "models":
return JSONResponse(model_manager.getModels(), headers=NOCACHE_HEADERS)
elif key == "modifiers":
return FileResponse(os.path.join(app.SD_UI_DIR, "modifiers.json"), headers=NOCACHE_HEADERS)
elif key == "ui_plugins":
return JSONResponse(app.getUIPlugins(), headers=NOCACHE_HEADERS)
else:
raise HTTPException(status_code=404, detail=f"Request for unknown {key}") # HTTP404 Not Found
def ping_internal(session_id: str = None):
if task_manager.is_alive() <= 0: # Check that render threads are alive.
if task_manager.current_state_error:
raise HTTPException(status_code=500, detail=str(task_manager.current_state_error))
raise HTTPException(status_code=500, detail="Render thread is dead.")
if task_manager.current_state_error and not isinstance(task_manager.current_state_error, StopAsyncIteration):
raise HTTPException(status_code=500, detail=str(task_manager.current_state_error))
# Alive # Alive
response = {'status': str(task_manager.current_state)} response = {"status": str(task_manager.current_state)}
if session_id: if session_id:
session = task_manager.get_cached_session(session_id, update_ttl=True) session = task_manager.get_cached_session(session_id, update_ttl=True)
response['tasks'] = {id(t): t.status for t in session.tasks} response["tasks"] = {id(t): t.status for t in session.tasks}
response['devices'] = task_manager.get_devices() response["devices"] = task_manager.get_devices()
return JSONResponse(response, headers=NOCACHE_HEADERS) return JSONResponse(response, headers=NOCACHE_HEADERS)
def render_internal(req: dict): def render_internal(req: dict):
try: try:
# separate out the request data into rendering and task-specific data # separate out the request data into rendering and task-specific data
render_req: GenerateImageRequest = GenerateImageRequest.parse_obj(req) render_req: GenerateImageRequest = GenerateImageRequest.parse_obj(req)
task_data: TaskData = TaskData.parse_obj(req) task_data: TaskData = TaskData.parse_obj(req)
render_req.init_image_mask = req.get('mask') # hack: will rename this in the HTTP API in a future revision # Overwrite user specified save path
config = app.getConfig()
if "force_save_path" in config:
task_data.save_to_disk_path = config["force_save_path"]
app.save_to_config(task_data.use_stable_diffusion_model, task_data.use_vae_model, task_data.use_hypernetwork_model, task_data.vram_usage_level) render_req.init_image_mask = req.get("mask") # hack: will rename this in the HTTP API in a future revision
app.save_to_config(
task_data.use_stable_diffusion_model,
task_data.use_vae_model,
task_data.use_hypernetwork_model,
task_data.vram_usage_level,
)
# enqueue the task # enqueue the task
new_task = task_manager.render(render_req, task_data) new_task = task_manager.render(render_req, task_data)
response = { response = {
'status': str(task_manager.current_state), "status": str(task_manager.current_state),
'queue': len(task_manager.tasks_queue), "queue": len(task_manager.tasks_queue),
'stream': f'/image/stream/{id(new_task)}', "stream": f"/image/stream/{id(new_task)}",
'task': id(new_task) "task": id(new_task),
} }
return JSONResponse(response, headers=NOCACHE_HEADERS) return JSONResponse(response, headers=NOCACHE_HEADERS)
except ChildProcessError as e: # Render thread is dead except ChildProcessError as e: # Render thread is dead
raise HTTPException(status_code=500, detail=f'Rendering thread has died.') # HTTP500 Internal Server Error raise HTTPException(status_code=500, detail=f"Rendering thread has died.") # HTTP500 Internal Server Error
except ConnectionRefusedError as e: # Unstarted task pending limit reached, deny queueing too many. except ConnectionRefusedError as e: # Unstarted task pending limit reached, deny queueing too many.
raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def model_merge_internal(req: dict): def model_merge_internal(req: dict):
try: try:
from sdkit.train import merge_models from sdkit.train import merge_models
from easydiffusion.utils.save_utils import filename_regex from easydiffusion.utils.save_utils import filename_regex
mergeReq: MergeRequest = MergeRequest.parse_obj(req) mergeReq: MergeRequest = MergeRequest.parse_obj(req)
merge_models(model_manager.resolve_model_to_use(mergeReq.model0,'stable-diffusion'), merge_models(
model_manager.resolve_model_to_use(mergeReq.model1,'stable-diffusion'), model_manager.resolve_model_to_use(mergeReq.model0, "stable-diffusion"),
mergeReq.ratio, model_manager.resolve_model_to_use(mergeReq.model1, "stable-diffusion"),
os.path.join(app.MODELS_DIR, 'stable-diffusion', filename_regex.sub('_', mergeReq.out_path)), mergeReq.ratio,
mergeReq.use_fp16 os.path.join(app.MODELS_DIR, "stable-diffusion", filename_regex.sub("_", mergeReq.out_path)),
mergeReq.use_fp16,
) )
return JSONResponse({'status':'OK'}, headers=NOCACHE_HEADERS) return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
def stream_internal(task_id:int):
#TODO Move to WebSockets ?? def stream_internal(task_id: int):
# TODO Move to WebSockets ??
task = task_manager.get_cached_task(task_id, update_ttl=True) task = task_manager.get_cached_task(task_id, update_ttl=True)
if not task: raise HTTPException(status_code=404, detail=f'Request {task_id} not found.') # HTTP404 NotFound if not task:
#if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict raise HTTPException(status_code=404, detail=f"Request {task_id} not found.") # HTTP404 NotFound
# if (id(task) != task_id): raise HTTPException(status_code=409, detail=f'Wrong task id received. Expected:{id(task)}, Received:{task_id}') # HTTP409 Conflict
if task.buffer_queue.empty() and not task.lock.locked(): if task.buffer_queue.empty() and not task.lock.locked():
if task.response: if task.response:
#log.info(f'Session {session_id} sending cached response') # log.info(f'Session {session_id} sending cached response')
return JSONResponse(task.response, headers=NOCACHE_HEADERS) return JSONResponse(task.response, headers=NOCACHE_HEADERS)
raise HTTPException(status_code=425, detail='Too Early, task not started yet.') # HTTP425 Too Early raise HTTPException(status_code=425, detail="Too Early, task not started yet.") # HTTP425 Too Early
#log.info(f'Session {session_id} opened live render stream {id(task.buffer_queue)}') # log.info(f'Session {session_id} opened live render stream {id(task.buffer_queue)}')
return StreamingResponse(task.read_buffer_generator(), media_type='application/json') return StreamingResponse(task.read_buffer_generator(), media_type="application/json")
def stop_internal(task: int): def stop_internal(task: int):
if not task: if not task:
if task_manager.current_state == task_manager.ServerStates.Online or task_manager.current_state == task_manager.ServerStates.Unavailable: if (
raise HTTPException(status_code=409, detail='Not currently running any tasks.') # HTTP409 Conflict task_manager.current_state == task_manager.ServerStates.Online
task_manager.current_state_error = StopAsyncIteration('') or task_manager.current_state == task_manager.ServerStates.Unavailable
return {'OK'} ):
raise HTTPException(status_code=409, detail="Not currently running any tasks.") # HTTP409 Conflict
task_manager.current_state_error = StopAsyncIteration("")
return {"OK"}
task_id = task task_id = task
task = task_manager.get_cached_task(task_id, update_ttl=False) task = task_manager.get_cached_task(task_id, update_ttl=False)
if not task: raise HTTPException(status_code=404, detail=f'Task {task_id} was not found.') # HTTP404 Not Found if not task:
if isinstance(task.error, StopAsyncIteration): raise HTTPException(status_code=409, detail=f'Task {task_id} is already stopped.') # HTTP409 Conflict raise HTTPException(status_code=404, detail=f"Task {task_id} was not found.") # HTTP404 Not Found
task.error = StopAsyncIteration(f'Task {task_id} stop requested.') if isinstance(task.error, StopAsyncIteration):
return {'OK'} raise HTTPException(status_code=409, detail=f"Task {task_id} is already stopped.") # HTTP409 Conflict
task.error = StopAsyncIteration(f"Task {task_id} stop requested.")
return {"OK"}
def get_image_internal(task_id: int, img_id: int): def get_image_internal(task_id: int, img_id: int):
task = task_manager.get_cached_task(task_id, update_ttl=True) task = task_manager.get_cached_task(task_id, update_ttl=True)
if not task: raise HTTPException(status_code=410, detail=f'Task {task_id} could not be found.') # HTTP404 NotFound if not task:
if not task.temp_images[img_id]: raise HTTPException(status_code=425, detail='Too Early, task data is not available yet.') # HTTP425 Too Early raise HTTPException(status_code=410, detail=f"Task {task_id} could not be found.") # HTTP404 NotFound
if not task.temp_images[img_id]:
raise HTTPException(status_code=425, detail="Too Early, task data is not available yet.") # HTTP425 Too Early
try: try:
img_data = task.temp_images[img_id] img_data = task.temp_images[img_id]
img_data.seek(0) img_data.seek(0)
return StreamingResponse(img_data, media_type='image/jpeg') return StreamingResponse(img_data, media_type="image/jpeg")
except KeyError as e: except KeyError as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@ -7,7 +7,7 @@ Notes:
import json import json
import traceback import traceback
TASK_TTL = 15 * 60 # seconds, Discard last session's task timeout TASK_TTL = 15 * 60 # seconds, Discard last session's task timeout
import torch import torch
import queue, threading, time, weakref import queue, threading, time, weakref
@ -19,71 +19,98 @@ from easydiffusion.utils import log
from sdkit.utils import gc from sdkit.utils import gc
THREAD_NAME_PREFIX = '' THREAD_NAME_PREFIX = ""
ERR_LOCK_FAILED = ' failed to acquire lock within timeout.' ERR_LOCK_FAILED = " failed to acquire lock within timeout."
LOCK_TIMEOUT = 15 # Maximum locking time in seconds before failing a task. LOCK_TIMEOUT = 15 # Maximum locking time in seconds before failing a task.
# It's better to get an exception than a deadlock... ALWAYS use timeout in critical paths. # It's better to get an exception than a deadlock... ALWAYS use timeout in critical paths.
DEVICE_START_TIMEOUT = 60 # seconds - Maximum time to wait for a render device to init. DEVICE_START_TIMEOUT = 60 # seconds - Maximum time to wait for a render device to init.
class SymbolClass(type): # Print nicely formatted Symbol names.
def __repr__(self):
return self.__qualname__
def __str__(self):
return self.__name__
class Symbol(metaclass=SymbolClass):
pass
class SymbolClass(type): # Print nicely formatted Symbol names.
def __repr__(self): return self.__qualname__
def __str__(self): return self.__name__
class Symbol(metaclass=SymbolClass): pass
class ServerStates: class ServerStates:
class Init(Symbol): pass class Init(Symbol):
class LoadingModel(Symbol): pass pass
class Online(Symbol): pass
class Rendering(Symbol): pass
class Unavailable(Symbol): pass
class RenderTask(): # Task with output queue and completion lock. class LoadingModel(Symbol):
pass
class Online(Symbol):
pass
class Rendering(Symbol):
pass
class Unavailable(Symbol):
pass
class RenderTask: # Task with output queue and completion lock.
def __init__(self, req: GenerateImageRequest, task_data: TaskData): def __init__(self, req: GenerateImageRequest, task_data: TaskData):
task_data.request_id = id(self) task_data.request_id = id(self)
self.render_request: GenerateImageRequest = req # Initial Request self.render_request: GenerateImageRequest = req # Initial Request
self.task_data: TaskData = task_data self.task_data: TaskData = task_data
self.response: Any = None # Copy of the last reponse self.response: Any = None # Copy of the last reponse
self.render_device = None # Select the task affinity. (Not used to change active devices). self.render_device = None # Select the task affinity. (Not used to change active devices).
self.temp_images:list = [None] * req.num_outputs * (1 if task_data.show_only_filtered_image else 2) self.temp_images: list = [None] * req.num_outputs * (1 if task_data.show_only_filtered_image else 2)
self.error: Exception = None self.error: Exception = None
self.lock: threading.Lock = threading.Lock() # Locks at task start and unlocks when task is completed self.lock: threading.Lock = threading.Lock() # Locks at task start and unlocks when task is completed
self.buffer_queue: queue.Queue = queue.Queue() # Queue of JSON string segments self.buffer_queue: queue.Queue = queue.Queue() # Queue of JSON string segments
async def read_buffer_generator(self): async def read_buffer_generator(self):
try: try:
while not self.buffer_queue.empty(): while not self.buffer_queue.empty():
res = self.buffer_queue.get(block=False) res = self.buffer_queue.get(block=False)
self.buffer_queue.task_done() self.buffer_queue.task_done()
yield res yield res
except queue.Empty as e: yield except queue.Empty as e:
yield
@property @property
def status(self): def status(self):
if self.lock.locked(): if self.lock.locked():
return 'running' return "running"
if isinstance(self.error, StopAsyncIteration): if isinstance(self.error, StopAsyncIteration):
return 'stopped' return "stopped"
if self.error: if self.error:
return 'error' return "error"
if not self.buffer_queue.empty(): if not self.buffer_queue.empty():
return 'buffer' return "buffer"
if self.response: if self.response:
return 'completed' return "completed"
return 'pending' return "pending"
@property @property
def is_pending(self): def is_pending(self):
return bool(not self.response and not self.error) return bool(not self.response and not self.error)
# Temporary cache to allow to query tasks results for a short time after they are completed. # Temporary cache to allow to query tasks results for a short time after they are completed.
class DataCache(): class DataCache:
def __init__(self): def __init__(self):
self._base = dict() self._base = dict()
self._lock: threading.Lock = threading.Lock() self._lock: threading.Lock = threading.Lock()
def _get_ttl_time(self, ttl: int) -> int: def _get_ttl_time(self, ttl: int) -> int:
return int(time.time()) + ttl return int(time.time()) + ttl
def _is_expired(self, timestamp: int) -> bool: def _is_expired(self, timestamp: int) -> bool:
return int(time.time()) >= timestamp return int(time.time()) >= timestamp
def clean(self) -> None: def clean(self) -> None:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.clean' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.clean" + ERR_LOCK_FAILED)
try: try:
# Create a list of expired keys to delete # Create a list of expired keys to delete
to_delete = [] to_delete = []
@ -95,20 +122,26 @@ class DataCache():
for key in to_delete: for key in to_delete:
(_, val) = self._base[key] (_, val) = self._base[key]
if isinstance(val, RenderTask): if isinstance(val, RenderTask):
log.debug(f'RenderTask {key} expired. Data removed.') log.debug(f"RenderTask {key} expired. Data removed.")
elif isinstance(val, SessionState): elif isinstance(val, SessionState):
log.debug(f'Session {key} expired. Data removed.') log.debug(f"Session {key} expired. Data removed.")
else: else:
log.debug(f'Key {key} expired. Data removed.') log.debug(f"Key {key} expired. Data removed.")
del self._base[key] del self._base[key]
finally: finally:
self._lock.release() self._lock.release()
def clear(self) -> None: def clear(self) -> None:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.clear' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
try: self._base.clear() raise Exception("DataCache.clear" + ERR_LOCK_FAILED)
finally: self._lock.release() try:
self._base.clear()
finally:
self._lock.release()
def delete(self, key: Hashable) -> bool: def delete(self, key: Hashable) -> bool:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.delete' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.delete" + ERR_LOCK_FAILED)
try: try:
if key not in self._base: if key not in self._base:
return False return False
@ -116,8 +149,10 @@ class DataCache():
return True return True
finally: finally:
self._lock.release() self._lock.release()
def keep(self, key: Hashable, ttl: int) -> bool: def keep(self, key: Hashable, ttl: int) -> bool:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.keep' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.keep" + ERR_LOCK_FAILED)
try: try:
if key in self._base: if key in self._base:
_, value = self._base.get(key) _, value = self._base.get(key)
@ -126,12 +161,12 @@ class DataCache():
return False return False
finally: finally:
self._lock.release() self._lock.release()
def put(self, key: Hashable, value: Any, ttl: int) -> bool: def put(self, key: Hashable, value: Any, ttl: int) -> bool:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.put' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.put" + ERR_LOCK_FAILED)
try: try:
self._base[key] = ( self._base[key] = (self._get_ttl_time(ttl), value)
self._get_ttl_time(ttl), value
)
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return False return False
@ -139,35 +174,41 @@ class DataCache():
return True return True
finally: finally:
self._lock.release() self._lock.release()
def tryGet(self, key: Hashable) -> Any: def tryGet(self, key: Hashable) -> Any:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('DataCache.tryGet' + ERR_LOCK_FAILED) if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.tryGet" + ERR_LOCK_FAILED)
try: try:
ttl, value = self._base.get(key, (None, None)) ttl, value = self._base.get(key, (None, None))
if ttl is not None and self._is_expired(ttl): if ttl is not None and self._is_expired(ttl):
log.debug(f'Session {key} expired. Discarding data.') log.debug(f"Session {key} expired. Discarding data.")
del self._base[key] del self._base[key]
return None return None
return value return value
finally: finally:
self._lock.release() self._lock.release()
manager_lock = threading.RLock() manager_lock = threading.RLock()
render_threads = [] render_threads = []
current_state = ServerStates.Init current_state = ServerStates.Init
current_state_error:Exception = None current_state_error: Exception = None
tasks_queue = [] tasks_queue = []
session_cache = DataCache() session_cache = DataCache()
task_cache = DataCache() task_cache = DataCache()
weak_thread_data = weakref.WeakKeyDictionary() weak_thread_data = weakref.WeakKeyDictionary()
idle_event: threading.Event = threading.Event() idle_event: threading.Event = threading.Event()
class SessionState():
class SessionState:
def __init__(self, id: str): def __init__(self, id: str):
self._id = id self._id = id
self._tasks_ids = [] self._tasks_ids = []
@property @property
def id(self): def id(self):
return self._id return self._id
@property @property
def tasks(self): def tasks(self):
tasks = [] tasks = []
@ -176,6 +217,7 @@ class SessionState():
if task: if task:
tasks.append(task) tasks.append(task)
return tasks return tasks
def put(self, task, ttl=TASK_TTL): def put(self, task, ttl=TASK_TTL):
task_id = id(task) task_id = id(task)
self._tasks_ids.append(task_id) self._tasks_ids.append(task_id)
@ -185,10 +227,12 @@ class SessionState():
self._tasks_ids.pop(0) self._tasks_ids.pop(0)
return True return True
def thread_get_next_task(): def thread_get_next_task():
from easydiffusion import renderer from easydiffusion import renderer
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
log.warn(f'Render thread on device: {renderer.context.device} failed to acquire manager lock.') log.warn(f"Render thread on device: {renderer.context.device} failed to acquire manager lock.")
return None return None
if len(tasks_queue) <= 0: if len(tasks_queue) <= 0:
manager_lock.release() manager_lock.release()
@ -202,10 +246,10 @@ def thread_get_next_task():
continue # requested device alive, skip current one. continue # requested device alive, skip current one.
else: else:
# Requested device is not active, return error to UI. # Requested device is not active, return error to UI.
queued_task.error = Exception(queued_task.render_device + ' is not currently active.') queued_task.error = Exception(queued_task.render_device + " is not currently active.")
task = queued_task task = queued_task
break break
if not queued_task.render_device and renderer.context.device == 'cpu' and is_alive() > 1: if not queued_task.render_device and renderer.context.device == "cpu" and is_alive() > 1:
# not asking for any specific devices, cpu want to grab task but other render devices are alive. # not asking for any specific devices, cpu want to grab task but other render devices are alive.
continue # Skip Tasks, don't run on CPU unless there is nothing else or user asked for it. continue # Skip Tasks, don't run on CPU unless there is nothing else or user asked for it.
task = queued_task task = queued_task
@ -216,17 +260,19 @@ def thread_get_next_task():
finally: finally:
manager_lock.release() manager_lock.release()
def thread_render(device): def thread_render(device):
global current_state, current_state_error global current_state, current_state_error
from easydiffusion import renderer, model_manager from easydiffusion import renderer, model_manager
try: try:
renderer.init(device) renderer.init(device)
weak_thread_data[threading.current_thread()] = { weak_thread_data[threading.current_thread()] = {
'device': renderer.context.device, "device": renderer.context.device,
'device_name': renderer.context.device_name, "device_name": renderer.context.device_name,
'alive': True "alive": True,
} }
current_state = ServerStates.LoadingModel current_state = ServerStates.LoadingModel
@ -235,17 +281,14 @@ def thread_render(device):
current_state = ServerStates.Online current_state = ServerStates.Online
except Exception as e: except Exception as e:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
weak_thread_data[threading.current_thread()] = { weak_thread_data[threading.current_thread()] = {"error": e, "alive": False}
'error': e,
'alive': False
}
return return
while True: while True:
session_cache.clean() session_cache.clean()
task_cache.clean() task_cache.clean()
if not weak_thread_data[threading.current_thread()]['alive']: if not weak_thread_data[threading.current_thread()]["alive"]:
log.info(f'Shutting down thread for device {renderer.context.device}') log.info(f"Shutting down thread for device {renderer.context.device}")
model_manager.unload_all(renderer.context) model_manager.unload_all(renderer.context)
return return
if isinstance(current_state_error, SystemExit): if isinstance(current_state_error, SystemExit):
@ -258,39 +301,47 @@ def thread_render(device):
continue continue
if task.error is not None: if task.error is not None:
log.error(task.error) log.error(task.error)
task.response = {"status": 'failed', "detail": str(task.error)} task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response)) task.buffer_queue.put(json.dumps(task.response))
continue continue
if current_state_error: if current_state_error:
task.error = current_state_error task.error = current_state_error
task.response = {"status": 'failed', "detail": str(task.error)} task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response)) task.buffer_queue.put(json.dumps(task.response))
continue continue
log.info(f'Session {task.task_data.session_id} starting task {id(task)} on {renderer.context.device_name}') log.info(f"Session {task.task_data.session_id} starting task {id(task)} on {renderer.context.device_name}")
if not task.lock.acquire(blocking=False): raise Exception('Got locked task from queue.') if not task.lock.acquire(blocking=False):
raise Exception("Got locked task from queue.")
try: try:
def step_callback(): def step_callback():
global current_state_error global current_state_error
if isinstance(current_state_error, SystemExit) or isinstance(current_state_error, StopAsyncIteration) or isinstance(task.error, StopAsyncIteration): if (
isinstance(current_state_error, SystemExit)
or isinstance(current_state_error, StopAsyncIteration)
or isinstance(task.error, StopAsyncIteration)
):
renderer.context.stop_processing = True renderer.context.stop_processing = True
if isinstance(current_state_error, StopAsyncIteration): if isinstance(current_state_error, StopAsyncIteration):
task.error = current_state_error task.error = current_state_error
current_state_error = None current_state_error = None
log.info(f'Session {task.task_data.session_id} sent cancel signal for task {id(task)}') log.info(f"Session {task.task_data.session_id} sent cancel signal for task {id(task)}")
current_state = ServerStates.LoadingModel current_state = ServerStates.LoadingModel
model_manager.resolve_model_paths(task.task_data) model_manager.resolve_model_paths(task.task_data)
model_manager.reload_models_if_necessary(renderer.context, task.task_data) model_manager.reload_models_if_necessary(renderer.context, task.task_data)
current_state = ServerStates.Rendering current_state = ServerStates.Rendering
task.response = renderer.make_images(task.render_request, task.task_data, task.buffer_queue, task.temp_images, step_callback) task.response = renderer.make_images(
task.render_request, task.task_data, task.buffer_queue, task.temp_images, step_callback
)
# Before looping back to the generator, mark cache as still alive. # Before looping back to the generator, mark cache as still alive.
task_cache.keep(id(task), TASK_TTL) task_cache.keep(id(task), TASK_TTL)
session_cache.keep(task.task_data.session_id, TASK_TTL) session_cache.keep(task.task_data.session_id, TASK_TTL)
except Exception as e: except Exception as e:
task.error = str(e) task.error = str(e)
task.response = {"status": 'failed', "detail": str(task.error)} task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response)) task.buffer_queue.put(json.dumps(task.response))
log.error(traceback.format_exc()) log.error(traceback.format_exc())
finally: finally:
@ -299,21 +350,25 @@ def thread_render(device):
task_cache.keep(id(task), TASK_TTL) task_cache.keep(id(task), TASK_TTL)
session_cache.keep(task.task_data.session_id, TASK_TTL) session_cache.keep(task.task_data.session_id, TASK_TTL)
if isinstance(task.error, StopAsyncIteration): if isinstance(task.error, StopAsyncIteration):
log.info(f'Session {task.task_data.session_id} task {id(task)} cancelled!') log.info(f"Session {task.task_data.session_id} task {id(task)} cancelled!")
elif task.error is not None: elif task.error is not None:
log.info(f'Session {task.task_data.session_id} task {id(task)} failed!') log.info(f"Session {task.task_data.session_id} task {id(task)} failed!")
else: else:
log.info(f'Session {task.task_data.session_id} task {id(task)} completed by {renderer.context.device_name}.') log.info(
f"Session {task.task_data.session_id} task {id(task)} completed by {renderer.context.device_name}."
)
current_state = ServerStates.Online current_state = ServerStates.Online
def get_cached_task(task_id:str, update_ttl:bool=False):
def get_cached_task(task_id: str, update_ttl: bool = False):
# By calling keep before tryGet, wont discard if was expired. # By calling keep before tryGet, wont discard if was expired.
if update_ttl and not task_cache.keep(task_id, TASK_TTL): if update_ttl and not task_cache.keep(task_id, TASK_TTL):
# Failed to keep task, already gone. # Failed to keep task, already gone.
return None return None
return task_cache.tryGet(task_id) return task_cache.tryGet(task_id)
def get_cached_session(session_id:str, update_ttl:bool=False):
def get_cached_session(session_id: str, update_ttl: bool = False):
if update_ttl: if update_ttl:
session_cache.keep(session_id, TASK_TTL) session_cache.keep(session_id, TASK_TTL)
session = session_cache.tryGet(session_id) session = session_cache.tryGet(session_id)
@ -322,64 +377,68 @@ def get_cached_session(session_id:str, update_ttl:bool=False):
session_cache.put(session_id, session, TASK_TTL) session_cache.put(session_id, session, TASK_TTL)
return session return session
def get_devices(): def get_devices():
devices = { devices = {
'all': {}, "all": {},
'active': {}, "active": {},
} }
def get_device_info(device): def get_device_info(device):
if device == 'cpu': if device == "cpu":
return {'name': device_manager.get_processor_name()} return {"name": device_manager.get_processor_name()}
mem_free, mem_total = torch.cuda.mem_get_info(device) mem_free, mem_total = torch.cuda.mem_get_info(device)
mem_free /= float(10**9) mem_free /= float(10**9)
mem_total /= float(10**9) mem_total /= float(10**9)
return { return {
'name': torch.cuda.get_device_name(device), "name": torch.cuda.get_device_name(device),
'mem_free': mem_free, "mem_free": mem_free,
'mem_total': mem_total, "mem_total": mem_total,
'max_vram_usage_level': device_manager.get_max_vram_usage_level(device), "max_vram_usage_level": device_manager.get_max_vram_usage_level(device),
} }
# list the compatible devices # list the compatible devices
gpu_count = torch.cuda.device_count() gpu_count = torch.cuda.device_count()
for device in range(gpu_count): for device in range(gpu_count):
device = f'cuda:{device}' device = f"cuda:{device}"
if not device_manager.is_device_compatible(device): if not device_manager.is_device_compatible(device):
continue continue
devices['all'].update({device: get_device_info(device)}) devices["all"].update({device: get_device_info(device)})
devices['all'].update({'cpu': get_device_info('cpu')}) devices["all"].update({"cpu": get_device_info("cpu")})
# list the activated devices # list the activated devices
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('get_devices' + ERR_LOCK_FAILED) if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("get_devices" + ERR_LOCK_FAILED)
try: try:
for rthread in render_threads: for rthread in render_threads:
if not rthread.is_alive(): if not rthread.is_alive():
continue continue
weak_data = weak_thread_data.get(rthread) weak_data = weak_thread_data.get(rthread)
if not weak_data or not 'device' in weak_data or not 'device_name' in weak_data: if not weak_data or not "device" in weak_data or not "device_name" in weak_data:
continue continue
device = weak_data['device'] device = weak_data["device"]
devices['active'].update({device: get_device_info(device)}) devices["active"].update({device: get_device_info(device)})
finally: finally:
manager_lock.release() manager_lock.release()
return devices return devices
def is_alive(device=None): def is_alive(device=None):
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('is_alive' + ERR_LOCK_FAILED) if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("is_alive" + ERR_LOCK_FAILED)
nbr_alive = 0 nbr_alive = 0
try: try:
for rthread in render_threads: for rthread in render_threads:
if device is not None: if device is not None:
weak_data = weak_thread_data.get(rthread) weak_data = weak_thread_data.get(rthread)
if weak_data is None or not 'device' in weak_data or weak_data['device'] is None: if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
continue continue
thread_device = weak_data['device'] thread_device = weak_data["device"]
if thread_device != device: if thread_device != device:
continue continue
if rthread.is_alive(): if rthread.is_alive():
@ -388,11 +447,13 @@ def is_alive(device=None):
finally: finally:
manager_lock.release() manager_lock.release()
def start_render_thread(device): def start_render_thread(device):
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('start_render_thread' + ERR_LOCK_FAILED) if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
log.info(f'Start new Rendering Thread on device: {device}') raise Exception("start_render_thread" + ERR_LOCK_FAILED)
log.info(f"Start new Rendering Thread on device: {device}")
try: try:
rthread = threading.Thread(target=thread_render, kwargs={'device': device}) rthread = threading.Thread(target=thread_render, kwargs={"device": device})
rthread.daemon = True rthread.daemon = True
rthread.name = THREAD_NAME_PREFIX + device rthread.name = THREAD_NAME_PREFIX + device
rthread.start() rthread.start()
@ -400,8 +461,8 @@ def start_render_thread(device):
finally: finally:
manager_lock.release() manager_lock.release()
timeout = DEVICE_START_TIMEOUT timeout = DEVICE_START_TIMEOUT
while not rthread.is_alive() or not rthread in weak_thread_data or not 'device' in weak_thread_data[rthread]: while not rthread.is_alive() or not rthread in weak_thread_data or not "device" in weak_thread_data[rthread]:
if rthread in weak_thread_data and 'error' in weak_thread_data[rthread]: if rthread in weak_thread_data and "error" in weak_thread_data[rthread]:
log.error(f"{rthread}, {device}, error: {weak_thread_data[rthread]['error']}") log.error(f"{rthread}, {device}, error: {weak_thread_data[rthread]['error']}")
return False return False
if timeout <= 0: if timeout <= 0:
@ -410,25 +471,27 @@ def start_render_thread(device):
time.sleep(1) time.sleep(1)
return True return True
def stop_render_thread(device): def stop_render_thread(device):
try: try:
device_manager.validate_device_id(device, log_prefix='stop_render_thread') device_manager.validate_device_id(device, log_prefix="stop_render_thread")
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return False return False
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT): raise Exception('stop_render_thread' + ERR_LOCK_FAILED) if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
log.info(f'Stopping Rendering Thread on device: {device}') raise Exception("stop_render_thread" + ERR_LOCK_FAILED)
log.info(f"Stopping Rendering Thread on device: {device}")
try: try:
thread_to_remove = None thread_to_remove = None
for rthread in render_threads: for rthread in render_threads:
weak_data = weak_thread_data.get(rthread) weak_data = weak_thread_data.get(rthread)
if weak_data is None or not 'device' in weak_data or weak_data['device'] is None: if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
continue continue
thread_device = weak_data['device'] thread_device = weak_data["device"]
if thread_device == device: if thread_device == device:
weak_data['alive'] = False weak_data["alive"] = False
thread_to_remove = rthread thread_to_remove = rthread
break break
if thread_to_remove is not None: if thread_to_remove is not None:
@ -439,44 +502,51 @@ def stop_render_thread(device):
return False return False
def update_render_threads(render_devices, active_devices): def update_render_threads(render_devices, active_devices):
devices_to_start, devices_to_stop = device_manager.get_device_delta(render_devices, active_devices) devices_to_start, devices_to_stop = device_manager.get_device_delta(render_devices, active_devices)
log.debug(f'devices_to_start: {devices_to_start}') log.debug(f"devices_to_start: {devices_to_start}")
log.debug(f'devices_to_stop: {devices_to_stop}') log.debug(f"devices_to_stop: {devices_to_stop}")
for device in devices_to_stop: for device in devices_to_stop:
if is_alive(device) <= 0: if is_alive(device) <= 0:
log.debug(f'{device} is not alive') log.debug(f"{device} is not alive")
continue continue
if not stop_render_thread(device): if not stop_render_thread(device):
log.warn(f'{device} could not stop render thread') log.warn(f"{device} could not stop render thread")
for device in devices_to_start: for device in devices_to_start:
if is_alive(device) >= 1: if is_alive(device) >= 1:
log.debug(f'{device} already registered.') log.debug(f"{device} already registered.")
continue continue
if not start_render_thread(device): if not start_render_thread(device):
log.warn(f'{device} failed to start.') log.warn(f"{device} failed to start.")
if is_alive() <= 0: # No running devices, probably invalid user config. if is_alive() <= 0: # No running devices, probably invalid user config.
raise EnvironmentError('ERROR: No active render devices! Please verify the "render_devices" value in config.json') raise EnvironmentError(
'ERROR: No active render devices! Please verify the "render_devices" value in config.json'
)
log.debug(f"active devices: {get_devices()['active']}") log.debug(f"active devices: {get_devices()['active']}")
def shutdown_event(): # Signal render thread to close on shutdown
def shutdown_event(): # Signal render thread to close on shutdown
global current_state_error global current_state_error
current_state_error = SystemExit('Application shutting down.') current_state_error = SystemExit("Application shutting down.")
def render(render_req: GenerateImageRequest, task_data: TaskData): def render(render_req: GenerateImageRequest, task_data: TaskData):
current_thread_count = is_alive() current_thread_count = is_alive()
if current_thread_count <= 0: # Render thread is dead if current_thread_count <= 0: # Render thread is dead
raise ChildProcessError('Rendering thread has died.') raise ChildProcessError("Rendering thread has died.")
# Alive, check if task in cache # Alive, check if task in cache
session = get_cached_session(task_data.session_id, update_ttl=True) session = get_cached_session(task_data.session_id, update_ttl=True)
pending_tasks = list(filter(lambda t: t.is_pending, session.tasks)) pending_tasks = list(filter(lambda t: t.is_pending, session.tasks))
if current_thread_count < len(pending_tasks): if current_thread_count < len(pending_tasks):
raise ConnectionRefusedError(f'Session {task_data.session_id} already has {len(pending_tasks)} pending tasks out of {current_thread_count}.') raise ConnectionRefusedError(
f"Session {task_data.session_id} already has {len(pending_tasks)} pending tasks out of {current_thread_count}."
)
new_task = RenderTask(render_req, task_data) new_task = RenderTask(render_req, task_data)
if session.put(new_task, TASK_TTL): if session.put(new_task, TASK_TTL):
@ -489,4 +559,4 @@ def render(render_req: GenerateImageRequest, task_data: TaskData):
return new_task return new_task
finally: finally:
manager_lock.release() manager_lock.release()
raise RuntimeError('Failed to add task to cache.') raise RuntimeError("Failed to add task to cache.")

View File

@ -1,6 +1,7 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import Any from typing import Any
class GenerateImageRequest(BaseModel): class GenerateImageRequest(BaseModel):
prompt: str = "" prompt: str = ""
negative_prompt: str = "" negative_prompt: str = ""
@ -18,28 +19,32 @@ class GenerateImageRequest(BaseModel):
prompt_strength: float = 0.8 prompt_strength: float = 0.8
preserve_init_image_color_profile = False preserve_init_image_color_profile = False
sampler_name: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms" sampler_name: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
hypernetwork_strength: float = 0 hypernetwork_strength: float = 0
class TaskData(BaseModel): class TaskData(BaseModel):
request_id: str = None request_id: str = None
session_id: str = "session" session_id: str = "session"
save_to_disk_path: str = None save_to_disk_path: str = None
vram_usage_level: str = "balanced" # or "low" or "medium" vram_usage_level: str = "balanced" # or "low" or "medium"
use_face_correction: str = None # or "GFPGANv1.3" use_face_correction: str = None # or "GFPGANv1.3"
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B" use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
upscale_amount: int = 4 # or 2 upscale_amount: int = 4 # or 2
use_stable_diffusion_model: str = "sd-v1-4" use_stable_diffusion_model: str = "sd-v1-4"
# use_stable_diffusion_config: str = "v1-inference" # use_stable_diffusion_config: str = "v1-inference"
use_vae_model: str = None use_vae_model: str = None
use_hypernetwork_model: str = None use_hypernetwork_model: str = None
show_only_filtered_image: bool = False show_only_filtered_image: bool = False
output_format: str = "jpeg" # or "png" block_nsfw: bool = False
output_format: str = "jpeg" # or "png" or "webp"
output_quality: int = 75 output_quality: int = 75
metadata_output_format: str = "txt" # or "json" metadata_output_format: str = "txt" # or "json"
stream_image_progress: bool = False stream_image_progress: bool = False
stream_image_progress_interval: int = 5
class MergeRequest(BaseModel): class MergeRequest(BaseModel):
model0: str = None model0: str = None
@ -48,8 +53,9 @@ class MergeRequest(BaseModel):
out_path: str = "mix" out_path: str = "mix"
use_fp16 = True use_fp16 = True
class Image: class Image:
data: str # base64 data: str # base64
seed: int seed: int
is_nsfw: bool is_nsfw: bool
path_abs: str = None path_abs: str = None
@ -65,6 +71,7 @@ class Image:
"path_abs": self.path_abs, "path_abs": self.path_abs,
} }
class Response: class Response:
render_request: GenerateImageRequest render_request: GenerateImageRequest
task_data: TaskData task_data: TaskData
@ -80,7 +87,7 @@ class Response:
del self.render_request.init_image_mask del self.render_request.init_image_mask
res = { res = {
"status": 'succeeded', "status": "succeeded",
"render_request": self.render_request.dict(), "render_request": self.render_request.dict(),
"task_data": self.task_data.dict(), "task_data": self.task_data.dict(),
"output": [], "output": [],
@ -91,5 +98,6 @@ class Response:
return res return res
class UserInitiatedStop(Exception): class UserInitiatedStop(Exception):
pass pass

View File

@ -1,8 +1,8 @@
import logging import logging
log = logging.getLogger('easydiffusion') log = logging.getLogger("easydiffusion")
from .save_utils import ( from .save_utils import (
save_images_to_disk, save_images_to_disk,
get_printable_request, get_printable_request,
) )

View File

@ -7,82 +7,126 @@ from easydiffusion.types import TaskData, GenerateImageRequest
from sdkit.utils import save_images, save_dicts from sdkit.utils import save_images, save_dicts
filename_regex = re.compile('[^a-zA-Z0-9._-]') filename_regex = re.compile("[^a-zA-Z0-9._-]")
# keep in sync with `ui/media/js/dnd.js` # keep in sync with `ui/media/js/dnd.js`
TASK_TEXT_MAPPING = { TASK_TEXT_MAPPING = {
'prompt': 'Prompt', "prompt": "Prompt",
'width': 'Width', "width": "Width",
'height': 'Height', "height": "Height",
'seed': 'Seed', "seed": "Seed",
'num_inference_steps': 'Steps', "num_inference_steps": "Steps",
'guidance_scale': 'Guidance Scale', "guidance_scale": "Guidance Scale",
'prompt_strength': 'Prompt Strength', "prompt_strength": "Prompt Strength",
'use_face_correction': 'Use Face Correction', "use_face_correction": "Use Face Correction",
'use_upscale': 'Use Upscaling', "use_upscale": "Use Upscaling",
'upscale_amount': 'Upscale By', "upscale_amount": "Upscale By",
'sampler_name': 'Sampler', "sampler_name": "Sampler",
'negative_prompt': 'Negative Prompt', "negative_prompt": "Negative Prompt",
'use_stable_diffusion_model': 'Stable Diffusion model', "use_stable_diffusion_model": "Stable Diffusion model",
'use_hypernetwork_model': 'Hypernetwork model', "use_vae_model": "VAE model",
'hypernetwork_strength': 'Hypernetwork Strength' "use_hypernetwork_model": "Hypernetwork model",
"hypernetwork_strength": "Hypernetwork Strength",
} }
def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData): def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData):
now = time.time() now = time.time()
save_dir_path = os.path.join(task_data.save_to_disk_path, filename_regex.sub('_', task_data.session_id)) save_dir_path = os.path.join(task_data.save_to_disk_path, filename_regex.sub("_", task_data.session_id))
metadata_entries = get_metadata_entries_for_request(req, task_data) metadata_entries = get_metadata_entries_for_request(req, task_data)
make_filename = make_filename_callback(req, now=now) make_filename = make_filename_callback(req, now=now)
if task_data.show_only_filtered_image or filtered_images is images: if task_data.show_only_filtered_image or filtered_images is images:
save_images(filtered_images, save_dir_path, file_name=make_filename, output_format=task_data.output_format, output_quality=task_data.output_quality) save_images(
save_dicts(metadata_entries, save_dir_path, file_name=make_filename, output_format=task_data.metadata_output_format) filtered_images,
save_dir_path,
file_name=make_filename,
output_format=task_data.output_format,
output_quality=task_data.output_quality,
)
if task_data.metadata_output_format.lower() in ["json", "txt", "embed"]:
save_dicts(
metadata_entries,
save_dir_path,
file_name=make_filename,
output_format=task_data.metadata_output_format,
file_format=task_data.output_format,
)
else: else:
make_filter_filename = make_filename_callback(req, now=now, suffix='filtered') make_filter_filename = make_filename_callback(req, now=now, suffix="filtered")
save_images(
images,
save_dir_path,
file_name=make_filename,
output_format=task_data.output_format,
output_quality=task_data.output_quality,
)
save_images(
filtered_images,
save_dir_path,
file_name=make_filter_filename,
output_format=task_data.output_format,
output_quality=task_data.output_quality,
)
if task_data.metadata_output_format.lower() in ["json", "txt", "embed"]:
save_dicts(
metadata_entries,
save_dir_path,
file_name=make_filter_filename,
output_format=task_data.metadata_output_format,
file_format=task_data.output_format,
)
save_images(images, save_dir_path, file_name=make_filename, output_format=task_data.output_format, output_quality=task_data.output_quality)
save_images(filtered_images, save_dir_path, file_name=make_filter_filename, output_format=task_data.output_format, output_quality=task_data.output_quality)
save_dicts(metadata_entries, save_dir_path, file_name=make_filter_filename, output_format=task_data.metadata_output_format)
def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskData): def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskData):
metadata = get_printable_request(req) metadata = get_printable_request(req)
metadata.update({ metadata.update(
'use_stable_diffusion_model': task_data.use_stable_diffusion_model, {
'use_vae_model': task_data.use_vae_model, "use_stable_diffusion_model": task_data.use_stable_diffusion_model,
'use_hypernetwork_model': task_data.use_hypernetwork_model, "use_vae_model": task_data.use_vae_model,
'use_face_correction': task_data.use_face_correction, "use_hypernetwork_model": task_data.use_hypernetwork_model,
'use_upscale': task_data.use_upscale, "use_face_correction": task_data.use_face_correction,
}) "use_upscale": task_data.use_upscale,
if metadata['use_upscale'] is not None: }
metadata['upscale_amount'] = task_data.upscale_amount )
if metadata["use_upscale"] is not None:
metadata["upscale_amount"] = task_data.upscale_amount
if task_data.use_hypernetwork_model is None:
del metadata["hypernetwork_strength"]
# if text, format it in the text format expected by the UI # if text, format it in the text format expected by the UI
is_txt_format = (task_data.metadata_output_format.lower() == 'txt') is_txt_format = task_data.metadata_output_format.lower() == "txt"
if is_txt_format: if is_txt_format:
metadata = {TASK_TEXT_MAPPING[key]: val for key, val in metadata.items() if key in TASK_TEXT_MAPPING} metadata = {TASK_TEXT_MAPPING[key]: val for key, val in metadata.items() if key in TASK_TEXT_MAPPING}
entries = [metadata.copy() for _ in range(req.num_outputs)] entries = [metadata.copy() for _ in range(req.num_outputs)]
for i, entry in enumerate(entries): for i, entry in enumerate(entries):
entry['Seed' if is_txt_format else 'seed'] = req.seed + i entry["Seed" if is_txt_format else "seed"] = req.seed + i
return entries return entries
def get_printable_request(req: GenerateImageRequest): def get_printable_request(req: GenerateImageRequest):
metadata = req.dict() metadata = req.dict()
del metadata['init_image'] del metadata["init_image"]
del metadata['init_image_mask'] del metadata["init_image_mask"]
if req.init_image is None:
del metadata["prompt_strength"]
return metadata return metadata
def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None): def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None):
if now is None: if now is None:
now = time.time() now = time.time()
def make_filename(i):
img_id = base64.b64encode(int(now+i).to_bytes(8, 'big')).decode() # Generate unique ID based on time.
img_id = img_id.translate({43:None, 47:None, 61:None})[-8:] # Remove + / = and keep last 8 chars.
prompt_flattened = filename_regex.sub('_', req.prompt)[:50] def make_filename(i):
img_id = base64.b64encode(int(now + i).to_bytes(8, "big")).decode() # Generate unique ID based on time.
img_id = img_id.translate({43: None, 47: None, 61: None})[-8:] # Remove + / = and keep last 8 chars.
prompt_flattened = filename_regex.sub("_", req.prompt)[:50]
name = f"{prompt_flattened}_{img_id}" name = f"{prompt_flattened}_{img_id}"
name = name if suffix is None else f'{name}_{suffix}' name = name if suffix is None else f"{name}_{suffix}"
return name return name
return make_filename return make_filename

View File

@ -14,6 +14,7 @@
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css"> <link rel="stylesheet" href="/media/css/modifier-thumbnails.css">
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css"> <link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
<link rel="stylesheet" href="/media/css/image-editor.css"> <link rel="stylesheet" href="/media/css/image-editor.css">
<link rel="stylesheet" href="/media/css/searchable-models.css">
<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>
@ -25,7 +26,7 @@
<div id="logo"> <div id="logo">
<h1> <h1>
Easy Diffusion Easy Diffusion
<small>v2.5.15 <span id="updateBranchLabel"></span></small> <small>v2.5.22 <span id="updateBranchLabel"></span></small>
</h1> </h1>
</div> </div>
<div id="server-status"> <div id="server-status">
@ -50,7 +51,7 @@
<div id="editor"> <div id="editor">
<div id="editor-inputs"> <div id="editor-inputs">
<div id="editor-inputs-prompt" class="row"> <div id="editor-inputs-prompt" class="row">
<label for="prompt"><b>Enter Prompt</b></label> <small>or</small> <button id="promptsFromFileBtn">Load from a file</button> <label for="prompt"><b>Enter Prompt</b></label> <small>or</small> <button id="promptsFromFileBtn" class="tertiaryButton">Load from a file</button>
<textarea id="prompt" class="col-free">a photograph of an astronaut riding a horse</textarea> <textarea id="prompt" class="col-free">a photograph of an astronaut riding a horse</textarea>
<input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden --> <input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden -->
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle"> <label for="negative_prompt" class="collapsible" id="negative_prompt_handle">
@ -69,7 +70,7 @@
<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"> <div id="init_image_wrapper">
<img id="init_image_preview" src="" /> <img id="init_image_preview" src="" />
<span id="init_image_size_box"></span> <span id="init_image_size_box" class="img_bottom_label"></span>
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button> <button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
</div> </div>
<div id="init_image_buttons"> <div id="init_image_buttons">
@ -97,7 +98,7 @@
</div> </div>
<div id="editor-inputs-tags-container" class="row"> <div id="editor-inputs-tags-container" class="row">
<label>Image Modifiers <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">click an Image Modifier to remove it, right-click to temporarily disable it, use Ctrl+Mouse Wheel to adjust its weight</span></i>:</label> <label>Image Modifiers <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">click an Image Modifier to remove it, right-click to temporarily disable it, use Ctrl+Mouse Wheel to adjust its weight</span></i></label>
<div id="editor-inputs-tags-list"></div> <div id="editor-inputs-tags-list"></div>
</div> </div>
@ -125,10 +126,9 @@
<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 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" 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 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> <tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td class="model-input">
<select id="stable_diffusion_model" name="stable_diffusion_model"> <input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<!-- <option value="sd-v1-4" selected>sd-v1-4</option> --> <button id="reload-models" class="secondaryButton reloadModels"><i class='fa-solid fa-rotate'></i></button>
</select>
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about custom models</span></i></a> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about custom models</span></i></a>
</td></tr> </td></tr>
<!-- <tr id="modelConfigSelection" class="pl-5"><td><label for="model_config">Model Config:</i></label></td><td> <!-- <tr id="modelConfigSelection" class="pl-5"><td><label for="model_config">Model Config:</i></label></td><td>
@ -136,9 +136,7 @@
</select> </select>
</td></tr> --> </td></tr> -->
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td> <tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td>
<select id="vae_model" name="vae_model"> <input id="vae_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<!-- <option value="" selected>None</option> -->
</select>
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about VAEs</span></i></a> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about VAEs</span></i></a>
</td></tr> </td></tr>
<tr id="samplerSelection" class="pl-5"><td><label for="sampler_name">Sampler:</label></td><td> <tr id="samplerSelection" class="pl-5"><td><label for="sampler_name">Sampler:</label></td><td>
@ -157,6 +155,11 @@
<option value="dpmpp_sde">DPM++ SDE</option> <option value="dpmpp_sde">DPM++ SDE</option>
<option value="dpm_fast">DPM Fast</option> <option value="dpm_fast">DPM Fast</option>
<option value="dpm_adaptive">DPM Adaptive</option> <option value="dpm_adaptive">DPM Adaptive</option>
<option value="unipc_snr">UniPC SNR</option>
<option value="unipc_tu">UniPC TU</option>
<option value="unipc_snr_2">UniPC SNR 2</option>
<option value="unipc_tu_2">UniPC TC 2</option>
<option value="unipc_tq">UniPC TQ</option>
</select> </select>
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about samplers</span></i></a> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use#samplers" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Click to learn more about samplers</span></i></a>
</td></tr> </td></tr>
@ -210,9 +213,7 @@
<tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="11" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr> <tr class="pl-5"><td><label for="guidance_scale_slider">Guidance Scale:</label></td><td> <input id="guidance_scale_slider" name="guidance_scale_slider" class="editor-slider" value="75" type="range" min="11" max="500"> <input id="guidance_scale" name="guidance_scale" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"></td></tr>
<tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr> <tr id="prompt_strength_container" class="pl-5"><td><label for="prompt_strength_slider">Prompt Strength:</label></td><td> <input id="prompt_strength_slider" name="prompt_strength_slider" class="editor-slider" value="80" type="range" min="0" max="99"> <input id="prompt_strength" name="prompt_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td></tr>
<tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td> <tr class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td>
<select id="hypernetwork_model" name="hypernetwork_model"> <input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<!-- <option value="" selected>None</option> -->
</select>
</td></tr> </td></tr>
<tr id="hypernetwork_strength_container" class="pl-5"> <tr id="hypernetwork_strength_container" class="pl-5">
<td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td> <td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td>
@ -222,9 +223,10 @@
<select id="output_format" name="output_format"> <select id="output_format" name="output_format">
<option value="jpeg" selected>jpeg</option> <option value="jpeg" selected>jpeg</option>
<option value="png">png</option> <option value="png">png</option>
<option value="webp">webp</option>
</select> </select>
</td></tr> </td></tr>
<tr class="pl-5" id="output_quality_row"><td><label for="output_quality">JPEG Quality:</label></td><td> <tr class="pl-5" id="output_quality_row"><td><label for="output_quality">Image Quality:</label></td><td>
<input id="output_quality_slider" name="output_quality" class="editor-slider" value="75" type="range" min="10" max="95"> <input id="output_quality" name="output_quality" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"> <input id="output_quality_slider" name="output_quality" class="editor-slider" value="75" type="range" min="10" max="95"> <input id="output_quality" name="output_quality" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)">
</td></tr> </td></tr>
</table></div> </table></div>
@ -232,7 +234,7 @@
<div><ul> <div><ul>
<li><b class="settings-subheader">Render Settings</b></li> <li><b class="settings-subheader">Render Settings</b></li>
<li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, slower images)</small></label></li> <li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, slower images)</small></label></li>
<li class="pl-5"><input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes <small>(uses GFPGAN)</small></label></li> <li class="pl-5"><input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes</label> <div style="display:inline-block;"><input id="gfpgan_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" /></div></li>
<li class="pl-5"> <li class="pl-5">
<input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Scale up by</label> <input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Scale up by</label>
<select id="upscale_amount" name="upscale_amount"> <select id="upscale_amount" name="upscale_amount">
@ -281,8 +283,34 @@
and selecting the desired modifiers.<br/><br/> and selecting the desired modifiers.<br/><br/>
Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :) Click "Image Settings" for additional settings like seed, image size, number of images to generate etc.<br/><br/>Enjoy! :)
</div> </div>
<div id="preview-tools">
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can"></i> Clear All</button> <div id="preview-content">
<div id="preview-tools">
<button id="clear-all-previews" class="secondaryButton"><i class="fa-solid fa-trash-can icon"></i> Clear All</button>
<button id="save-all-images" class="tertiaryButton"><i class="fa-solid fa-download icon"></i> Download All Images</button>
<div class="display-settings">
<button id="auto_scroll_btn" class="tertiaryButton">
<i class="fa-solid fa-arrows-up-to-line icon"></i>
<input id="auto_scroll" name="auto_scroll" type="checkbox" style="display: none">
<span class="simple-tooltip left">
Scroll to generated image (<span class="state">OFF</span>)
</span>
</button>
<button class="dropdown tertiaryButton">
<i class="fa-solid fa-magnifying-glass-plus icon dropbtn"></i>
<span class="simple-tooltip left">
Image Size
</span>
</button>
<div class="dropdown-content">
<div class="dropdown-item">
<input id="thumbnail_size" name="thumbnail_size" class="editor-slider" type="range" value="70" min="5" max="200" oninput="sliderUpdate(event)">
<input id="thumbnail_size-input" name="thumbnail_size-input" size="3" value="70" pattern="^[0-9.]+$" onkeypress="preventNonNumericalInput(event)" oninput="sliderUpdate(event)">&nbsp;%
</div>
</div>
</div>
<div class="clearfix" style="clear: both;"></div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -425,6 +453,7 @@
<script src="media/js/image-modifiers.js"></script> <script src="media/js/image-modifiers.js"></script>
<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/main.js"></script> <script src="media/js/main.js"></script>
<script src="media/js/themes.js"></script> <script src="media/js/themes.js"></script>
<script src="media/js/dnd.js"></script> <script src="media/js/dnd.js"></script>

View File

@ -31,7 +31,7 @@
} }
.editor-options-container > * > *.active { .editor-options-container > * > *.active {
border: 2px solid #3584e4; border: 1px solid #3584e4;
} }
.image_editor_opacity .editor-options-container > * > *:not(.active) { .image_editor_opacity .editor-options-container > * > *:not(.active) {
@ -160,6 +160,7 @@
padding: var(--popup-padding); padding: var(--popup-padding);
min-height: calc(100vh - (2 * var(--popup-margin))); min-height: calc(100vh - (2 * var(--popup-margin)));
max-width: none; max-width: none;
min-width: fit-content;
} }
.image-editor-popup h1 { .image-editor-popup h1 {

View File

@ -123,6 +123,9 @@ code {
.imgPreviewItemClearBtn { .imgPreviewItemClearBtn {
opacity: 0; opacity: 0;
} }
.imgContainer .img_bottom_label {
opacity: 0;
}
.imgPreviewItemClearBtn:hover { .imgPreviewItemClearBtn:hover {
background: rgb(177, 27, 0); background: rgb(177, 27, 0);
} }
@ -132,6 +135,9 @@ code {
.imgContainer:hover > .imgPreviewItemClearBtn { .imgContainer:hover > .imgPreviewItemClearBtn {
opacity: 1; opacity: 1;
} }
.imgContainer:hover > .img_bottom_label {
opacity: 60%;
}
.imgItemInfo * { .imgItemInfo * {
margin-bottom: 7px; margin-bottom: 7px;
} }
@ -193,7 +199,7 @@ code {
flex: 0 0 70px; flex: 0 0 70px;
background: var(--accent-color); background: var(--accent-color);
border: var(--primary-button-border); border: var(--primary-button-border);
color: rgb(255, 221, 255); color: var(--accent-text-color);
width: 100%; width: 100%;
height: 30pt; height: 30pt;
} }
@ -402,10 +408,8 @@ div.img-preview img {
display: none; display: none;
position: absolute; position: absolute;
z-index: 2; z-index: 2;
width: max-content;
background: var(--background-color4);
border: 2px solid var(--background-color2);
border-radius: 7px;
padding: 5px; padding: 5px;
margin-bottom: 15px; margin-bottom: 15px;
box-shadow: 0 20px 28px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15); box-shadow: 0 20px 28px 0 rgba(0, 0, 0, 0.15), 0 6px 20px 0 rgba(0, 0, 0, 0.15);
@ -413,6 +417,36 @@ div.img-preview img {
.dropdown:hover .dropdown-content { .dropdown:hover .dropdown-content {
display: block; display: block;
} }
.dropdown:hover + .dropdown-content {
display: block;
}
.dropdown-content:hover {
display: block;
}
.display-settings {
float: right;
position: relative;
}
.display-settings .dropdown-content {
right: 0px;
top: 12pt;
}
.dropdown-item {
padding: 4px;
background: var(--background-color4);
border: 2px solid var(--background-color2);
}
.dropdown-item:first-child {
border-radius: 7px 7px 0px 0px;
}
.dropdown-item:last-child {
border-radius: 0px 0px 7px 7px;
}
.imageTaskContainer { .imageTaskContainer {
border: 1px solid var(--background-color2); border: 1px solid var(--background-color2);
@ -468,6 +502,7 @@ div.img-preview img {
background: var(--accent-color); background: var(--accent-color);
border: var(--primary-button-border); border: var(--primary-button-border);
color: rgb(255, 221, 255); color: rgb(255, 221, 255);
padding: 3pt 6pt;
} }
.secondaryButton { .secondaryButton {
background: rgb(132, 8, 0); background: rgb(132, 8, 0);
@ -479,17 +514,26 @@ div.img-preview img {
.secondaryButton:hover { .secondaryButton:hover {
background: rgb(177, 27, 0); background: rgb(177, 27, 0);
} }
.useSettings { .tertiaryButton {
background: var(--accent-color); background: var(--tertiary-background-color);
border: 1px solid var(--accent-color); color: var(--tertiary-color);
color: rgb(255, 221, 255); border: 1px solid var(--tertiary-border-color);
padding: 3pt 6pt; padding: 3pt 6pt;
border-radius: 5px;
}
.tertiaryButton:hover {
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
color: var(--accent-text-color);
}
.tertiaryButton.pressed {
border-style: inset;
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
color: var(--accent-text-color);
}
.useSettings {
margin-right: 6pt; margin-right: 6pt;
float: right; float: right;
} }
.useSettings:hover {
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
}
.stopTask { .stopTask {
float: right; float: right;
} }
@ -577,6 +621,9 @@ div.img-preview img {
} */ } */
#init_image_size_box { #init_image_size_box {
border-radius: 6px 0px;
}
.img_bottom_label {
position: absolute; position: absolute;
right: 0px; right: 0px;
bottom: 0px; bottom: 0px;
@ -586,7 +633,6 @@ div.img-preview img {
text-shadow: 0px 0px 4px black; text-shadow: 0px 0px 4px black;
opacity: 60%; opacity: 60%;
font-size: 12px; font-size: 12px;
border-radius: 6px 0px;
} }
#editor-settings { #editor-settings {
@ -603,7 +649,6 @@ div.img-preview img {
} }
#editor-settings-entries ul { #editor-settings-entries ul {
margin: 0px;
padding: 0px; padding: 0px;
} }
@ -750,6 +795,13 @@ input::file-selector-button {
right: calc(var(--input-border-size) + var(--input-switch-padding)); right: calc(var(--input-border-size) + var(--input-switch-padding));
opacity: 1; opacity: 1;
} }
.model-filter {
width: 90%;
padding-right: 20px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Small screens */ /* Small screens */
@media screen and (max-width: 1265px) { @media screen and (max-width: 1265px) {
@ -787,12 +839,6 @@ input::file-selector-button {
width: 100%; width: 100%;
object-fit: contain; object-fit: contain;
} }
.dropdown-content {
width: auto !important;
transform: none !important;
left: 0px;
right: 0px;
}
#editor { #editor {
padding: 16px 8px; padding: 16px 8px;
} }
@ -825,6 +871,12 @@ input::file-selector-button {
.simple-tooltip { .simple-tooltip {
display: none; display: none;
} }
#preview-tools button {
font-size: 0px;
}
#preview-tools button .icon {
font-size: 12pt;
}
} }
@media screen and (max-width: 500px) { @media screen and (max-width: 500px) {
@ -857,7 +909,7 @@ input::file-selector-button {
#promptsFromFileBtn { #promptsFromFileBtn {
font-size: 9pt; font-size: 9pt;
display: inline; display: inline;
background-color: var(--accent-color); padding: 2pt;
} }
.section-button { .section-button {
@ -890,18 +942,19 @@ input::file-selector-button {
/* SIMPLE TOOTIP */ /* SIMPLE TOOTIP */
.simple-tooltip { .simple-tooltip {
border-radius: 3px; border-radius: 3px;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
background-color: var(--background-color3); background-color: var(--background-color3);
visibility: hidden; visibility: hidden;
opacity: 0; opacity: 0;
position: absolute; position: absolute;
width: max-content; width: max-content;
max-width: 300px; max-width: 300px;
padding: 8px 12px; padding: 8px 12px;
transition: 0.3s all; transition: 0.3s all;
z-index: 1000;
pointer-events: none; pointer-events: none;
} }
@ -1203,3 +1256,7 @@ body.wait-pause {
.jconfirm.jconfirm-modern .jconfirm-box { .jconfirm.jconfirm-modern .jconfirm-box {
background-color: var(--background-color1); background-color: var(--background-color1);
} }
.displayNone {
display:none !important;
}

View File

@ -0,0 +1,99 @@
.model-list {
position: absolute;
margin-block-start: 2px;
display: none;
padding-inline-start: 0;
max-height: 200px;
overflow: auto;
background: var(--input-background-color);
border: var(--input-border-size) solid var(--input-border-color);
border-radius: var(--input-border-radius);
color: var(--input-text-color);
z-index: 1;
line-height: normal;
}
.model-list ul {
padding-right: 20px;
padding-inline-start: 0;
margin-top: 3pt;
}
.model-list li {
padding-top: 3px;
padding-bottom: 3px;
}
.model-list .icon {
padding-right: 3pt;
}
.model-result {
list-style: none;
}
.model-no-result {
color: var(--text-color);
list-style: none;
padding: 3px 6px 3px 6px;
font-size: 9pt;
font-style: italic;
display: none;
}
.model-list li.model-folder {
color: var(--text-color);
list-style: none;
padding: 6px 6px 6px 6px;
font-size: 9pt;
font-weight: bold;
border-top: 1px solid var(--background-color1);
}
.model-list li.model-file {
color: var(--input-text-color);
list-style: none;
padding-left: 12px;
padding-right:20px;
font-size: 10pt;
font-weight: normal;
transition: none;
transition:property: none;
cursor: default;
}
.model-list li.model-file.in-root-folder {
padding-left: 6px;
}
.model-list li.model-file.selected {
background: grey;
}
.model-selector {
cursor: pointer;
}
.model-selector-arrow {
position: absolute;
width: 17px;
margin: 5px -17px;
padding-top: 3px;
cursor: pointer;
font-size: 8pt;
transition: none;
}
.model-input {
white-space: nowrap;
}
.reloadModels {
background: var(--background-color2);
border: none;
padding: 0px 0px;
}
#reload-models.secondaryButton:hover {
background: var(--background-color2);
}

View File

@ -27,9 +27,13 @@
--input-border-size: 1px; --input-border-size: 1px;
--accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness)); --accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness));
--accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover)); --accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover));
--accent-text-color: rgb(255, 221, 255);
--primary-button-border: none; --primary-button-border: none;
--input-switch-padding: 1px; --input-switch-padding: 1px;
--input-height: 18px; --input-height: 18px;
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (2 * var(--value-step))));
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3 * var(--value-step))));
--tertiary-color: var(--input-text-color)
/* Main theme color, hex color fallback. */ /* Main theme color, hex color fallback. */
--theme-color-fallback: #673AB6; --theme-color-fallback: #673AB6;
@ -48,6 +52,11 @@
--input-border-color: grey; --input-border-color: grey;
--theme-color-fallback: #aaaaaa; --theme-color-fallback: #aaaaaa;
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (16.8 * var(--value-step))));
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (12 * var(--value-step))));
--accent-text-color: white;
} }
.theme-discord { .theme-discord {
@ -64,6 +73,10 @@
--input-border-color: var(--input-background-color); --input-border-color: var(--input-background-color);
--theme-color-fallback: #202225; --theme-color-fallback: #202225;
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
--accent-text-color: white;
} }
.theme-cool-blue { .theme-cool-blue {
@ -81,6 +94,10 @@
--accent-hue: 212; --accent-hue: 212;
--theme-color-fallback: #0056b8; --theme-color-fallback: #0056b8;
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
--accent-text-color: #f7fbff;
} }
@ -97,6 +114,9 @@
--input-background-color: var(--background-color3); --input-background-color: var(--background-color3);
--theme-color-fallback: #5300b8; --theme-color-fallback: #5300b8;
--tertiary-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3.5 * var(--value-step))));
--tertiary-border-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (4.5 * var(--value-step))));
} }
.theme-super-dark { .theme-super-dark {
@ -131,6 +151,9 @@
--input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step)))); --input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
--input-text-color: #FF0000; --input-text-color: #FF0000;
--input-border-color: #005E05; --input-border-color: #005E05;
--tertiary-color: white;
--accent-text-color: #f7fbff;
} }

View File

@ -27,8 +27,10 @@ const SETTINGS_IDS_LIST = [
"negative_prompt", "negative_prompt",
"stream_image_progress", "stream_image_progress",
"use_face_correction", "use_face_correction",
"gfpgan_model",
"use_upscale", "use_upscale",
"upscale_amount", "upscale_amount",
"block_nsfw",
"show_only_filtered_image", "show_only_filtered_image",
"upscale_model", "upscale_model",
"preview-image", "preview-image",
@ -42,7 +44,9 @@ const SETTINGS_IDS_LIST = [
"metadata_output_format", "metadata_output_format",
"auto_save_settings", "auto_save_settings",
"apply_color_correction", "apply_color_correction",
"process_order_toggle" "process_order_toggle",
"thumbnail_size",
"auto_scroll"
] ]
const IGNORE_BY_DEFAULT = [ const IGNORE_BY_DEFAULT = [
@ -92,6 +96,9 @@ async function initSettings() {
} }
function getSetting(element) { function getSetting(element) {
if (element.dataset && 'path' in element.dataset) {
return element.dataset.path
}
if (typeof element === "string" || element instanceof String) { if (typeof element === "string" || element instanceof String) {
element = SETTINGS[element].element element = SETTINGS[element].element
} }
@ -101,6 +108,10 @@ function getSetting(element) {
return element.value return element.value
} }
function setSetting(element, value) { function setSetting(element, value) {
if (element.dataset && 'path' in element.dataset) {
element.dataset.path = value
return // no need to dispatch any event here because the models are not loaded yet
}
if (typeof element === "string" || element instanceof String) { if (typeof element === "string" || element instanceof String) {
element = SETTINGS[element].element element = SETTINGS[element].element
} }

View File

@ -2,7 +2,7 @@
const EXT_REGEX = /(?:\.([^.]+))?$/ const EXT_REGEX = /(?:\.([^.]+))?$/
const TEXT_EXTENSIONS = ['txt', 'json'] const TEXT_EXTENSIONS = ['txt', 'json']
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga'] const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga', 'webp']
function parseBoolean(stringValue) { function parseBoolean(stringValue) {
if (typeof stringValue === 'boolean') { if (typeof stringValue === 'boolean') {
@ -154,10 +154,21 @@ const TASK_MAPPING = {
use_face_correction: { name: 'Use Face Correction', use_face_correction: { name: 'Use Face Correction',
setUI: (use_face_correction) => { setUI: (use_face_correction) => {
useFaceCorrectionField.checked = parseBoolean(use_face_correction) const oldVal = gfpganModelField.value
gfpganModelField.value = getModelPath(use_face_correction, ['.pth'])
if (gfpganModelField.value) { // Is a valid value for the field.
useFaceCorrectionField.checked = true
gfpganModelField.disabled = false
} else { // Not a valid value, restore the old value and disable the filter.
gfpganModelField.disabled = true
gfpganModelField.value = oldVal
useFaceCorrectionField.checked = false
}
//useFaceCorrectionField.checked = parseBoolean(use_face_correction)
}, },
readUI: () => useFaceCorrectionField.checked, readUI: () => (useFaceCorrectionField.checked ? gfpganModelField.value : undefined),
parse: (val) => parseBoolean(val) parse: (val) => val
}, },
use_upscale: { name: 'Use Upscaling', use_upscale: { name: 'Use Upscaling',
setUI: (use_upscale) => { setUI: (use_upscale) => {
@ -324,6 +335,7 @@ function restoreTaskToUI(task, fieldsToSkip) {
// properly reset checkboxes // properly reset checkboxes
if (!('use_face_correction' in task.reqBody)) { if (!('use_face_correction' in task.reqBody)) {
useFaceCorrectionField.checked = false useFaceCorrectionField.checked = false
gfpganModelField.disabled = true
} }
if (!('use_upscale' in task.reqBody)) { if (!('use_upscale' in task.reqBody)) {
useUpscalingField.checked = false useUpscalingField.checked = false
@ -345,6 +357,7 @@ function restoreTaskToUI(task, fieldsToSkip) {
initImagePreview.addEventListener('load', function() { initImagePreview.addEventListener('load', function() {
if (Boolean(task.reqBody.mask)) { if (Boolean(task.reqBody.mask)) {
imageInpainter.setImg(task.reqBody.mask) imageInpainter.setImg(task.reqBody.mask)
maskSetting.checked = true
} }
}, { once: true }) }, { once: true })
initImagePreview.src = task.reqBody.init_image initImagePreview.src = task.reqBody.init_image
@ -363,12 +376,19 @@ function readUI() {
} }
function getModelPath(filename, extensions) function getModelPath(filename, extensions)
{ {
let pathIdx = filename.lastIndexOf('/') // Linux, Mac paths if (typeof filename !== "string") {
if (pathIdx < 0) { return
pathIdx = filename.lastIndexOf('\\') // Windows paths. }
let pathIdx
if (filename.includes('/models/stable-diffusion/')) {
pathIdx = filename.indexOf('/models/stable-diffusion/') + 25 // Linux, Mac paths
}
else if (filename.includes('\\models\\stable-diffusion\\')) {
pathIdx = filename.indexOf('\\models\\stable-diffusion\\') + 25 // Linux, Mac paths
} }
if (pathIdx >= 0) { if (pathIdx >= 0) {
filename = filename.slice(pathIdx + 1) filename = filename.slice(pathIdx)
} }
extensions.forEach(ext => { extensions.forEach(ext => {
if (filename.endsWith(ext)) { if (filename.endsWith(ext)) {
@ -513,7 +533,7 @@ function dragOverHandler(ev) {
ev.dataTransfer.dropEffect = "copy" ev.dataTransfer.dropEffect = "copy"
let img = new Image() let img = new Image()
img.src = location.host + '/media/images/favicon-32x32.png' img.src = '//' + location.host + '/media/images/favicon-32x32.png'
ev.dataTransfer.setDragImage(img, 16, 16) ev.dataTransfer.setDragImage(img, 16, 16)
} }

View File

@ -741,6 +741,7 @@
"stream_progress_updates": true, "stream_progress_updates": true,
"stream_image_progress": true, "stream_image_progress": true,
"show_only_filtered_image": true, "show_only_filtered_image": true,
"block_nsfw": false,
"output_format": "png", "output_format": "png",
"output_quality": 75, "output_quality": 75,
} }

View File

@ -244,8 +244,8 @@ var IMAGE_EDITOR_SECTIONS = [
var sub_element = document.createElement("div") var sub_element = document.createElement("div")
sub_element.style.background = `var(--background-color3)` sub_element.style.background = `var(--background-color3)`
sub_element.style.filter = `blur(${blur_amount}px)` sub_element.style.filter = `blur(${blur_amount}px)`
sub_element.style.width = `${size - 4}px` sub_element.style.width = `${size - 2}px`
sub_element.style.height = `${size - 4}px` sub_element.style.height = `${size - 2}px`
sub_element.style['border-radius'] = `${size}px` sub_element.style['border-radius'] = `${size}px`
element.style.background = "none" element.style.background = "none"
element.appendChild(sub_element) element.appendChild(sub_element)

View File

@ -16,7 +16,7 @@ const modifierThumbnailPath = 'media/modifier-thumbnails'
const activeCardClass = 'modifier-card-active' const activeCardClass = 'modifier-card-active'
const CUSTOM_MODIFIERS_KEY = "customModifiers" const CUSTOM_MODIFIERS_KEY = "customModifiers"
function createModifierCard(name, previews) { function createModifierCard(name, previews, removeBy) {
const modifierCard = document.createElement('div') const modifierCard = document.createElement('div')
modifierCard.className = 'modifier-card' modifierCard.className = 'modifier-card'
modifierCard.innerHTML = ` modifierCard.innerHTML = `
@ -44,10 +44,10 @@ function createModifierCard(name, previews) {
} }
const maxLabelLength = 30 const maxLabelLength = 30
const nameWithoutBy = name.replace('by ', '') const cardLabel = removeBy ? name.replace('by ', '') : name
if(nameWithoutBy.length <= maxLabelLength) { if(cardLabel.length <= maxLabelLength) {
label.querySelector('p').innerText = nameWithoutBy label.querySelector('p').innerText = cardLabel
} else { } else {
const tooltipText = document.createElement('span') const tooltipText = document.createElement('span')
tooltipText.className = 'tooltip-text' tooltipText.className = 'tooltip-text'
@ -56,13 +56,14 @@ function createModifierCard(name, previews) {
label.classList.add('tooltip') label.classList.add('tooltip')
label.appendChild(tooltipText) label.appendChild(tooltipText)
label.querySelector('p').innerText = nameWithoutBy.substring(0, maxLabelLength) + '...' label.querySelector('p').innerText = cardLabel.substring(0, maxLabelLength) + '...'
} }
label.querySelector('p').dataset.fullName = name // preserve the full name
return modifierCard return modifierCard
} }
function createModifierGroup(modifierGroup, initiallyExpanded) { function createModifierGroup(modifierGroup, initiallyExpanded, removeBy) {
const title = modifierGroup.category const title = modifierGroup.category
const modifiers = modifierGroup.modifiers const modifiers = modifierGroup.modifiers
@ -79,9 +80,9 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
modifiers.forEach(modObj => { modifiers.forEach(modObj => {
const modifierName = modObj.modifier const modifierName = modObj.modifier
const modifierPreviews = modObj?.previews?.map(preview => `${modifierThumbnailPath}/${preview.path}`) const modifierPreviews = modObj?.previews?.map(preview => `${IMAGE_REGEX.test(preview.image) ? preview.image : modifierThumbnailPath + '/' + preview.path}`)
const modifierCard = createModifierCard(modifierName, modifierPreviews) const modifierCard = createModifierCard(modifierName, modifierPreviews, removeBy)
if(typeof modifierCard == 'object') { if(typeof modifierCard == 'object') {
modifiersEl.appendChild(modifierCard) modifiersEl.appendChild(modifierCard)
@ -114,6 +115,7 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
modifiersEl.appendChild(brk) modifiersEl.appendChild(brk)
let e = document.createElement('div') let e = document.createElement('div')
e.className = 'modifier-category'
e.appendChild(titleEl) e.appendChild(titleEl)
e.appendChild(modifiersEl) e.appendChild(modifiersEl)
@ -137,7 +139,7 @@ async function loadModifiers() {
res.reverse() res.reverse()
res.forEach((modifierGroup, idx) => { res.forEach((modifierGroup, idx) => {
createModifierGroup(modifierGroup, idx === res.length - 1) createModifierGroup(modifierGroup, idx === res.length - 1, modifierGroup === 'Artist' ? true : false) // only remove "By " for artists
}) })
createCollapsibles(editorModifierEntries) createCollapsibles(editorModifierEntries)
@ -153,7 +155,7 @@ async function loadModifiers() {
function refreshModifiersState(newTags) { function refreshModifiersState(newTags) {
// clear existing modifiers // clear existing modifiers
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => { document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
const modifierName = modifierCard.querySelector('.modifier-card-label').innerText const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName // pick the full modifier name
if (activeTags.map(x => x.name).includes(modifierName)) { if (activeTags.map(x => x.name).includes(modifierName)) {
modifierCard.classList.remove(activeCardClass) modifierCard.classList.remove(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+' modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
@ -165,13 +167,16 @@ function refreshModifiersState(newTags) {
newTags.forEach(tag => { newTags.forEach(tag => {
let found = false let found = false
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => { document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
const modifierName = modifierCard.querySelector('.modifier-card-label').innerText const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName
if (tag == modifierName) { const shortModifierName = modifierCard.querySelector('.modifier-card-label p').innerText
if (trimModifiers(tag) == trimModifiers(modifierName)) {
// add modifier to active array // add modifier to active array
if (!activeTags.map(x => x.name).includes(tag)) { // only add each tag once even if several custom modifier cards share the same tag if (!activeTags.map(x => x.name).includes(tag)) { // only add each tag once even if several custom modifier cards share the same tag
const imageModifierCard = modifierCard.cloneNode(true)
imageModifierCard.querySelector('.modifier-card-label p').innerText = shortModifierName
activeTags.push({ activeTags.push({
'name': modifierName, 'name': modifierName,
'element': modifierCard.cloneNode(true), 'element': imageModifierCard,
'originElement': modifierCard 'originElement': modifierCard
}) })
} }
@ -181,7 +186,7 @@ function refreshModifiersState(newTags) {
} }
}) })
if (found == false) { // custom tag went missing, create one here if (found == false) { // custom tag went missing, create one here
let modifierCard = createModifierCard(tag, undefined) // create a modifier card for the missing tag, no image let modifierCard = createModifierCard(tag, undefined, false) // create a modifier card for the missing tag, no image
modifierCard.addEventListener('click', () => { modifierCard.addEventListener('click', () => {
if (activeTags.map(x => x.name).includes(tag)) { if (activeTags.map(x => x.name).includes(tag)) {

View File

@ -33,18 +33,23 @@ let promptStrengthField = document.querySelector('#prompt_strength')
let samplerField = document.querySelector('#sampler_name') let samplerField = document.querySelector('#sampler_name')
let samplerSelectionContainer = document.querySelector("#samplerSelection") let samplerSelectionContainer = document.querySelector("#samplerSelection")
let useFaceCorrectionField = document.querySelector("#use_face_correction") let useFaceCorrectionField = document.querySelector("#use_face_correction")
let gfpganModelField = new ModelDropdown(document.querySelector("#gfpgan_model"), 'gfpgan')
let useUpscalingField = document.querySelector("#use_upscale") let useUpscalingField = document.querySelector("#use_upscale")
let upscaleModelField = document.querySelector("#upscale_model") let upscaleModelField = document.querySelector("#upscale_model")
let upscaleAmountField = document.querySelector("#upscale_amount") let upscaleAmountField = document.querySelector("#upscale_amount")
let stableDiffusionModelField = document.querySelector('#stable_diffusion_model') let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion')
let vaeModelField = document.querySelector('#vae_model') let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None')
let hypernetworkModelField = document.querySelector('#hypernetwork_model') let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None')
let hypernetworkStrengthSlider = document.querySelector('#hypernetwork_strength_slider') let hypernetworkStrengthSlider = document.querySelector('#hypernetwork_strength_slider')
let hypernetworkStrengthField = document.querySelector('#hypernetwork_strength') let hypernetworkStrengthField = document.querySelector('#hypernetwork_strength')
let outputFormatField = document.querySelector('#output_format') let outputFormatField = document.querySelector('#output_format')
let blockNSFWField = document.querySelector('#block_nsfw')
let showOnlyFilteredImageField = document.querySelector("#show_only_filtered_image") let showOnlyFilteredImageField = document.querySelector("#show_only_filtered_image")
let updateBranchLabel = document.querySelector("#updateBranchLabel") let updateBranchLabel = document.querySelector("#updateBranchLabel")
let streamImageProgressField = document.querySelector("#stream_image_progress") let streamImageProgressField = document.querySelector("#stream_image_progress")
let thumbnailSizeField = document.querySelector("#thumbnail_size-input")
let autoscrollBtn = document.querySelector("#auto_scroll_btn")
let autoScroll = document.querySelector("#auto_scroll")
let makeImageBtn = document.querySelector('#makeImage') let makeImageBtn = document.querySelector('#makeImage')
let stopImageBtn = document.querySelector('#stopImage') let stopImageBtn = document.querySelector('#stopImage')
@ -60,12 +65,14 @@ let promptStrengthContainer = document.querySelector('#prompt_strength_container
let initialText = document.querySelector("#initial-text") let initialText = document.querySelector("#initial-text")
let previewTools = document.querySelector("#preview-tools") let previewTools = document.querySelector("#preview-tools")
let clearAllPreviewsBtn = document.querySelector("#clear-all-previews") let clearAllPreviewsBtn = document.querySelector("#clear-all-previews")
let saveAllImagesBtn = document.querySelector("#save-all-images")
let maskSetting = document.querySelector('#enable_mask') let maskSetting = document.querySelector('#enable_mask')
const processOrder = document.querySelector('#process_order_toggle') const processOrder = document.querySelector('#process_order_toggle')
let imagePreview = document.querySelector("#preview") let imagePreview = document.querySelector("#preview")
let imagePreviewContent = document.querySelector("#preview-content")
imagePreview.addEventListener('drop', function(ev) { imagePreview.addEventListener('drop', function(ev) {
const data = ev.dataTransfer?.getData("text/plain"); const data = ev.dataTransfer?.getData("text/plain");
if (!data) { if (!data) {
@ -77,7 +84,7 @@ imagePreview.addEventListener('drop', function(ev) {
} }
ev.preventDefault() ev.preventDefault()
let moveTarget = ev.target let moveTarget = ev.target
while (moveTarget && typeof moveTarget === 'object' && moveTarget.parentNode !== imagePreview) { while (moveTarget && typeof moveTarget === 'object' && moveTarget.parentNode !== imagePreviewContent) {
moveTarget = moveTarget.parentNode moveTarget = moveTarget.parentNode
} }
if (moveTarget === initialText || moveTarget === previewTools) { if (moveTarget === initialText || moveTarget === previewTools) {
@ -87,17 +94,17 @@ imagePreview.addEventListener('drop', function(ev) {
return return
} }
if (moveTarget) { if (moveTarget) {
const childs = Array.from(imagePreview.children) const childs = Array.from(imagePreviewContent.children)
if (moveTarget.nextSibling && childs.indexOf(movedTask) < childs.indexOf(moveTarget)) { if (moveTarget.nextSibling && childs.indexOf(movedTask) < childs.indexOf(moveTarget)) {
// Move after the target if lower than current position. // Move after the target if lower than current position.
moveTarget = moveTarget.nextSibling moveTarget = moveTarget.nextSibling
} }
} }
const newNode = imagePreview.insertBefore(movedTask, moveTarget || previewTools.nextSibling) const newNode = imagePreviewContent.insertBefore(movedTask, moveTarget || previewTools.nextSibling)
if (newNode === movedTask) { if (newNode === movedTask) {
return return
} }
imagePreview.removeChild(movedTask) imagePreviewContent.removeChild(movedTask)
const task = htmlTaskMap.get(movedTask) const task = htmlTaskMap.get(movedTask)
if (task) { if (task) {
htmlTaskMap.delete(movedTask) htmlTaskMap.delete(movedTask)
@ -264,9 +271,26 @@ function showImages(reqBody, res, outputContainer, livePreview) {
<span class="imgSeedLabel"></span> <span class="imgSeedLabel"></span>
</div> </div>
<button class="imgPreviewItemClearBtn image_clear_btn"><i class="fa-solid fa-xmark"></i></button> <button class="imgPreviewItemClearBtn image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
<span class="img_bottom_label"></span>
</div> </div>
` `
outputContainer.appendChild(imageItemElem) outputContainer.appendChild(imageItemElem)
const imageRemoveBtn = imageItemElem.querySelector('.imgPreviewItemClearBtn')
let parentTaskContainer = imageRemoveBtn.closest('.imageTaskContainer')
imageRemoveBtn.addEventListener('click', (e) => {
shiftOrConfirm(e, "Remove the image from the results?", () => {
imageItemElem.style.display = 'none'
let allHidden = true;
let children = parentTaskContainer.querySelectorAll('.imgItem');
for(let x = 0; x < children.length; x++) {
let child = children[x];
if(child.style.display != "none") {
allHidden = false;
}
}
if(allHidden === true) {parentTaskContainer.classList.add("displayNone")}
})
})
} }
const imageElem = imageItemElem.querySelector('img') const imageElem = imageItemElem.querySelector('img')
imageElem.src = imageData imageElem.src = imageData
@ -276,12 +300,11 @@ function showImages(reqBody, res, outputContainer, livePreview) {
imageElem.setAttribute('data-steps', imageInferenceSteps) imageElem.setAttribute('data-steps', imageInferenceSteps)
imageElem.setAttribute('data-guidance', imageGuidanceScale) imageElem.setAttribute('data-guidance', imageGuidanceScale)
const imageRemoveBtn = imageItemElem.querySelector('.imgPreviewItemClearBtn') imageElem.addEventListener('load', function() {
imageRemoveBtn.addEventListener('click', (e) => { imageItemElem.querySelector('.img_bottom_label').innerText = `${this.naturalWidth} x ${this.naturalHeight}`
console.log(e)
shiftOrConfirm(e, "Remove the image from the results?", () => { imageItemElem.style.display = 'none' })
}) })
const imageInfo = imageItemElem.querySelector('.imgItemInfo') const imageInfo = imageItemElem.querySelector('.imgItemInfo')
imageInfo.style.visibility = (livePreview ? 'hidden' : 'visible') imageInfo.style.visibility = (livePreview ? 'hidden' : 'visible')
@ -413,7 +436,7 @@ function onUpscaleClick(req, img) {
function onFixFacesClick(req, img) { function onFixFacesClick(req, img) {
enqueueImageVariationTask(req, img, { enqueueImageVariationTask(req, img, {
use_face_correction: 'GFPGANv1.3' use_face_correction: gfpganModelField.value
}) })
} }
@ -706,12 +729,18 @@ async function onTaskStart(task) {
if (task.batchCount > 1) { 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. // 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) 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 startSeed = task.seed || newTaskReqBody.seed
const genSeeds = Boolean(typeof newTaskReqBody.seed !== 'number' || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1)) const genSeeds = Boolean(typeof newTaskReqBody.seed !== 'number' || (newTaskReqBody.seed === task.seed && task.numOutputsTotal > 1))
if (genSeeds) { if (genSeeds) {
newTaskReqBody.seed = parseInt(startSeed) + (task.batchesDone * newTaskReqBody.num_outputs) newTaskReqBody.seed = parseInt(startSeed) + (task.batchesDone * task.reqBody.num_outputs)
} }
// Update the seed *before* starting the processing so it's retained if user stops the task // Update the seed *before* starting the processing so it's retained if user stops the task
@ -774,7 +803,10 @@ function createInitImageHover(taskEntry) {
img.src = taskEntry.querySelector('div.task-initimg > img').src img.src = taskEntry.querySelector('div.task-initimg > img').src
$tooltip.append(img) $tooltip.append(img)
$tooltip.append(`<div class="top-right"><button>Use as Input</button></div>`) $tooltip.append(`<div class="top-right"><button>Use as Input</button></div>`)
$tooltip.find('button').on('click', (e) => { onUseAsInputClick(null,img) } ) $tooltip.find('button').on('click', (e) => {
e.stopPropagation()
onUseAsInputClick(null,img)
})
} }
let startX, startY; let startX, startY;
@ -839,7 +871,7 @@ function createTask(task) {
<i class="drag-handle fa-solid fa-grip"></i> <i class="drag-handle fa-solid fa-grip"></i>
<div class="taskStatusLabel">Enqueued</div> <div class="taskStatusLabel">Enqueued</div>
<button class="secondaryButton stopTask"><i class="fa-solid fa-trash-can"></i> Remove</button> <button class="secondaryButton stopTask"><i class="fa-solid fa-trash-can"></i> Remove</button>
<button class="secondaryButton useSettings"><i class="fa-solid fa-redo"></i> Use these settings</button> <button class="tertiaryButton useSettings"><i class="fa-solid fa-redo"></i> Use these settings</button>
<div class="preview-prompt"></div> <div class="preview-prompt"></div>
<div class="taskConfig">${taskConfig}</div> <div class="taskConfig">${taskConfig}</div>
<div class="outputMsg"></div> <div class="outputMsg"></div>
@ -906,7 +938,7 @@ function createTask(task) {
}) })
task.isProcessing = true task.isProcessing = true
taskEntry = imagePreview.insertBefore(taskEntry, previewTools.nextSibling) taskEntry = imagePreviewContent.insertBefore(taskEntry, previewTools.nextSibling)
htmlTaskMap.set(taskEntry, task) htmlTaskMap.set(taskEntry, task)
task.previewPrompt.innerText = task.reqBody.prompt task.previewPrompt.innerText = task.reqBody.prompt
@ -929,6 +961,7 @@ function getCurrentUserRequest() {
reqBody: { reqBody: {
seed, seed,
used_random_seed: randomSeedField.checked,
negative_prompt: negativePromptField.value.trim(), negative_prompt: negativePromptField.value.trim(),
num_outputs: numOutputsParallel, num_outputs: numOutputsParallel,
num_inference_steps: parseInt(numInferenceStepsField.value), num_inference_steps: parseInt(numInferenceStepsField.value),
@ -943,9 +976,10 @@ function getCurrentUserRequest() {
stream_progress_updates: true, stream_progress_updates: true,
stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked), stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked),
show_only_filtered_image: showOnlyFilteredImageField.checked, show_only_filtered_image: showOnlyFilteredImageField.checked,
block_nsfw: blockNSFWField.checked,
output_format: outputFormatField.value, output_format: outputFormatField.value,
output_quality: parseInt(outputQualityField.value), output_quality: parseInt(outputQualityField.value),
metadata_output_format: document.querySelector('#metadata_output_format').value, metadata_output_format: metadataOutputFormatField.value,
original_prompt: promptField.value, original_prompt: promptField.value,
active_tags: (activeTags.map(x => x.name)), active_tags: (activeTags.map(x => x.name)),
inactive_tags: (activeTags.filter(tag => tag.inactive === true).map(x => x.name)) inactive_tags: (activeTags.filter(tag => tag.inactive === true).map(x => x.name))
@ -970,7 +1004,7 @@ function getCurrentUserRequest() {
newTask.reqBody.save_to_disk_path = diskPathField.value.trim() newTask.reqBody.save_to_disk_path = diskPathField.value.trim()
} }
if (useFaceCorrectionField.checked) { if (useFaceCorrectionField.checked) {
newTask.reqBody.use_face_correction = 'GFPGANv1.3' newTask.reqBody.use_face_correction = gfpganModelField.value
} }
if (useUpscalingField.checked) { if (useUpscalingField.checked) {
newTask.reqBody.use_upscale = upscaleModelField.value newTask.reqBody.use_upscale = upscaleModelField.value
@ -1015,6 +1049,8 @@ function getPrompts(prompts) {
promptsToMake = applyPermuteOperator(promptsToMake) promptsToMake = applyPermuteOperator(promptsToMake)
promptsToMake = applySetOperator(promptsToMake) promptsToMake = applySetOperator(promptsToMake)
PLUGINS['GET_PROMPTS_HOOK'].forEach(fn => { promptsToMake = fn(promptsToMake) })
return promptsToMake return promptsToMake
} }
@ -1099,7 +1135,7 @@ function createFileName(prompt, seed, steps, guidance, outputFormat) {
// fileName += `${tagString}` // fileName += `${tagString}`
// add the file extension // add the file extension
fileName += '.' + (outputFormat === 'png' ? 'png' : 'jpeg') fileName += '.' + outputFormat
return fileName return fileName
} }
@ -1134,6 +1170,20 @@ clearAllPreviewsBtn.addEventListener('click', (e) => { shiftOrConfirm(e, "Clear
taskEntries.forEach(removeTask) taskEntries.forEach(removeTask)
})}) })})
saveAllImagesBtn.addEventListener('click', (e) => {
let i = 0
document.querySelectorAll(".imageTaskContainer").forEach(container => {
let req = htmlTaskMap.get(container)
container.querySelectorAll(".imgContainer img").forEach(img => {
if (img.closest('.imgItem').style.display === 'none') {
return
}
setTimeout(() => {onDownloadImageClick(req, img)}, i*200)
i = i+1
})
})
})
stopImageBtn.addEventListener('click', (e) => { shiftOrConfirm(e, "Stop all the tasks?", async function(e) { stopImageBtn.addEventListener('click', (e) => { shiftOrConfirm(e, "Stop all the tasks?", async function(e) {
await stopAllTasks() await stopAllTasks()
})}) })})
@ -1142,7 +1192,7 @@ widthField.addEventListener('change', onDimensionChange)
heightField.addEventListener('change', onDimensionChange) heightField.addEventListener('change', onDimensionChange)
function renameMakeImageButton() { function renameMakeImageButton() {
let totalImages = Math.max(parseInt(numOutputsTotalField.value), parseInt(numOutputsParallelField.value)) let totalImages = Math.max(parseInt(numOutputsTotalField.value), parseInt(numOutputsParallelField.value)) * getPrompts().length
let imageLabel = 'Image' let imageLabel = 'Image'
if (totalImages > 1) { if (totalImages > 1) {
imageLabel = totalImages + ' Images' imageLabel = totalImages + ' Images'
@ -1168,6 +1218,12 @@ function onDimensionChange() {
} }
diskPathField.disabled = !saveToDiskField.checked diskPathField.disabled = !saveToDiskField.checked
metadataOutputFormatField.disabled = !saveToDiskField.checked
gfpganModelField.disabled = !useFaceCorrectionField.checked
useFaceCorrectionField.addEventListener('change', function(e) {
gfpganModelField.disabled = !this.checked
})
upscaleModelField.disabled = !useUpscalingField.checked upscaleModelField.disabled = !useUpscalingField.checked
upscaleAmountField.disabled = !useUpscalingField.checked upscaleAmountField.disabled = !useUpscalingField.checked
@ -1254,7 +1310,7 @@ function updateHypernetworkStrengthContainer() {
hypernetworkModelField.addEventListener('change', updateHypernetworkStrengthContainer) hypernetworkModelField.addEventListener('change', updateHypernetworkStrengthContainer)
updateHypernetworkStrengthContainer() updateHypernetworkStrengthContainer()
/********************* JPEG Quality **********************/ /********************* JPEG/WEBP Quality **********************/
function updateOutputQuality() { function updateOutputQuality() {
outputQualityField.value = 0 | outputQualitySlider.value outputQualityField.value = 0 | outputQualitySlider.value
outputQualityField.dispatchEvent(new Event("change")) outputQualityField.dispatchEvent(new Event("change"))
@ -1276,77 +1332,43 @@ outputQualityField.addEventListener('input', debounce(updateOutputQualitySlider,
updateOutputQuality() updateOutputQuality()
outputFormatField.addEventListener('change', e => { outputFormatField.addEventListener('change', e => {
if (outputFormatField.value == 'jpeg') { if (outputFormatField.value === 'png') {
outputQualityRow.style.display='table-row'
} else {
outputQualityRow.style.display='none' outputQualityRow.style.display='none'
} else {
outputQualityRow.style.display='table-row'
} }
}) })
/********************* Zoom Slider **********************/
async function getModels() { thumbnailSizeField.addEventListener('change', () => {
try { (function (s) {
const sd_model_setting_key = "stable_diffusion_model" for (var j =0; j < document.styleSheets.length; j++) {
const vae_model_setting_key = "vae_model" let cssSheet = document.styleSheets[j]
const hypernetwork_model_key = "hypernetwork_model" for (var i = 0; i < cssSheet.cssRules.length; i++) {
const selectedSDModel = SETTINGS[sd_model_setting_key].value var rule = cssSheet.cssRules[i];
const selectedVaeModel = SETTINGS[vae_model_setting_key].value if (rule.selectorText == "div.img-preview img") {
const selectedHypernetworkModel = SETTINGS[hypernetwork_model_key].value rule.style['max-height'] = s+'vh';
rule.style['max-width'] = s+'vw';
const models = await SD.getModels() return;
const modelsOptions = models['options']
if ("scan-error" in models) {
// let previewPane = document.getElementById('tab-content-wrapper')
let previewPane = document.getElementById('preview')
previewPane.style.background="red"
previewPane.style.textAlign="center"
previewPane.innerHTML = '<H1>🔥Malware alert!🔥</H1><h2>The file <i>' + models['scan-error'] + '</i> in your <tt>models/stable-diffusion</tt> folder is probably malware infected.</h2><h2>Please delete this file from the folder before proceeding!</h2>After deleting the file, reload this page.<br><br><button onClick="window.location.reload();">Reload Page</button>'
makeImageBtn.disabled = true
}
const stableDiffusionOptions = modelsOptions['stable-diffusion']
const vaeOptions = modelsOptions['vae']
const hypernetworkOptions = modelsOptions['hypernetwork']
vaeOptions.unshift('') // add a None option
hypernetworkOptions.unshift('') // add a None option
function createModelOptions(modelField, selectedModel, path="") {
return function fn(modelName) {
if (typeof(modelName) == 'string') {
const modelOption = document.createElement('option')
modelOption.value = path + modelName
modelOption.innerHTML = modelName !== '' ? (path != "" ? "&nbsp;&nbsp;"+modelName : modelName) : 'None'
if (path + modelName === selectedModel) {
modelOption.selected = true
}
modelField.appendChild(modelOption)
} else {
const modelGroup = document.createElement('optgroup')
modelGroup.label = path + modelName[0]
modelField.appendChild(modelGroup)
modelName[1].forEach( createModelOptions(modelField, selectedModel, path + modelName[0] + "/" ) )
} }
} }
} }
})(thumbnailSizeField.value)
})
stableDiffusionOptions.forEach(createModelOptions(stableDiffusionModelField, selectedSDModel)) function onAutoScrollUpdate() {
vaeOptions.forEach(createModelOptions(vaeModelField, selectedVaeModel)) if (autoScroll.checked) {
hypernetworkOptions.forEach(createModelOptions(hypernetworkModelField, selectedHypernetworkModel)) autoscrollBtn.classList.add('pressed')
} else {
stableDiffusionModelField.dispatchEvent(new Event('change')) autoscrollBtn.classList.remove('pressed')
vaeModelField.dispatchEvent(new Event('change'))
hypernetworkModelField.dispatchEvent(new Event('change'))
// TODO: set default for model here too
SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0]
if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') {
setSetting(sd_model_setting_key, stableDiffusionOptions[0])
}
} catch (e) {
console.log('get models error', e)
} }
autoscrollBtn.querySelector(".state").innerHTML = (autoScroll.checked ? "ON" : "OFF")
} }
autoscrollBtn.addEventListener('click', function() {
autoScroll.checked = !autoScroll.checked
autoScroll.dispatchEvent(new Event("change"))
onAutoScrollUpdate()
})
autoScroll.addEventListener('change', onAutoScrollUpdate)
function checkRandomSeed() { function checkRandomSeed() {
if (randomSeedField.checked) { if (randomSeedField.checked) {
@ -1490,6 +1512,9 @@ function resumeClient() {
}) })
} }
promptField.addEventListener("input", debounce( renameMakeImageButton, 1000) )
pauseBtn.addEventListener("click", function () { pauseBtn.addEventListener("click", function () {
pauseClient = true pauseClient = true
pauseBtn.style.display="none" pauseBtn.style.display="none"
@ -1522,3 +1547,7 @@ window.addEventListener("beforeunload", function(e) {
createCollapsibles() createCollapsibles()
prettifyInputs(document); prettifyInputs(document);
// set the textbox as focused on start
promptField.focus()
promptField.selectionStart = promptField.value.length

View File

@ -7,6 +7,7 @@
checkbox: "checkbox", checkbox: "checkbox",
select: "select", select: "select",
select_multiple: "select_multiple", select_multiple: "select_multiple",
slider: "slider",
custom: "custom", custom: "custom",
}; };
@ -60,6 +61,10 @@ var PARAMETERS = [
note: "will be saved to disk in this format", note: "will be saved to disk in this format",
default: "txt", default: "txt",
options: [ options: [
{
value: "none",
label: "none"
},
{ {
value: "txt", value: "txt",
label: "txt" label: "txt"
@ -67,9 +72,21 @@ var PARAMETERS = [
{ {
value: "json", value: "json",
label: "json" label: "json"
},
{
value: "embed",
label: "embed"
} }
], ],
}, },
{
id: "block_nsfw",
type: ParameterType.checkbox,
label: "Block NSFW images",
note: "blurs out NSFW images",
icon: "fa-land-mine-on",
default: false,
},
{ {
id: "sound_toggle", id: "sound_toggle",
type: ParameterType.checkbox, type: ParameterType.checkbox,
@ -183,6 +200,18 @@ function getParameterSettingsEntry(id) {
return parameter[0].settingsEntry return parameter[0].settingsEntry
} }
function sliderUpdate(event) {
if (event.srcElement.id.endsWith('-input')) {
let slider = document.getElementById(event.srcElement.id.slice(0,-6))
slider.value = event.srcElement.value
slider.dispatchEvent(new Event("change"))
} else {
let field = document.getElementById(event.srcElement.id+'-input')
field.value = event.srcElement.value
field.dispatchEvent(new Event("change"))
}
}
function getParameterElement(parameter) { function getParameterElement(parameter) {
switch (parameter.type) { switch (parameter.type) {
case ParameterType.checkbox: case ParameterType.checkbox:
@ -193,6 +222,8 @@ function getParameterElement(parameter) {
var options = (parameter.options || []).map(option => `<option value="${option.value}">${option.label}</option>`).join("") var options = (parameter.options || []).map(option => `<option value="${option.value}">${option.label}</option>`).join("")
var multiple = (parameter.type == ParameterType.select_multiple ? 'multiple' : '') var multiple = (parameter.type == ParameterType.select_multiple ? 'multiple' : '')
return `<select id="${parameter.id}" name="${parameter.id}" ${multiple}>${options}</select>` return `<select id="${parameter.id}" name="${parameter.id}" ${multiple}>${options}</select>`
case ParameterType.slider:
return `<input id="${parameter.id}" name="${parameter.id}" class="editor-slider" type="range" value="${parameter.default}" min="${parameter.slider_min}" max="${parameter.slider_max}" oninput="sliderUpdate(event)"> <input id="${parameter.id}-input" name="${parameter.id}-input" size="4" value="${parameter.default}" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)" oninput="sliderUpdate(event)">&nbsp;${parameter.slider_unit}`
case ParameterType.custom: case ParameterType.custom:
return parameter.render(parameter) return parameter.render(parameter)
default: default:
@ -226,6 +257,7 @@ let autoPickGPUsField = document.querySelector('#auto_pick_gpus')
let useGPUsField = document.querySelector('#use_gpus') let useGPUsField = document.querySelector('#use_gpus')
let saveToDiskField = document.querySelector('#save_to_disk') let saveToDiskField = document.querySelector('#save_to_disk')
let diskPathField = document.querySelector('#diskPath') let diskPathField = document.querySelector('#diskPath')
let metadataOutputFormatField = document.querySelector('#metadata_output_format')
let listenToNetworkField = document.querySelector("#listen_to_network") let listenToNetworkField = document.querySelector("#listen_to_network")
let listenPortField = document.querySelector("#listen_port") let listenPortField = document.querySelector("#listen_port")
let useBetaChannelField = document.querySelector("#use_beta_channel") let useBetaChannelField = document.querySelector("#use_beta_channel")
@ -279,6 +311,7 @@ async function getAppConfig() {
saveToDiskField.addEventListener('change', function(e) { saveToDiskField.addEventListener('change', function(e) {
diskPathField.disabled = !this.checked diskPathField.disabled = !this.checked
metadataOutputFormatField.disabled = !this.checked
}) })
function getCurrentRenderDeviceSelection() { function getCurrentRenderDeviceSelection() {
@ -329,9 +362,9 @@ autoPickGPUsField.addEventListener('click', function() {
gpuSettingEntry.style.display = (this.checked ? 'none' : '') gpuSettingEntry.style.display = (this.checked ? 'none' : '')
}) })
async function setDiskPath(defaultDiskPath) { async function setDiskPath(defaultDiskPath, force=false) {
var diskPath = getSetting("diskPath") var diskPath = getSetting("diskPath")
if (diskPath == '' || diskPath == undefined || diskPath == "undefined") { if (force || diskPath == '' || diskPath == undefined || diskPath == "undefined") {
setSetting("diskPath", defaultDiskPath) setSetting("diskPath", defaultDiskPath)
} }
} }
@ -407,7 +440,17 @@ async function getSystemInfo() {
setDeviceInfo(devices) setDeviceInfo(devices)
setHostInfo(res['hosts']) setHostInfo(res['hosts'])
setDiskPath(res['default_output_dir']) let force = false
if (res['enforce_output_dir'] !== undefined) {
force = res['enforce_output_dir']
if (force == true) {
saveToDiskField.checked = true
metadataOutputFormatField.disabled = false
}
saveToDiskField.disabled = force
diskPathField.disabled = force
}
setDiskPath(res['default_output_dir'], force)
} catch (e) { } catch (e) {
console.log('error fetching devices', e) console.log('error fetching devices', e)
} }
@ -433,3 +476,4 @@ saveSettingsBtn.addEventListener('click', function() {
saveSettingsBtn.classList.add('active') saveSettingsBtn.classList.add('active')
asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active')) asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active'))
}) })

View File

@ -25,11 +25,13 @@ const PLUGINS = {
* }) * })
*/ */
IMAGE_INFO_BUTTONS: [], IMAGE_INFO_BUTTONS: [],
GET_PROMPTS_HOOK: [],
MODIFIERS_LOAD: [], MODIFIERS_LOAD: [],
TASK_CREATE: [], TASK_CREATE: [],
OUTPUTS_FORMATS: new ServiceContainer( OUTPUTS_FORMATS: new ServiceContainer(
function png() { return (reqBody) => new SD.RenderTask(reqBody) } function png() { return (reqBody) => new SD.RenderTask(reqBody) }
, function jpeg() { return (reqBody) => new SD.RenderTask(reqBody) } , function jpeg() { return (reqBody) => new SD.RenderTask(reqBody) }
, function webp() { return (reqBody) => new SD.RenderTask(reqBody) }
), ),
} }
PLUGINS.OUTPUTS_FORMATS.register = function(...args) { PLUGINS.OUTPUTS_FORMATS.register = function(...args) {

View File

@ -0,0 +1,687 @@
"use strict"
let modelsCache
let modelsOptions
/*
*** SEARCHABLE MODELS ***
Creates searchable dropdowns for SD, VAE, or HN models.
Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models).
More reload buttons may be added at strategic UI locations as needed.
Merely calling getModels() makes all the magic happen behind the scene to refresh the dropdowns.
HOW TO CREATE A MODEL DROPDOWN:
1) Create an input element. Make sure to add a data-path property, as this is how model dropdowns are identified in auto-save.js.
<input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
2) Just declare one of these for your own dropdown (remember to change the element id, e.g. #stable_diffusion_models to your own input's id).
let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion')
let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None')
let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None')
3) Model dropdowns will be refreshed automatically when the reload models button is invoked.
*/
class ModelDropdown
{
modelFilter //= document.querySelector("#model-filter")
modelFilterArrow //= document.querySelector("#model-filter-arrow")
modelList //= document.querySelector("#model-list")
modelResult //= document.querySelector("#model-result")
modelNoResult //= document.querySelector("#model-no-result")
currentSelection //= { elem: undefined, value: '', path: ''}
highlightedModelEntry //= undefined
activeModel //= undefined
inputModels //= undefined
modelKey //= undefined
flatModelList //= []
noneEntry //= ''
modelFilterInitialized //= undefined
/* MIMIC A REGULAR INPUT FIELD */
get parentElement() {
return this.modelFilter.parentElement
}
get parentNode() {
return this.modelFilter.parentNode
}
get value() {
return this.modelFilter.dataset.path
}
set value(path) {
this.modelFilter.dataset.path = path
this.selectEntry(path)
}
get disabled() {
return this.modelFilter.disabled
}
set disabled(state) {
this.modelFilter.disabled = state
if (this.modelFilterArrow) {
this.modelFilterArrow.style.color = state ? 'dimgray' : ''
}
}
get modelElements() {
return this.modelList.querySelectorAll('.model-file')
}
addEventListener(type, listener, options) {
return this.modelFilter.addEventListener(type, listener, options)
}
dispatchEvent(event) {
return this.modelFilter.dispatchEvent(event)
}
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)
}
}
/* SEARCHABLE INPUT */
constructor (input, modelKey, noneEntry = '') {
this.modelFilter = input
this.noneEntry = noneEntry
this.modelKey = modelKey
if (modelsOptions !== undefined) { // reuse models from cache (only useful for plugins, which are loaded after models)
this.inputModels = modelsOptions[this.modelKey]
this.populateModels()
}
document.addEventListener("refreshModels", this.bind(function(e) {
// reload the models
this.inputModels = modelsOptions[this.modelKey]
this.populateModels()
}, this))
}
saveCurrentSelection(elem, value, path) {
this.currentSelection.elem = elem
this.currentSelection.value = value
this.currentSelection.path = path
this.modelFilter.dataset.path = path
this.modelFilter.value = value
this.modelFilter.dispatchEvent(new Event('change'))
}
processClick(e) {
e.preventDefault()
if (e.srcElement.classList.contains('model-file') || e.srcElement.classList.contains('fa-file')) {
const elem = e.srcElement.classList.contains('model-file') ? e.srcElement : e.srcElement.parentElement
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
this.hideModelList()
this.modelFilter.focus()
this.modelFilter.select()
}
}
getPreviousVisibleSibling(elem) {
const modelElements = Array.from(this.modelElements)
const index = modelElements.indexOf(elem)
if (index <= 0) {
return undefined
}
return modelElements.slice(0, index).reverse().find(e => e.style.display === 'list-item')
}
getLastVisibleChild(elem) {
let lastElementChild = elem.lastElementChild
if (lastElementChild.style.display == 'list-item') return lastElementChild
return this.getPreviousVisibleSibling(lastElementChild)
}
getNextVisibleSibling(elem) {
const modelElements = Array.from(this.modelElements)
const index = modelElements.indexOf(elem)
return modelElements.slice(index + 1).find(e => e.style.display === 'list-item')
}
getFirstVisibleChild(elem) {
let firstElementChild = elem.firstElementChild
if (firstElementChild.style.display == 'list-item') return firstElementChild
return this.getNextVisibleSibling(firstElementChild)
}
selectModelEntry(elem) {
if (elem) {
if (this.highlightedModelEntry !== undefined) {
this.highlightedModelEntry.classList.remove('selected')
}
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
elem.classList.add('selected')
elem.scrollIntoView({block: 'nearest'})
this.highlightedModelEntry = elem
}
}
selectPreviousFile() {
const elem = this.getPreviousVisibleSibling(this.highlightedModelEntry)
if (elem) {
this.selectModelEntry(elem)
}
else
{
//this.highlightedModelEntry.parentElement.parentElement.scrollIntoView({block: 'nearest'})
this.highlightedModelEntry.closest('.model-list').scrollTop = 0
}
this.modelFilter.select()
}
selectNextFile() {
this.selectModelEntry(this.getNextVisibleSibling(this.highlightedModelEntry))
this.modelFilter.select()
}
selectFirstFile() {
this.selectModelEntry(this.modelList.querySelector('.model-file'))
this.highlightedModelEntry.scrollIntoView({block: 'nearest'})
this.modelFilter.select()
}
selectLastFile() {
const elems = this.modelList.querySelectorAll('.model-file:last-child')
this.selectModelEntry(elems[elems.length -1])
this.modelFilter.select()
}
resetSelection() {
this.hideModelList()
this.showAllEntries()
this.modelFilter.value = this.currentSelection.value
this.modelFilter.focus()
this.modelFilter.select()
}
validEntrySelected() {
return (this.modelNoResult.style.display === 'none')
}
processKey(e) {
switch (e.key) {
case 'Escape':
e.preventDefault()
this.resetSelection()
break
case 'Enter':
e.preventDefault()
if (this.validEntrySelected()) {
if (this.modelList.style.display != 'block') {
this.showModelList()
}
else
{
this.saveCurrentSelection(this.highlightedModelEntry, this.highlightedModelEntry.innerText, this.highlightedModelEntry.dataset.path)
this.hideModelList()
this.showAllEntries()
}
this.modelFilter.focus()
}
else
{
this.resetSelection()
}
break
case 'ArrowUp':
e.preventDefault()
if (this.validEntrySelected()) {
this.selectPreviousFile()
}
break
case 'ArrowDown':
e.preventDefault()
if (this.validEntrySelected()) {
this.selectNextFile()
}
break
case 'ArrowLeft':
if (this.modelList.style.display != 'block') {
e.preventDefault()
}
break
case 'ArrowRight':
if (this.modelList.style.display != 'block') {
e.preventDefault()
}
break
case 'PageUp':
e.preventDefault()
if (this.validEntrySelected()) {
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
this.selectPreviousFile()
}
break
case 'PageDown':
e.preventDefault()
if (this.validEntrySelected()) {
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
this.selectNextFile()
}
break
case 'Home':
//if (this.modelList.style.display != 'block') {
e.preventDefault()
if (this.validEntrySelected()) {
this.selectFirstFile()
}
//}
break
case 'End':
//if (this.modelList.style.display != 'block') {
e.preventDefault()
if (this.validEntrySelected()) {
this.selectLastFile()
}
//}
break
default:
//console.log(e.key)
}
}
modelListFocus() {
this.selectEntry()
this.showAllEntries()
}
showModelList() {
this.modelList.style.display = 'block'
this.selectEntry()
this.showAllEntries()
//this.modelFilter.value = ''
this.modelFilter.select() // preselect the entire string so user can just start typing.
this.modelFilter.focus()
this.modelFilter.style.cursor = 'auto'
}
hideModelList() {
this.modelList.style.display = 'none'
this.modelFilter.value = this.currentSelection.value
this.modelFilter.style.cursor = ''
}
toggleModelList(e) {
e.preventDefault()
if (!this.modelFilter.disabled) {
if (this.modelList.style.display != 'block') {
this.showModelList()
}
else
{
this.hideModelList()
this.modelFilter.select()
}
}
}
selectEntry(path) {
if (path !== undefined) {
const entries = this.modelElements;
for (const elem of entries) {
if (elem.dataset.path == path) {
this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path)
this.highlightedModelEntry = elem
elem.scrollIntoView({block: 'nearest'})
break
}
}
}
if (this.currentSelection.elem !== undefined) {
// select the previous element
if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != this.currentSelection.elem) {
this.highlightedModelEntry.classList.remove('selected')
}
this.currentSelection.elem.classList.add('selected')
this.highlightedModelEntry = this.currentSelection.elem
this.currentSelection.elem.scrollIntoView({block: 'nearest'})
}
else
{
this.selectFirstFile()
}
}
highlightModelAtPosition(e) {
let elem = document.elementFromPoint(e.clientX, e.clientY)
if (elem.classList.contains('model-file')) {
this.highlightModel(elem)
}
}
highlightModel(elem) {
if (elem.classList.contains('model-file')) {
if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != elem) {
this.highlightedModelEntry.classList.remove('selected')
}
elem.classList.add('selected')
this.highlightedModelEntry = elem
}
}
showAllEntries() {
this.modelList.querySelectorAll('li').forEach(function(li) {
if (li.id !== 'model-no-result') {
li.style.display = 'list-item'
}
})
this.modelNoResult.style.display = 'none'
}
filterList(e) {
const filter = this.modelFilter.value.toLowerCase()
let found = false
let showAllChildren = false
this.modelList.querySelectorAll('li').forEach(function(li) {
if (li.classList.contains('model-folder')) {
showAllChildren = false
}
if (filter == '') {
li.style.display = 'list-item'
found = true
} else if (showAllChildren || li.textContent.toLowerCase().match(filter)) {
li.style.display = 'list-item'
if (li.classList.contains('model-folder') && li.firstChild.textContent.toLowerCase().match(filter)) {
showAllChildren = true
}
found = true
} else {
li.style.display = 'none'
}
})
if (found) {
this.modelResult.style.display = 'list-item'
this.modelNoResult.style.display = 'none'
const elem = this.getNextVisibleSibling(this.modelList.querySelector('.model-file'))
this.highlightModel(elem)
elem.scrollIntoView({block: 'nearest'})
}
else
{
this.modelResult.style.display = 'none'
this.modelNoResult.style.display = 'list-item'
}
this.modelList.style.display = 'block'
}
/* MODEL LOADER */
getElementDimensions(element) {
// Clone the element
const clone = element.cloneNode(true)
// Copy the styles of the original element to the cloned element
const originalStyles = window.getComputedStyle(element)
for (let i = 0; i < originalStyles.length; i++) {
const property = originalStyles[i]
clone.style[property] = originalStyles.getPropertyValue(property)
}
// Set its visibility to hidden and display to inline-block
clone.style.visibility = "hidden"
clone.style.display = "inline-block"
// Put the cloned element next to the original element
element.parentNode.insertBefore(clone, element.nextSibling)
// Get its width and height
const width = clone.offsetWidth
const height = clone.offsetHeight
// Remove it from the DOM
clone.remove()
// Return its width and height
return { width, height }
}
/**
* @param {Array<string>} models
*/
sortStringArray(models) {
models.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' }))
}
populateModels() {
this.activeModel = this.modelFilter.dataset.path
this.currentSelection = { elem: undefined, value: '', path: ''}
this.highlightedModelEntry = undefined
this.flatModelList = []
if(this.modelList !== undefined) {
this.modelList.remove()
this.modelFilterArrow.remove()
}
this.createDropdown()
}
createDropdown() {
// create dropdown entries
let rootModelList = this.createRootModelList(this.inputModels)
this.modelFilter.insertAdjacentElement('afterend', rootModelList)
this.modelFilter.insertAdjacentElement(
'afterend',
this.createElement(
'i',
{ id: `${this.modelFilter.id}-model-filter-arrow` },
['model-selector-arrow', 'fa-solid', 'fa-angle-down'],
),
)
this.modelFilter.classList.add('model-selector')
this.modelFilterArrow = document.querySelector(`#${this.modelFilter.id}-model-filter-arrow`)
if (this.modelFilterArrow) {
this.modelFilterArrow.style.color = this.modelFilter.disabled ? 'dimgray' : ''
}
this.modelList = document.querySelector(`#${this.modelFilter.id}-model-list`)
this.modelResult = document.querySelector(`#${this.modelFilter.id}-model-result`)
this.modelNoResult = document.querySelector(`#${this.modelFilter.id}-model-no-result`)
if (this.modelFilterInitialized !== true) {
this.modelFilter.addEventListener('input', this.bind(this.filterList, this))
this.modelFilter.addEventListener('focus', this.bind(this.modelListFocus, this))
this.modelFilter.addEventListener('blur', this.bind(this.hideModelList, this))
this.modelFilter.addEventListener('click', this.bind(this.showModelList, this))
this.modelFilter.addEventListener('keydown', this.bind(this.processKey, this))
this.modelFilterInitialized = true
}
this.modelFilterArrow.addEventListener('mousedown', this.bind(this.toggleModelList, this))
this.modelList.addEventListener('mousemove', this.bind(this.highlightModelAtPosition, this))
this.modelList.addEventListener('mousedown', this.bind(this.processClick, this))
let mf = this.modelFilter
this.modelFilter.addEventListener('focus', function() {
let modelFilterStyle = window.getComputedStyle(mf)
rootModelList.style.minWidth = modelFilterStyle.width
})
this.selectEntry(this.activeModel)
}
/**
*
* @param {string} tag
* @param {object} attributes
* @param {Array<string>} classes
* @returns {HTMLElement}
*/
createElement(tagName, attributes, classes, text, icon) {
const element = document.createElement(tagName)
if (attributes) {
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value)
})
}
if (classes) {
classes.forEach(className => element.classList.add(className))
}
if (icon) {
let iconEl = document.createElement('i')
iconEl.className = icon + ' icon'
element.appendChild(iconEl)
}
if (text) {
element.appendChild(document.createTextNode(text))
}
return element
}
/**
* @param {Array<string | object} modelTree
* @param {string} folderName
* @param {boolean} isRootFolder
* @returns {HTMLElement}
*/
createModelNodeList(folderName, modelTree, isRootFolder) {
const listElement = this.createElement('ul')
const foldersMap = new Map()
const modelsMap = new Map()
modelTree.forEach(model => {
if (Array.isArray(model)) {
const [childFolderName, childModels] = model
foldersMap.set(
childFolderName,
this.createModelNodeList(
`${folderName || ''}/${childFolderName}`,
childModels,
false,
),
)
} else {
const classes = ['model-file']
if (isRootFolder) {
classes.push('in-root-folder')
}
// Remove the leading slash from the model path
const fullPath = folderName ? `${folderName.substring(1)}/${model}` : model
modelsMap.set(
model,
this.createElement('li', { 'data-path': fullPath }, classes, model, 'fa-regular fa-file'),
)
}
})
const childFolderNames = Array.from(foldersMap.keys())
this.sortStringArray(childFolderNames)
const folderElements = childFolderNames.map(name => foldersMap.get(name))
const modelNames = Array.from(modelsMap.keys())
this.sortStringArray(modelNames)
const modelElements = modelNames.map(name => modelsMap.get(name))
if (modelElements.length && folderName) {
listElement.appendChild(this.createElement('li', undefined, ['model-folder'], folderName.substring(1), 'fa-solid fa-folder-open'))
}
// const allModelElements = isRootFolder ? [...folderElements, ...modelElements] : [...modelElements, ...folderElements]
const allModelElements = [...modelElements, ...folderElements]
allModelElements.forEach(e => listElement.appendChild(e))
return listElement
}
/**
* @param {object} modelTree
* @returns {HTMLElement}
*/
createRootModelList(modelTree) {
const rootList = this.createElement(
'ul',
{ id: `${this.modelFilter.id}-model-list` },
['model-list'],
)
rootList.appendChild(
this.createElement(
'li',
{ id: `${this.modelFilter.id}-model-no-result` },
['model-no-result'],
'No result'
),
)
if (this.noneEntry) {
rootList.appendChild(
this.createElement(
'li',
{ 'data-path': '' },
['model-file', 'in-root-folder'],
this.noneEntry,
),
)
}
if (modelTree.length > 0) {
const containerListItem = this.createElement(
'li',
{ id: `${this.modelFilter.id}-model-result` },
['model-result'],
)
//console.log(containerListItem)
containerListItem.appendChild(this.createModelNodeList(undefined, modelTree, true))
rootList.appendChild(containerListItem)
}
return rootList
}
}
/* (RE)LOAD THE MODELS */
async function getModels() {
try {
modelsCache = await SD.getModels()
modelsOptions = modelsCache['options']
if ("scan-error" in modelsCache) {
// let previewPane = document.getElementById('tab-content-wrapper')
let previewPane = document.getElementById('preview')
previewPane.style.background="red"
previewPane.style.textAlign="center"
previewPane.innerHTML = '<H1>🔥Malware alert!🔥</H1><h2>The file <i>' + modelsCache['scan-error'] + '</i> in your <tt>models/stable-diffusion</tt> folder is probably malware infected.</h2><h2>Please delete this file from the folder before proceeding!</h2>After deleting the file, reload this page.<br><br><button onClick="window.location.reload();">Reload Page</button>'
makeImageBtn.disabled = true
}
/* This code should no longer be needed. Commenting out for now, will cleanup later.
const sd_model_setting_key = "stable_diffusion_model"
const vae_model_setting_key = "vae_model"
const hypernetwork_model_key = "hypernetwork_model"
const stableDiffusionOptions = modelsOptions['stable-diffusion']
const vaeOptions = modelsOptions['vae']
const hypernetworkOptions = modelsOptions['hypernetwork']
// TODO: set default for model here too
SETTINGS[sd_model_setting_key].default = stableDiffusionOptions[0]
if (getSetting(sd_model_setting_key) == '' || SETTINGS[sd_model_setting_key].value == '') {
setSetting(sd_model_setting_key, stableDiffusionOptions[0])
}
*/
// notify ModelDropdown objects to refresh
document.dispatchEvent(new Event('refreshModels'))
} catch (e) {
console.log('get models error', e)
}
}
// reload models button
document.querySelector('#reload-models').addEventListener('click', getModels)

View File

@ -20,19 +20,6 @@ function getNextSibling(elem, selector) {
} }
} }
function findClosestAncestor(element, selector) {
if (!element || !element.parentNode) {
// reached the top of the DOM tree, return null
return null;
} else if (element.parentNode.matches(selector)) {
// found an ancestor that matches the selector, return it
return element.parentNode;
} else {
// continue searching upwards
return findClosestAncestor(element.parentNode, selector);
}
}
/* Panel Stuff */ /* Panel Stuff */
@ -522,6 +509,9 @@ function makeQuerablePromise(promise) {
/* inserts custom html to allow prettifying of inputs */ /* inserts custom html to allow prettifying of inputs */
function prettifyInputs(root_element) { function prettifyInputs(root_element) {
root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => { root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => {
if (element.style.display === "none") {
return
}
var parent = element.parentNode; var parent = element.parentNode;
if (!parent.classList.contains("input-toggle")) { if (!parent.classList.contains("input-toggle")) {
var wrapper = document.createElement("div"); var wrapper = document.createElement("div");

View File

@ -1,27 +1,7 @@
(function () { (function () {
"use strict" "use strict"
var styleSheet = document.createElement("style");
styleSheet.textContent = `
.auto-scroll {
float: right;
}
`;
document.head.appendChild(styleSheet);
const autoScrollControl = document.createElement('div');
autoScrollControl.innerHTML = `<input id="auto_scroll" name="auto_scroll" type="checkbox">
<label for="auto_scroll">Scroll to generated image</label>`
autoScrollControl.className = "auto-scroll"
clearAllPreviewsBtn.parentNode.insertBefore(autoScrollControl, clearAllPreviewsBtn.nextSibling)
prettifyInputs(document);
let autoScroll = document.querySelector("#auto_scroll") let autoScroll = document.querySelector("#auto_scroll")
// save/restore the toggle state
autoScroll.addEventListener('click', (e) => {
localStorage.setItem('auto_scroll', autoScroll.checked)
})
autoScroll.checked = localStorage.getItem('auto_scroll') == "true"
// observe for changes in the preview pane // observe for changes in the preview pane
var observer = new MutationObserver(function (mutations) { var observer = new MutationObserver(function (mutations) {
@ -39,7 +19,10 @@
function Autoscroll(target) { function Autoscroll(target) {
if (autoScroll.checked && target !== null) { if (autoScroll.checked && target !== null) {
target.parentElement.parentElement.parentElement.scrollIntoView(); const img = target.querySelector('img')
img.addEventListener('load', function() {
img.closest('.imageTaskContainer').scrollIntoView()
}, { once: true })
} }
} }
})() })()

View File

@ -134,7 +134,7 @@
/////////////////////// Tab implementation /////////////////////// Tab implementation
document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', `
<span id="tab-merge" class="tab"> <span id="tab-merge" class="tab">
<span><i class="fa fa-code-merge icon"></i> Merge models <small>(beta)</small></span> <span><i class="fa fa-code-merge icon"></i> Merge models</span>
</span> </span>
`) `)
@ -241,13 +241,9 @@
<div class="merge-container panel-box"> <div class="merge-container panel-box">
<div class="merge-input"> <div class="merge-input">
<p><label for="#mergeModelA">Select Model A:</label></p> <p><label for="#mergeModelA">Select Model A:</label></p>
<select id="mergeModelA"> <input id="mergeModelA" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<option>A</option>
</select>
<p><label for="#mergeModelB">Select Model B:</label></p> <p><label for="#mergeModelB">Select Model B:</label></p>
<select id="mergeModelB"> <input id="mergeModelB" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<option>A</option>
</select>
<br/><br/> <br/><br/>
<p id="merge-warning"><small><b>Important:</b> Please merge models of similar type.<br/>For e.g. <code>SD 1.4</code> models with only <code>SD 1.4/1.5</code> models,<br/><code>SD 2.0</code> with <code>SD 2.0</code>-type, and <code>SD 2.1</code> with <code>SD 2.1</code>-type models.</small></p> <p id="merge-warning"><small><b>Important:</b> Please merge models of similar type.<br/>For e.g. <code>SD 1.4</code> models with only <code>SD 1.4/1.5</code> models,<br/><code>SD 2.0</code> with <code>SD 2.0</code>-type, and <code>SD 2.1</code> with <code>SD 2.1</code>-type models.</small></p>
<br/> <br/>
@ -338,19 +334,10 @@
linkTabContents(tabSettingsSingle) linkTabContents(tabSettingsSingle)
linkTabContents(tabSettingsBatch) linkTabContents(tabSettingsBatch)
/////////////////////// Event Listener console.log('Activate')
document.addEventListener('tabClick', (e) => { let mergeModelAField = new ModelDropdown(document.querySelector('#mergeModelA'), 'stable-diffusion')
if (e.detail.name == 'merge') { let mergeModelBField = new ModelDropdown(document.querySelector('#mergeModelB'), 'stable-diffusion')
console.log('Activate') updateChart()
let modelList = stableDiffusionModelField.cloneNode(true)
modelList.id = "mergeModelA"
document.querySelector("#mergeModelA").replaceWith(modelList)
modelList = stableDiffusionModelField.cloneNode(true)
modelList.id = "mergeModelB"
document.querySelector("#mergeModelB").replaceWith(modelList)
updateChart()
}
})
// slider // slider
const singleMergeRatioField = document.querySelector('#single-merge-ratio') const singleMergeRatioField = document.querySelector('#single-merge-ratio')

View File

@ -38,9 +38,9 @@
i.parentElement.classList.add('modifier-toggle-inactive') i.parentElement.classList.add('modifier-toggle-inactive')
} }
// refresh activeTags // refresh activeTags
let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].dataset.fullName
activeTags = activeTags.map(obj => { activeTags = activeTags.map(obj => {
if (obj.name === modifierName) { if (trimModifiers(obj.name) === trimModifiers(modifierName)) {
return {...obj, inactive: (obj.element.classList.contains('modifier-toggle-inactive'))}; return {...obj, inactive: (obj.element.classList.contains('modifier-toggle-inactive'))};
} }