Compare commits

...

1178 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
070e51fcab Changelog 2023-02-09 19:01:25 +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
7bc95b68c8 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-08 19:43:31 +05:30
0332cc8cb3 Don't force the user to 'low' VRAM usage automatically, if their GPU is less than 4 GB of VRAM. We need a better way to set 'low' as the default in the UI, but the user should be able to override it if they want 2023-02-08 19:41:55 +05:30
ce192f4ad7 Merge pull request #839 from cmdr2/main
Merge from main
2023-02-08 11:44:51 +05:30
cbdb715918 Merge pull request #838 from cmdr2/beta
Beta
2023-02-08 11:19:59 +05:30
5537102fd3 changelog 2023-02-08 11:19:16 +05:30
1ea294f15c Fix broken auto-save settings. We renamed sampler to sampler_name, which causes old settings to fail 2023-02-08 11:18:28 +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
4c8da67bb1 Use "python -m pip" instead of "pip" (#835)
* Use "python -m pip" instead of "pip"

https://discord.com/channels/1014774730907209781/1072423234676461619

* Use "python -m" instead of "pip" (Linux=
2023-02-07 15:39:02 +05:30
a0178e15b3 More robust relative path calculation 2023-02-06 22:19:57 -08:00
43a1c3901f ED favicon (#832) 2023-02-07 11:32:55 +05:30
a4c6f28a70 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-07 11:32:06 +05:30
f8bca93170 ED favicon 2023-02-07 11:31:56 +05:30
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
19b05659b5 Update README.md 2023-02-06 23:07:11 +05:30
7e5c7ca1b7 Easy Diffusion 2.5 2023-02-06 22:50:18 +05:30
1156c159f9 Merge pull request #827 from cmdr2/beta
v2.5
2023-02-06 20:11:18 +05:30
5c6c2303ba Why does this script file keep losing exec permission? 2023-02-06 20:05:40 +05:30
a0a58bcfa8 Merge branch 'main' into beta 2023-02-06 19:42:24 +05:30
8a28b265a3 Preserve the id of the top-level tabs container, to avoid breaking plugins that rely on it 2023-02-06 19:09:39 +05:30
86dc08130b typo 2023-02-06 16:47:48 +05:30
5cd8a732c7 grammar 2023-02-06 16:29:46 +05:30
fafbbf68a4 changelog 2023-02-06 16:20:38 +05:30
0cbb553564 Follow the theme in the popup dialog box 2023-02-06 15:32:54 +05:30
f4512bb291 Color of close button 2023-02-06 15:19:10 +05:30
99205b4d03 Show an X over an image, instead of a remove button in image options 2023-02-06 15:14:47 +05:30
d48e6554d5 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-06 13:49:38 +05:30
d0c4e95de3 Simplify the UI of the model merge tab; Allows a user to merge a single model, or a batch of variations; Also fixes a few logging bugs in the model merge tab 2023-02-06 13:49:15 +05:30
0b3a35c4b6 Make the tabs container a class, to make it reusable for other tab groups 2023-02-06 13:48:18 +05:30
ded6a41f86 Only disable the sibling tabs when a particular tab is selected. This allows the 'tab' management code to be reused for nested tabs 2023-02-06 13:46:40 +05:30
f4063e63d3 Merge pull request #824 from JeLuF/pause2
Fix 'Pause All' function
2023-02-06 10:18:51 +05:30
23ba912db0 Fix 'Pause All' function
If 'pause all' is clicked during the last scheduled job, the 'resume all' button gets hidden when the jobs terminates, making it
impossible to unpause the engine.
https://discord.com/channels/1014774730907209781/1014780368890630164/1071584183417323602
2023-02-05 17:33:43 +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
368967fbcf Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-02-03 21:41:23 +05:30
a9d0fc9978 changelog 2023-02-03 21:41:12 +05:30
b6f3d2ec02 Formatting 2023-02-03 21:40:08 +05:30
78e917a6fb Fix the broken 'Make Similar Images' button 2023-02-03 21:40:03 +05:30
96b45385e8 Merge pull request #803 from JeLuF/patch-10
Add T600 to list of FP only GPUs
2023-02-03 19:56:54 +05:30
db47888a75 changelog 2023-02-01 11:54:05 +05:30
51443741b8 Proactively delete the partial samples from the callbacks 2023-02-01 11:50:50 +05:30
3e7f14af2c Don't use Rich Tracebacks, can cause a memory leak. It keeps a reference to the Exception object (which in turn keeps references to any torch Tensors in the stack, preventing their garbage-collection) 2023-02-01 11:50:27 +05:30
733439da07 Fix a memory leak. Apparently the Exception object keeps references to torch Tensors in the stack, so keeping a reference to the Exception object prevents those Tensors from getting garbage-collected. 2023-02-01 11:49:18 +05:30
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
efba81cb66 Add T1000, make Quadro equivalent to nvidia or geforce 2023-01-28 20:51:01 +01:00
b2cc5dcf4b Add T600 to list of FP only GPUs
https://discord.com/channels/1014774730907209781/1068948110304354314
2023-01-28 20:18:07 +01:00
fab86ddf35 changelog 2023-01-27 09:46:50 +05:30
f3a90ce02d Formatting tweaks and tip about merging similar type of models 2023-01-25 20:05:27 +05:30
4886616c48 changelog 2023-01-25 19:52:28 +05:30
dcd8121009 Revert "Temporarily disable the Merge Models UI"
This reverts commit 59adaf6225.
2023-01-25 19:51:12 +05:30
59adaf6225 Temporarily disable the Merge Models UI 2023-01-25 19:46:55 +05:30
0055cd9b2e Merge pull request #734 from JeLuF/mrguipi
Frontend of the batch merger
2023-01-25 19:39:19 +05:30
fe89d487f6 Merge pull request #733 from JeLuF/mrgui
Backend side merge API
2023-01-25 19:38:21 +05:30
01368ac496 Add support for Windows path names 2023-01-25 02:47:50 -08:00
495064985e Reduce VRAM usage of img2img in balanced mode, without reducing the speed of rendering 2023-01-24 18:58:15 +05:30
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
e12387a377 changelog 2023-01-23 21:40:50 +05:30
5d3fb9091a Reduce the VRAM usage for balanced mode, without sacrificing the rendering speed 2023-01-23 19:36:00 +05:30
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
e2ae2715a3 Revert "Revert "Don't set the specific vram optimizations to use, instead use the new sdkit API for setting the vram usage level directly""
This reverts commit 52458ae273.
2023-01-18 17:03:14 +05:30
52458ae273 Revert "Don't set the specific vram optimizations to use, instead use the new sdkit API for setting the vram usage level directly"
This reverts commit 42f9abdfe3.
2023-01-18 10:30:56 +05:30
79d112ca7b Warn when running installer from git checkout 2023-01-18 00:11:23 +01:00
9b1a9cc7c8 changelog 2023-01-17 21:34:41 +05:30
42f9abdfe3 Don't set the specific vram optimizations to use, instead use the new sdkit API for setting the vram usage level directly 2023-01-17 21:33:15 +05:30
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
0a1197055c changelog 2023-01-16 18:32:09 +05:30
649cbf07e3 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-01-16 18:30:46 +05:30
5089ac5ad1 Fix a bug where the .vae.pt extension wouldn't get picked up. Thanks Madrang, rbertus2000 and JeLuf 2023-01-16 18:30:22 +05:30
d99e3f7974 Merge pull request #776 from JeLuF/patch-8
Add NVIDIA T1200 to the list of FP GPUs
2023-01-16 18:09:06 +05:30
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
b5d1912c94 Add NVIDIA T1200 to the list of FP GPUs
Fixes https://discord.com/channels/1014774730907209781/1014774732018683926/1064269949339697163
2023-01-16 00:42:02 +01: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
8ee4364065 Merge pull request #768 from rbertus2000/beta
bugfix for FP GPUs
2023-01-13 17:39:49 +05:30
152aa7de09 bugfix for FP GPUs 2023-01-13 12:54:11 +01:00
85c90cbee1 Merge pull request #764 from JeLuF/patch-7
Add NVIDIA T550 to list of FP GPUs #755
2023-01-13 10:18:24 +05:30
7302927e4c Add NVIDIA T550 to list of FP GPUs #755
The Nvidia T550 needs full precision to work correctly.
2023-01-12 14:16:35 +01:00
df3d00ef94 Merge pull request #763 from patriceac/patch-18
Another fix for high res images
2023-01-12 10:23:01 +05:30
bb47835256 Another fix for high res images
This time to address the height.
2023-01-11 17:25:54 -08:00
037512ca5c Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2023-01-11 18:25:16 +05:30
a13713adaf Don't search for a yaml config file next to the model, since sdkit now does this automatically 2023-01-11 18:23:56 +05:30
ad073252e7 Merge pull request #762 from patriceac/patch-17
Fix the restoring of the previous nested model
2023-01-11 14:58:25 +05:30
d24a7a5c5e Fix the restoring of the last selected model 2023-01-10 19:00:19 -08:00
192fd223b4 use config.json instead of config.bat 2023-01-10 23:40:35 +01:00
a671dd8e00 Fix import, remove debug output 2023-01-10 20:34:17 +01:00
8b764a8fd3 changelog 2023-01-10 21:58:29 +05:30
aa576e68e3 Bring back the default opacity of 0.4 for inpainting mask, even though it leads to some other bugs. It's not a good UX to have an inpainting mask with full opacity 2023-01-10 21:56:26 +05:30
ad5508a14d Fix typo 2023-01-10 21:54:31 +05:30
4fafc8aa67 Merge pull request #685 from mdiller/mdiller_bugfixes
Mdiller bugfixes
2023-01-10 21:44:40 +05:30
0aab3d0f12 Merge pull request #744 from AssassinJN/patch-2
return taskEntry.id on createTask
2023-01-10 21:41:56 +05:30
a5d88bdfcc changelog 2023-01-10 21:09:08 +05:30
5173957368 Minor refactor of save file 2023-01-10 20:13:39 +05:30
4b3e3d900d Merge pull request #745 from JeLuF/sync-fn
Synchronize .img and .txt autosave file names
2023-01-10 20:07:17 +05:30
9ea51b174a Merge branch 'beta' into sync-fn 2023-01-10 20:06:58 +05:30
80e265e547 Merge pull request #746 from JeLuF/modelload
Don't crash on unsupported models
2023-01-10 20:01:24 +05:30
c3e6e63023 Merge pull request #754 from patriceac/patch-15
Fix display of very large images
2023-01-10 20:00:00 +05:30
9b5a262d63 Merge pull request #758 from patriceac/patch-16
Fix image editor display
2023-01-10 19:56:18 +05:30
1309f1480c Tabs to spaces 2023-01-10 19:48:36 +05:30
12ba5b8096 Merge pull request #753 from JeLuF/modeldir
Recursive scanning for models
2023-01-10 19:29:27 +05:30
156c5f4792 Fix incorrect seeds returned when no filters were applied. Fixes https://github.com/cmdr2/stable-diffusion-ui/pull/748 2023-01-10 19:23:17 +05:30
1da4b3d94a Not all browsers return the PerformanceEntry object on performance.measure(). Fix credit @JeLuf 2023-01-10 10:01:24 +05:30
18aca98e41 Fix image editor display
Fix for the cut off controls
2023-01-09 09:29:31 -08:00
a88afb0956 Add paths to the value field 2023-01-09 18:24:04 +01:00
bfa1f57930 Fix rendering of very large images
See comments for screenshots.
2023-01-09 09:21:16 -08:00
a5350eb3cc changelog 2023-01-09 19:42:06 +05:30
3ed4d792b3 Check whether the browser supports performance.measure/mark before calling them. Fixes https://github.com/cmdr2/stable-diffusion-ui/pull/757 2023-01-09 19:41:10 +05:30
fb0c9405cf changelog 2023-01-09 19:40:17 +05:30
a17a9044ad Check whether the browser supports performance.measure/mark before calling them. Fixes https://github.com/cmdr2/stable-diffusion-ui/pull/757 2023-01-09 19:33:23 +05:30
73af7f5481 Use a boolean .includes() instead of a regex match() for checking string contains 2023-01-09 19:19:30 +05:30
57ead7f0c0 Merge pull request #752 from patriceac/patch-14
Fix parsing of text file tasks
2023-01-09 19:16:36 +05:30
bf490c910a changelog 2023-01-09 18:48:15 +05:30
40f806efa8 Merge pull request #742 from JeLuF/noise
Prevent flooding the log with warnings for GPU<3GB
2023-01-09 18:47:20 +05:30
226ba8b06e Bump version 2023-01-09 18:39:24 +05:30
b11aa4833d Merge pull request #724 from patriceac/img2img-settings-restoration
Img2img settings restoration
2023-01-09 18:36:32 +05:30
8d9cd0e30b Fix display of very large images 2023-01-07 15:04:07 -08:00
9532928998 Recursive scanning for models 2023-01-07 19:04:15 +01:00
420f7549a2 Fix parsing of text file tasks
parseContent(text) doesn't check the text content being passed actually described a task, which causes some corner case scenarios to break (image task settings are incorrectly cleared because an empty image task is created).
2023-01-07 00:47:30 -08:00
ed64b9bfed Don't crash on unsupported models 2023-01-06 01:41:55 +01:00
5d5ebfdef6 Synchronize .img and .txt autosave file names 2023-01-04 16:51:18 +01:00
567c02bf5d return taskEntry.id on createTask
I would like to have createTask return the taskEntry.id in order to allow for watchers or callbacks to be able to reference tasks by id more easily.
2023-01-04 10:04:52 -05:00
60f7c73c8a prevent flooding the log with warnings for GPU<3GB 2023-01-04 02:45:51 +01:00
ac4c5003f1 also empty VAE and hypernetwork fields 2023-01-03 08:23:42 +01:00
d5e76e662f Enforce a autosave directory 2022-12-30 21:05:25 +01:00
23d5f85d17 Frontend batch merger 2022-12-30 10:13:34 +01:00
f75adc1e22 added fill tool and updated as requested in pull request 2022-12-30 01:07:46 -08:00
15a1436c8b Backend side merge API 2022-12-30 10:07:23 +01:00
813edec808 Removing one more unnecessary custom event 2022-12-29 09:43:12 -08:00
21e3299b7a Applying changes from latest CR
- Replaced custom event with load event
- Removed the custom event dispatch
2022-12-29 09:26:32 -08:00
f7193966fb Addressing Cmdr2's comments and more
Only triggers events when there actually was a state  change. Also opportunistically removed the hardcoded delay in favor of an even-driven flow, which makes the whole thing more robust and much more reactive.
2022-12-29 01:16:44 -08:00
2d9853f1f4 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-29 13:25:25 +05:30
ced79a187d changelog 2022-12-29 13:25:13 +05:30
7832524963 Merge pull request #729 from patriceac/patch-12
ESC keyboard shortcut to close the image editor
2022-12-29 13:23:00 +05:30
58c7f3ba15 ESC keyboard shortcut to close the image editor 2022-12-28 23:50:56 -08:00
90ec8f0575 changelog 2022-12-29 13:17:26 +05:30
64ced3b3f6 Tag v2.4.23, to be able to revert back incase of an emergency 2022-12-29 13:04:44 +05:30
493526c478 If downgrading to 2.4 (from 2.5), move the default models back to the legacy location 2022-12-29 13:00:57 +05:30
b86617e3af Merge pull request #720 from patriceac/restore-inactive-modifiers
Proper restoration of inactive image modifiers
2022-12-29 10:28:28 +05:30
f3db6d84fb Merge pull request #721 from patriceac/patch-8
Fix restoration of hypernetwork dropdown
2022-12-29 10:26:54 +05:30
f9b9ecf754 Merge branch 'beta' into patch-8 2022-12-29 10:26:48 +05:30
af43a92a2f Merge pull request #725 from patriceac/patch-9
Limit the size of zoomed-in source images
2022-12-29 10:18:17 +05:30
4dbdc642f9 Merge pull request #726 from patriceac/patch-10
Persist the processing order toggle across sessions
2022-12-29 10:17:24 +05:30
8f2c87ce94 Merge pull request #717 from jsuelwald/patch-1
Restore download link for Linux in beta, ...
2022-12-29 10:16:59 +05:30
5149040496 Merge pull request #727 from patriceac/patch-11
Restore the original prompt if provided
2022-12-29 10:15:22 +05:30
5b1078e0db Merge pull request #719 from patriceac/fix-duplicate-image
Fix for duplicate images
2022-12-29 10:13:51 +05:30
ae31813239 Restore the original prompt if provided
Restore the original prompt if provided... including if it's empty now that empty prompts are allowed if there are modifiers.
2022-12-28 18:52:18 -08:00
f6b3cde286 Persist the process order toggle across sessions
🤷
2022-12-28 17:50:18 -08:00
0f05f9c32c Limit the size of zoomed-in source images
If the source image has a high enough resolution it won't fit on the screen when hovering over it. This simple fix limits the max size so the user always has a chance to see the full image.
2022-12-28 17:30:59 -08:00
89170af721 Proper source image unloading 2022-12-28 17:00:38 -08:00
5fddae589b Reverting duplicate hypernetwork fix 2022-12-28 16:54:36 -08:00
19c16af5fa Fix img2img task restoration
Fix source image, mask, and color profile restoration for use settings, copy/paste, and d&d.
2022-12-28 16:43:35 -08:00
019f8f69f4 Fix restoration of hypernetwork dropdown
Fix for https://discord.com/channels/1014774730907209781/1014774732018683928/1055508538228748368
2022-12-28 15:55:59 -08:00
ad8d1f77df Proper restoration of inactive image modifiers
Inactive image modifiers (right click on image tag) are not properly restored by Use Settings and Copy/Paste settings. This PR fixes that.
2022-12-28 13:41:36 -08:00
e82a8a7f3d Fix for duplicate images
When eye correction, upscaling, and only show filtered image are ALL disabled, the UI still generates two of the same image, and increments the second's seed by 1 (although it's the same image). It doesn't perform an additional process, but the item is shown twice.
2022-12-28 12:06:36 -08:00
ad07aeb041 Restore download link for Linux in beta, ...
and make shellscripts in scripts/ executable
2022-12-28 17:52:49 +01:00
451ab7e84c Create the folders before moving to them 2022-12-28 19:40:08 +05:30
083390da83 Fix a bug where the task and req data needed to print with a backslash 2022-12-28 19:23:36 +05:30
dc6d48580b Merge pull request #715 from jsuelwald/beta
Convert [ to \[ so the logging backend...
2022-12-28 19:20:28 +05:30
27d69e2ac3 Upgrade stable-diffusion-sdkit during startup 2022-12-28 19:19:53 +05:30
91274a4df8 Move the mandatory models to the models folder, instead of the legacy location inside the stable-diffusion folder 2022-12-28 19:08:39 +05:30
6eafcdfafd Update renderer.py
Use .replace on pformat in both lines
2022-12-28 14:27:07 +01:00
5e44744ff7 Update renderer.py
Updated (replace doesn't work on sets)
2022-12-28 13:49:52 +01:00
37b293fe74 Force full precision on NVIDIA T400 2022-12-28 17:46:24 +05:30
280f0be690 Disable symlink warnings on Windows for huggingface cache 2022-12-28 16:48:12 +05:30
183bc8321c Convert [ to \[ so the logging backend...
doesn't interpret that as a colour or other command
2022-12-28 10:43:39 +01:00
a973e4d1ef version 2022-12-28 14:30:01 +05:30
eed1066967 Merge pull request #714 from patriceac/patch-7
Default to 4x in taskConfig when factor not present in task
2022-12-28 13:09:27 +05:30
2859c94fea Applying Madrang's suggestion 2022-12-27 23:36:43 -08:00
dbcce2ee5d Default to 4x in taskConfig 2022-12-27 23:27:25 -08:00
25071c238c Remove the width for better formatting (uses what Bonsi suggested in the first place) 2022-12-27 21:14:31 +05:30
9995ffb5f3 Merge pull request #711 from jsuelwald/patch-1
Update renderer.py for better readable console output
2022-12-27 21:11:44 +05:30
c867c35e45 Update renderer.py 2022-12-27 16:23:36 +01:00
6f60e88ca6 Update renderer.py for better readable console output 2022-12-27 15:41:10 +01:00
11730dcbe4 changelog 2022-12-27 17:07:43 +05:30
e155bac445 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-27 17:06:55 +05:30
15a4682665 Fix broken drag-and-drop for text files and clipboard paste 2022-12-27 17:06:46 +05:30
08675b39f7 Merge pull request #710 from patriceac/image-modifiers-events
Adding image modifier events to core plugins
2022-12-27 16:39:11 +05:30
2c7d5adb80 Adding image modifier events to core plugins
Sorry, forgot these in the first PR.
2022-12-27 02:58:46 -08:00
51c7faee3c Changelog 2022-12-27 16:23:57 +05:30
852e129f9c Support upscaling by 2x or 4x (previously only supported 4x) 2022-12-27 16:20:16 +05:30
6eb2d800fa Tweak low GPU wording 2022-12-27 14:58:08 +05:30
0a2c70595d Turbo be gone 2022-12-27 14:51:03 +05:30
f13e16af15 Disable unused config for now 2022-12-27 12:21:51 +05:30
f364958c13 Merge pull request #705 from patriceac/fix-cut-off-tooltips-display
Fix cut off tooltips display
2022-12-27 10:26:46 +05:30
e65150647d Merge pull request #708 from patriceac/patch-6
Add icon to "Process newest jobs first" setting
2022-12-27 10:25:45 +05:30
3c435b9593 Merge pull request #707 from patriceac/image-modifiers-events
Adding image modifiers events
2022-12-27 10:25:20 +05:30
871b96a450 Add icon to "Process newest jobs first" setting 2022-12-26 19:10:37 -08:00
48a3254ad2 Adding image modifiers events
Adding events to allow plugins to listen for image modifiers loaded and refreshed events respectively.
2022-12-26 12:16:36 -08:00
2c0bdd6377 Fix cut off tooltips display 2022-12-26 10:04:36 -08:00
8cedeb349d Changes to allow rolling back from the upcoming sdkit-based system 2022-12-26 23:04:45 +05:30
e241ef25e5 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-26 21:00:57 +05:30
5e553dd958 Skip sdkit upgrade if in developer mode 2022-12-26 21:00:46 +05:30
19ee87d2cd Merge pull request #692 from JeLuF/remove-result
Add "Remove" button to each image's hover menu (Fixes #682)
2022-12-26 17:38:00 +05:30
72b3598687 Merge pull request #703 from JeLuF/patch-5
Bring back Linux download link
2022-12-26 17:36:43 +05:30
33b120f6cd Merge pull request #702 from patriceac/fix-copy-to-clipboard
Fix copy image settings to clipboard
2022-12-26 16:25:44 +05:30
0bfb9d00c8 Fix copy image settings to clipboard
Regression was caused by the processing of the legacy turbo field, which I understand to now be obsolete.
2022-12-26 02:10:36 -08:00
b1a2d36c2d Bring back Linux download link 2022-12-26 10:16:43 +01:00
517ddca22d Changelog 2022-12-26 13:12:56 +05:30
41c7b08418 Keep euler_a as the default 2022-12-26 11:59:44 +05:30
c7c1b5a570 changelog 2022-12-25 17:18:31 +05:30
87b6dfb1a9 Changelog 2022-12-25 17:17:10 +05:30
46c56f3706 Use a model config yaml file if placed next to the model (with the same name). This can override a known model as well 2022-12-25 17:07:00 +05:30
32bab80508 Show sdkit version during startup 2022-12-25 16:38:37 +05:30
b6f1194c93 Typo 2022-12-25 00:23:51 +05:30
206f9b97bb Merge pull request #695 from cmdr2/refactor
v2.5 - move to sdkit
2022-12-24 23:28:10 +05:30
13721f160e changelog grammar 2022-12-24 23:22:47 +05:30
102e5623f7 Merge branch 'beta' into refactor 2022-12-24 23:14:02 +05:30
9a975321db v2.5 changelog 2022-12-24 23:11:13 +05:30
6743ec14f1 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-24 22:17:31 +05:30
daec5e5426 Changes to allow rolling back from the upcoming sdkit-based system 2022-12-24 22:17:16 +05:30
a2b55c0df7 Report precision 2022-12-24 21:44:42 +05:30
01320ac735 Rename project to Easy Diffusion 2022-12-24 21:36:47 +05:30
84bddee2ce Treat none as a boolean false in drag-and-drop 2022-12-24 19:41:36 +05:30
e636dd3649 Merge pull request #694 from cmdr2/beta
Beta
2022-12-24 19:18:28 +05:30
5f6b798e35 Stop printing annoying ok messages 2022-12-24 19:13:17 +05:30
9137f3793e Merge pull request #693 from madrang/mobile-fixes
Add a debounce delay to allow mobile to bouble tap.
2022-12-24 15:53:31 +05:30
73e92a688f color logging 2 2022-12-24 15:43:06 +05:30
7a9f219037 color logging 2022-12-24 15:41:19 +05:30
a4728190c0 Refactor server.py 2022-12-24 15:29:49 +05:30
04d67a24b6 Don't allow the results to be collapsed when clicking draghandle 2022-12-24 04:55:28 -05:00
55049ba9d2 Add a debounce delay to allow mobile to bouble tap. 2022-12-24 04:42:43 -05:00
e0b33a4feb Install rich 2022-12-24 15:10:46 +05:30
fb5c0a3db7 Install python 3.8.5 during installation. Torch isn't available for 3.11 2022-12-24 14:57:57 +05:30
8154a5709b disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan) 2022-12-24 14:01:33 +05:30
3a6780bd50 Copy check_modules.py the first time an existing user runs the new version 2022-12-24 13:56:05 +05:30
b7a76d4212 Merge branch 'beta' into refactor 2022-12-24 13:45:53 +05:30
ba7cae683a Bump to 2.5 2022-12-24 13:39:28 +05:30
243556656e Temporarily disable the model config dropdown in the UI 2022-12-24 13:38:55 +05:30
6662dc66d5 Updated scripts to install sdkit into existing installations, while still working with new installations 2022-12-24 13:37:50 +05:30
107112d1c4 Integration bugs 2022-12-24 12:37:20 +05:30
4eae540086 Add "Remove" button to each image's hover menu 2022-12-24 01:02:38 +01:00
21108650f7 add findClosestAncestor
Function to find the closest ancestor of an element that matches the selection criterion
2022-12-24 00:58:52 +01:00
c5d343750c Merge pull request #691 from JeLuF/patch-4
Avoid guidance scale "1.0"
2022-12-23 17:55:41 +05:30
09b76dcd93 Avoid guidance scale "1.0"
Using a guidance scale of 1.0 will cause an exception in the renderer and return a very confusing error message.
https://discord.com/channels/1014774730907209781/1028195513377509376
2022-12-23 13:18:08 +01:00
b87bc033f5 Merge pull request #690 from cmdr2/beta
Update CHANGES.md
2022-12-23 11:26:42 +05:30
fb95d76e34 Update CHANGES.md 2022-12-23 11:26:14 +05:30
4e765a7948 Merge pull request #689 from cmdr2/beta
Speed up image creation, by removing a delay (regression) of 4-5 seconds between clicking Make Image and calling the server
2022-12-23 11:25:14 +05:30
cf2408013e Measure the click-to-render-request latency, only if the click button was used 2022-12-23 10:54:40 +05:30
d8543d1358 Use the sdkit model scan; Disable scan-per-load since we scan them before allowing them to be invoked 2022-12-22 16:47:59 +05:30
d8b79d8b5c Don't crash if IP listing fails. Thanks @JeLuf 2022-12-22 15:43:52 +05:30
c2bcf89f9a Merge branch 'beta' into refactor 2022-12-22 15:42:04 +05:30
5cb24f992c Bump version 2022-12-22 15:23:07 +05:30
21394b7d45 Reduce the delay between clicking 'Make Image' and making the render call to the server. Was nearly 4-5 seconds, now it's about 250-300ms. This is a hacky workaround until a better solution is found 2022-12-22 15:22:25 +05:30
6d08082693 Merge branch 'beta' 2022-12-22 13:43:50 +05:30
768fb2583a Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-22 13:43:09 +05:30
6e07b2354f Fix an unnecessary error when a task header is clicked 2022-12-22 13:42:47 +05:30
00597879bc Merge pull request #688 from cmdr2/beta
Update CHANGES.md
2022-12-22 13:26:15 +05:30
0cd0d6aadf Update CHANGES.md 2022-12-22 13:25:57 +05:30
9d201f82f1 Merge pull request #687 from cmdr2/beta
Undo/redo buttons in the image editor, Drag handle to reorder tasks, Pause button to pause all the tasks
2022-12-22 13:23:50 +05:30
d6c535c45c Merge branch 'main' into beta 2022-12-22 13:23:07 +05:30
babdb5b718 Prompt Matrix is in main 2022-12-22 12:26:32 +05:30
0ea8d038be Merge pull request #679 from SpecificKnot/main
Changes to Front Docs
2022-12-22 12:25:48 +05:30
c804a9971e Work-in-progress code for adding a model config dropdown in the UI. Doesn't work yet 2022-12-22 11:54:00 +05:30
4d7f6e4236 Change version number in beta 2022-12-22 10:32:40 +05:30
5474d1786f updated inpainter to not auto-clear itself whenever you draw etc 2022-12-21 16:35:03 -08:00
7f36473544 added a fill action 2022-12-21 16:20:07 -08:00
9d19698bf3 fixed opacity on inpainter to be 100% by default so no weird erasing 2022-12-21 16:09:56 -08:00
582b2d936f fixed theme css properties not being updated properly 2022-12-21 16:03:52 -08:00
6036ccdc1c Style Adjustments
Made a few adjustments to fit the needs of the project for new users.
2022-12-20 12:44:48 +00:00
5eeef41d8c Update to use the latest sdkit API 2022-12-20 15:16:47 +05:30
bacf266f0d Merge pull request #651 from madrang/release-notes
Update 'release-notes' to use loadScript
2022-12-20 10:21:07 +05:30
ba5c54043b Merge pull request #680 from AssassinJN/beta
Drag and Drop Styles
2022-12-20 10:19:30 +05:30
e33c858829 Merge pull request #1 from JeLuF/AJNdrag
Only activate the dragOver event listener when dragging tasks
2022-12-19 14:39:27 -05:00
e47e54de3f Only activate the dragOver event listener when dragging tasks 2022-12-19 20:34:06 +01:00
54f9e9bfe9 adding drag and drop styles
Add functions required for adding styles to imageTaskContainer to show where images will be dropped.
2022-12-19 13:45:42 -05:00
e1875c872c classes for drag and drop
Added classes for drag and drop.
2022-12-19 13:44:15 -05:00
27b8e173e8 Changes to Front Docs 2022-12-19 14:28:05 +00:00
47e3884994 Rename the python package name to easydiffusion (from sd_internal) 2022-12-19 19:39:15 +05:30
e483071894 Rename diffusionkit to sdkit; Delete runtime.py (historic moment) 2022-12-19 19:27:28 +05:30
af090cb289 Update README.md 2022-12-19 12:24:19 +05:30
9bbb25f16c Update README.md 2022-12-19 12:23:32 +05:30
3007f00c9b Update README.md 2022-12-19 12:22:27 +05:30
352dcfbe30 Update README.md 2022-12-19 12:20:46 +05:30
60b181a545 Update README.md 2022-12-19 12:11:01 +05:30
600482e2d7 Update README.md 2022-12-19 12:10:15 +05:30
39ccbbd72e Update README.md 2022-12-19 12:09:13 +05:30
6e69cbcdaf Merge pull request #674 from SpecificKnot/main
Simplified README
2022-12-19 11:55:04 +05:30
bf6c222a3b Merge pull request #641 from JeLuF/pause
Pause button
2022-12-19 11:52:55 +05:30
6afcf7570a Merge pull request #671 from patriceac/allow-empty-prompts
Allow empty prompts (image modifiers only)
2022-12-19 11:50:18 +05:30
c3126f7b4d Merge pull request #673 from jsuelwald/patch-1
Change time display on job
2022-12-19 11:48:38 +05:30
cb3b542363 Merge pull request #675 from JeLuF/drag
Add drag handle
2022-12-19 09:36:44 +05:30
1a5e15608c Merge pull request #676 from JeLuF/ipfix
Return empty list if hostname lookup fails
2022-12-19 09:14:40 +05:30
64a751ad79 Merge branch 'beta' into pause 2022-12-19 00:55:56 +01:00
57efe31959 Return empty list if hostname lookup fails 2022-12-19 00:42:48 +01:00
39350d554b Remove old code 2022-12-19 00:32:13 +01:00
8f4e03550c Add drag handle 2022-12-19 00:14:57 +01:00
d03823fb20 Last minute changes 2022-12-18 21:16:32 +00:00
00ec2b9d6f README Updates
Updates to README to make it easier to follow along.
2022-12-18 21:13:12 +00:00
70e4bc4582 Update README.md 2022-12-18 20:52:38 +00:00
5e56a437ef Update README.md 2022-12-18 20:48:19 +00:00
22ffd25619 Change time display on job
Change "Processed 1 image in 150.65 seconds" to "Processed 1 Image in 2 minutes 30 seconds" to be consistent with the approx. time remaining while rendering
2022-12-18 07:20:42 +01:00
127949c56b Allow empty prompts (image modifiers only)
Allows empty prompts as long as there are image modifiers. This allows the user to craft prompts just by using image modifiers if they so wish.
2022-12-17 17:06:07 -08:00
cdfef16a0e Merge pull request #670 from patriceac/collapsible-toggle-event
Fire an event when a collapsible is toggled
2022-12-17 16:49:51 +05:30
1595f1ed05 Add 6 new samplers; Fix a bug where new tasks wouldn't started if a previous task was stopped 2022-12-17 16:45:43 +05:30
1cae39b105 Fire an event when a collapsible is toggled
Need an event to know that a collapsible got toggled to be able to resize the panels accordingly. Thanks!
2022-12-17 03:05:43 -08:00
8189b38e6e Typo in decoding live preview images 2022-12-17 15:59:09 +05:30
c240d6932a Update CHANGES.md 2022-12-17 10:13:23 +05:30
c4548d9396 Merge pull request #669 from JeLuF/hover
CSS only initimg hover, 'use as input' button
2022-12-17 09:50:46 +05:30
aea70e3dd4 Merge pull request #668 from JeLuF/imgedit
Fix img resize issues, add redo/undo buttons
2022-12-17 09:50:07 +05:30
3b01e65e11 CSS only initimg hover, 'use as input' button 2022-12-17 01:30:30 +01:00
341c810bbb Fix img resize issues, add redo/undo buttons 2022-12-17 00:29:54 +01:00
85fd2dfaaa Merge pull request #664 from patriceac/tab-change-trigger
Fire an event upon tab change
2022-12-16 18:24:10 +05:30
bf4bc38c6c Merge pull request #662 from JeLuF/patch-7
Linux uses .zip, not .tar.xz (Fixes #657)
2022-12-16 18:23:44 +05:30
aa8b50280b Remove the test_sd2 flag, the code now works with SD 2.0 2022-12-16 15:31:55 +05:30
62553dc0fa Fire an event upon tab change
Fire an event upon tab change.
2022-12-16 01:45:58 -08:00
25639cc3f8 Tweak Memory Usage setting text; Fix a bug with the memory usage setting comparison 2022-12-16 14:11:55 +05:30
7982a9ae25 Change the performance field to GPU Memory Usage instead, and use the 'balanced' profile by default, since it's just 5% slower than 'high', and uses nearly 50% less VRAM 2022-12-16 11:34:49 +05:30
aa01fd058e Set performance level (low, medium, high) instead of a Turbo field. The previous Turbo field is equivalent to 'Medium' performance now 2022-12-15 23:30:06 +05:30
ef7e1575bd Linux uses .zip, not .tar.xz (Fixes #657) 2022-12-15 16:44:43 +01:00
fb075a0013 Fix whitespace 2022-12-14 16:53:50 +05:30
d1738baf44 Merge branch 'beta' into refactor 2022-12-14 16:53:23 +05:30
7eb29fa91b Fix: errors were overwritten by the time taken in the UI 2022-12-14 16:52:46 +05:30
34c00fb77f Fix: errors were overwritten by the time taken in the UI 2022-12-14 16:51:30 +05:30
7965318d9f Update task_manager.py 2022-12-14 16:49:59 +05:30
e73a514e29 Revert a recent change to task error reporting, seems unstable 2022-12-14 16:37:45 +05:30
35ff4f439e Refactor save_to_disk 2022-12-14 16:30:19 +05:30
12e0194c7f Allow None as the value type in dnd parsing 2022-12-14 16:30:08 +05:30
d1ac90e16d [metadata parsing] Support loading the flat JSON format saved by the next backend; Set the dropdown to None if the value is undefined or null in the metadata 2022-12-14 15:43:24 +05:30
7dc7f70582 Allow parsing .safetensors stable diffusion model path in the metadata parser 2022-12-14 10:34:36 +05:30
84d606408a Prompt is now a keyword in the new metadata format generated from diffusionkit 2022-12-14 10:31:19 +05:30
d103693811 Bug in the metadata generation - made an array of None 2022-12-14 10:22:24 +05:30
0dbce101ac sampler -> sampler_name 2022-12-14 10:21:44 +05:30
cb81e2aacd Fix a bug where the metadata output format wouldn't get sent to the backend 2022-12-14 10:18:01 +05:30
6cd0b530c5 Simplify the code for VAE loading, and make it faster to load VAEs (because we don't reload the entire SD model each time a VAE changes); Record the error and end the thread if the SD model fails to load during startup 2022-12-13 15:46:04 +05:30
35571eb14d Don't hang the task if something other than the renderer fails (e.g. model loading) 2022-12-13 12:03:34 +05:30
8e6102ad9a removeTask() 2022-12-13 12:03:30 +05:30
80bc80dc2c removeTask() 2022-12-13 12:02:43 +05:30
a483bd0800 No need to catch and report exceptions separately in the renderer now 2022-12-13 11:46:13 +05:30
47a39569bc Merge branch 'beta' into refactor 2022-12-13 11:45:43 +05:30
f00e1a92d8 Don't hang the task if something other than the renderer fails (e.g. model loading) 2022-12-13 11:44:20 +05:30
a289945e8e Merge pull request #654 from jsuelwald/beta
The exception should also mention dpm2
2022-12-12 21:05:03 +05:30
b750c0d7c3 The exception should also mention dpm2 2022-12-12 16:24:03 +01:00
a244a6873a Use the new 'diffusionkit' package name 2022-12-12 20:46:11 +05:30
ceff4f06c1 Merge branch 'beta' into refactor 2022-12-12 20:43:29 +05:30
0307114c8e Merge pull request #653 from cmdr2/beta
Don't collapse the task entry if 'Stop Task' is pressed
2022-12-12 19:56:49 +05:30
92030a3917 Don't collapse the task entry if 'Stop Task' is pressed 2022-12-12 19:56:27 +05:30
73ace121a4 Merge pull request #652 from cmdr2/beta
Beta
2022-12-12 19:49:21 +05:30
44d5809e46 Changelog 2022-12-12 19:46:13 +05:30
5c4e6f7e96 Tweak editor width 2022-12-12 19:42:43 +05:30
8c032579b8 Hide the hypernetwork strength slider if no hypernetwork model is selected; Support drag-n-drop for hypernetwork models 2022-12-12 19:31:59 +05:30
b53935bfd4 Revert "Scrolling panes (#632)"
This reverts commit e3184622e8.
2022-12-12 19:03:16 +05:30
d4db027cfa Move the hypernetwork options below the sampler settings; Whitespace fixes 2022-12-12 19:02:34 +05:30
27963decc9 Use the multi-filters API 2022-12-12 18:12:55 +05:30
25f488c6e1 Merge branch 'beta' into refactor 2022-12-12 15:47:13 +05:30
07bd580050 Typos 2022-12-12 15:44:22 +05:30
fb32a38d96 Rename sampler to sampler_name in the API 2022-12-12 15:21:02 +05:30
ac0961d7d4 Typos from the refactor 2022-12-12 15:18:56 +05:30
6b943f88d1 Set uvicorn log level to 'error' 2022-12-12 15:18:30 +05:30
4bbf683d15 Minor refactor 2022-12-12 14:41:36 +05:30
d0e50584ea Expose the metadata format option in the UI 2022-12-12 14:06:20 +05:30
b57649828d Refactor the save-to-disk code, moving parts of it to diffusionkit 2022-12-12 14:01:47 +05:30
1f44a283b3 Update 'release-notes' to use loadScript 2022-12-12 02:47:42 -05:00
9947c3bcfb Start timer to IDLE_COOLDOWN before idleEventPromise completes. (#649) 2022-12-12 11:12:11 +05:30
8faf6b9f52 Don't allow to make zero images, make at least one. (#647) 2022-12-12 11:11:33 +05:30
e45cbbf1ca Use the turbo setting if requested 2022-12-11 20:42:31 +05:30
1a5b6ef260 Rename runtime2.py to renderer.py; Will remove the old runtime soon 2022-12-11 20:21:25 +05:30
096556d8c9 Move away the remaining model-related code to the model_manager 2022-12-11 20:13:44 +05:30
97919c7e87 Simplify the runtime code 2022-12-11 19:58:12 +05:30
0aa7968503 Move color correction to diffusionkit; Rename color correction to 'Preserve color profile' 2022-12-11 19:34:07 +05:30
bd1bc78953 Use onIdle(), move pause button, quick resume without using the promise 2022-12-11 14:57:01 +01:00
6ce6dc3ff6 Get rid of the ugly copying around (and maintaining) of multiple request-related fields. Split into two objects: task-related fields, and render-related fields. Also remove the ability for request-defined full-precision. Full-precision can now be forced by using a USE_FULL_PRECISION environment variable 2022-12-11 18:16:29 +05:30
e6346775e7 Merge branch 'beta' into pause 2022-12-11 11:19:48 +01:00
d03eed3859 Simplify the logic for reloading gfpgan and realesrgan models (based on the request), using the code path used for the other model types 2022-12-11 14:14:59 +05:30
afb88616d8 Load the models after the device init, to let the UI load before the models finish loading 2022-12-11 13:30:16 +05:30
543f13f9a3 Tweak logging to increase the space available by 3 characters 2022-12-11 13:19:22 +05:30
af5c68051a Fix for the tooltips being cutoff (#636) 2022-12-11 12:59:23 +05:30
5b7cd11de8 Added support for Async events (#643)
* Added support for async events callbacks

* Don't fire IDLE event if the first callback hasn't completed execution.
2022-12-11 11:22:52 +05:30
d3c3496e55 Merge pull request #639 from madrang/newEngine
Check if window is defined. Not all JS execution environments have it.
2022-12-11 11:19:11 +05:30
c08c8b2789 Merge pull request #638 from JeLuF/initimg
show initimg in task list
2022-12-11 11:18:10 +05:30
069315e434 Merge pull request #642 from patriceac/patch-5
Fixing a typo
2022-12-11 11:16:24 +05:30
7e4ad83a1c Merge pull request #637 from madrang/mainjs_fixes
Fix (typeof stepUpdate !== 'object') not completing the task on stop.
2022-12-11 11:15:31 +05:30
400f9fd680 Merge pull request #635 from patriceac/patch-4
Store the auto-scroll checkbox setting in localStorage instead of using the auto-save framework
2022-12-11 11:06:19 +05:30
38951f5581 Pause button - check whether function is defined before calling it 2022-12-11 02:49:49 +01:00
b5329ee93d Fixing a typo
Yeah, I know... What can I say? I have my OCD too. 👀
2022-12-10 17:45:14 -08:00
c568bca69e Pause button 2022-12-11 02:31:23 +01:00
7b2be12587 Check if window is defined. Not all JS execution environments have it. 2022-12-10 18:26:48 -05:00
099fde2652 show initimg in task list 2022-12-10 17:17:37 +01:00
83e5410945 Fix (typeof stepUpdate !== 'object') not completing the task on stop. 2022-12-10 00:52:27 -05:00
b330c34b29 Fix auto-scroll setting management
After thinking about it, the auto-save toggle is meant for the *Editor* fields listed behind the Configure button. The auto-scroll toggle is not part of the Editor, and is more akin to a system setting, although it's placed in the main UI for convenience reasons related to its nature. As such, and especially considering it's a plugin, I lean towards decoupling auto-scroll from the auto-save settings, and just storing it independently.
2022-12-09 19:34:41 -08:00
e3184622e8 Scrolling panes (#632)
Decouple the editor and the preview panes. Scrollbars color updated as well as requested.
2022-12-09 23:11:39 +05:30
28f822afe0 Fix tags not being properly applied to prompt matrix (#610)
There is an issue on the beta where if you use pipe ( | ) in the prompt to make a prompt matrix, the optional prompts are only applied when the last prompt in the matrix is used.
2022-12-09 23:04:25 +05:30
a2af811ad2 Disable uvicorn access logging in favor of cleaner server-side logging, we already get all that info; Print the request metadata 2022-12-09 22:47:34 +05:30
cde8c2d3bd Use a logger 2022-12-09 21:30:18 +05:30
79cc84b611 Option to apply color correction (balances the histogram) during inpainting; Refactor the runtime to use a general-purpose dict 2022-12-09 19:39:56 +05:30
f1de0be679 Fix integration issues after the refactor 2022-12-09 17:50:33 +05:30
854e3d3576 Fix reading value from undefined. (#631) 2022-12-09 16:34:59 +05:30
dbac2655f5 Typo 2022-12-09 16:14:04 +05:30
0f656dbf2f Typo 2022-12-09 16:11:08 +05:30
3fbb3f6773 Use const 2022-12-09 16:09:10 +05:30
8820814002 Simplify the API for resolving model paths; Code cleanup 2022-12-09 15:45:36 +05:30
b40fb3a422 Model readme file write flag 2022-12-09 15:27:40 +05:30
aa59575df3 Remove unused patch files 2022-12-09 15:24:55 +05:30
accfec9007 Space 2022-12-09 15:22:56 +05:30
16410d90b8 Use the simplified model loading API in diffusion-kit; Catch and report exceptions while generating images 2022-12-09 15:21:49 +05:30
27c6113287 Support hypernetworks; moves the hypernetwork module to diffusion-kit 2022-12-09 13:29:06 +05:30
f4a6910ab4 Work-in-progress: refactored the end-to-end codebase. Missing: hypernetworks, turbo config, and SD 2. Not tested yet 2022-12-08 21:39:09 +05:30
bad89160cc Work-in-progress model loading 2022-12-08 13:50:46 +05:30
5782966d63 Merge branch 'beta' into refactor 2022-12-08 11:58:09 +05:30
ba2c966329 First draft of multi-task in a single session. (#622) 2022-12-08 11:12:46 +05:30
f8dee7e25f Add test sample to one of the plugin. (#626)
* Added test example from a plugin.

* Only load style if #news was created.
2022-12-08 10:57:50 +05:30
a8151176d7 SD 2.1 2022-12-08 10:04:33 +05:30
9ee0b7fe2e SD 2.1 2022-12-08 10:04:14 +05:30
fb6a7e04f5 Work-in-progress refactor of the backend, to move most of the logic to diffusion-kit and keeping this as a UI around that engine. Does not work yet. 2022-12-07 22:15:35 +05:30
bfdf487d52 SD2 models no longer need to be prefixed with 'sd2_' . The model loader now checks for a key that only SD2 models seem to have, to deduce which config file to use 2022-12-07 16:19:46 +05:30
b7aac1501d Don't show prompt strength when the app starts 2022-12-07 13:12:35 +05:30
273525e6f9 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-07 13:12:02 +05:30
064a4938c1 Don't show prompt strength when the app starts 2022-12-07 13:11:49 +05:30
182236e742 Hypernets mergefixes (#625)
* Add hypernetwork args definition in the engine.

* Add the values to reqBody

* Don't load hypernetwork.py with SD2 until it's compatible.
2022-12-07 12:35:36 +05:30
75cb052cca Paint editor - translucent mask, more brush size options 2022-12-07 12:28:28 +05:30
d4a378827f Paint editor - translucent mask, more brush size options 2022-12-07 12:27:40 +05:30
592d5e8c40 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-07 11:41:52 +05:30
733150111d Changelog 2022-12-07 11:41:36 +05:30
cbe91251ac Hypernetwork support (#619)
* Update README.md

* Update README.md

* Make on_sd_start.sh executable

* Merge pull request #542 from patriceac/patch-1

Fix restoration of model and VAE

* Merge pull request #541 from patriceac/patch-2

Fix restoration of parallel output setting

* Hypernetwork support

Adds support for hypernetworks. Hypernetworks are stored in /models/hypernetworks

* forgot to remove unused code

Co-authored-by: cmdr2 <secondary.cmdr2@gmail.com>
2022-12-07 11:24:16 +05:30
1283c6483d Use the reqBody exposed to events to allow plugins to change the request. (#620) 2022-12-07 09:34:04 +05:30
f24d3d69af Fix download pictures (#616)
Old link was broken. Apparently the "develop" branch was deleted.
2022-12-07 09:33:11 +05:30
7984327d81 Fixed tasks buttons by replacing the error with a warning when setting properties to undefined. (#618) 2022-12-06 21:49:05 +05:30
ef90832aea engine.js (#615)
* New engine.js first draft.

* Small fixes...

* Bump version for cache...

* Improved cancellation code.

* Cleaning

* Wrong argument used in Task.waitUntil

* session_id needs to always match SD.sessionId

* Removed passing explicit Session ID from UI.
Use SD.sessionID to replace.

* Cleaning... Removed a disabled line and a hardcoded value.

* Fix return if tasks are still waiting.

* Added checkbox to reverse processing order.

* Fixed progress not displaying properly.

* Renamed reverse label.

* Only hide progress bar inside onCompleted.

* Thanks to rbertus2000 for helping testing and debugging!

* Resolve async promises when used optionally.

* when removed var should have used let, not const.

* Renamed getTaskErrorHandler to onTaskErrorHandler to better reflect actual implementation.

* Switched to the unsafer and less git friendly end of lines comma as requested in review.

* Raised SERVER_STATE_VALIDITY_DURATION to 90 seconds to match the changes to Beta.

* Added logging.

* Added one more hook before those inside the SD engine.

* Added selftest.plugin.js as part of core.

* Removed a tests that wasn't yet implemented...

* Groupped task stopping and abort in single function.

* Added optional test for plugins.

* Allow prompt text to be selected.

* Added comment.

* Improved isServerAvailable for better mobile usage and added comments for easier debugging.

* Comments...

* Normalized EVENT_STATUS_CHANGED to follow the same pattern as the other events.

* Disable plugins if editorModifierTagsList is not defined.

* Adds a new ServiceContainer to register IOC handlers.

* Added expect test for a missing dependency in a ServiceContainer

* Moved all event code in it's own sub class for easier reuse.

* Removed forgotten unused var...

* Allow getPrompts to be reused be plugins.

* Renamed EventSource to GenericEventSource to avoid redefining an existing class name.

* Added missing time argument to debounce

* Added output_quality to engine.js

* output_quality need to be an int.

* Fixed typo.

* Replaced the default euler_a by dpm2 to work with both SD1.# and SD2

* Remove generic completed tasks from plugins on generator complete.

* dpm2 starts at step 2, replaced with plms to start at step 1.

* Merge error

* Merge error

* changelog

Co-authored-by: Marc-Andre Ferland <madrang@gmail.com>
2022-12-06 17:04:08 +05:30
9571b8addc Merge pull request #614 from cmdr2/beta
Beta
2022-12-06 16:18:24 +05:30
9601f304a5 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-12-06 16:17:55 +05:30
ff43dac2a7 Open the color box when the custom label is clicked 2022-12-06 16:17:45 +05:30
0a43305455 Merge pull request #613 from cmdr2/beta
Beta
2022-12-06 16:11:38 +05:30
54d8224de2 Update CHANGES.md 2022-12-06 16:09:58 +05:30
c9e34457cd Tweak text in editor 2022-12-06 15:39:27 +05:30
47c8eb304f Revert the button styling 2022-12-06 15:36:52 +05:30
2dd39fa218 Disable auto-save for the auto-scroll toggle, until a better way to save it is figured out. It currently breaks a few UI fields, since it calls initSettings() a second time 2022-12-06 15:20:31 +05:30
cb618efb98 Image Editor Updates (#612)
* fixed tools for image editor to be more modular and made cursor an actual cursor change

* fixed eraser cursor positioning

* updated opacity to not have a 100 option

* separated clear into an actions section

* added history support for image editor. ctrl-z and ctrl-y both work now

* removed extra console log debugging stuff

* updated buttons style

* updated the button ui on the main page as requested

* updated with a bunch of bugfixes
2022-12-06 13:56:51 +05:30
e7ca8090fd Make JPEG Output quality user controllable (#607)
Add a slider to the image options for the JPEG quality
For PNG images, the slider is hidden.
2022-12-05 11:02:33 +05:30
7861c57317 Safetensor support (Fixes #599) (#608)
* safetensors support
Add support for checkpoints in safetensors format: https://github.com/huggingface/safetensors

This format shall be safer than pickle files

* pip install safetensors
2022-12-05 10:59:48 +05:30
f701b8dc29 Simplify onUpscaleClick (#602)
* Simplified onUpscaleClick code.

* Updated fix with comment as to what it's fixing.

* Move the fix to enqueueImageVariationTask
2022-12-05 10:46:10 +05:30
bd10a850fa Fix upscaling when a source image is set (#593)
* Fix upscaling when a source image is set

If you have an image selected (img2img) then clicking Upscale on another unrelated image, the image for img2img is used and you get something very unexpected.

* Fix for img2img and mask gens
2022-12-03 22:25:14 +05:30
0f96688a54 Highlight artist modifiers when clicked (#596)
Artist modifiers, with the exception of Artstation (the first one), don't have the outline when selected. All the other modifiers, above or below, seem to work as intended

https://discord.com/channels/1014774730907209781/1014774732018683927/1048343258775949322
2022-12-03 22:18:57 +05:30
8eeca90d55 Fix weird scrolling when using a pen (#588)
With a pen, typing on a browser page, waiting a short moment, and then moving the pen scrolls the page.
Call event.preventDefault() to disable this default behaviour for events in the canvas area.
2022-12-02 14:40:21 +05:30
367e7f7065 Add dpm2 (#592)
* Move cond_stage_model to the right device

* Removed unused vars.

* Added 'dpm2'
2022-12-02 12:58:00 +05:30
ee19eaae62 Fix for RuntimeError, missing lines. (#591)
* Move cond_stage_model to the right device

* Removed unused vars.
2022-12-02 12:57:26 +05:30
8eb3a3536b Update on_sd_start.bat 2022-12-02 12:06:41 +05:30
cfd50231e1 Update on_sd_start.sh 2022-12-02 12:06:39 +05:30
1c8ab9e1b4 Temporarily set the display: flex style only on the image editor buttons 2022-12-01 16:59:12 +05:30
6094cd8578 Fix the 'load from file' button that had moved to the next line' 2022-12-01 16:10:20 +05:30
353c49a40b Bump version 2022-12-01 16:05:35 +05:30
277140f218 Image Editor (#574)
* started implementing hamunii's image editor, and added a hamunii theme

* fixed so active tab is main tab

* added some testing stuff for image ediotr

* re-implemented canvas drawing myself. just need to add layer stuff now

* moved everything to an image editor class and implement it so it actually works nicely now

* fixed a couple weird bugs and cleaned up the background image and sharpness stuff

* cleaned up a lot of stuff about the editor, added tools, buttons, made it mostly work in the current ui

* added inpainting support

* updated with more nice changes/updates to the inpainting and drawing editor

* made some more fixes and touchups to the image editor

* removed a bunch of semicolons

* remove old image inpainting system

* updated to work properly on mobile

* made a minor bugfix

* fixed img_size_box alignment

* Update index.html

Co-authored-by: cmdr2 <secondary.cmdr2@gmail.com>
Co-authored-by: cmdr2 <shashank.shekhar.global@gmail.com>
2022-12-01 16:01:09 +05:30
ca9413ccf4 Toggle image modifiers plugin (#558)
* Toggle image modifiers plugin

Right-click on image modifiers to temporarily turn them off without removing them. To quickly iterate and experiment with various combinations.

Please note this plugin required a minor tweak in getPrompts() to add support for image modifier inactive state.

* Fix tag matching

Co-authored-by: cmdr2 <secondary.cmdr2@gmail.com>
2022-12-01 15:10:36 +05:30
c9a0d090cb Merge pull request #569 from patriceac/Fix-seed-behavior
Tweak the seed behavior
2022-12-01 15:03:21 +05:30
1cd783d3a3 Merge pull request #534 from patriceac/Custom-modifiers-as-a-plugin
Custom modifiers as a plugin
2022-12-01 14:59:29 +05:30
1ead764a02 Merge branch 'beta' into Custom-modifiers-as-a-plugin 2022-12-01 14:57:39 +05:30
45f7b35954 Merge pull request #581 from patriceac/Hotfix-for-repeat-image-modifiers-handling
Hotfix for repeat image modifiers
2022-12-01 14:47:15 +05:30
6a41540749 Remove unused scripts from the previous installer 2022-12-01 14:44:20 +05:30
5b47da67f6 Merge pull request #582 from cmdr2/beta
Beta
2022-12-01 13:59:13 +05:30
292f68ff97 Typo in css path 2022-12-01 13:57:38 +05:30
3b554d881a Styling changes for the confirm dialog 2022-12-01 13:54:49 +05:30
40ebf468d3 Hotfix for repeat image modifiers
As per Discord conversation, this PR fixed the image modifiers behavior when a modifier appears more than once, and also fixes a regression introduced by ((weighted modifiers)).
2022-11-30 22:13:13 -08:00
4bc6e51862 Merge pull request #580 from JeLuF/patch-5
Add Quadro T2000 to force_full_precision list.
2022-12-01 10:35:32 +05:30
427861cf13 Add Quadro T2000 to force_full_precision list. 2022-12-01 00:59:12 +01:00
da3e7a2eb8 Fix the broken image close button 2022-11-30 21:14:18 +05:30
2979f04c82 Use socket.gethostname() instead of socket.getfqdn() 2022-11-30 20:17:18 +05:30
1949d8a50c Tweak modifiers help msg 2022-11-30 16:32:43 +05:30
ee66c799e0 Merge pull request #563 from patriceac/Mouse-wheel-behavior-fixes
Improved Mouse Wheel UX with Image Modifiers
2022-11-30 16:24:05 +05:30
7c50b8bf94 Merge branch 'beta' into Mouse-wheel-behavior-fixes 2022-11-30 16:22:45 +05:30
141ff74ece Merge pull request #557 from madrang/webmanifest
Added web manifest to allow installing the Url as a web app.
2022-11-30 16:19:04 +05:30
321e5f1ed6 Merge pull request #564 from patriceac/Fix-UI-display-when-removing-the-last-task
Fix UI display when removing the last task
2022-11-30 16:14:59 +05:30
6d131d9d8e Merge branch 'beta' into Fix-UI-display-when-removing-the-last-task 2022-11-30 16:14:28 +05:30
7e69b8eb31 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-30 16:11:22 +05:30
4e0b33e6a4 Merge pull request #566 from patriceac/Visual-feedback-on-buttons
Visual feedback on button click
2022-11-30 16:11:08 +05:30
54f7e6fcb8 SD2 fix - register buffer on the correct device 2022-11-30 16:05:06 +05:30
529169c4da Merge pull request #541 from patriceac/patch-2
Fix restoration of parallel output setting
2022-11-30 15:54:04 +05:30
a2c8c99215 Merge pull request #541 from patriceac/patch-2
Fix restoration of parallel output setting
2022-11-30 15:53:30 +05:30
e8bf3fd009 Merge pull request #542 from patriceac/patch-1
Fix restoration of model and VAE
2022-11-30 15:52:26 +05:30
465676e9ea Merge pull request #542 from patriceac/patch-1
Fix restoration of model and VAE
2022-11-30 15:51:31 +05:30
af53b57047 Changelog 2022-11-30 15:49:47 +05:30
54b5f75905 Rename auto-scroll to reflect its purpose 2022-11-30 15:47:24 +05:30
4348333497 Don't register listeners for an autosave setting, if they've already been registered 2022-11-30 15:45:30 +05:30
cc31110bcf Merge pull request #537 from patriceac/Generate-screen-layout
Auto-scroll plugin
2022-11-30 15:44:31 +05:30
f7c04bf7a6 bump version 2022-11-30 14:34:42 +05:30
029509ebad Unify IP info with devices, into a system_info table 2022-11-30 14:34:24 +05:30
65102bb64d Merge pull request #536 from JeLuF/serverip
Show network addresses in system settings
2022-11-30 14:00:18 +05:30
b96b55c5ce Merge branch 'beta' into serverip 2022-11-30 14:00:12 +05:30
1f5aba010e Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into webmanifest
# Conflicts:
#	ui/index.html
2022-11-30 03:29:46 -05:00
f0b3bea4e3 Also confirm before the 'Stop All' button acts; Tweak wording of confirm dialog 2022-11-30 13:54:42 +05:30
84fae2d9e0 Merge pull request #531 from JeLuF/confirm
Confirm 'Clear All' and 'Stop Task'
2022-11-30 13:48:14 +05:30
0b96fa112d Merge branch 'beta' into confirm 2022-11-30 13:47:08 +05:30
c64bcd23d3 Picklescanner is mandatory 2022-11-30 13:38:22 +05:30
efd9a22bb5 Merge pull request #530 from madrang/list-models
Scan model once as start, then only if changed.
2022-11-30 13:37:27 +05:30
159c3edfe3 Simplify the logic for toggling modifier cards, no need to loop through the cards, since we already have the card object in hand 2022-11-30 13:33:20 +05:30
f74fa8657b Merge pull request #518 from patriceac/patch-6
Fix duplicate custom modifiers activation states
2022-11-30 13:27:14 +05:30
648b142a4b Merge pull request #571 from madrang/tabs-css
Add a new css rule for screens smaller than 500px.
2022-11-30 13:24:38 +05:30
426f92595e Merge pull request #520 from madrang/fix-gfpgan
Fix the gfpgan fix for multi-gpu
2022-11-30 13:08:10 +05:30
82a8d9b644 Merge pull request #577 from cmdr2/beta
Beta
2022-11-30 12:19:44 +05:30
ff9430b8a2 Tabs to 4 spaces 2022-11-30 12:18:34 +05:30
2e69ffcb5e Merge pull request #576 from cmdr2/beta
v2.4.16 - Remove the use of git-apply
2022-11-30 12:12:17 +05:30
0ea38db7ef Show the SD 2.0 setting only to beta users 2022-11-30 12:05:46 +05:30
a69d4c279e Make seed field behavior deterministic
Copying the image settings while 'Random' is enabled would cause the seed to be randomized. This was misleading as what I see wasn't what I would get.
2022-11-29 19:04:42 -08:00
2706149399 Tweak left padding of editor panel 2022-11-29 15:27:13 +05:30
3d0cdc1cb6 Bump version 2022-11-29 13:32:29 +05:30
ac605e9352 Typos and minor fixes for sd 2 2022-11-29 13:30:08 +05:30
5432297691 Default to sd-v1-4 when trying to use a SD2 model with SD 1.4, and warn the user. This will eventually be unnecessary 2022-11-29 13:14:58 +05:30
e37be0f954 Remove the need to use yield in the core loop for streaming results. This removes the need to patch the Stable Diffusion code, which can be fragile 2022-11-29 13:03:57 +05:30
a99209b674 Add a new css rule for screens smaller than 500px. 2022-11-28 20:23:17 -05:00
cb02b5ba18 Merge pull request #567 from madrang/tabs-css
Improved tabs flow on small screens.
2022-11-28 18:27:29 +05:30
69f14edd80 Tweak the seed behavior
Update the seed *before* starting the processing, so interrupting the processing retains the seed being used for the batch being currently processed.

The idea behind that is that if I like the gen I'm currently seeing and want to build on top of it, I can create a new task with the same seed without having to wait for the current task to complete.
2022-11-28 01:19:31 -08:00
14714b950d Slight improvement of detection logic 2022-11-28 00:14:12 -08:00
13654cb8c0 Make on_sd_start.sh executable 2022-11-28 13:00:02 +05:30
00276228cf Make on_sd_start.sh executable 2022-11-28 12:59:33 +05:30
8583bb8d7b Improved tabs flow on small screens. 2022-11-27 20:37:20 -05:00
d48951fe00 Visual feedback on button click
When there are too many tasks and the top of the list is not visible, there is no visual feedback that a task has been successfully added to the queue.

Adding a subtle visual feedback on buttons upon click to reflect that the mouse event was taken into account.
2022-11-27 16:26:01 -08:00
99bdcfa0a5 Set theme-color from the current selected theme. 2022-11-27 15:49:54 -05:00
e64e1a92e6 Fix UI display when removing the last task
Clear All button properly shows the "welcome message", but Remove the last task would just result in a blank Preview pane.
2022-11-27 12:42:51 -08:00
e278e639a3 Fix removal of image modifiers with non-zero weights
Properly handles removal of image modifiers that had (((modifiers))) or [[[modifiers]]] updated at runtime.
2022-11-27 03:00:19 -08:00
c4bad5c454 Conciseness
Shortening the sentence.
2022-11-27 01:42:39 -08:00
da41a74efc Require Ctrl+Mouse Wheel for modifier weight adjustment
The current behavior is just too annoying, and scrolling the page is a much more frequent activity than tweaking the weights.
2022-11-27 01:35:47 -08:00
0dc970562a Reverting this unnecessary change 2022-11-26 18:27:04 -08:00
2d8401473d Revert "Update custom-modifiers.plugin.js"
This reverts commit e5c11ea214.
2022-11-26 16:57:54 -08:00
9c91f57b19 Added web manifest to allow installing the Url as a web app. 2022-11-26 15:51:26 -05:00
f14afcd129 Update README.md 2022-11-26 12:44:19 +05:30
5c1a3d82d7 Update README.md 2022-11-26 12:23:03 +05:30
e02a917569 Improved logic for auto-scroll toggle insertion
Updating the insertion logic to prepare for future UI improvements.
2022-11-25 22:51:49 -08:00
347fa0fda1 Update on_sd_start.bat 2022-11-26 01:50:30 +05:30
6510d4cb02 Merge pull request #553 from cmdr2/revert-552-beta
Revert "Patching patch again"
2022-11-26 01:45:57 +05:30
91e4ccf6f8 Update on_sd_start.bat 2022-11-26 01:43:41 +05:30
36249874bc Revert "Patching patch again" 2022-11-26 01:42:16 +05:30
d2b5d6cce9 Merge pull request #552 from jsuelwald/beta
Patching patch again
2022-11-26 01:40:02 +05:30
b2922741c9 Patching patch again 2022-11-25 21:06:19 +01:00
300f3e27db Merge pull request #551 from cmdr2/revert-549-patch-2
Revert "Update ddim_callback_sd2.patch"
2022-11-26 01:25:17 +05:30
d7330b80a9 Revert "Update ddim_callback_sd2.patch" 2022-11-26 01:22:35 +05:30
acdd7667b7 Merge pull request #549 from jsuelwald/patch-2
Update ddim_callback_sd2.patch
2022-11-26 01:19:08 +05:30
8114fa3f5d Update ddim_callback_sd2.patch 2022-11-25 20:46:24 +01:00
4bc5508f38 Rollback 2022-11-26 01:07:55 +05:30
e503c6092e Ddim decode for img2img 2022-11-26 00:55:39 +05:30
6a8985d8dd Update ddim_callback_sd2.patch 2022-11-26 00:49:15 +05:30
bee67fd883 Shape 2022-11-25 23:54:08 +05:30
a1d75d40aa Update runtime.py 2022-11-25 23:36:43 +05:30
29484867ca Typo 2022-11-25 23:32:56 +05:30
7fa983b971 Img2img sd2 attempt 2 2022-11-25 23:28:31 +05:30
617a8b2814 Fix for make_schedule error in sd2 2022-11-25 23:15:22 +05:30
b924d323d4 img2img attempt for sd2 2022-11-25 22:36:02 +05:30
a2efda41d3 Cleaning up the code 2022-11-25 03:50:47 -08:00
642c114501 Working txt2img 2022-11-25 14:29:24 +05:30
02dd3e457d Tweaks to load sd1 models in sd2 code, typos 2022-11-25 13:57:15 +05:30
ea7b28c9d5 Placeholder changes for SD 2.0 support, haven't tested yet 2022-11-25 12:17:44 +05:30
472ab4a9ce Fix restoration of parallel output setting 2022-11-24 14:15:27 -08:00
fca84e3edf Fix restoration of model and VAE
😅
2022-11-24 13:47:35 -08:00
b70235ff92 Set the PYTHONPATH in the developer console, before the prompt shows up 2022-11-24 11:48:27 +05:30
6eff591df7 System settings to disable the 'Are you sure?'-dialogs 2022-11-23 23:05:30 +01:00
d0b2bf736e Auto-scroll off by default 2022-11-23 03:23:51 -08:00
e5c11ea214 Update custom-modifiers.plugin.js
Removing the redundant initialization of the array.
2022-11-23 03:00:19 -08:00
6b6443406d Create Autoscroll.plugin.js 2022-11-23 02:57:07 -08:00
3452d7852a Merge branch 'beta' into serverip 2022-11-23 11:28:05 +01:00
f1fa10badd Show network addresses in system settings
Users sometimes struggle to get the IP address of their PC. This PR adds a button to the system settings pane that will list the server's IP
addresses.
2022-11-23 11:25:36 +01:00
1267621424 Merge pull request #535 from cmdr2/beta
Switch to new custom backend
2022-11-23 15:09:09 +05:30
8a0ec95fe1 Merge branch 'main' into beta 2022-11-23 15:08:34 +05:30
ba30a63407 Update custom-modifiers.plugin.js
Add a carriage return at the end
2022-11-22 23:07:44 -08:00
c56a2adbcb Custom modifiers as a plugin 2022-11-22 19:04:20 -08:00
2de96d4dc9 Scan model once as start, then only if changed. 2022-11-22 20:41:08 -05:00
a486f20892 Merge branch 'beta' into confirm 2022-11-22 21:33:18 +01:00
49535deb2e Confirm 'Clear All' and 'Stop Task'
Ask for a confimation before clearing the results pane or stopping a render task. The dialog can be skipped by holding down the shift key while clicking on the button.
2022-11-22 21:27:36 +01:00
7cbf62cf12 Revert whitespace fix 2022-11-22 23:30:03 +05:30
3b0ace3410 Revert whitespace fix 2022-11-22 23:27:46 +05:30
5a9c8e1d87 Warn but don't fix whitespaces in a patch 2022-11-22 23:21:11 +05:30
daaa65dc0a Warn but don't fix whitespaces in a patch 2022-11-22 23:20:24 +05:30
ab4e371524 Fix whitespace during git apply 2022-11-22 22:25:36 +05:30
927fd304b0 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-22 22:22:07 +05:30
5af84b8e90 Fix whitespace during git apply 2022-11-22 22:21:54 +05:30
d425dac499 Merge pull request #529 from madrang/dragNdrop
Fixing file drag and drop.
2022-11-22 21:57:34 +05:30
d056459e76 Merge pull request #529 from madrang/dragNdrop
Fixing file drag and drop.
2022-11-22 21:56:07 +05:30
3169485f33 Fixing file drag and drop. 2022-11-22 11:11:06 -05:00
d9b9f80a93 diffusion-kit upgrade 2022-11-22 17:39:51 +05:30
d429505b71 Update version of diffusion-kit 2022-11-22 17:14:20 +05:30
72ee708917 Remove the need to install realesrgan, gfpgan and certain specific package versions, since the new backend should install them directly 2022-11-22 16:50:10 +05:30
93bbfac29a Change the backend to a custom fork of SD, since basujindal's fork is no longer under development. This fork is intended to include the common models/tools used like RealESRGAN, GFPGAN, Codeformer etc, and is meant to be a community-developed project 2022-11-22 16:38:39 +05:30
040d7a6563 Merge pull request #528 from patriceac/patch-1
Add support for custom modifiers to d&d and clipboard
2022-11-22 16:06:26 +05:30
e8dd930a50 Add support for custom modifiers to d&d and clipboard
Add support for custom modifiers to d&d and clipboard and remove now-redundant code in restoreTaskToUI.
2022-11-22 00:06:43 -08:00
31c049ebfe Version css 2022-11-22 11:09:01 +05:30
d343a37fb2 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-22 11:08:07 +05:30
7097175c6f CSS tweak for logo and version 2022-11-22 11:07:50 +05:30
8e57c49043 Merge pull request #527 from cmdr2/beta
Beta
2022-11-22 11:00:25 +05:30
9f036ceefd Merge branch 'main' into beta 2022-11-22 10:59:51 +05:30
ff3ca8b36b link to new downloads 2022-11-22 10:48:43 +05:30
87a7b70a27 Shell error code check 2022-11-22 10:40:20 +05:30
9c71c966ca Shell error code check 2022-11-22 10:39:47 +05:30
6dc99e676e Reduce the width of the editor sidebar, regression 2022-11-21 18:45:37 +05:30
80ecb82cc2 Reduce the width of the editor sidebar, regression 2022-11-21 18:42:07 +05:30
7fc9509d4d Nowarn for fresh installation (git apply whitespace) 2022-11-21 17:20:09 +05:30
3bf5e11f94 Nowarn for fresh installation (git apply whitespace) 2022-11-21 17:19:55 +05:30
eef9af2266 Typo 2022-11-21 17:14:54 +05:30
fabdf5fe30 Typo 2022-11-21 17:14:34 +05:30
1cc27e524b Don't warn about whitespace in the git patch application 2022-11-21 17:11:57 +05:30
8316a002da Don't warn about whitespace in the git patch application 2022-11-21 17:11:38 +05:30
888c637f71 Merge pull request #432 from JeLuF/JeLuF-nsis
NSIS Installer
2022-11-21 16:30:49 +05:30
48b7d2587e bump version 2022-11-21 16:21:22 +05:30
923c889de8 Merge pull request #495 from madrang/pasteFix
Fix pasting in Firefox.
2022-11-21 16:15:24 +05:30
8ae575d67a Merge pull request #508 from patriceac/patch-4
Modifier weight via mouse wheel plugin
2022-11-21 16:10:23 +05:30
b51407486a Merge pull request #517 from patriceac/patch-5
Fix duplicate custom modifiers restoration
2022-11-21 16:05:17 +05:30
a689b34ed1 Merge pull request #506 from patriceac/patch-2
VAE support in Use Settings
2022-11-21 16:02:51 +05:30
aa98e60243 Merge pull request #507 from patriceac/patch-3
Modifier drag-and-drop plugin
2022-11-21 16:02:04 +05:30
c3bf767024 Merge pull request #525 from cmdr2/beta
Beta
2022-11-21 14:08:47 +05:30
b641f1a230 Bump version 2022-11-21 14:08:12 +05:30
9499685dda Check for enqueued tasks more frequently 2022-11-21 14:06:26 +05:30
80d23cbbbf Merge pull request #523 from cmdr2/main
Merge main
2022-11-21 12:43:23 +05:30
efa684c5e8 Merge pull request #522 from cmdr2/beta
v2.4.11 updates
2022-11-21 12:43:03 +05:30
2edf64985d Update CHANGES.md 2022-11-21 12:41:53 +05:30
5fe7807462 ERRORLEVEL is unreliable when conda is run 2022-11-21 12:38:46 +05:30
e96b9005ca Merge pull request #514 from JeLuF/checkinput
Prevent empty fields in make image request
2022-11-21 12:36:23 +05:30
8c29e735e7 Merge pull request #513 from JeLuF/ipconfig
CHANGE.md change for IP config
2022-11-21 12:35:15 +05:30
497e073a8c Merge pull request #480 from JeLuF/patch-3
Handle %TMP% and %TEMP%
2022-11-21 12:34:31 +05:30
d4ce54a3c2 Merge pull request #521 from JeLuF/patch-4
Add --whitespace=fix to git apply
2022-11-21 12:33:04 +05:30
de37a81902 Merge pull request #519 from JeLuF/relocate2
Rewrite easy_install.pth on each start
2022-11-21 12:15:15 +05:30
d6b8cb718a Merge pull request #519 from JeLuF/relocate2
Rewrite easy_install.pth on each start
2022-11-21 12:13:43 +05:30
ed435d2b72 Add --whitespace=fix also on Linux 2022-11-20 23:09:27 +01:00
2b1f8533b0 Add --whitespace=fix to git apply
For some users who have git preinstalled, `git apply` fails due to whitespace errors.

Aracon found that applying `--whitespace=fix` to the `git apply` invocation fixes the problem.
https://discord.com/channels/1014774730907209781/1036679816713359471/1037025435491516548

ryz confirmed that `--reject` wasn't needed for him to make it work, and this explanation from the "git apply" manpage suggests 
that we shouldn't include `--reject`:

> For atomicity, git apply by default fails the whole patch and does not touch the working tree when some 
> of the hunks do not apply. This option makes it apply the parts of the patch that are applicable, and leave 
> the rejected hunks in corresponding *.rej files.

After having a look at https://github.com/git/git/blob/master/apply.c, I think that they only check for `correct_ws_error` if they couldn't apply the patch. It doesn't impact 'normal' patching. If the patch can be applied, it will be done, and only if the apply fails, they'll check whether adding or removing WS might help. It should thus be save to be added and didn't produce any errors on my installation using SDUI-provided git.
2022-11-20 23:07:44 +01:00
0a21a69a9f Updated facexlib fix for usage on multi-gpu. 2022-11-20 13:04:22 -05:00
5ebc6b698c Rewrite easy_install.pth on each start
Fixes GFPGANer errors when the env has been moved.
2022-11-20 18:48:48 +01:00
cbc48e31e1 Fix duplicate custom modifiers activation states
Fixing activation state for custom modifier cards sharing the same tag where only one of the cards gets (de)activated.
2022-11-19 19:25:28 -08:00
577dd9048f Fix duplicate custom modifiers restoration
Fix for duplicate image modifiers when restoring a task in which several custom modifier cards share the same tag.
2022-11-19 19:21:36 -08:00
ae409dd0ec Prevent empty fields in make image request
Prevent render jobs to fail with HTTP 422 due to empty fields in the image settings.
https://discord.com/channels/1014774730907209781/1043481789706031215/1043481789706031215
https://discord.com/channels/1014774730907209781/1014774732018683927/1042768986871443516

minor change, no CHANGE.md entry
2022-11-19 21:00:41 +01:00
cde855e1dc Change note 2022-11-19 20:31:36 +01:00
adcd4368e7 Merge pull request #510 from JeLuF/ipconfig
Add network settings to the UI
2022-11-20 00:55:42 +05:30
8bcdb205ed Merge pull request #512 from cmdr2/beta
Use the correct device name when moving the model to cpu
2022-11-20 00:50:58 +05:30
2cf8b2a453 Use the correct device name when moving the model to cpu 2022-11-20 00:43:38 +05:30
6c156380f9 Add network settings to the UI
Allow users to choose the uvicorn port
Allow users to restrict uvicorn to only listen on localhost
2022-11-19 17:10:45 +01:00
369d0ee502 Modifier weight via mouse wheel plugin 2022-11-19 01:17:24 -08:00
4971a212e9 Modifier drag-and-drop plugin 2022-11-19 01:09:59 -08:00
2111a81d18 Proper PR for VAE support in Use Settings 2022-11-19 00:56:44 -08:00
6799b3d7da Merge pull request #505 from cmdr2/beta
v2.4.11
2022-11-19 13:54:55 +05:30
a3463274ee changelog 2022-11-19 12:02:52 +05:30
c10e773401 Speed up the model move, by using the earlier function to move modelCS and modelFS to the cpu 2022-11-19 11:53:33 +05:30
f7af259576 Scan only the model files (check by extension), minor refactoring of the
scanning code
2022-11-19 10:44:32 +05:30
87c6a54634 changelog 2022-11-18 21:11:59 +05:30
d03521bf12 Use as Input -> Use these settings 2022-11-18 21:11:34 +05:30
3eb1919c81 Fix the missing (beta) label next to the version number 2022-11-18 19:38:16 +05:30
e53d6dbd5c changelog 2022-11-18 19:33:34 +05:30
01d2db8e96 Changelog 2022-11-18 17:36:23 +05:30
b18c2aea05 Don't cache css and js files, it's really annoying after merging every PR to have to update the version number in index.html. Disabling this until we figure out a better way 2022-11-18 17:31:20 +05:30
a6e3c272e2 Tweak CSS for button press color 2022-11-18 17:14:58 +05:30
4000f98ba4 Merge pull request #490 from JeLuF/ui1
Visual feedback for the save button in the system settings
2022-11-18 17:10:04 +05:30
d06fd404ae Font size of negative prompt textbox 2022-11-18 17:09:42 +05:30
c6f0e19e2f Merge pull request #493 from JeLuF/negative
Textarea for negative prompts
2022-11-18 17:07:37 +05:30
462af9989a Merge pull request #491 from JeLuF/inst1
Fix typo in the installer's error messages
2022-11-18 17:07:06 +05:30
eedea2fdcd Bump version 2022-11-18 17:06:43 +05:30
9c3d946de0 Theme tweaks 2022-11-18 17:01:20 +05:30
ace3102601 Reduce the size of the toggle switches 2022-11-18 16:24:49 +05:30
48946100e9 Bump version 2022-11-18 16:21:12 +05:30
0067e46192 Merge pull request #501 from mdiller/mdiller_fancyswitches
Better Toggle Inputs & Updated Settings UI
2022-11-18 16:17:54 +05:30
921711a679 Don't crash if an invalid model file is beign scanned 2022-11-18 16:12:45 +05:30
32dfb765dd Bump JS version 2022-11-18 16:04:26 +05:30
8482f12909 Bump version 2022-11-18 16:03:15 +05:30
306a56124c Merge pull request #492 from JeLuF/scanner
Picklescan of model files
2022-11-18 16:02:23 +05:30
1f815d7562 Merge branch 'beta' into scanner 2022-11-18 16:01:50 +05:30
f25d35fad8 changelog 2022-11-18 15:55:53 +05:30
f74c57449e Cosmetic changes to Use Settings 2022-11-18 15:54:47 +05:30
a697bd935a Refactor the Use Settings code, and move that to the common restoreTask() function 2022-11-18 15:38:17 +05:30
ec294227bd Bump version 2022-11-18 15:14:35 +05:30
f67758eaf3 Bring back some styling that was removed accidentally by the PR merge 2022-11-18 14:58:21 +05:30
f7ed65d749 Bump version 2022-11-18 14:51:05 +05:30
7ffeb3964b Merge pull request #463 from patriceac/beta
Adding Use Settings
2022-11-18 14:40:04 +05:30
025d4df774 Don't crash if a VAE file fails to load 2022-11-18 13:11:48 +05:30
45086a4b6e updated hamunii theme name to gnomie 2022-11-17 22:25:55 -08:00
2db0023653 updated themes to work nicely with the new stuff 2022-11-17 20:15:39 -08:00
bfc21220a7 added hamunii theme 2022-11-17 18:13:08 -08:00
507491fbec added fancy switches and updated the ui of the settings tab 2022-11-17 17:58:09 -08:00
c890ef6917 Merge pull request #497 from cmdr2/beta
Beta
2022-11-17 13:40:26 +05:30
6756fb4fe7 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-17 13:39:40 +05:30
6c089c0a78 Link to Multi GPU wiki page 2022-11-17 13:39:29 +05:30
b2ab3f987c Merge pull request #496 from cmdr2/beta
v2.4.7
2022-11-17 13:20:20 +05:30
c99b2edf98 Update CHANGES.md 2022-11-17 13:17:20 +05:30
e052610184 Removed debugging log calls. 2022-11-17 02:45:09 -05:00
13f1725105 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-17 13:13:12 +05:30
f2367932e1 Style code tags in the What's New markdown 2022-11-17 13:13:01 +05:30
7e94ec986e Update CHANGES.md 2022-11-17 13:07:35 +05:30
7bda3e6994 Release summary and detailed changelog 2022-11-17 13:06:36 +05:30
3a18606385 Disable when targeting input elements. 2022-11-17 02:36:14 -05:00
e25a94e815 Bug fix, forgot to await promise. 2022-11-17 02:20:01 -05:00
c13f662e2d Use the document paste event for pasting json and text data. 2022-11-17 02:16:20 -05:00
97ee085f30 Fix a bug where Face Correction (GFPGAN) would fail on cuda:N (i.e. GPUs other than cuda:0), as well as fail on CPU if the system had an incompatible GPU. 2022-11-17 12:27:06 +05:30
1364fd5c45 Fix pasting in Firefox.
Should not display button.
https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#browser_compatibility
2022-11-17 01:44:11 -05:00
cc3186a683 Highlight the Save settings button when pressed 2022-11-17 11:01:10 +05:30
0c93c4754d Tabs to spaces 2022-11-17 10:49:22 +05:30
7b4cfbeeaa Update CHANGES.md 2022-11-16 23:47:24 +01:00
8cebb53147 Textarea for negative prompts 2022-11-16 23:35:27 +01:00
3e18f2f09c Picklescan of model files
During getModel(), the server picklescans the model files for potential
malicious code in the pickled python objects. If a malicious file is
found, the web UI will show a big red error message, the makeImage
button will be disabled, and the user must remove the malicious file
and reload the UI page.
2022-11-16 22:34:02 +01:00
add09e52ef Fix typo in the installer's error messages 2022-11-16 20:56:11 +01:00
5429a509c6 Visual feedback for the save button in the system settings 2022-11-16 20:52:40 +01:00
3555fa36aa Update CHANGES.md 2022-11-16 19:40:33 +05:30
ee31519552 Merge pull request #489 from cmdr2/main
v2.4.6 - merge hotfix from main - bring back the VRAM usage (during startup) to what it was in the previous main version (v2.3.5)
2022-11-16 19:36:49 +05:30
06b41aee58 Fix - reduce the amount of VRAM occupied when the program starts up, this caused a regression and failures on GPUs with 4 gb or less of VRAM 2022-11-16 19:29:04 +05:30
cd19d50e1d Update CHANGES.md 2022-11-16 17:00:31 +05:30
a5e4eea5ca Smaller version numbers in CHANGES 2022-11-16 16:56:35 +05:30
c6a6270e16 Typo 2022-11-16 16:55:11 +05:30
18d9d2602a Add a 'What's New?' tab as a core plugin, which fetches the contents of CHANGES.md from the app's release branch 2022-11-16 16:54:28 +05:30
f7ec9f2073 Merge pull request #488 from JeLuF/beta
Add Change history
2022-11-16 16:05:24 +05:30
4c5f66185d Add Change history 2022-11-16 10:44:56 +01:00
105c1893cb Merge branch 'beta' of https://github.com/patriceac/stable-diffusion-ui into beta 2022-11-16 00:40:00 -08:00
061cee207f Delete modifiers-dnd.plugin.js 2022-11-16 00:39:41 -08:00
af4a925b54 Delete modifiers-dnd.plugin.js 2022-11-16 00:38:17 -08:00
a6f3e87921 Image modifiers drag-and-drop plugin 2022-11-16 00:35:02 -08:00
9764d9109f Merge remote-tracking branch 'upstream/main' into beta 2022-11-16 00:29:52 -08:00
46dfa57ee0 Allow the core project to ship UI plugins 2022-11-16 13:30:40 +05:30
2c861c65d4 Merge pull request #485 from cmdr2/beta
UI setting for preventing browser autostart
2022-11-16 12:45:51 +05:30
a59bac4b40 UI setting for preventing browser autostart 2022-11-16 12:43:46 +05:30
cf214bf367 Merge pull request #484 from cmdr2/main
Merge main
2022-11-16 12:29:09 +05:30
75724797f7 Don't show a 500 error when the config json file doesn't exist 2022-11-16 12:20:25 +05:30
d04aeb55ad Fix default render device 2022-11-16 12:16:46 +05:30
47bd6dc6b8 Fix render devices auto 2022-11-16 12:14:06 +05:30
5e0f525932 Merge pull request #483 from cmdr2/beta
v2.4.4
2022-11-16 12:10:01 +05:30
1f66daf2f3 Write the config script files only if necessary 2022-11-16 11:40:51 +05:30
ded9cb0358 Check if config contains update_branch before trying to write it to a script file 2022-11-16 11:36:04 +05:30
04f201933b space apart the stop button 2022-11-16 11:33:05 +05:30
f5ec1cb3a4 Don't show the list of files that have been copied on startup 2022-11-16 11:31:16 +05:30
6c23e3f534 Bump version 2022-11-16 11:19:42 +05:30
e99d54d1f6 Merge main 2022-11-16 11:19:10 +05:30
7f436061b8 RFC: how to handle %TMP% and %TEMP% - UNTESTED
`conda activate` fails if %TMP% or %TEMP% contains spaces. So instead of setting the temp variables in the installer steps, we need to set it at the beginning of the script. 

Would this break anything? 
https://discord.com/channels/1014774730907209781/1042190192049668106/1042190192049668106
2022-11-15 23:29:32 +01:00
3c71200eb4 Update index.html 2022-11-15 16:06:50 +05:30
f124cf8318 Make the task config summary labels bold 2022-11-15 16:06:35 +05:30
9d2b944063 Remove unused variable 2022-11-15 13:18:00 +05:30
8e1ec5903b Don't throw an exception when an invalid device is being checked for compatibility. Report and return false 2022-11-15 12:41:10 +05:30
5cf763d51f Add a 'Save' button in settings, to avoid starting/stopping threads while a user is still modifying their GPU settings 2022-11-15 12:22:55 +05:30
3546859fe5 Bump version 2022-11-15 11:05:39 +05:30
6530e45178 Merge pull request #478 from madrang/beta
Changed update_render_threads to use SetAppConfigRequest.
2022-11-15 11:04:52 +05:30
07f0036b2b Merge pull request #476 from JeLuF/patch-2
Incr. Server State Validtiy to 90s
2022-11-15 10:19:45 +05:30
5237f55a71 Removed extra line, use only save_render_devices_to_config 2022-11-14 22:29:55 -05:00
a108e5067d Typos in comments. 2022-11-14 22:20:21 -05:00
a4a24b1a1a Fixed calling get_device_delta with a single cuda device inside config.json at boot. 2022-11-14 22:14:03 -05:00
ffe0eb1544 Changed update_render_threads to use SetAppConfigRequest to set which devices are active.
Keep ImageRequest.render_device for affinity only. (Send a task to an already active device.)
2022-11-14 21:54:24 -05:00
288e8a65f3 Incr. Server State Validtiy to 90s
By default, healthCheck() is run every 5s. On background tabs, this may get extended. My tests have shown pings every 60s. The ping was older than 10s, so the condition in line 490 evaluates to `false` and the client tries to access the stream before the server is ready. By increasing the validity this can be avoided - at least until the browser runs the healthcheck even less often.

See https://discord.com/channels/1014774730907209781/1041811939380178964/1041812021018120262 for the analysis.
2022-11-14 23:18:03 +01:00
0ebfbca93e Merge pull request #475 from JeLuF/beta
🔥Fix system info for CPU mode
2022-11-14 22:41:38 +05:30
f22f57495e Fix system info for CPU mode 2022-11-14 17:55:36 +01:00
8786a9d21d Fix border color of the image task container 2022-11-14 21:25:57 +05:30
f06a97d30b Move system info into settings 2022-11-14 21:21:48 +05:30
2329c47faf Bump version 2022-11-14 21:13:38 +05:30
2967261acb Ensure that we only pick better GPUs than the current one, during the subsequent tasks 2022-11-14 21:13:24 +05:30
64ff1ecbb6 Formatting for mem free 2022-11-14 21:02:17 +05:30
8707f88c07 Show mem free info 2022-11-14 20:35:47 +05:30
36846618ec Allow configuring whether the browser is opened by default 2022-11-14 20:15:54 +05:30
0cb2f19e29 Mark multi GPU as experimental in the UI 2022-11-14 20:06:20 +05:30
125a50ae87 Include the gpu id in the gpu list and system info 2022-11-14 20:01:57 +05:30
9d37ea23f8 Bump version 2022-11-14 19:53:55 +05:30
31617ae340 Show a system info tab, which shows the active GPUs 2022-11-14 19:53:40 +05:30
950614fb81 Bump version 2022-11-14 19:42:57 +05:30
14bbd7b7ae Merge pull request #474 from JeLuF/beta
Add paste button next to copy button
2022-11-14 19:06:52 +05:30
257cd34101 Merge branch 'beta' into beta 2022-11-14 19:06:35 +05:30
39814a89b6 Fix - setting can be null sometimes (autosave) 2022-11-14 18:09:25 +05:30
24fbbf8aa8 Remove unused variables 2022-11-14 16:26:16 +05:30
338ceffa6d Use 'auto' as the default render_device 2022-11-14 15:14:58 +05:30
371e104b00 Pick the device id 2022-11-14 13:43:37 +05:30
d5aba8eaf1 Show free/total mem while starting up 2022-11-14 13:40:55 +05:30
1d2b3a4ed8 Hide/show the GPUs list depending on whether auto is selected 2022-11-14 13:14:33 +05:30
f904945d40 Disable the GPU list if auto is enabled 2022-11-14 13:02:36 +05:30
027b2e1b88 Use the 65 percentile of free_mem for GPU selection, instead of 75 percentile 2022-11-14 12:26:21 +05:30
d79eb5e1a6 Typo 2022-11-14 11:51:56 +05:30
f6651b03b5 Workaround to run gfpgan on cuda:0 even if it's not enabled in the multi-gpu setup 2022-11-14 11:51:18 +05:30
5f880a179c Remove idle CPU unloading (when GPUs are active), because now a CPU can never be used along with GPUs 2022-11-14 11:24:30 +05:30
ea03fd22db Start on multiple GPUs by default (top 75 percentile by free_mem); UI selection for 'cpu' or 'auto' or a list of specific GPUs, which is now linked to the backend; Dynamically start/stop render threads for the devices, without requiring a full program restart 2022-11-14 11:23:22 +05:30
e561e4de0b Visual feedback for the copy and paste icons 2022-11-14 01:58:24 +01:00
1c3d5cd851 Add paste button next to copy button 2022-11-14 01:23:04 +01:00
332f2b0678 Hotfix for CSS layout regression 2022-11-12 19:04:23 -08:00
a19ba40672 Typo 2022-11-12 13:31:59 +05:30
3983cb001f Save the VAE model to the metadata text file 2022-11-12 13:29:24 +05:30
78b464b404 Merge pull request #464 from madrang/beta
Always return a byte buffer. Sending the picture as URL text fails in some browsers.
2022-11-12 11:51:52 +05:30
aa21115e26 Always return a byte buffer. Sending the picture as URL text fails in some browsers. 2022-11-11 20:44:39 -05:00
a39f845835 current_vae_path needs to be global 2022-11-11 19:30:33 +05:30
3fdd8d91e2 Handle device init failures and record that as an error, if the GPU has less than 3 gb of VRAM 2022-11-11 16:13:27 +05:30
c13bccc7ae Fix the error where a device named 'None' would get assigned for incompatible GPUs 2022-11-11 15:43:20 +05:30
b4f7d6bf25 Bump js version 2022-11-11 15:12:04 +05:30
fa0c2f7138 Temp change to get beta working and use a single GPU until the rest of the changes come through 2022-11-11 15:09:25 +05:30
453cc2a951 Bump version 2022-11-11 14:46:27 +05:30
bd56795c62 Switch to using cuda:N instead of N (integer device ids) 2022-11-11 14:46:05 +05:30
2c54b7f289 Remove the WIP line for render devices 2022-11-11 14:43:14 +05:30
cd5f847b55 Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-11 12:03:14 +05:30
a25544baea Fix the editor width on Chrome 2022-11-11 12:02:58 +05:30
39b6c5d6f4 Merge remote-tracking branch 'upstream/beta' into beta 2022-11-10 22:30:28 -08:00
f954542dda Merge pull request #461 from JeLuF/dontleave
Add event listener beforeunload
2022-11-11 10:58:15 +05:30
9fec7d236c Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-11 10:48:47 +05:30
67656accf8 Bump css version. This is annoying 2022-11-11 10:48:30 +05:30
64952a536c Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-11 10:47:52 +05:30
65e0d5f511 Attempt to fix horizontal resizing of the prompt textbox, thanks @Bilbo 2022-11-11 10:44:52 +05:30
903acff924 Adding Use Settings 2022-11-10 18:36:39 -08:00
5a06946469 Add event listener beforeunload
When closing the window, a warning is shown if there are any render results.
2022-11-10 23:23:20 +01:00
baef31b2c7 Send 'auto' as the render_device from the UI
, if no GPU is selected and CPU is unchecked)
2022-11-10 22:23:15 +05:30
b9a12d1562 Restrict device selection id to 'cpu' or integers (and 'auto' in the initial device selection functions) 2022-11-10 20:03:11 +05:30
3f26d03166 Show GPU list in the UI only if the PC has more than 1 GPU 2022-11-10 16:34:01 +05:30
1fed3ad532 Don't propagate events in the Stop Task button 2022-11-10 15:33:39 +05:30
929b245f5f Merge branch 'beta' of github.com:cmdr2/stable-diffusion-ui into beta 2022-11-10 14:59:11 +05:30
0da6354825 Press Ctrl+Enter to start a task 2022-11-10 14:59:01 +05:30
716a28891d Merge pull request #460 from JeLuF/helppage
Add Wiki TOC to the Help&Community tab
2022-11-10 13:17:26 +05:30
93a2e91694 Use theme variable for bottom border design 2022-11-10 00:44:42 +01:00
4913dc1aad Replace hr by border-bottom 2022-11-09 23:57:48 +01:00
087df18fea Add Wiki links to help&community page 2022-11-09 23:42:56 +01:00
058ce6fe82 UI-side changes for selecting multiple GPUs, and keeping the Use CPU option synchronized with the backend. This change isn't ready to be shipped, it still needs python-side changes to support the req.render_device config 2022-11-09 19:17:44 +05:30
087c10d52d Sort models by name 2022-11-09 17:35:55 +05:30
18292e447c Make the models dir if required 2022-11-09 16:28:58 +05:30
6c1dda47c0 Don't change the page when something other than an image or text file is dropped into the page (or an image is dropped outside the init image box) 2022-11-09 15:21:41 +05:30
ad1fc8f3d8 Bump version 2022-11-09 13:47:21 +05:30
bca98269bb Fix a bug where the custom image modifiers button would close the modifiers panel 2022-11-09 13:46:50 +05:30
1bebaf933d Bring back the old style panels for image settings and modifiers 2022-11-09 13:43:43 +05:30
166eb996a9 Bump versions 2022-11-09 12:26:27 +05:30
10fae34754 Bump js/css versions 2022-11-09 12:25:16 +05:30
aa4d97e8df Merge pull request #458 from mdiller/mdiller_ui_reorganize
UI Reorganization & Adding Tabs
2022-11-09 12:22:38 +05:30
dbbb9d7877 Temporarily remove the default-on behavior for GFPGAN, until the CPU version is fixed 2022-11-09 11:25:50 +05:30
3ff213b3e8 removed status on mobile 2022-11-08 21:51:12 -08:00
69c7f22053 Merge branch 'beta' into mdiller_ui_reorganize 2022-11-08 21:22:22 -08:00
75a964167a hid the now shown collapsible handle 2022-11-08 21:00:56 -08:00
c5768c81e1 Merge pull request #453 from patriceac/beta
Fix modifier and system settings popup position
2022-11-09 10:23:12 +05:30
4eb2b818e7 shrank system settings a bit so it fits on monible 2022-11-08 20:47:55 -08:00
f742aad810 Merge pull request #446 from JeLuF/sd-ui-bind
Protect SD_UI_BIND_PORT and SD_UI_BIND_IP in config files
2022-11-09 10:09:28 +05:30
d061eb2c64 updated to work decently for mobile 2022-11-08 20:37:49 -08:00
69aa115178 updated the about tab to be help and community, and fixed footer to act nicely 2022-11-08 20:19:31 -08:00
e175b87384 updated so tabs work now, and we have a settings tab and an about tab 2022-11-08 19:54:41 -08:00
f216ee739a updated with latest updates for this support 2022-11-08 19:22:14 -08:00
16d6644573 Merge pull request #455 from JeLuF/util-js
Fix: Uncaught TypeError: Cannot set properties of null
2022-11-09 08:22:46 +05:30
38afc6e6f8 Fix: Uncaught TypeError: Cannot set properties of null 2022-11-08 19:05:28 +01:00
8f1d214b12 Bring back CPU unloading of models when idle for a while (applies only when GPUs are present) 2022-11-08 19:23:35 +05:30
51fb1a43de Temporarily disable the idle CPU unloading behavior, since it's not clear whether it'll reload the model if a future request for the CPU is received after it has unloaded the model 2022-11-08 19:02:21 +05:30
a86b6bfbd6 Fix a bug with drag-and-drop where the upscale dropdown would not get enabled/disabled based on the setting 2022-11-08 18:46:55 +05:30
1176ddcc85 Fix a bug in drag-and-drop where an empty Negative Prompt line would result in the next line getting assigned to negative prompts; Simplify the drag-and-drop text file parsing logic to use a single algorithm, the files are small enough that we don't need over-optimization and confuse new developers 2022-11-08 18:44:11 +05:30
fa080e380c Fix a bug where images could no longer be dragged and dropped onto the initial image box 2022-11-08 18:14:26 +05:30
57c3acd9d8 Single line comment for Live Preview 2022-11-08 17:51:51 +05:30
302cf5b10b Show a tooltip over the ? help buttons 2022-11-08 17:49:46 +05:30
e2a9e81dbc Show tooltips on 'Copy Image Settings' 2022-11-08 17:40:47 +05:30
b1cf7391ce Add links to help docs for certain UI elements 2022-11-08 17:19:20 +05:30
9bc7521de0 Make custom VAE an Image Setting, rather than a System Setting; Don't load a VAE into memory by default 2022-11-08 16:54:15 +05:30
a68ebd2b76 Fixing the popup position on larger screens
Fixing the popup position on larger screens; Smaller screens still get the current rendering experience.
2022-11-08 02:17:26 -08:00
47f7c938ae Update main.css 2022-11-07 23:15:59 -08:00
bd5a6e6fb3 FS check, CV2.DLL fix, cleanup
- Only install on NTFS (FAT installations fail)
- Ask the user to install the Media Feature Pack on Windows 10 N (fixes python cv2 errors)
- Code cleanup
2022-11-08 00:20:36 +01:00
67cca3bc00 Print the devices for which rendering threads have started; Prettier print of the model data 2022-11-07 18:26:10 +05:30
90b1609d4e device_selection is already a string, since we've used string functions before this line 2022-11-07 18:08:43 +05:30
abbfae2fc0 Simplify the logic used for displaying the GFPGAN warning 2022-11-07 17:55:27 +05:30
b52b854270 Merge pull request #444 from madrang/beta
Remove prompt_strength and init_image when not using in-painting
2022-11-07 14:43:48 +05:30
58b759f652 Fix tabs to spaces 2022-11-06 16:50:23 +01:00
74ca756a53 Protect SD_UI_BIND_PORT and SD_UI_BIND_IP in config files 2022-11-06 00:27:11 +01:00
3576214920 Remove prompt_strength and init_image when not using in-painting 2022-11-05 13:39:19 -04:00
f964fe3750 Add on/off support for parsing boolean. 2022-11-05 13:33:38 -04:00
749c72e6a6 Fix https://github.com/cmdr2/stable-diffusion-ui/issues/441 - numerical validation 2022-11-04 19:48:34 +05:30
c3129a40f1 Merge pull request #440 from madrang/dragNdrop
Drag&Drop Fixes
2022-11-04 09:13:14 +05:30
d04aa89812 Fix 'Use Upscaling' dropdown getting blank on False. 2022-11-03 20:34:51 -04:00
d5f854d376 Fix use_face_correction not disabling on false 2022-11-03 20:34:12 -04:00
6c57fa078b Merge pull request #437 from madrang/dragNdrop
Requested fixes for Drag&Drop
2022-11-03 13:13:41 +05:30
c3cc75feff Adds a list of properties to not export by default. 2022-11-03 03:16:20 -04:00
d2e6011089 Windows paths... 2022-11-03 03:12:11 -04:00
5a18144366 Enable/disable seedField when updating randomSeedField.checked 2022-11-03 03:11:58 -04:00
8a0a22bfb0 Merge pull request #427 from madrang/dragNdrop
Add support for drag&drop for the text files made by the backend
2022-11-03 11:41:21 +05:30
950b226374 Moved copy icon css to main.css 2022-11-03 02:09:42 -04:00
74e64a4387 Merge pull request #435 from JeLuF/sanitize
Sanitize session id's before using them as path components
2022-11-03 10:57:12 +05:30
59e4c1cf79 Sanitize session id's before using them as path components 2022-11-03 00:43:44 +01:00
9b89ede9c4 Only require 8GB instead of 80GB
80GB was a leftover from the code test
2022-11-03 00:28:47 +01:00
bf205de3a1 NSIS script enhancements
- Enable Unicode support
- Activate LongPathsEnabled if the user tries to install into a directory 
  with more than 30 characters length
- Change welcome text page. No need to close other programs during our setup
- Warn if the user has less than 8GB of RAM
- Formatting, comments
2022-11-02 21:12:20 +01:00
045ad78bb9 Added calls to update sliders. 2022-11-02 10:53:48 -04:00
c0350e5be7 Moved file ext to a var. 2022-11-02 10:45:51 -04:00
2b3e38f77e Merge pull request #421 from madrang/beta
Fix plugins needing to specify many params or they would be missing in the render request.
2022-11-02 12:34:45 +05:30
d04fe5d582 Increase CSS version 2022-11-02 12:23:36 +05:30
17ab4caa5e Merge pull request #426 from ayunami2000/beta
Improve UI on mobile devices
2022-11-02 12:22:18 +05:30
976bc727dd Merge pull request #422 from madrang/device-select
Implement complete device selection in the backend.
2022-11-02 12:05:59 +05:30
484e53cc08 made first large swathe of changes for ui reorganization 2022-11-01 23:03:05 -07:00
b09b80933d Print device name on task start and complete to avoid doubt from users on what device selected the task. 2022-11-01 22:28:10 -04:00
8165086d02 NSIS scripts 2022-11-02 01:07:52 +01:00
82f14b087a NSIS Readme 2022-11-02 01:07:23 +01:00
93b3419737 Better human formatted JSON 2022-11-01 04:54:38 -04:00
19290fe467 Merge pull request #431 from JeLuF/patch-8
Copy CUDA_VISIBLE_DEVICES to config.*, it it has been set
2022-11-01 13:40:11 +05:30
d2f679030b Don't put CUDA_VISIBLE_DEVICES hints if it's already set 2022-11-01 01:16:29 +01:00
268d7495cc Naming... 2022-10-31 01:13:04 -04:00
ce16e61e63 Adds a copy as JSON button. 2022-10-31 01:02:23 -04:00
f92bca58fa Lines endings... 2022-10-31 01:01:56 -04:00
83d541b60d Fixed model parsing... 2022-10-30 23:41:26 -04:00
965efc3a13 Restore old values if invalid values for the dropdown was used. 2022-10-30 23:35:42 -04:00
d656c34bd4 Add support for drag&drop for the text files made by the backend and also supports JSON. 2022-10-30 23:21:39 -04:00
7f151cbeba Copy CUDA_VISIBLE_DEVICES to config.*, it it has been set
Don't delete CUDA_VISIBLE_DEVICES settings when generating a new config file
2022-10-31 00:48:18 +01:00
bc2f9204e9 Improve UI on mobile devices 2022-10-30 18:16:31 -04:00
a922a93016 Can work with one or more params, don't need a minimum of two.
Still works just the same.
2022-10-30 14:09:12 -04:00
eb596ba866 Allow start_render_thread to proceed faster in case of failure. 2022-10-30 06:04:06 -04:00
2208545612 Don't display this warning if on CPU. 2022-10-30 05:39:45 -04:00
c687091ce9 Only return valid data for alive threads. 2022-10-30 01:38:32 -04:00
eb994716e6 Indentation... 2022-10-30 01:33:17 -04:00
70acc8a7c0 Syntax... 2022-10-29 19:02:07 -04:00
bf97781232 Don't let users register the same device twice. 2022-10-29 18:57:31 -04:00
099727d671 Added auto unload to CPU if GPUs are active. 2022-10-29 18:57:10 -04:00
6229cdb1ba Added a missing device_name 2022-10-29 17:47:45 -04:00
b7a663ed20 Implement complete device selection in the backend. 2022-10-29 17:34:53 -04:00
3bd97352ba Don't reset reqBody, only replace using req as we use a new task object created from UI inputs.
Fix plugins needing to specify many params or they would be missing in the render request.
2022-10-29 14:47:58 -04:00
5e22360cb1 Change the JS/CSS version 2022-10-29 18:10:23 +05:30
840348b4eb fix: change to the correct working directory
changes to the directory containing `start.sh` prior to activating the conda environment

this allows you to run the program without first changing to the correct directory, eg: `$ ~/bin/stable-diffusion-ui/start.sh`
2022-10-29 15:09:04 +05:30
cf04738594 Merge pull request #420 from madrang/beta
Missing .lower() cause CUDA:0 to fail check where cuda:0 works.
2022-10-29 15:01:13 +05:30
03757632cf Missing .lower() cause CUDA:0 to fail check where cuda:0 works. 2022-10-29 04:33:14 -04:00
e818f5a93f Bump version 2022-10-29 12:29:28 +05:30
ab9b08770a Merge pull request #417 from mdiller/mdiller_parameters
Moved System Settings & Reworked into "Parameters"
2022-10-29 12:04:21 +05:30
40df8b68ad Merge pull request #418 from madrang/beta
Changed failure in start_render_thread to return false instead of throwing exception
2022-10-29 11:33:36 +05:30
9f5202fee3 Improved readability and comments. 2022-10-29 00:43:02 -04:00
902ccbd203 Don't try to start cuda:0 if auto used cpu mode. 2022-10-29 00:36:26 -04:00
4675da4d16 Display warning on start failure.
Removes spam from exception and continue starting other devices.
2022-10-28 22:53:55 -04:00
86da27a7a1 Moved wait outside lock and now returns false on failure. 2022-10-28 22:52:00 -04:00
fc2a6567da Moved import before use of runtime.thread_data.device 2022-10-28 22:51:04 -04:00
7c611d9b62 added some shadow and animation to popups 2022-10-28 18:41:41 -07:00
784c7465d1 updated settings labels 2022-10-28 18:31:46 -07:00
301af7bd7a added parameters 2022-10-28 18:25:54 -07:00
09c11a385d normalized popups 2022-10-28 16:48:32 -07:00
ef6f491d94 Write lines, please 2022-10-28 22:42:11 +05:30
9dcef00fbb New lines for config.sh 2022-10-28 22:35:04 +05:30
e781e5dd43 Need to wrap the filter() output in a list 2022-10-28 22:30:05 +05:30
d3e672d811 Replace os-specific newlines with writelines() 2022-10-28 22:23:52 +05:30
dad1554ec2 Fix a bug where config.bat would not get written properly 2022-10-28 21:07:18 +05:30
30bf96c6cd Fix a bug where beta wouldn't switch properly because the config.bat/sh files weren't being written 2022-10-28 21:00:25 +05:30
a8c16e39b8 Support custom VAE files; Use vae-ft-mse-840000-ema-pruned as the default VAE, which can be overridden by putting a .vae.pt file inside models/stable-diffusion with the same name as the ckpt model file. The UI / System Settings allows setting the default VAE model to use 2022-10-28 20:06:44 +05:30
79a7cd2938 Merge pull request #414 from madrang/beta
Set online after preload. Move ident to include in if check.
2022-10-28 13:54:44 +05:30
26562e445f Set online after preload. Move ident to include in if check. 2022-10-28 04:09:34 -04:00
2432491bfc Merge pull request #411 from madrang/beta
Apply force_full_precision if was set on device_select.
2022-10-28 11:50:16 +05:30
a09ce3e026 Merge pull request #412 from mdiller/mdiller_progressbar
Quick Fix for Progressbar Behavior
2022-10-28 11:49:42 +05:30
c52fc843f6 Comment... 2022-10-28 02:09:11 -04:00
02240bda25 Moved up to not duplicate if statement. 2022-10-28 02:05:48 -04:00
0185ef7c83 Apply force_full_precision if was set on device_select. 2022-10-28 02:02:09 -04:00
7d29b9901c updated progressbar to end more consistently 2022-10-27 22:47:08 -07:00
ae553dfed3 Merge pull request #410 from madrang/beta
Only default to cpu on auto or current when cuda not available.
2022-10-28 10:43:28 +05:30
71c6beadb4 Only default to cpu on auto or current.
Not when a specific device was requested.
2022-10-28 01:09:38 -04:00
d939629c09 Bump version 2022-10-28 10:39:23 +05:30
0a569146a8 Merge pull request #406 from mdiller/mdiller_progressbar
Fixed/Implemented Progressbar
2022-10-28 10:34:03 +05:30
d5a012d49f Merge pull request #407 from madrang/beta
Wait until device is fully ready before proceeding.
2022-10-28 10:28:16 +05:30
22a11769fa Enable preload on cpu when no other devices are alive. 2022-10-27 21:57:50 -04:00
7dc7ba9977 Removed old comments. 2022-10-27 21:47:44 -04:00
fa4059a4b9 Removed all async code since now start_render_thread wait for init to complete making this useless. 2022-10-27 21:40:16 -04:00
7f4786f9dd Wait until device is fully ready before proceding. 2022-10-27 20:27:21 -04:00
5a6e7a46d1 added progressbar 2022-10-27 17:03:09 -07:00
0dfaf9159d Put back the check to only preload on GPU 2022-10-28 00:04:33 +05:30
5d8bda1178 Merge pull request #404 from cmdr2/multi-gpu
Support for multiple GPUs, and improvements to RAM and VRAM usage
2022-10-27 23:52:11 +05:30
9ad1e0d529 Allow the user to specify any disk path to the model, in the config 2022-10-27 23:39:29 +05:30
389e3397ec Preload the model even in the CPU mode 2022-10-27 23:17:41 +05:30
284b95213e Fix a bug where the device wouldn't get set if no cuda-compatible hardware was found 2022-10-27 22:59:55 +05:30
952854f64e Revert 554650c18d 2022-10-27 22:59:17 +05:30
554650c18d Fix a bug where the device wouldn't get set if no cuda-compatible hardware was found 2022-10-27 22:51:45 +05:30
01a2fa7c2d Fix a bug where the default model would not load if the user hadn't already configured a custom model (e.g. in a fresh installation); Check for the model in the models/stable-diffusion folder first, before checking in the direct folder 2022-10-27 22:34:23 +05:30
7d03719816 Merge pull request #403 from cmdr2/main
Merge main
2022-10-27 20:50:09 +05:30
7c5bbca2fa Bump version 2022-10-27 20:49:05 +05:30
3fb5d886dc Merge pull request #398 from madrang/mGpu-crashHandling
mGpu crash handling
2022-10-27 13:44:26 +05:30
b57cd8d5c2 Merge pull request #397 from madrang/mGpu-fixtype
Fix TypeError: string indices must be integers
2022-10-27 13:44:19 +05:30
d3df113fb0 When reduced_memory is True, on crash only move model back to Cpu. 2022-10-26 16:52:31 -04:00
06c2ab045a Fix TypeError: string indices must be integers 2022-10-26 16:14:29 -04:00
ec14429238 Merge pull request #348 from madrang/multi-gpu
Multi gpu
2022-10-26 19:33:44 +05:30
ae40b6ba8c Missed a is_alive check in the conversion. 2022-10-25 03:00:50 -04:00
d482427e0d Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-25 02:51:31 -04:00
c41baf3aeb Moved img_id creation inside save image loop. 2022-10-25 02:10:52 -04:00
189d31cc29 Specify update_ttl on all get_cached_task calls. 2022-10-24 05:12:08 -04:00
fc875651d3 Removed unused vars 2022-10-23 05:00:21 -04:00
0d62123a0b Replaced missing gpu_name by device_name 2022-10-22 21:28:12 -04:00
28fed6281f Merge branch 'beta' into multi-gpu 2022-10-22 21:20:02 -04:00
8adf965d0b Formatting changes. 2022-10-22 19:02:02 -04:00
026dd38480 Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-22 18:07:22 -04:00
364e364429 Added get_cached_task to replace task_cache.tryGet in server.py
Now updated cache TTL on /stream and temp images endpoints.
Keep images alive longer when browser keeps reading the endpoints.
2022-10-22 13:52:13 -04:00
46a46877ed Missing model_path replaced by model_name 2022-10-22 13:49:23 -04:00
2c1a897c4e Missing newline. 2022-10-22 12:50:53 -04:00
344dd92c85 Improved checks on '/render' requests 2022-10-22 12:29:01 -04:00
4167c65acf Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-22 12:17:12 -04:00
cd6d49860f Missing a 'r' in progress 2022-10-22 01:23:39 -04:00
8a10fcf7ea updated print statement. 2022-10-22 00:34:33 -04:00
3b5f96a133 Fixed stopping tasks and more cleaning. 2022-10-21 22:45:19 -04:00
ce2b711b1f Newlines... 2022-10-21 21:44:15 -04:00
667fb438cb Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu
# Conflicts:
#	ui/media/js/main.js
2022-10-21 21:10:02 -04:00
7befa94e6d More comments and cleanup. 2022-10-21 20:56:24 -04:00
88ef1a3c5b Moved time before model.to 2022-10-21 20:22:34 -04:00
ccb7a553c2 Memory improvements 2022-10-21 19:34:29 -04:00
1442748f58 When starting with profiler cuda devices are slower to init. 2022-10-21 03:53:26 -04:00
56ed4fe6f2 Fix VisualStudio Type Warning. 2022-10-21 01:30:49 -04:00
849d1d7ebd Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu
# Conflicts:
#	ui/media/js/main.js
#	ui/sd_internal/runtime.py
#	ui/server.py
2022-10-20 20:08:23 -04:00
fc8660df78 Faster response on invalid settings when CPU was specified with GFPGANer. 2022-10-19 05:19:16 -04:00
4e5ddca3bd Display the failure detail when there is one at that step.
Was checking the json object, not the server response.
2022-10-19 05:10:37 -04:00
3bdc90451a Dont preload on cpu. 2022-10-19 04:34:54 -04:00
a036b2981a Removed forgotten mention of CPU in message to user. 2022-10-19 04:31:57 -04:00
8fae83dab7 Print value to console for better debug from logs. 2022-10-19 04:26:09 -04:00
ef68e5b13d Added warning about validating config. 2022-10-19 04:16:46 -04:00
21afe077d7 Removed Cpu from the devices allowed to run GFPGANer.
Added clear error for the user.
2022-10-19 03:02:26 -04:00
3fc66ec525 Removed empty lines left over from merge. 2022-10-19 00:27:51 -04:00
0da0c6bd77 Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-19 00:26:09 -04:00
6098b196dc Text header, comments and better validations. 2022-10-18 23:58:55 -04:00
53cdeeff03 More fixes to devices changing names. 2022-10-18 21:08:04 -04:00
fcdb086daf Fixed is_alive to work with devices that can change name after init. 2022-10-18 20:33:37 -04:00
cfd6751777 Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-18 13:21:26 -04:00
5e461e9b6b Fixed is_alive with render_threads that can update the device name after starting. 2022-10-18 13:21:15 -04:00
940236b4a4 Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-18 03:23:42 -04:00
3c8692d06c Merge branch 'beta' of https://github.com/cmdr2/stable-diffusion-ui.git into multi-gpu 2022-10-18 02:52:50 -04:00
04fe81e001 Merge branch 'HTTPException-fix' into multi-gpu
# Conflicts:
#	ui/server.py
2022-10-18 02:36:57 -04:00
578b3ba4f4 Force encoding to utf-8 on text file operations Fixes #332
# Conflicts:
#	ui/server.py
2022-10-17 23:15:36 -04:00
e24be913e5 Merge branch 'beta' into multi-gpu 2022-10-17 21:35:24 -04:00
4d3358ba66 Fixed file path bugs introduced by mistake and made img_id sequential based on time for better sorting of renders. 2022-10-17 21:29:14 -04:00
87f93b34a3 Fixed a typo when adding a comment. 2022-10-17 14:44:53 -04:00
c92129ac63 Improved detection of missing cuda:0 and added warning to console about how to fix. 2022-10-17 03:32:23 -04:00
554b67a2f0 Fixing bug in is_alive. 2022-10-17 01:05:51 -04:00
012243a880 Process GPU tasks on CPU when there are no cuda devices at all. 2022-10-17 01:05:27 -04:00
d4a348a2b2 Process GFPGANer on cuda:0 when possible, otherwise use cpu. 2022-10-16 23:12:46 -04:00
1d4c5cc96f Added clear error response when submitting tasks that requires GFPGANer if cuda:0 and cpu rendering is disabled. 2022-10-16 23:07:55 -04:00
41bfb96b6b Fixed bug in task_manager.is_alive and added way to check for first device. 2022-10-16 23:06:41 -04:00
994d62ac65 Added a clear error message when targeting CPU if not enabled in config. 2022-10-16 22:26:05 -04:00
7c72608e1c First draft for Multi-GPU support 2022-10-16 21:41:39 -04:00
92 changed files with 23151 additions and 3369 deletions

27
3rd-PARTY-LICENSES Normal file
View File

@ -0,0 +1,27 @@
jquery-confirm
==============
https://craftpip.github.io/jquery-confirm/
jquery-confirm is licensed under the MIT license:
The MIT License (MIT)
Copyright (c) 2019 Boniface Pereira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

134
CHANGES.md Normal file
View File

@ -0,0 +1,134 @@
# What's new?
## v2.5
### Major Changes
- **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.
- **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.
- **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
- **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).
- **Color correction for img2img** - an option to preserve the color profile (histogram) of the initial image. This is especially useful if you're getting red-tinted images after inpainting/masking.
- **Three GPU Memory Usage Settings** - `High` (fastest, maximum VRAM usage), `Balanced` (default - almost as fast, significantly lower VRAM usage), `Low` (slowest, very low VRAM usage). The `Low` setting is applied automatically for GPUs with less than 4 GB of VRAM.
- **Find models in sub-folders** - This allows you to organize your models into sub-folders inside `models/stable-diffusion`, instead of keeping them all in a single folder.
- **Save metadata as JSON** - You can now save the metadata files as either text or json files (choose in the Settings tab).
- **Major rewrite of the code** - Most of the codebase has been reorganized and rewritten, to make it more manageable and easier for new developers to contribute features. We've separated our core engine into a new project called `sdkit`, which allows anyone to easily integrate Stable Diffusion (and related modules like GFPGAN etc) into their programming projects (via a simple `pip install sdkit`): https://github.com/easydiffusion/sdkit/
- **Name change** - Last, and probably the least, the UI is now called "Easy Diffusion". It indicates the focus of this project - an easy way for people to play with Stable Diffusion.
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
* 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.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 - 3 Feb 2023 - Fix the 'Make Similar Images' button, which was producing incorrect images (weren't very similar).
* 2.5.13 - 1 Feb 2023 - Fix the remaining GPU memory leaks, including a better fix (more comprehensive) for the change in 2.5.12 (27 Jan).
* 2.5.12 - 27 Jan 2023 - Fix a memory leak, which made the UI unresponsive after an out-of-memory error. The allocated memory is now freed-up after an error.
* 2.5.11 - 25 Jan 2023 - UI for Merging Models. Thanks @JeLuf. More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/Model-Merging
* 2.5.10 - 24 Jan 2023 - Reduce the VRAM usage for img2img in 'balanced' mode (without reducing the rendering speed), to make it similar to v2.4 of this UI.
* 2.5.9 - 23 Jan 2023 - Fix a bug where img2img would produce poorer-quality images for the same settings, as compared to version 2.4 of this UI.
* 2.5.9 - 23 Jan 2023 - Reduce the VRAM usage for 'balanced' mode (without reducing the rendering speed), to make it similar to v2.4 of the UI.
* 2.5.8 - 17 Jan 2023 - Fix a bug where 'Low' VRAM usage would consume a LOT of VRAM (on higher-end GPUs). Also fixed a bug that caused out-of-memory errors on SD 2.1-768 models, on 'high' VRAM usage setting.
* 2.5.7 - 16 Jan 2023 - Fix a bug where VAE files ending with .vae.pt weren't getting displayed. Thanks Madrang, rbertus2000 and JeLuf.
* 2.5.6 - 10 Jan 2023 - `Fill` tool for the Image Editor, to allow filling areas with color (or the entire image). And some bug fixes to the Image Editor. Thanks @mdiller.
* 2.5.6 - 10 Jan 2023 - Find Stable Diffusion models in sub-folders inside `models/stable-diffusion`. This allows you to organize your models into sub-folders, instead of keeping them all in a single folder. Thanks @JeLuf.
* 2.5.5 - 9 Jan 2023 - Lots of bug fixes. Thanks @patriceac and @JeLuf.
* 2.5.4 - 29 Dec 2022 - Press Esc key on the keyboard to close the Image Editor. Thanks @patriceac.
* 2.5.4 - 29 Dec 2022 - Lots of bug fixes in the UI. Thanks @patriceac.
* 2.5.4 - 28 Dec 2022 - Full support for running tasks in parallel on multiple GPUs. Warning: 'Euler Ancestral', 'DPM2 Ancestral' and 'DPM++ 2s Ancestral' may produce slight variations in the image (if run in parallel), so we recommend using the other samplers.
* 2.5.3 - 27 Dec 2022 - Fix broken drag-and-drop for text metadata files (as well as paste in clipboard).
* 2.5.3 - 27 Dec 2022 - Allow upscaling by 2x as well as 4x.
* 2.5.3 - 27 Dec 2022 - Fix broken renders on a second GPU.
* 2.5.3 - 26 Dec 2022 - Add a `Remove` button on each image. Thanks @JeLuf.
* 2.5.2 - 26 Dec 2022 - Fix broken inpainting if using non-square target images.
* 2.5.2 - 26 Dec 2022 - Fix a bug where an incorrect model config would get used for some SD 2.1 models.
* 2.5.2 - 26 Dec 2022 - Slight performance and memory improvement while rendering using SD 2.1 models.
* 2.5.1 - 25 Dec 2022 - Allow custom config yaml files for models. You can put a config file (`.yaml`) next to the model file, with the same name as the model. For e.g. if you put `robo-diffusion-v2-base.yaml` next to `robo-diffusion-v2-base.ckpt`, it'll automatically use that config file.
* 2.5.1 - 25 Dec 2022 - Fix broken rendering for SD 2.1-768 models. Fix broken rendering SD 2.0 safetensor models.
* 2.5.0 - 25 Dec 2022 - Major new release! Nearly twice as fast, Full support for SD 2.1 (including low GPU RAM optimizations), 6 new samplers, Model Merging, Fast loading/unloading of VAEs, Database of known models, Color correction for img2img, Three GPU Memory Usage Settings, Save metadata as JSON, Major rewrite of the code, Name change.
## v2.4
### Major Changes
- **Allow reordering the task queue** (by dragging and dropping tasks). Thanks @madrang
- **Automatic scanning for malicious model files** - using `picklescan`, and support for `safetensor` model format. Thanks @JeLuf
- **Image Editor** - for drawing simple images for guiding the AI. Thanks @mdiller
- **Use pre-trained hypernetworks** - for improving the quality of images. Thanks @C0bra5
- **Support for custom VAE models**. You can place your VAE files in the `models/vae` folder, and refresh the browser page to use them. More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder
- **Experimental support for multiple GPUs!** It should work automatically. Just open one browser tab per GPU, and spread your tasks across your GPUs. For e.g. open our UI in two browser tabs if you have two GPUs. You can customize which GPUs it should use in the "Settings" tab, otherwise let it automatically pick the best GPUs. Thanks @madrang . More info: https://github.com/cmdr2/stable-diffusion-ui/wiki/Run-on-Multiple-GPUs
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
- **Progress bar.** Thanks @mdiller
- **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
- Drag and Drop **text files generated from previously saved images**, and copy settings to clipboard. Thanks @madrang
- Paste settings from clipboard. Thanks @JeLuf
- Bug fixes to reduce the chances of tasks crashing during long multi-hour runs (chrome can put long-running background tabs to sleep). Thanks @JeLuf and @madrang
- **Improved documentation.** Thanks @JeLuf and @jsuelwald
- Improved the codebase for dealing with system settings and UI settings. Thanks @mdiller
- Help instructions next to some setttings, and in the tab
- Show system info in the settings tab
- Keyboard shortcut: Ctrl+Enter to start a task
- Configuration to prevent the browser from opening on startup
- Lots of minor bug fixes
- A `What's New?` tab in the UI
- Ask for a confimation before clearing the results pane or stopping a render task. The dialog can be skipped by holding down the shift key while clicking on the button.
- Show the network addresses of the server in the systems setting dialog
- Support loading models in the safetensor format, for improved safety
### Detailed changelog
* 2.4.24 - 9 Jan 2022 - Urgent fix for failures on old/long-term-support browsers. Thanks @JeLuf.
* 2.4.23/22 - 29 Dec 2022 - Allow rolling back from the upcoming v2.5 change (in beta).
* 2.4.21 - 23 Dec 2022 - Speed up image creation, by removing a delay (regression) of 4-5 seconds between clicking the `Make Image` button and calling the server.
* 2.4.20 - 22 Dec 2022 - `Pause All` button to pause all the pending tasks. Thanks @JeLuf
* 2.4.20 - 22 Dec 2022 - `Undo`/`Redo` buttons in the image editor. Thanks @JeLuf
* 2.4.20 - 22 Dec 2022 - Drag handle to reorder the tasks. This fixed a bug where the metadata was no longer selectable (for copying). Thanks @JeLuf
* 2.4.19 - 17 Dec 2022 - Add Undo/Redo buttons in the Image Editor. Thanks @JeLuf
* 2.4.19 - 10 Dec 2022 - Show init img in task list
* 2.4.19 - 7 Dec 2022 - Use pre-trained hypernetworks while generating images. Thanks @C0bra5
* 2.4.19 - 6 Dec 2022 - Allow processing new tasks first. Thanks @madrang
* 2.4.19 - 6 Dec 2022 - Allow reordering the task queue (by dragging tasks). Thanks @madrang
* 2.4.19 - 6 Dec 2022 - Re-organize the code, to make it easier to write user plugins. Thanks @madrang
* 2.4.18 - 5 Dec 2022 - Make JPEG Output quality user controllable. Thanks @JeLuf
* 2.4.18 - 5 Dec 2022 - Support loading models in the safetensor format, for improved safety. Thanks @JeLuf
* 2.4.18 - 1 Dec 2022 - Image Editor, for drawing simple images for guiding the AI. Thanks @mdiller
* 2.4.18 - 1 Dec 2022 - Disable an image modifier temporarily by right-clicking it. Thanks @patriceac
* 2.4.17 - 30 Nov 2022 - Scroll to generated image. Thanks @patriceac
* 2.4.17 - 30 Nov 2022 - Show the network addresses of the server in the systems setting dialog. Thanks @JeLuf
* 2.4.17 - 30 Nov 2022 - Fix a bug where GFPGAN wouldn't work properly when multiple GPUs tried to run it at the same time. Thanks @madrang
* 2.4.17 - 30 Nov 2022 - Confirm before stopping or clearing all the tasks. Thanks @JeLuf
* 2.4.16 - 29 Nov 2022 - Bug fixes for SD 2.0 - remove the need for patching, default to SD 1.4 model if trying to load an SD2 model in SD1.4.
* 2.4.15 - 25 Nov 2022 - Experimental support for SD 2.0. Uses lots of memory, not optimized, probably GPU-only.
* 2.4.14 - 22 Nov 2022 - Change the backend to a custom fork of Stable Diffusion
* 2.4.13 - 21 Nov 2022 - Change the modifier weight via mouse wheel, drag to reorder selected modifiers, and some more modifier-related fixes. Thanks @patriceac
* 2.4.12 - 21 Nov 2022 - Another fix for improving how long images take to generate. Reduces the time taken for an enqueued task to start processing.
* 2.4.11 - 21 Nov 2022 - Installer improvements: avoid crashing if the username contains a space or special characters, allow moving/renaming the folder after installation on Windows, whitespace fix on git apply
* 2.4.11 - 21 Nov 2022 - Validate inputs before submitting the Image request
* 2.4.11 - 19 Nov 2022 - New system settings to manage the network config (port number and whether to only listen on localhost)
* 2.4.11 - 19 Nov 2022 - Address a regression in how long images take to generate. Use the previous code for moving a model to CPU. This improves things by a second or two per image, but we still have a regression (investigating).
* 2.4.10 - 18 Nov 2022 - Textarea for negative prompts. Thanks @JeLuf
* 2.4.10 - 18 Nov 2022 - Improved design for Settings, and rounded toggle buttons instead of checkboxes for a more modern look. Thanks @mdiller
* 2.4.9 - 18 Nov 2022 - Add Picklescan - a scanner for malicious model files. If it finds a malicious file, it will halt the web application and alert the user. Thanks @JeLuf
* 2.4.8 - 18 Nov 2022 - A `Use these settings` button to use the settings from a previously generated image task. Thanks @patriceac
* 2.4.7 - 18 Nov 2022 - Don't crash if a VAE file fails to load
* 2.4.7 - 17 Nov 2022 - Fix a bug where Face Correction (GFPGAN) would fail on cuda:N (i.e. GPUs other than cuda:0), as well as fail on CPU if the system had an incompatible GPU.
* 2.4.6 - 16 Nov 2022 - Fix a regression in VRAM usage during startup, which caused 'Out of Memory' errors when starting on GPUs with 4gb (or less) VRAM
* 2.4.5 - 16 Nov 2022 - Add checkbox for "Open browser on startup".
* 2.4.5 - 16 Nov 2022 - Add a directory for core plugins that ship with Stable Diffusion UI by default.
* 2.4.5 - 16 Nov 2022 - Add a "What's New?" tab as a core plugin, which fetches the contents of CHANGES.md from the app's release branch.

View File

@ -6,7 +6,7 @@ Thanks
# For developers:
If you would like to contribute to this project, there is a discord for dicussion:
If you would like to contribute to this project, there is a discord for discussion:
[![Discord Server](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.com/invite/u9yhsFmEkB)
## Development environment for UI (frontend and server) changes
@ -40,6 +40,7 @@ or for Windows
`mklink /J \projects\stable-diffusion-ui-archive\ui \projects\stable-diffusion-ui-repo\ui` (link name first, source repo dir second)
9) Run the project again (like in step 2) and ensure you can still use the UI.
10) Congrats, now any changes you make in your repo `ui` folder are linked to this running archive of the app and can be previewed in the browser.
11) Please update CHANGES.md in your pull requests.
Check the `ui/frontend/build/README.md` for instructions on running and building the React code.

1
NSIS/README.md Normal file
View File

@ -0,0 +1 @@
Scripts to be used with the Nullsoft Scriptable Installation System

BIN
NSIS/astro.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
NSIS/sd.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

265
NSIS/sdui.nsi Normal file
View File

@ -0,0 +1,265 @@
; Script generated by the HM NIS Edit Script Wizard.
Target x86-unicode
Unicode True
!AddPluginDir /x86-unicode "."
; HM NIS Edit Wizard helper defines
!define PRODUCT_NAME "Stable Diffusion UI"
!define PRODUCT_VERSION "Installer 2.35"
!define PRODUCT_PUBLISHER "cmdr2 and contributors"
!define PRODUCT_WEB_SITE "https://stable-diffusion-ui.github.io"
!define PRODUCT_DIR_REGKEY "Software\Microsoft\Cmdr2\App Paths\installer.exe"
; MUI 1.67 compatible ------
!include "MUI.nsh"
!include "LogicLib.nsh"
!include "nsDialogs.nsh"
Var Dialog
Var Label
Var Button
Var InstDirLen
Var LongPathsEnabled
Var AccountType
;---------------------------------------------------------------------------------------------------------
; This function returns the number of spaces in a string.
; The string is passed on the stack (using Push $STRING)
; The result is also returned on the stack and can be consumed with Pop $var
; https://nsis.sourceforge.io/Check_for_spaces_in_a_directory_path
Function CheckForSpaces
Exch $R0
Push $R1
Push $R2
Push $R3
StrCpy $R1 -1
StrCpy $R3 $R0
StrCpy $R0 0
loop:
StrCpy $R2 $R3 1 $R1
IntOp $R1 $R1 - 1
StrCmp $R2 "" done
StrCmp $R2 " " 0 loop
IntOp $R0 $R0 + 1
Goto loop
done:
Pop $R3
Pop $R2
Pop $R1
Exch $R0
FunctionEnd
;---------------------------------------------------------------------------------------------------------
; The function DirectoryLeave is called after the user chose the installation directory.
; If it calls "abort", the user is sent back to choose a different directory.
Function DirectoryLeave
; check whether the installation directory path is longer than 30 characters.
; If yes, we suggest to the user to enable long filename support
;----------------------------------------------------------------------------
StrLen $InstDirLen "$INSTDIR"
; Check whether the registry key that allows for >260 characters in a path name is set
ReadRegStr $LongPathsEnabled HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled"
${If} $InstDirLen > 30
${AndIf} $LongPathsEnabled == "0"
; Check whether we're in the Admin group
UserInfo::GetAccountType
Pop $AccountType
${If} $AccountType == "Admin"
${AndIf} ${Cmd} `MessageBox MB_YESNO|MB_ICONQUESTION 'The path name is too long. $\n$\nYou can either enable long file name support in Windows,$\nor you can go back and choose a different path.$\n$\nFor details see: shorturl.at/auBD1$\n$\nEnable long path name support in Windows?' IDYES`
; Enable long path names
WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Control\FileSystem" "LongPathsEnabled" 1
${Else}
MessageBox MB_OK|MB_ICONEXCLAMATION "Installation path name too long. The installation path must not have more than 30 characters."
abort
${EndIf}
${EndIf}
; Check for spaces in the installation directory path.
; ----------------------------------------------------
; $R0 = CheckForSpaces( $INSTDIR )
Push $INSTDIR # Input string (install path).
Call CheckForSpaces
Pop $R0 # The function returns the number of spaces found in the input string.
; Check if any spaces exist in $INSTDIR.
${If} $R0 != 0
; Plural if more than 1 space in $INSTDIR.
; If $R0 == 1: $R1 = ""; else: $R1 = "s"
StrCmp $R0 1 0 +3
StrCpy $R1 ""
Goto +2
StrCpy $R1 "s"
; Show message box then take the user back to the Directory page.
MessageBox MB_OK|MB_ICONEXCLAMATION "Error: The Installaton directory \
has $R0 space character$R1.$\nPlease choose an installation directory without space characters."
Abort
${EndIf}
; Check for NTFS filesystem. Installations on FAT fail.
; -----------------------------------------------------
StrCpy $5 $INSTDIR 3
System::Call 'Kernel32::GetVolumeInformation(t "$5",t,i ${NSIS_MAX_STRLEN},*i,*i,*i,t.r1,i ${NSIS_MAX_STRLEN})i.r0'
${If} $0 <> 0
${AndIf} $1 == "NTFS"
MessageBox mb_ok "$5 has filesystem type '$1'.$\nOnly NTFS filesystems are supported.$\nPlease choose a different drive."
Abort
${EndIf}
FunctionEnd
;---------------------------------------------------------------------------------------------------------
; Open the MS download page in a browser and enable the [Next] button
Function MSMediaFeaturepack
ExecShell "open" "https://www.microsoft.com/en-us/software-download/mediafeaturepack"
GetDlgItem $0 $HWNDPARENT 1
EnableWindow $0 1
FunctionEnd
;---------------------------------------------------------------------------------------------------------
; Install the MS Media Feature Pack, if it is missing (e.g. on Windows 10 N)
Function MediaPackDialog
!insertmacro MUI_HEADER_TEXT "Windows Media Feature Pack" "Required software module is missing"
; Skip this dialog if mf.dll is installed
${If} ${FileExists} "$WINDIR\system32\mf.dll"
Abort
${EndIf}
nsDialogs::Create 1018
Pop $Dialog
${If} $Dialog == error
Abort
${EndIf}
${NSD_CreateLabel} 0 0 100% 48u "The Windows Media Feature Pack is missing on this computer. It is required for the Stable Diffusion UI.$\nYou can continue the installation after installing the Windows Media Feature Pack."
Pop $Label
${NSD_CreateButton} 10% 49u 80% 12u "Download Meda Feature Pack from Microsoft"
Pop $Button
GetFunctionAddress $0 MSMediaFeaturePack
nsDialogs::OnClick $Button $0
GetDlgItem $0 $HWNDPARENT 1
EnableWindow $0 0
nsDialogs::Show
FunctionEnd
;---------------------------------------------------------------------------------------------------------
; MUI Settings
;---------------------------------------------------------------------------------------------------------
!define MUI_ABORTWARNING
!define MUI_ICON "sd.ico"
!define MUI_WELCOMEFINISHPAGE_BITMAP "astro.bmp"
; Welcome page
!define MUI_WELCOMEPAGE_TEXT "This installer will guide you through the installation of Stable Diffusion UI.$\n$\n\
Click Next to continue."
!insertmacro MUI_PAGE_WELCOME
Page custom MediaPackDialog
; License page
!insertmacro MUI_PAGE_LICENSE "..\LICENSE"
!insertmacro MUI_PAGE_LICENSE "..\CreativeML Open RAIL-M License"
; Directory page
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "DirectoryLeave"
!insertmacro MUI_PAGE_DIRECTORY
; Instfiles page
!insertmacro MUI_PAGE_INSTFILES
; Finish page
!define MUI_FINISHPAGE_RUN "$INSTDIR\Start Stable Diffusion UI.cmd"
!insertmacro MUI_PAGE_FINISH
; Language files
!insertmacro MUI_LANGUAGE "English"
;---------------------------------------------------------------------------------------------------------
; MUI end
;---------------------------------------------------------------------------------------------------------
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
OutFile "Install Stable Diffusion UI.exe"
InstallDir "C:\Stable-Diffusion-UI\"
InstallDirRegKey HKLM "${PRODUCT_DIR_REGKEY}" ""
ShowInstDetails show
;---------------------------------------------------------------------------------------------------------
; List of files to be installed
Section "MainSection" SEC01
SetOutPath "$INSTDIR"
File "..\CreativeML Open RAIL-M License"
File "..\How to install and run.txt"
File "..\LICENSE"
File "..\Start Stable Diffusion UI.cmd"
SetOutPath "$INSTDIR\scripts"
File "..\scripts\bootstrap.bat"
File "..\scripts\install_status.txt"
File "..\scripts\on_env_start.bat"
File "C:\windows\system32\curl.exe"
CreateDirectory "$INSTDIR\profile"
CreateDirectory "$SMPROGRAMS\Stable Diffusion UI"
CreateShortCut "$SMPROGRAMS\Stable Diffusion UI\Start Stable Diffusion UI.lnk" "$INSTDIR\Start Stable Diffusion UI.cmd"
SectionEnd
;---------------------------------------------------------------------------------------------------------
; Our installer only needs 25 KB, but once it has run, we need 25 GB
; So we need to overwrite the automatically detected space requirements.
; https://nsis.sourceforge.io/Docs/Chapter4.html#4.9.13.7
; The example in section 4.9.13.7 seems to be wrong: the number
; needs to be provided in Kilobytes.
Function .onInit
; Set required size of section 'SEC01' to 25 Gigabytes
SectionSetSize ${SEC01} 26214400
; Check system meory size. We need at least 8GB
; ----------------------------------------------------
; allocate a few bytes of memory
System::Alloc 64
Pop $1
; Retrieve HW info from the Windows Kernel
System::Call "*$1(i64)"
System::Call "Kernel32::GlobalMemoryStatusEx(i r1)"
; unpack the data into $R2 - $R10
System::Call "*$1(i.r2, i.r3, l.r4, l.r5, l.r6, l.r7, l.r8, l.r9, l.r10)"
# free up the memory
System::Free $1
; Result mapping:
; "Structure size: $2 bytes"
; "Memory load: $3%"
; "Total physical memory: $4 bytes"
; "Free physical memory: $5 bytes"
; "Total page file: $6 bytes"
; "Free page file: $7 bytes"
; "Total virtual: $8 bytes"
; "Free virtual: $9 bytes"
; Mem size in MB
System::Int64Op $4 / 1048576
Pop $4
${If} $4 < "8000"
MessageBox MB_OK|MB_ICONEXCLAMATION "Warning!$\n$\nYour system has less than 8GB of memory (RAM).$\n$\n\
You can still try to install Stable Diffusion UI,$\nbut it might have problems to start, or run$\nvery slowly."
${EndIf}
FunctionEnd
;Section -Post
; WriteRegStr HKLM "${PRODUCT_DIR_REGKEY}" "" "$INSTDIR\installer.exe"
;SectionEnd

151
README.md
View File

@ -1,67 +1,114 @@
# Stable Diffusion UI
### Easiest way to install and use [Stable Diffusion](https://github.com/CompVis/stable-diffusion) on your own computer. No dependencies or technical knowledge required. 1-click install, powerful features.
# Easy Diffusion 2.5
### The easiest way to install and use [Stable Diffusion](https://github.com/CompVis/stable-diffusion) on your own computer.
[![Discord Server](https://img.shields.io/discord/1014774730907209781?label=Discord)](https://discord.com/invite/u9yhsFmEkB) (for support, and development discussion) | [Troubleshooting guide for common problems](Troubleshooting.md)
Does not require technical knowledge, does not require pre-installed software. 1-click install, powerful features, friendly community.
----
[Installation guide](#step-1-download-and-extract-the-installer) | [Troubleshooting guide](https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting) | <sub>[![Discord Server](https://img.shields.io/discord/1014774730907209781?label=Discord)](https://discord.com/invite/u9yhsFmEkB)</sub> <sup>(for support queries, and development discussions)</sup>
## Step 1: Download the installer
![t2i](https://raw.githubusercontent.com/Stability-AI/stablediffusion/main/assets/stable-samples/txt2img/768/merged-0006.png)
# Step 1: Download and extract the installer
Click the download button for your operating system:
<p float="left">
<a href="#installation"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/develop/media/download-win.png" width="200" /></a>
<a href="#installation"><img src="https://github.com/cmdr2/stable-diffusion-ui/raw/develop/media/download-linux.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.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>
## Step 2: Run the program
- On Windows: Double-click `Start Stable Diffusion UI.cmd`
- On Linux: Run `./start.sh` in a terminal
## On Windows:
1. Unzip/extract the folder `stable-diffusion-ui` which should be in your downloads folder, unless you changed your default downloads destination.
2. Move the `stable-diffusion-ui` folder to your `C:` drive (or any other drive like `D:`, at the top root level). `C:\stable-diffusion-ui` or `D:\stable-diffusion-ui` as examples. This will avoid a common problem with Windows (file path length limits).
## On Linux:
1. Unzip/extract the folder `stable-diffusion-ui` which should be in your downloads folder, unless you changed your default downloads destination.
2. Open a terminal window, and navigate to the `stable-diffusion-ui` directory.
## Step 3: There is no step 3!
It's simple to get started. You don't need to install or struggle with Python, Anaconda, Docker etc.
# Step 2: Run the program
## On Windows:
Double-click `Start Stable Diffusion UI.cmd`.
If Windows SmartScreen prevents you from running the program click `More info` and then `Run anyway`.
## On Linux:
Run `./start.sh` (or `bash start.sh`) in a terminal.
The installer will take care of whatever is needed. A friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) will help you if you face any problems.
The installer will take care of whatever is needed. If you face any problems, you can join the friendly [Discord community](https://discord.com/invite/u9yhsFmEkB) and ask for assistance.
# Step 3: There is no Step 3. It's that simple!
**To Uninstall:** Just delete the `stable-diffusion-ui` folder to uninstall all the downloaded packages.
----
# Easy for new users, powerful features for advanced users
### Features:
- **No Dependencies or Technical Knowledge Required**: 1-click install for Windows 10/11 and Linux. *No dependencies*, no need for WSL or Docker or Conda or technical setup. Just download and run!
- **Clutter-free UI**: a friendly and simple UI, while providing a lot of powerful features
- Supports "*Text to Image*" and "*Image to Image*"
- **Custom Models**: Use your own `.ckpt` file, by placing it inside the `models/stable-diffusion` folder!
- **Live Preview**: See the image as the AI is drawing it
- **Task Queue**: Queue up all your ideas, without waiting for the current task to finish
- **In-Painting**: Specify areas of your image to paint into
- **Face Correction (GFPGAN) and Upscaling (RealESRGAN)**
## Features:
### User experience
- **Hassle-free installation**: Does not require technical knowledge, does not require pre-installed software. Just download and run!
- **Clutter-free UI**: A friendly and simple UI, while providing a lot of powerful features.
- **Task Queue**: Queue up all your ideas, without waiting for the current task to finish.
- **Intelligent Model Detection**: Automatically figures out the YAML config file to use for the chosen model (via a models database).
- **Live Preview**: See the image as the AI is drawing it.
- **Image Modifiers**: A library of *modifier tags* like *"Realistic"*, *"Pencil Sketch"*, *"ArtStation"* etc. Experiment with various styles quickly.
- **Loopback**: Use the output image as the input image for the next img2img task
- **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!
- **UI Themes**: Customize the program to your liking.
- **Searchable models dropdown**: organize your models into sub-folders, and search through them in the UI.
### Image generation
- **Supports**: "*Text to Image*" and "*Image to Image*".
- **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.
- **Simple Drawing Tool**: Draw basic images to guide the AI, without needing an external drawing program.
- **Face Correction (GFPGAN)**
- **Upscaling (RealESRGAN)**
- **Loopback**: Use the output image as the input image for the next img2img task.
- **Negative Prompt**: Specify aspects of the image to *remove*.
- **Attention/Emphasis:** () in the prompt increases the model's attention to enclosed words, and [] decreases it
- **Weighted Prompts:** Use weights for specific words in your prompt to change their importance, e.g. `red:2.4 dragon:1.2`
- **Prompt Matrix:** (in beta) Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut riding a horse | illustration | cinematic lighting`
- **Lots of Samplers:** ddim, plms, heun, euler, euler_a, dpm2, dpm2_a, lms
- **Multiple Prompts File:** Queue multiple prompts by entering one prompt per line, or by running a text file
- **NSFW Setting**: A setting in the UI to control *NSFW content*
- **JPEG/PNG output**
- **Save generated images to disk**
- **Attention/Emphasis**: () in the prompt increases the model's attention to enclosed words, and [] decreases it.
- **Weighted Prompts**: Use weights for specific words in your prompt to change their importance, e.g. `red:2.4 dragon:1.2`.
- **Prompt Matrix**: Quickly create multiple variations of your prompt, e.g. `a photograph of an astronaut riding a horse | illustration | cinematic lighting`.
- **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.
- **NSFW Setting**: A setting in the UI to control *NSFW content*.
- **JPEG/PNG/WEBP output**: Multiple file formats.
### Advanced features
- **Custom Models**: Use your own `.ckpt` or `.safetensors` file, by placing it inside the `models/stable-diffusion` folder!
- **Stable Diffusion 2.1 support**
- **Merge Models**
- **Use custom VAE models**
- **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!
### Performance and security
- **Fast**: Creates a 512x512 image with euler_a in 5 seconds, on an NVIDIA 3060 12GB.
- **Low Memory Usage**: Create 512x512 images with less than 3 GB of GPU RAM, and 768x768 images with less than 4 GB of GPU RAM!
- **Use CPU setting**: If you don't have a compatible graphics card, but still want to run it on your CPU.
- **Multi-GPU support**: Automatically spreads your tasks across multiple GPUs (if available), for faster performance!
- **Auto scan for malicious models**: Uses picklescan to prevent malicious models.
- **Safetensors support**: Support loading models in the safetensor format, for improved safety.
- **Auto-updater**: Gets you the latest improvements and bug-fixes to a rapidly evolving project.
- **Low Memory Usage**: Creates 512x512 images with less than 4GB of VRAM!
- **Developer Console**: A developer-mode for those who want to modify their Stable Diffusion code, and edit the conda environment.
### Easy for new users:
![Screenshot of the initial UI](media/shot-v10-simple.jpg?raw=true)
**(and a lot more)**
### Powerful features for advanced users:
![Screenshot of advanced settings](media/shot-v10.jpg?raw=true)
----
### Live Preview
## Easy for new users:
![Screenshot of the initial UI](https://user-images.githubusercontent.com/844287/217043152-29454d15-0387-4228-b70d-9a4b84aeb8ba.png)
## Powerful features for advanced users:
![Screenshot of advanced settings](https://user-images.githubusercontent.com/844287/217042588-fc53c975-bacd-4a9c-af88-37408734ade3.png)
## Live Preview
Useful for judging (and stopping) an image quickly, without waiting for it to finish rendering.
![live-512](https://user-images.githubusercontent.com/844287/192097249-729a0a1e-a677-485e-9ccc-16a9e848fabe.gif)
### Task Queue
![Screenshot of task queue](media/task-queue-v1.jpg?raw=true)
## Task Queue
![Screenshot of task queue](https://user-images.githubusercontent.com/844287/217043984-0b35f73b-1318-47cb-9eed-a2a91b430490.png)
# System Requirements
1. Windows 10/11, or Linux. Experimental support for Mac is coming soon.
@ -70,23 +117,10 @@ Useful for judging (and stopping) an image quickly, without waiting for it to fi
You don't need to install or struggle with Python, Anaconda, Docker etc. The installer will take care of whatever is needed.
# Installation
1. **Download** [for Windows](https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.3.5/stable-diffusion-ui-windows.zip) or [for Linux](https://github.com/cmdr2/stable-diffusion-ui/releases/download/v2.3.5/stable-diffusion-ui-linux.zip).
2. **Extract**:
- For Windows: After unzipping the file, please move the `stable-diffusion-ui` folder to your `C:` (or any drive like D:, at the top root level), e.g. `C:\stable-diffusion-ui`. This will avoid a common problem with Windows (file path length limits).
- For Linux: After extracting the .tar.xz file, please open a terminal, and go to the `stable-diffusion-ui` directory.
3. **Run**:
- For Windows: `Start Stable Diffusion UI.cmd` by double-clicking it.
- For Linux: In the terminal, run `./start.sh` (or `bash start.sh`)
This will automatically install Stable Diffusion, set it up, and start the interface. No additional steps are needed.
**To Uninstall:** Just delete the `stable-diffusion-ui` folder to uninstall all the downloaded packages.
----
# How to use?
Please use our [guide](https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use) to understand how to use the features in this UI.
Please refer to our [guide](https://github.com/cmdr2/stable-diffusion-ui/wiki/How-to-Use) to understand how to use the features in this UI.
# Bugs reports and code contributions welcome
If there are any problems or suggestions, please feel free to ask on the [discord server](https://discord.com/invite/u9yhsFmEkB) or [file an issue](https://github.com/cmdr2/stable-diffusion-ui/issues).
@ -102,4 +136,11 @@ If you have any code contributions in mind, please feel free to say Hi to us on
# Disclaimer
The authors of this project are not responsible for any content generated using this interface.
The license of this software forbids you from sharing any content that violates any laws, produce any harm to a person, disseminate any personal information that would be meant for harm, spread misinformation, or target vulnerable groups. For the full list of restrictions please read [the license](LICENSE). You agree to these terms by using this software.
The license of this software forbids you from sharing any content that:
- Violates any laws.
- Produces any harm to a person or persons.
- Disseminates (spreads) any personal information that would be meant for harm.
- Spreads misinformation.
- Target vulnerable groups.
For the full list of restrictions please read [the License](LICENSE). You agree to these terms by using this software.

View File

@ -1 +0,0 @@
Moved to https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting

View File

@ -23,12 +23,21 @@ call conda --version
echo.
@rem activate the environment
call conda activate .\stable-diffusion\env
@rem activate the legacy environment (if present) and set PYTHONPATH
if exist "installer_files\env" (
set PYTHONPATH=%cd%\installer_files\env\lib\site-packages
)
if exist "stable-diffusion\env" (
call conda activate .\stable-diffusion\env
set PYTHONPATH=%cd%\stable-diffusion\env\lib\site-packages
)
call where python
call python --version
echo PYTHONPATH=%PYTHONPATH%
@rem done
echo.
cmd /k

View File

@ -1,8 +1,27 @@
@echo off
cd /d %~dp0
echo Install dir: %~dp0
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
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
setlocal enabledelayedexpansion
@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).
@ -24,14 +25,14 @@ if exist "%INSTALL_ENV_DIR%" set PATH=%INSTALL_ENV_DIR%;%INSTALL_ENV_DIR%\Librar
set PACKAGES_TO_INSTALL=
if not exist "%LEGACY_INSTALL_ENV_DIR%\etc\profile.d\conda.sh" (
if not exist "%INSTALL_ENV_DIR%\etc\profile.d\conda.sh" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% conda
if not exist "%INSTALL_ENV_DIR%\etc\profile.d\conda.sh" set PACKAGES_TO_INSTALL=%PACKAGES_TO_INSTALL% conda python=3.8.5
)
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
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
if "%PACKAGES_TO_INSTALL%" NEQ "" (
@ -42,11 +43,11 @@ if "%PACKAGES_TO_INSTALL%" NEQ "" (
mkdir "%MAMBA_ROOT_PREFIX%"
call curl -Lk "%MICROMAMBA_DOWNLOAD_URL%" > "%MAMBA_ROOT_PREFIX%\micromamba.exe"
@REM if "%ERRORLEVEL%" NEQ "0" (
@REM echo "There was a problem downloading micromamba. Cannot continue."
@REM pause
@REM exit /b
@REM )
if "!ERRORLEVEL!" NEQ "0" (
echo "There was a problem downloading micromamba. Cannot continue."
pause
exit /b
)
mkdir "%APPDATA%"
mkdir "%USERPROFILE%"

View File

@ -21,9 +21,19 @@ OS_ARCH=$(uname -m)
case "${OS_ARCH}" in
x86_64*) OS_ARCH="64";;
arm64*) OS_ARCH="arm64";;
aarch64*) OS_ARCH="arm64";;
*) echo "Unknown system architecture: $OS_ARCH! This script runs only on x86_64 or arm64" && exit
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
if [ "$OS_NAME" == "linux" ] && [ "$OS_ARCH" == "arm64" ]; then OS_ARCH="aarch64"; fi
@ -39,7 +49,7 @@ if [ -e "$INSTALL_ENV_DIR" ]; then export PATH="$INSTALL_ENV_DIR/bin:$PATH"; fi
PACKAGES_TO_INSTALL=""
if [ ! -e "$LEGACY_INSTALL_ENV_DIR/etc/profile.d/conda.sh" ] && [ ! -e "$INSTALL_ENV_DIR/etc/profile.d/conda.sh" ]; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL conda"; fi
if [ ! -e "$LEGACY_INSTALL_ENV_DIR/etc/profile.d/conda.sh" ] && [ ! -e "$INSTALL_ENV_DIR/etc/profile.d/conda.sh" ]; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL conda python=3.8.5"; fi
if ! hash "git" &>/dev/null; then PACKAGES_TO_INSTALL="$PACKAGES_TO_INSTALL git"; fi
if "$MAMBA_ROOT_PREFIX/micromamba" --version &>/dev/null; then umamba_exists="T"; fi
@ -51,7 +61,7 @@ if [ "$PACKAGES_TO_INSTALL" != "" ]; then
echo "Downloading micromamba from $MICROMAMBA_DOWNLOAD_URL to $MAMBA_ROOT_PREFIX/micromamba"
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
echo

13
scripts/check_modules.py Normal file
View File

@ -0,0 +1,13 @@
'''
This script checks if the given modules exist
'''
import sys
import pkgutil
modules = sys.argv[1:]
missing_modules = []
for m in modules:
if pkgutil.find_loader(m) is None:
print('module', m, 'not found')
exit(1)

View File

@ -26,15 +26,26 @@ if [ "$0" == "bash" ]; then
echo ""
# activate the environment
CONDA_BASEPATH=$(conda info --base)
source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # otherwise conda complains about 'shell not initialized' (needed when running in a script)
# activate the legacy environment (if present) and set PYTHONPATH
if [ -e "installer_files/env" ]; then
export PYTHONPATH="$(pwd)/installer_files/env/lib/python3.8/site-packages"
fi
if [ -e "stable-diffusion/env" ]; then
CONDA_BASEPATH=$(conda info --base)
source "$CONDA_BASEPATH/etc/profile.d/conda.sh" # otherwise conda complains about 'shell not initialized' (needed when running in a script)
conda activate ./stable-diffusion/env
conda activate ./stable-diffusion/env
export PYTHONPATH="$(pwd)/stable-diffusion/env/lib/python3.8/site-packages"
fi
which python
python --version
echo "PYTHONPATH=$PYTHONPATH"
# done
echo ""
else
file_name=$(basename "${BASH_SOURCE[0]}")

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. & echo "Stable Diffusion UI - v2" & echo.
@echo. & echo "Easy Diffusion - v2" & echo.
set PATH=C:\Windows\System32;%PATH%
@ -28,7 +28,7 @@ if "%update_branch%"=="" (
@>nul findstr /m "sd_ui_git_cloned" scripts\install_status.txt
@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
@ -38,21 +38,22 @@ if "%update_branch%"=="" (
@cd ..
) else (
@echo. & echo "Downloading Stable Diffusion UI.." & echo.
@echo. & echo "Downloading Easy Diffusion..." & 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 && (
@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
@exit /b
)
)
@xcopy sd-ui-files\ui ui /s /i /Y
@xcopy sd-ui-files\ui ui /s /i /Y /q
@copy sd-ui-files\scripts\on_sd_start.bat scripts\ /Y
@copy sd-ui-files\scripts\bootstrap.bat scripts\ /Y
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
@copy "sd-ui-files\scripts\Start Stable Diffusion UI.cmd" . /Y
@copy "sd-ui-files\scripts\Developer Console.cmd" . /Y

View File

@ -2,7 +2,7 @@
source ./scripts/functions.sh
printf "\n\nStable Diffusion UI\n\n"
printf "\n\nEasy Diffusion\n\n"
if [ -f "scripts/config.sh" ]; then
source scripts/config.sh
@ -13,7 +13,7 @@ if [ "$update_branch" == "" ]; then
fi
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
@ -23,7 +23,7 @@ if [ -f "scripts/install_status.txt" ] && [ `grep -c sd_ui_git_cloned scripts/in
cd ..
else
printf "\n\nDownloading Stable Diffusion UI..\n\n"
printf "\n\nDownloading Easy Diffusion..\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
@ -37,9 +37,9 @@ rm -rf ui
cp -Rf sd-ui-files/ui .
cp sd-ui-files/scripts/on_sd_start.sh scripts/
cp sd-ui-files/scripts/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/
cp sd-ui-files/scripts/start.sh .
cp sd-ui-files/scripts/developer_console.sh .
cp sd-ui-files/scripts/functions.sh scripts/
./scripts/on_sd_start.sh
read -p "Press any key to continue"
exec ./scripts/on_sd_start.sh

View File

@ -1,183 +1,156 @@
@echo off
@REM Caution, this file will make your eyes and brain bleed. It's such an unholy mess.
@REM Note to self: Please rewrite this in Python. For the sake of your own sanity.
@copy sd-ui-files\scripts\on_env_start.bat scripts\ /Y
@copy sd-ui-files\scripts\bootstrap.bat scripts\ /Y
@copy sd-ui-files\scripts\check_modules.py scripts\ /Y
if exist "%cd%\profile" (
set USERPROFILE=%cd%\profile
)
@rem set the correct installer path (current vs legacy)
if exist "%cd%\installer_files\env" (
set INSTALL_ENV_DIR=%cd%\installer_files\env
)
if exist "%cd%\stable-diffusion\env" (
set INSTALL_ENV_DIR=%cd%\stable-diffusion\env
)
@mkdir tmp
@set TMP=%cd%\tmp
@set TEMP=%cd%\tmp
@rem activate the installer env
call conda activate
@REM Caution, this file will make your eyes and brain bleed. It's such an unholy mess.
@REM Note to self: Please rewrite this in Python. For the sake of your own sanity.
@if "%ERRORLEVEL%" NEQ "0" (
@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
exit /b
)
@REM remove the old version of the dev console script, if it's still present
if exist "Open Developer Console.cmd" del "Open Developer Console.cmd"
@call python -c "import os; import shutil; frm = 'sd-ui-files\\ui\\hotfix\\9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'; dst = os.path.join(os.path.expanduser('~'), '.cache', 'huggingface', 'transformers', '9c24e6cd9f499d02c4f21a033736dabd365962dc80fe3aeb57a8f85ea45a20a3.26fead7ea4f0f843f6eb4055dfd25693f1a71f3c6871b184042d4b126244e142'); shutil.copyfile(frm, dst) if os.path.exists(dst) else print(''); print('Hotfixed broken JSON file from OpenAI');"
@>nul findstr /m "sd_git_cloned" scripts\install_status.txt
@if "%ERRORLEVEL%" EQU "0" (
@echo "Stable Diffusion's git repository was already installed. Updating.."
@rem create the stable-diffusion folder, to work with legacy installations
if not exist "stable-diffusion" mkdir stable-diffusion
cd stable-diffusion
@cd stable-diffusion
@call git reset --hard
@call git pull
@call git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
@call git apply ..\ui\sd_internal\ddim_callback.patch
@call git apply ..\ui\sd_internal\env_yaml.patch
@cd ..
) else (
@echo. & echo "Downloading Stable Diffusion.." & echo.
@call git clone https://github.com/basujindal/stable-diffusion.git && (
@echo sd_git_cloned >> scripts\install_status.txt
) || (
@echo "Error downloading 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!"
pause
@exit /b
)
@cd stable-diffusion
@call git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
@call git apply ..\ui\sd_internal\ddim_callback.patch
@call git apply ..\ui\sd_internal\env_yaml.patch
@cd ..
@rem activate the old stable-diffusion env, if it exists
if exist "env" (
call conda activate .\env
)
@cd stable-diffusion
@rem disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan)
if exist src rename src src-old
if exist ldm rename ldm ldm-old
@>nul findstr /m "conda_sd_env_created" ..\scripts\install_status.txt
@if "%ERRORLEVEL%" EQU "0" (
@echo "Packages necessary for Stable Diffusion were already installed"
if not exist "..\models\stable-diffusion" mkdir "..\models\stable-diffusion"
if not exist "..\models\gfpgan" mkdir "..\models\gfpgan"
if not exist "..\models\realesrgan" mkdir "..\models\realesrgan"
if not exist "..\models\vae" mkdir "..\models\vae"
@call conda activate .\env
@rem migrate the legacy models to the correct path (if already downloaded)
if exist "sd-v1-4.ckpt" move sd-v1-4.ckpt ..\models\stable-diffusion\
if exist "custom-model.ckpt" move custom-model.ckpt ..\models\stable-diffusion\
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_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
call python ..\scripts\check_modules.py torch torchvision
if "%ERRORLEVEL%" EQU "0" (
echo "torch and torchvision have already been installed."
) else (
@echo. & echo "Downloading packages necessary for Stable Diffusion.." & echo. & echo "***** This will take some time (depending on the speed of the Internet connection) and may appear to be stuck, but please be patient ***** .." & echo.
echo "Installing torch and torchvision.."
@rmdir /s /q .\env
@REM prevent from using packages from the user's home directory, to avoid conflicts
set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
@REM prevent conda from using packages from the user's home directory, to avoid conflicts
@set PYTHONNOUSERSITE=1
set USERPROFILE=%cd%\profile
set TMP=%cd%\tmp
set TEMP=%cd%\tmp
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
@call conda env create --prefix env -f environment.yaml || (
@echo. & echo "Error installing the packages necessary 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.
call python -m pip install --upgrade torch torchvision --extra-index-url https://download.pytorch.org/whl/cu116 || (
echo "Error installing torch. 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
exit /b
)
@call conda activate .\env
@call conda install -c conda-forge -y --prefix env antlr4-python3-runtime=4.8 || (
@echo. & echo "Error installing antlr4-python3-runtime 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.
pause
exit /b
)
for /f "tokens=*" %%a in ('python -c "import torch; import ldm; import transformers; import numpy; import antlr4; print(42)"') do if "%%a" NEQ "42" (
@echo. & echo "Dependency test failed! Error installing the packages necessary 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.
pause
exit /b
)
@echo conda_sd_env_created >> ..\scripts\install_status.txt
)
set PATH=C:\Windows\System32;%PATH%
@>nul findstr /m "conda_sd_gfpgan_deps_installed" ..\scripts\install_status.txt
@if "%ERRORLEVEL%" EQU "0" (
@echo "Packages necessary for GFPGAN (Face Correction) were already installed"
@rem install/upgrade sdkit
call python ..\scripts\check_modules.py sdkit sdkit.models ldm transformers numpy antlr4 gfpgan realesrgan
if "%ERRORLEVEL%" EQU "0" (
echo "sdkit is already installed."
@rem skip sdkit upgrade if in developer-mode
if not exist "..\src\sdkit" (
@REM prevent from using packages from the user's home directory, to avoid conflicts
set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
call python -m pip install --upgrade sdkit==1.0.43 -q || (
echo "Error updating sdkit"
)
)
) else (
@echo. & echo "Downloading packages necessary for GFPGAN (Face Correction).." & echo.
echo "Installing sdkit: https://pypi.org/project/sdkit/"
@set PYTHONNOUSERSITE=1
@REM prevent from using packages from the user's home directory, to avoid conflicts
set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
set USERPROFILE=%cd%\profile
set TMP=%cd%\tmp
set TEMP=%cd%\tmp
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
@call pip install -e git+https://github.com/TencentARC/GFPGAN#egg=GFPGAN || (
@echo. & echo "Error installing the packages necessary for GFPGAN (Face Correction). 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.
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!"
pause
exit /b
)
@call pip install basicsr==1.4.2 || (
@echo. & echo "Error installing the basicsr package necessary for GFPGAN (Face Correction). 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
exit /b
)
for /f "tokens=*" %%a in ('python -c "from gfpgan import GFPGANer; print(42)"') do if "%%a" NEQ "42" (
@echo. & echo "Dependency test failed! Error installing the packages necessary for GFPGAN (Face Correction). 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
exit /b
)
@echo conda_sd_gfpgan_deps_installed >> ..\scripts\install_status.txt
)
@>nul findstr /m "conda_sd_esrgan_deps_installed" ..\scripts\install_status.txt
@if "%ERRORLEVEL%" EQU "0" (
@echo "Packages necessary for ESRGAN (Resolution Upscaling) were already installed"
call python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))"
@rem upgrade stable-diffusion-sdkit
call python -m pip install --upgrade stable-diffusion-sdkit==2.1.3 -q || (
echo "Error updating stable-diffusion-sdkit"
)
call python -c "from importlib.metadata import version; print('stable-diffusion version:', version('stable-diffusion-sdkit'))"
@rem install rich
call python ..\scripts\check_modules.py rich
if "%ERRORLEVEL%" EQU "0" (
echo "rich has already been installed."
) else (
@echo. & echo "Downloading packages necessary for ESRGAN (Resolution Upscaling).." & echo.
echo "Installing rich.."
@set PYTHONNOUSERSITE=1
set PYTHONNOUSERSITE=1
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
set USERPROFILE=%cd%\profile
set TMP=%cd%\tmp
set TEMP=%cd%\tmp
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
@call pip install -e git+https://github.com/xinntao/Real-ESRGAN#egg=realesrgan || (
@echo. & echo "Error installing the packages necessary for ESRGAN (Resolution Upscaling). 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.
call python -m pip install rich || (
echo "Error installing rich. 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
exit /b
)
for /f "tokens=*" %%a in ('python -c "from basicsr.archs.rrdbnet_arch import RRDBNet; from realesrgan import RealESRGANer; print(42)"') do if "%%a" NEQ "42" (
@echo. & echo "Dependency test failed! Error installing the packages necessary for ESRGAN (Resolution Upscaling). 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
exit /b
)
@echo conda_sd_esrgan_deps_installed >> ..\scripts\install_status.txt
)
@>nul findstr /m "conda_sd_ui_deps_installed" ..\scripts\install_status.txt
set PATH=C:\Windows\System32;%PATH%
call python ..\scripts\check_modules.py uvicorn fastapi
@if "%ERRORLEVEL%" EQU "0" (
echo "Packages necessary for Stable Diffusion UI were already installed"
echo "Packages necessary for Easy Diffusion were already installed"
) 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 USERPROFILE=%cd%\profile
set TMP=%cd%\tmp
set TEMP=%cd%\tmp
set PYTHONPATH=%cd%;%cd%\env\lib\site-packages
@call conda install -c conda-forge -y --prefix env 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!"
@call conda install -c conda-forge -y uvicorn fastapi || (
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
exit /b
)
@ -196,35 +169,30 @@ call WHERE uvicorn > .tmp
@echo conda_sd_ui_deps_installed >> ..\scripts\install_status.txt
)
if not exist "..\models\stable-diffusion" mkdir "..\models\stable-diffusion"
echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
@if exist "sd-v1-4.ckpt" (
for %%I in ("sd-v1-4.ckpt") do if "%%~zI" EQU "4265380512" (
@if exist "..\models\stable-diffusion\sd-v1-4.ckpt" (
for %%I in ("..\models\stable-diffusion\sd-v1-4.ckpt") do if "%%~zI" EQU "4265380512" (
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the HuggingFace 4 GB Model."
) else (
for %%J in ("sd-v1-4.ckpt") do if "%%~zJ" EQU "7703807346" (
for %%J in ("..\models\stable-diffusion\sd-v1-4.ckpt") do if "%%~zJ" EQU "7703807346" (
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the HuggingFace 7 GB Model."
) else (
for %%K in ("sd-v1-4.ckpt") do if "%%~zK" EQU "7703810927" (
for %%K in ("..\models\stable-diffusion\sd-v1-4.ckpt") do if "%%~zK" EQU "7703810927" (
echo "Data files (weights) necessary for Stable Diffusion were already downloaded. Using the Waifu Model."
) else (
echo. & echo "The model file present at %cd%\sd-v1-4.ckpt is invalid. It is only %%~zK bytes in size. Re-downloading.." & echo.
del "sd-v1-4.ckpt"
echo. & echo "The model file present at models\stable-diffusion\sd-v1-4.ckpt is invalid. It is only %%~zK bytes in size. Re-downloading.." & echo.
del "..\models\stable-diffusion\sd-v1-4.ckpt"
)
)
)
)
@if not exist "sd-v1-4.ckpt" (
@if not exist "..\models\stable-diffusion\sd-v1-4.ckpt" (
@echo. & echo "Downloading data files (weights) for Stable Diffusion.." & echo.
@call curl -L -k https://me.cmdr2.org/stable-diffusion-ui/sd-v1-4.ckpt > sd-v1-4.ckpt
@call 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 exist "sd-v1-4.ckpt" (
for %%I in ("sd-v1-4.ckpt") do if "%%~zI" NEQ "4265380512" (
@if exist "..\models\stable-diffusion\sd-v1-4.ckpt" (
for %%I in ("..\models\stable-diffusion\sd-v1-4.ckpt") do if "%%~zI" NEQ "4265380512" (
echo. & echo "Error: The downloaded model file was invalid! Bytes downloaded: %%~zI" & echo.
echo. & echo "Error downloading the data files (weights) 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.
pause
@ -239,22 +207,22 @@ echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
@if exist "GFPGANv1.3.pth" (
for %%I in ("GFPGANv1.3.pth") do if "%%~zI" EQU "348632874" (
@if exist "..\models\gfpgan\GFPGANv1.3.pth" (
for %%I in ("..\models\gfpgan\GFPGANv1.3.pth") do if "%%~zI" EQU "348632874" (
echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded"
) else (
echo. & echo "The GFPGAN model file present at %cd%\GFPGANv1.3.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "GFPGANv1.3.pth"
echo. & echo "The GFPGAN model file present at models\gfpgan\GFPGANv1.3.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "..\models\gfpgan\GFPGANv1.3.pth"
)
)
@if not exist "GFPGANv1.3.pth" (
@if not exist "..\models\gfpgan\GFPGANv1.3.pth" (
@echo. & echo "Downloading data files (weights) for GFPGAN (Face Correction).." & echo.
@call curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > GFPGANv1.3.pth
@call curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > ..\models\gfpgan\GFPGANv1.3.pth
@if exist "GFPGANv1.3.pth" (
for %%I in ("GFPGANv1.3.pth") do if "%%~zI" NEQ "348632874" (
@if exist "..\models\gfpgan\GFPGANv1.3.pth" (
for %%I in ("..\models\gfpgan\GFPGANv1.3.pth") do if "%%~zI" NEQ "348632874" (
echo. & echo "Error: The downloaded GFPGAN model file was invalid! Bytes downloaded: %%~zI" & echo.
echo. & echo "Error downloading the data files (weights) for GFPGAN (Face Correction). 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
@ -269,22 +237,22 @@ echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
@if exist "RealESRGAN_x4plus.pth" (
for %%I in ("RealESRGAN_x4plus.pth") do if "%%~zI" EQU "67040989" (
@if exist "..\models\realesrgan\RealESRGAN_x4plus.pth" (
for %%I in ("..\models\realesrgan\RealESRGAN_x4plus.pth") do if "%%~zI" EQU "67040989" (
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded"
) else (
echo. & echo "The GFPGAN model file present at %cd%\RealESRGAN_x4plus.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "RealESRGAN_x4plus.pth"
echo. & echo "The RealESRGAN model file present at models\realesrgan\RealESRGAN_x4plus.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "..\models\realesrgan\RealESRGAN_x4plus.pth"
)
)
@if not exist "RealESRGAN_x4plus.pth" (
@if not exist "..\models\realesrgan\RealESRGAN_x4plus.pth" (
@echo. & echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus.." & echo.
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > RealESRGAN_x4plus.pth
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > ..\models\realesrgan\RealESRGAN_x4plus.pth
@if exist "RealESRGAN_x4plus.pth" (
for %%I in ("RealESRGAN_x4plus.pth") do if "%%~zI" NEQ "67040989" (
@if exist "..\models\realesrgan\RealESRGAN_x4plus.pth" (
for %%I in ("..\models\realesrgan\RealESRGAN_x4plus.pth") do if "%%~zI" NEQ "67040989" (
echo. & echo "Error: The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: %%~zI" & echo.
echo. & echo "Error downloading the data files (weights) for ESRGAN (Resolution Upscaling) x4plus. 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
@ -299,22 +267,22 @@ echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
@if exist "RealESRGAN_x4plus_anime_6B.pth" (
for %%I in ("RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" EQU "17938799" (
@if exist "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" (
for %%I in ("..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" EQU "17938799" (
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded"
) else (
echo. & echo "The GFPGAN model file present at %cd%\RealESRGAN_x4plus_anime_6B.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "RealESRGAN_x4plus_anime_6B.pth"
echo. & echo "The RealESRGAN model file present at models\realesrgan\RealESRGAN_x4plus_anime_6B.pth is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth"
)
)
@if not exist "RealESRGAN_x4plus_anime_6B.pth" (
@if not exist "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" (
@echo. & echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime.." & echo.
@call curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > 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 "RealESRGAN_x4plus_anime_6B.pth" (
for %%I in ("RealESRGAN_x4plus_anime_6B.pth") do if "%%~zI" NEQ "17938799" (
@if exist "..\models\realesrgan\RealESRGAN_x4plus_anime_6B.pth" (
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 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
@ -329,20 +297,46 @@ echo. > "..\models\stable-diffusion\Put your custom ckpt files here.txt"
@if exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
for %%I in ("..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt") do if "%%~zI" EQU "334695179" (
echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded"
) else (
echo. & echo "The default VAE (sd-vae-ft-mse-original) file present at models\vae\vae-ft-mse-840000-ema-pruned.ckpt is invalid. It is only %%~zI bytes in size. Re-downloading.." & echo.
del "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt"
)
)
@if not exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
@echo. & echo "Downloading data files (weights) for the default VAE (sd-vae-ft-mse-original).." & echo.
@call 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 exist "..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt" (
for %%I in ("..\models\vae\vae-ft-mse-840000-ema-pruned.ckpt") do if "%%~zI" NEQ "334695179" (
echo. & echo "Error: The downloaded default VAE (sd-vae-ft-mse-original) file was invalid! Bytes downloaded: %%~zI" & echo.
echo. & echo "Error downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). 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
exit /b
)
) else (
@echo. & echo "Error downloading the data files (weights) for the default VAE (sd-vae-ft-mse-original). 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
exit /b
)
)
@>nul findstr /m "sd_install_complete" ..\scripts\install_status.txt
@if "%ERRORLEVEL%" NEQ "0" (
@echo sd_weights_downloaded >> ..\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%
@cd env\lib\site-packages
@set PYTHONPATH=%SD_DIR%;%cd%
@cd ..\..\..
@echo PYTHONPATH=%PYTHONPATH%
set PYTHONPATH=%INSTALL_ENV_DIR%\lib\site-packages
echo PYTHONPATH=%PYTHONPATH%
call where python
call python --version
@ -351,9 +345,12 @@ call python --version
@set SD_UI_PATH=%cd%\ui
@cd stable-diffusion
@rem set any overrides
set HF_HUB_DISABLE_SYMLINKS_WARNING=true
@if NOT DEFINED SD_UI_BIND_PORT set SD_UI_BIND_PORT=9000
@if NOT DEFINED SD_UI_BIND_IP set SD_UI_BIND_IP=0.0.0.0
@uvicorn server:app --app-dir "%SD_UI_PATH%" --port %SD_UI_BIND_PORT% --host %SD_UI_BIND_IP%
@uvicorn main:server_api --app-dir "%SD_UI_PATH%" --port %SD_UI_BIND_PORT% --host %SD_UI_BIND_IP% --log-level error
@pause

View File

@ -1,9 +1,11 @@
#!/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/bootstrap.sh scripts/
cp sd-ui-files/scripts/check_modules.py scripts/
source ./scripts/functions.sh
# activate the installer env
CONDA_BASEPATH=$(conda info --base)
@ -21,129 +23,110 @@ python -c "import os; import shutil; frm = 'sd-ui-files/ui/hotfix/9c24e6cd9f499d
# Caution, this file will make your eyes and brain bleed. It's such an unholy mess.
# Note to self: Please rewrite this in Python. For the sake of your own sanity.
if [ -e "scripts/install_status.txt" ] && [ `grep -c sd_git_cloned scripts/install_status.txt` -gt "0" ]; then
echo "Stable Diffusion's git repository was already installed. Updating.."
cd stable-diffusion
git reset --hard
git pull
git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
git apply ../ui/sd_internal/ddim_callback.patch || fail "ddim patch failed"
git apply ../ui/sd_internal/env_yaml.patch || fail "yaml patch failed"
cd ..
else
printf "\n\nDownloading Stable Diffusion..\n\n"
if git clone https://github.com/basujindal/stable-diffusion.git ; then
echo sd_git_cloned >> scripts/install_status.txt
else
fail "git clone of basujindal/stable-diffusion.git failed"
fi
cd stable-diffusion
git -c advice.detachedHead=false checkout f6cfebffa752ee11a7b07497b8529d5971de916c
git apply ../ui/sd_internal/ddim_callback.patch || fail "ddim patch failed"
git apply ../ui/sd_internal/env_yaml.patch || fail "yaml patch failed"
cd ..
# set the correct installer path (current vs legacy)
if [ -e "installer_files/env" ]; then
export INSTALL_ENV_DIR="$(pwd)/installer_files/env"
fi
if [ -e "stable-diffusion/env" ]; then
export INSTALL_ENV_DIR="$(pwd)/stable-diffusion/env"
fi
# create the stable-diffusion folder, to work with legacy installations
if [ ! -e "stable-diffusion" ]; then mkdir stable-diffusion; fi
cd stable-diffusion
if [ `grep -c conda_sd_env_created ../scripts/install_status.txt` -gt "0" ]; then
echo "Packages necessary for Stable Diffusion were already installed"
# activate the old stable-diffusion env, if it exists
if [ -e "env" ]; then
conda activate ./env || fail "conda activate failed"
else
printf "\n\nDownloading packages necessary for Stable Diffusion..\n"
printf "\n\n***** This will take some time (depending on the speed of the Internet connection) and may appear to be stuck, but please be patient ***** ..\n\n"
# prevent conda from using packages from the user's home directory, to avoid conflicts
export PYTHONNOUSERSITE=1
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
if conda env create --prefix env --force -f environment.yaml ; then
echo "Installed. Testing.."
else
fail "'conda env create' failed"
fi
conda activate ./env || fail "conda activate failed"
if conda install -c conda-forge --prefix ./env -y antlr4-python3-runtime=4.8 ; then
echo "Installed. Testing.."
else
fail "Error installing antlr4-python3-runtime"
fi
out_test=`python -c "import torch; import ldm; import transformers; import numpy; import antlr4; print(42)"`
if [ "$out_test" != "42" ]; then
fail "Dependency test failed"
fi
echo conda_sd_env_created >> ../scripts/install_status.txt
fi
if [ `grep -c conda_sd_gfpgan_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
echo "Packages necessary for GFPGAN (Face Correction) were already installed"
# disable the legacy src and ldm folder (otherwise this prevents installing gfpgan and realesrgan)
if [ -e "src" ]; then mv src src-old; fi
if [ -e "ldm" ]; then mv ldm ldm-old; fi
mkdir -p "../models/stable-diffusion"
mkdir -p "../models/gfpgan"
mkdir -p "../models/realesrgan"
mkdir -p "../models/vae"
# migrate the legacy models to the correct path (if already downloaded)
if [ -e "sd-v1-4.ckpt" ]; then mv sd-v1-4.ckpt ../models/stable-diffusion/; fi
if [ -e "custom-model.ckpt" ]; then mv custom-model.ckpt ../models/stable-diffusion/; fi
if [ -e "GFPGANv1.3.pth" ]; then mv GFPGANv1.3.pth ../models/gfpgan/; fi
if [ -e "RealESRGAN_x4plus.pth" ]; then mv RealESRGAN_x4plus.pth ../models/realesrgan/; fi
if [ -e "RealESRGAN_x4plus_anime_6B.pth" ]; then mv RealESRGAN_x4plus_anime_6B.pth ../models/realesrgan/; fi
# install torch and torchvision
if python ../scripts/check_modules.py torch torchvision; then
echo "torch and torchvision have already been installed."
else
printf "\n\nDownloading packages necessary for GFPGAN (Face Correction)..\n"
echo "Installing torch and torchvision.."
export PYTHONNOUSERSITE=1
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
if pip install -e git+https://github.com/TencentARC/GFPGAN#egg=GFPGAN ; then
echo "Installed. Testing.."
if python -m pip install --upgrade torch torchvision --extra-index-url https://download.pytorch.org/whl/cu116 ; then
echo "Installed."
else
fail "Error installing the packages necessary for GFPGAN (Face Correction)."
fail "torch install failed"
fi
out_test=`python -c "from gfpgan import GFPGANer; print(42)"`
if [ "$out_test" != "42" ]; then
echo "EE The dependency check has failed. This usually means that some system libraries are missing."
echo "EE On Debian/Ubuntu systems, this are often these packages: libsm6 libxext6 libxrender-dev"
echo "EE Other Linux distributions might have different package names for these libraries."
fail "GFPGAN dependency test failed"
fi
echo conda_sd_gfpgan_deps_installed >> ../scripts/install_status.txt
fi
if [ `grep -c conda_sd_esrgan_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
echo "Packages necessary for ESRGAN (Resolution Upscaling) were already installed"
# install/upgrade sdkit
if python ../scripts/check_modules.py sdkit sdkit.models ldm transformers numpy antlr4 gfpgan realesrgan ; then
echo "sdkit is already installed."
# skip sdkit upgrade if in developer-mode
if [ ! -e "../src/sdkit" ]; then
export PYTHONNOUSERSITE=1
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
python -m pip install --upgrade sdkit==1.0.43 -q
fi
else
printf "\n\nDownloading packages necessary for ESRGAN (Resolution Upscaling)..\n"
echo "Installing sdkit: https://pypi.org/project/sdkit/"
export PYTHONNOUSERSITE=1
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
if pip install -e git+https://github.com/xinntao/Real-ESRGAN#egg=realesrgan ; then
echo "Installed. Testing.."
if python -m pip install sdkit==1.0.43 ; then
echo "Installed."
else
fail "Error installing the packages necessary for ESRGAN"
fail "sdkit install failed"
fi
out_test=`python -c "from basicsr.archs.rrdbnet_arch import RRDBNet; from realesrgan import RealESRGANer; print(42)"`
if [ "$out_test" != "42" ]; then
fail "ESRGAN dependency test failed"
fi
echo conda_sd_esrgan_deps_installed >> ../scripts/install_status.txt
fi
if [ `grep -c conda_sd_ui_deps_installed ../scripts/install_status.txt` -gt "0" ]; then
echo "Packages necessary for Stable Diffusion UI were already installed"
python -c "from importlib.metadata import version; print('sdkit version:', version('sdkit'))"
# upgrade stable-diffusion-sdkit
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'))"
# install rich
if python ../scripts/check_modules.py rich; then
echo "rich has already been installed."
else
printf "\n\nDownloading packages necessary for Stable Diffusion UI..\n\n"
echo "Installing rich.."
export PYTHONNOUSERSITE=1
export PYTHONPATH="$(pwd):$(pwd)/env/lib/site-packages"
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
if conda install -c conda-forge --prefix ./env -y uvicorn fastapi ; then
if python -m pip install rich ; then
echo "Installed."
else
fail "Install failed for rich"
fi
fi
if python ../scripts/check_modules.py uvicorn fastapi ; then
echo "Packages necessary for Easy Diffusion were already installed"
else
printf "\n\nDownloading packages necessary for Easy Diffusion..\n\n"
export PYTHONNOUSERSITE=1
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
if conda install -c conda-forge -y uvicorn fastapi ; then
echo "Installed. Testing.."
else
fail "'conda install uvicorn' failed"
@ -152,33 +135,26 @@ else
if ! command -v uvicorn &> /dev/null; then
fail "UI packages not found!"
fi
echo conda_sd_ui_deps_installed >> ../scripts/install_status.txt
fi
mkdir -p "../models/stable-diffusion"
echo "" > "../models/stable-diffusion/Put your custom ckpt files here.txt"
if [ -f "sd-v1-4.ckpt" ]; then
model_size=`find "sd-v1-4.ckpt" -printf "%s"`
if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"`
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"
else
printf "\n\nThe model file present at $PWD/sd-v1-4.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.."
rm sd-v1-4.ckpt
printf "\n\nThe model file present at models/stable-diffusion/sd-v1-4.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.."
rm ../models/stable-diffusion/sd-v1-4.ckpt
fi
fi
if [ ! -f "sd-v1-4.ckpt" ]; then
if [ ! -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
echo "Downloading data files (weights) for Stable Diffusion.."
curl -L -k https://me.cmdr2.org/stable-diffusion-ui/sd-v1-4.ckpt > 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 "sd-v1-4.ckpt" ]; then
model_size=`find "sd-v1-4.ckpt" -printf "%s"`
if [ -f "../models/stable-diffusion/sd-v1-4.ckpt" ]; then
model_size=`filesize "../models/stable-diffusion/sd-v1-4.ckpt"`
if [ ! "$model_size" == "4265380512" ]; then
fail "The downloaded model file was invalid! Bytes downloaded: $model_size"
fi
@ -188,24 +164,24 @@ if [ ! -f "sd-v1-4.ckpt" ]; then
fi
if [ -f "GFPGANv1.3.pth" ]; then
model_size=`find "GFPGANv1.3.pth" -printf "%s"`
if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"`
if [ "$model_size" -eq "348632874" ]; then
echo "Data files (weights) necessary for GFPGAN (Face Correction) were already downloaded"
else
printf "\n\nThe model file present at $PWD/GFPGANv1.3.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm GFPGANv1.3.pth
printf "\n\nThe model file present at models/gfpgan/GFPGANv1.3.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm ../models/gfpgan/GFPGANv1.3.pth
fi
fi
if [ ! -f "GFPGANv1.3.pth" ]; then
if [ ! -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
echo "Downloading data files (weights) for GFPGAN (Face Correction).."
curl -L -k https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth > 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 "GFPGANv1.3.pth" ]; then
model_size=`find "GFPGANv1.3.pth" -printf "%s"`
if [ -f "../models/gfpgan/GFPGANv1.3.pth" ]; then
model_size=`filesize "../models/gfpgan/GFPGANv1.3.pth"`
if [ ! "$model_size" -eq "348632874" ]; then
fail "The downloaded GFPGAN model file was invalid! Bytes downloaded: $model_size"
fi
@ -215,24 +191,24 @@ if [ ! -f "GFPGANv1.3.pth" ]; then
fi
if [ -f "RealESRGAN_x4plus.pth" ]; then
model_size=`find "RealESRGAN_x4plus.pth" -printf "%s"`
if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"`
if [ "$model_size" -eq "67040989" ]; then
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus were already downloaded"
else
printf "\n\nThe model file present at $PWD/RealESRGAN_x4plus.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm RealESRGAN_x4plus.pth
printf "\n\nThe model file present at models/realesrgan/RealESRGAN_x4plus.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm ../models/realesrgan/RealESRGAN_x4plus.pth
fi
fi
if [ ! -f "RealESRGAN_x4plus.pth" ]; then
if [ ! -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus.."
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth > 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 "RealESRGAN_x4plus.pth" ]; then
model_size=`find "RealESRGAN_x4plus.pth" -printf "%s"`
if [ -f "../models/realesrgan/RealESRGAN_x4plus.pth" ]; then
model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus.pth"`
if [ ! "$model_size" -eq "67040989" ]; then
fail "The downloaded ESRGAN x4plus model file was invalid! Bytes downloaded: $model_size"
fi
@ -242,24 +218,24 @@ if [ ! -f "RealESRGAN_x4plus.pth" ]; then
fi
if [ -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`find "RealESRGAN_x4plus_anime_6B.pth" -printf "%s"`
if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"`
if [ "$model_size" -eq "17938799" ]; then
echo "Data files (weights) necessary for ESRGAN (Resolution Upscaling) x4plus_anime were already downloaded"
else
printf "\n\nThe model file present at $PWD/RealESRGAN_x4plus_anime_6B.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm RealESRGAN_x4plus_anime_6B.pth
printf "\n\nThe model file present at models/realesrgan/RealESRGAN_x4plus_anime_6B.pth is invalid. It is only $model_size bytes in size. Re-downloading.."
rm ../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth
fi
fi
if [ ! -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
if [ ! -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
echo "Downloading data files (weights) for ESRGAN (Resolution Upscaling) x4plus_anime.."
curl -L -k https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth > 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 "RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`find "RealESRGAN_x4plus_anime_6B.pth" -printf "%s"`
if [ -f "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth" ]; then
model_size=`filesize "../models/realesrgan/RealESRGAN_x4plus_anime_6B.pth"`
if [ ! "$model_size" -eq "17938799" ]; then
fail "The downloaded ESRGAN x4plus_anime model file was invalid! Bytes downloaded: $model_size"
fi
@ -269,15 +245,47 @@ if [ ! -f "RealESRGAN_x4plus_anime_6B.pth" ]; then
fi
if [ -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"`
if [ "$model_size" -eq "334695179" ]; then
echo "Data files (weights) necessary for the default VAE (sd-vae-ft-mse-original) were already downloaded"
else
printf "\n\nThe model file present at models/vae/vae-ft-mse-840000-ema-pruned.ckpt is invalid. It is only $model_size bytes in size. Re-downloading.."
rm ../models/vae/vae-ft-mse-840000-ema-pruned.ckpt
fi
fi
if [ ! -f "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt" ]; then
echo "Downloading data files (weights) for the default VAE (sd-vae-ft-mse-original).."
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
model_size=`filesize "../models/vae/vae-ft-mse-840000-ema-pruned.ckpt"`
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 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"
read -p "Press any key to continue"
exit
fi
else
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"
read -p "Press any key to continue"
exit
fi
fi
if [ `grep -c sd_install_complete ../scripts/install_status.txt` -gt "0" ]; then
echo sd_weights_downloaded >> ../scripts/install_status.txt
echo sd_install_complete >> ../scripts/install_status.txt
fi
printf "\n\nStable Diffusion is ready!\n\n"
printf "\n\nEasy Diffusion installation complete, starting the server!\n\n"
SD_PATH=`pwd`
export PYTHONPATH="$SD_PATH:$SD_PATH/env/lib/python3.8/site-packages"
export PYTHONPATH="$INSTALL_ENV_DIR/lib/python3.8/site-packages"
echo "PYTHONPATH=$PYTHONPATH"
which python
@ -287,6 +295,6 @@ cd ..
export SD_UI_PATH=`pwd`/ui
cd stable-diffusion
uvicorn server:app --app-dir "$SD_UI_PATH" --port ${SD_UI_BIND_PORT:-9000} --host ${SD_UI_BIND_IP:-0.0.0.0}
uvicorn main:server_api --app-dir "$SD_UI_PATH" --port ${SD_UI_BIND_PORT:-9000} --host ${SD_UI_BIND_IP:-0.0.0.0} --log-level error
read -p "Press any key to continue"

View File

@ -1,6 +0,0 @@
@call conda --version
@call git --version
cd %CONDA_PREFIX%\..\scripts
on_env_start.bat

View File

@ -1,12 +0,0 @@
#!/bin/bash
conda-unpack
source $CONDA_PREFIX/etc/profile.d/conda.sh
conda --version
git --version
cd $CONDA_PREFIX/../scripts
./on_env_start.sh

View File

@ -2,6 +2,24 @@
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
if [ -e "installer" ]; then export PATH="$(pwd)/installer/bin:$PATH"; fi
@ -19,4 +37,5 @@ which conda
conda --version || exit 1
# Download the rest of the installer and UI
chmod +x scripts/*.sh
scripts/on_env_start.sh

View File

236
ui/easydiffusion/app.py Normal file
View File

@ -0,0 +1,236 @@
import os
import socket
import sys
import json
import traceback
import logging
import shlex
from rich.logging import RichHandler
from sdkit.utils import log as sdkit_log # hack, so we can overwrite the log config
from easydiffusion import task_manager
from easydiffusion.utils import log
# Remove all handlers associated with the root logger object.
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
LOG_FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s %(threadName)s %(message)s"
logging.basicConfig(
level=logging.INFO,
format=LOG_FORMAT,
datefmt="%X",
handlers=[RichHandler(markup=True, rich_tracebacks=False, show_time=False, show_level=False)],
)
SD_DIR = os.getcwd()
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(USER_SERVER_PLUGINS_DIR)
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
PRESERVE_CONFIG_VARS = ["FORCE_FULL_PRECISION"]
TASK_TTL = 15 * 60 # Discard last session's task timeout
APP_CONFIG_DEFAULTS = {
# 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)
"update_branch": "main",
"ui": {
"open_browser_on_start": True,
},
}
def init():
os.makedirs(USER_UI_PLUGINS_DIR, exist_ok=True)
os.makedirs(USER_SERVER_PLUGINS_DIR, exist_ok=True)
load_server_plugins()
update_render_threads()
def getConfig(default_val=APP_CONFIG_DEFAULTS):
try:
config_json_path = os.path.join(CONFIG_DIR, "config.json")
if not os.path.exists(config_json_path):
config = default_val
else:
with open(config_json_path, "r", encoding="utf-8") as f:
config = json.load(f)
if "net" not in config:
config["net"] = {}
if os.getenv("SD_UI_BIND_PORT") is not None:
config["net"]["listen_port"] = int(os.getenv("SD_UI_BIND_PORT"))
else:
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:
log.warn(traceback.format_exc())
return default_val
def setConfig(config):
try: # config.json
config_json_path = os.path.join(CONFIG_DIR, "config.json")
with open(config_json_path, "w", encoding="utf-8") as f:
json.dump(config, f)
except:
log.error(traceback.format_exc())
try: # config.bat
config_bat_path = os.path.join(CONFIG_DIR, "config.bat")
config_bat = []
if "update_branch" in config:
config_bat.append(f"@set update_branch={config['update_branch']}")
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"
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:
with open(config_bat_path, "w", encoding="utf-8") as f:
f.write("\n".join(config_bat))
except:
log.error(traceback.format_exc())
try: # config.sh
config_sh_path = os.path.join(CONFIG_DIR, "config.sh")
config_sh = ["#!/bin/bash"]
if "update_branch" in config:
config_sh.append(f"export update_branch={config['update_branch']}")
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"
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:
with open(config_sh_path, "w", encoding="utf-8") as f:
f.write("\n".join(config_sh))
except:
log.error(traceback.format_exc())
def save_to_config(ckpt_model_name, vae_model_name, hypernetwork_model_name, vram_usage_level):
config = getConfig()
if "model" not in config:
config["model"] = {}
config["model"]["stable-diffusion"] = ckpt_model_name
config["model"]["vae"] = vae_model_name
config["model"]["hypernetwork"] = hypernetwork_model_name
if vae_model_name is None or vae_model_name == "":
del config["model"]["vae"]
if hypernetwork_model_name is None or hypernetwork_model_name == "":
del config["model"]["hypernetwork"]
config["vram_usage_level"] = vram_usage_level
setConfig(config)
def update_render_threads():
config = getConfig()
render_devices = config.get("render_devices", "auto")
active_devices = task_manager.get_devices()["active"].keys()
log.debug(f"requesting for render_devices: {render_devices}")
task_manager.update_render_threads(render_devices, active_devices)
def getUIPlugins():
plugins = []
for plugins_dir, dir_prefix in UI_PLUGINS_SOURCES:
for file in os.listdir(plugins_dir):
if file.endswith(".plugin.js"):
plugins.append(f"/plugins/{dir_prefix}/{file}")
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():
try:
ips = socket.gethostbyname_ex(socket.gethostname())
ips[2].append(ips[0])
return ips[2]
except Exception as e:
log.exception(e)
return []
def open_browser():
config = getConfig()
ui = config.get("ui", {})
net = config.get("net", {"listen_port": 9000})
port = net.get("listen_port", 9000)
if ui.get("open_browser_on_start", True):
import webbrowser
webbrowser.open(f"http://localhost:{port}")

View File

@ -0,0 +1,236 @@
import os
import torch
import traceback
import re
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).
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).
"""
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
def get_device_delta(render_devices, active_devices):
"""
render_devices: 'cpu', or 'auto' or ['cuda:N'...]
active_devices: ['cpu', 'cuda:N'...]
"""
if render_devices in ("cpu", "auto"):
render_devices = [render_devices]
elif render_devices is not None:
if isinstance(render_devices, str):
render_devices = [render_devices]
if isinstance(render_devices, list) and len(render_devices) > 0:
render_devices = list(filter(lambda x: x.startswith("cuda:"), render_devices))
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"}'
)
render_devices = list(filter(lambda x: is_device_compatible(x), render_devices))
if len(render_devices) == 0:
raise Exception(
"Sorry, none of the render_devices configured in config.json are compatible with Stable Diffusion"
)
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"}'
)
else:
render_devices = ["auto"]
if "auto" in render_devices:
render_devices = auto_pick_devices(active_devices)
if "cpu" in render_devices:
log.warn("WARNING: Could not find a compatible GPU. Using the CPU, but this will be very slow!")
active_devices = set(active_devices)
render_devices = set(render_devices)
devices_to_start = render_devices - active_devices
devices_to_stop = active_devices - render_devices
return devices_to_start, devices_to_stop
def auto_pick_devices(currently_active_devices):
global mem_free_threshold
if not torch.cuda.is_available():
return ["cpu"]
device_count = torch.cuda.device_count()
if device_count == 1:
return ["cuda:0"] if is_device_compatible("cuda:0") else ["cpu"]
log.debug("Autoselecting GPU. Using most free memory.")
devices = []
for device in range(device_count):
device = f"cuda:{device}"
if not is_device_compatible(device):
continue
mem_free, mem_total = torch.cuda.mem_get_info(device)
mem_free /= float(10**9)
mem_total /= float(10**9)
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"
)
devices.append({"device": device, "device_name": device_name, "mem_free": mem_free})
devices.sort(key=lambda x: x["mem_free"], reverse=True)
max_mem_free = devices[0]["mem_free"]
curr_mem_free_threshold = COMPARABLE_GPU_PERCENTILE * max_mem_free
mem_free_threshold = max(curr_mem_free_threshold, mem_free_threshold)
# Auto-pick algorithm:
# 1. Pick the top 75 percentile of the GPUs, sorted by free_mem.
# 2. Also include already-running devices (GPU-only), otherwise their free_mem will
# 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.
# 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(map(lambda x: x["device"], devices))
return devices
def device_init(context, device):
"""
This function assumes the 'device' has already been verified to be compatible.
`get_device_delta()` has already filtered out incompatible devices.
"""
validate_device_id(device, log_prefix="device_init")
if device == "cpu":
context.device = "cpu"
context.device_name = get_processor_name()
context.half_precision = False
log.debug(f"Render device CPU available as {context.device_name}")
return
context.device_name = torch.cuda.get_device_name(device)
context.device = device
# Force full precision on 1660 and 1650 NVIDIA cards to avoid creating green images
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}")
# Apply force_full_precision now before models are loaded.
context.half_precision = False
log.info(f'Setting {device} as active, with precision: {"half" if context.half_precision else "full"}')
torch.cuda.device(device)
return
def needs_to_force_full_precision(context):
if "FORCE_FULL_PRECISION" in os.environ:
return True
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
)
) or ("tesla k40m" in device_name)
def get_max_vram_usage_level(device):
if device != "cpu":
_, mem_total = torch.cuda.mem_get_info(device)
mem_total /= float(10**9)
if mem_total < 4.5:
return "low"
elif mem_total < 6.5:
return "balanced"
return "high"
def validate_device_id(device, log_prefix=""):
def is_valid():
if not isinstance(device, str):
return False
if device == "cpu":
return True
if not device.startswith("cuda:") or not device[5:].isnumeric():
return False
return True
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}"
)
def is_device_compatible(device):
"""
Returns True/False, and prints any compatibility errors
"""
# static variable "history".
is_device_compatible.history = getattr(is_device_compatible, "history", {})
try:
validate_device_id(device, log_prefix="is_device_compatible")
except:
log.error(str(e))
return False
if device == "cpu":
return True
# Memory check
try:
_, mem_total = torch.cuda.mem_get_info(device)
mem_total /= float(10**9)
if mem_total < 3.0:
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")
is_device_compatible.history[device] = 1
return False
except RuntimeError as e:
log.error(str(e))
return False
return True
def get_processor_name():
try:
import platform, subprocess
if platform.system() == "Windows":
return platform.processor()
elif platform.system() == "Darwin":
os.environ["PATH"] = os.environ["PATH"] + os.pathsep + "/usr/sbin"
command = "sysctl -n machdep.cpu.brand_string"
return subprocess.check_output(command).strip()
elif platform.system() == "Linux":
command = "cat /proc/cpuinfo"
all_info = subprocess.check_output(command, shell=True).decode().strip()
for line in all_info.split("\n"):
if "model name" in line:
return re.sub(".*model name.*:", "", line, 1).strip()
except:
log.error(traceback.format_exc())
return "cpu"

View File

@ -0,0 +1,255 @@
import os
from easydiffusion import app
from easydiffusion.types import TaskData
from easydiffusion.utils import log
from sdkit import Context
from sdkit.models import load_model, unload_model, scan_model
KNOWN_MODEL_TYPES = ["stable-diffusion", "vae", "hypernetwork", "gfpgan", "realesrgan"]
MODEL_EXTENSIONS = {
"stable-diffusion": [".ckpt", ".safetensors"],
"vae": [".vae.pt", ".ckpt", ".safetensors"],
"hypernetwork": [".pt", ".safetensors"],
"gfpgan": [".pth"],
"realesrgan": [".pth"],
}
DEFAULT_MODELS = {
"stable-diffusion": [ # needed to support the legacy installations
"custom-model", # only one custom model file was supported initially, creatively named 'custom-model'
"sd-v1-4", # Default fallback.
],
"gfpgan": ["GFPGANv1.3"],
"realesrgan": ["RealESRGAN_x4plus"],
}
MODELS_TO_LOAD_ON_START = ["stable-diffusion", "vae", "hypernetwork"]
known_models = {}
def init():
make_model_folders()
getModels() # run this once, to cache the picklescan results
def load_default_models(context: Context):
set_vram_optimizations(context)
# init default model paths
for model_type in MODELS_TO_LOAD_ON_START:
context.model_paths[model_type] = resolve_model_to_use(model_type=model_type)
try:
load_model(context, model_type)
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: {e}[/red]")
log.error(f"[red]Consider removing the model from the model folder.[red]")
def unload_all(context: Context):
for model_type in KNOWN_MODEL_TYPES:
unload_model(context, model_type)
def resolve_model_to_use(model_name: str = None, model_type: str = None):
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
default_models = DEFAULT_MODELS.get(model_type, [])
config = app.getConfig()
model_dirs = [os.path.join(app.MODELS_DIR, model_type), app.SD_DIR]
if not model_name: # When None try user configured model.
# config = getConfig()
if "model" in config and model_type in config["model"]:
model_name = config["model"][model_type]
if model_name:
# Check models directory
models_dir_path = os.path.join(app.MODELS_DIR, model_type, model_name)
for model_extension in model_extensions:
if os.path.exists(models_dir_path + model_extension):
return models_dir_path + model_extension
if os.path.exists(model_name + model_extension):
return os.path.abspath(model_name + model_extension)
# Default locations
if model_name in default_models:
default_model_path = os.path.join(app.SD_DIR, model_name)
for model_extension in model_extensions:
if os.path.exists(default_model_path + model_extension):
return default_model_path + model_extension
# Can't find requested model, check the default paths.
for default_model in default_models:
for model_dir in model_dirs:
default_model_path = os.path.join(model_dir, default_model)
for model_extension in model_extensions:
if os.path.exists(default_model_path + model_extension):
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}"
)
return default_model_path + model_extension
return None
def reload_models_if_necessary(context: Context, task_data: TaskData):
model_paths_in_req = {
"stable-diffusion": task_data.use_stable_diffusion_model,
"vae": task_data.use_vae_model,
"hypernetwork": task_data.use_hypernetwork_model,
"gfpgan": task_data.use_face_correction,
"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
}
if set_vram_optimizations(context): # reload SD
models_to_reload["stable-diffusion"] = model_paths_in_req["stable-diffusion"]
for model_type, model_path_in_req in models_to_reload.items():
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(context, model_type, scan_model=False) # we've scanned them already
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_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")
def set_vram_optimizations(context: Context):
config = app.getConfig()
vram_usage_level = config.get("vram_usage_level", "balanced")
if vram_usage_level != context.vram_usage_level:
context.vram_usage_level = vram_usage_level
return True
return False
def make_model_folders():
for model_type in KNOWN_MODEL_TYPES:
model_dir_path = os.path.join(app.MODELS_DIR, model_type)
os.makedirs(model_dir_path, exist_ok=True)
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))}'
with open(os.path.join(model_dir_path, help_file_name), "w", encoding="utf-8") as f:
f.write(help_file_contents)
def is_malicious_model(file_path):
try:
if file_path.endswith(".safetensors"):
return False
scan_result = scan_model(file_path)
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)
)
return True
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)
)
return False
except Exception as e:
log.error(f"error while scanning: {file_path}, error: {e}")
return False
def getModels():
models = {
"active": {
"stable-diffusion": "sd-v1-4",
"vae": "",
"hypernetwork": "",
},
"options": {
"stable-diffusion": ["sd-v1-4"],
"vae": [],
"hypernetwork": [],
},
}
models_scanned = 0
class MaliciousModelException(Exception):
"Raised when picklescan reports a problem with a model"
pass
def scan_directory(directory, suffixes, directoriesFirst: bool = True):
nonlocal models_scanned
tree = []
for entry in sorted(
os.scandir(directory), key=lambda entry: (entry.is_file() == directoriesFirst, entry.name.lower())
):
if entry.is_file():
matching_suffix = list(filter(lambda s: entry.name.endswith(s), suffixes))
if len(matching_suffix) == 0:
continue
matching_suffix = matching_suffix[0]
mtime = entry.stat().st_mtime
mod_time = known_models[entry.path] if entry.path in known_models else -1
if mod_time != mtime:
models_scanned += 1
if is_malicious_model(entry.path):
raise MaliciousModelException(entry.path)
known_models[entry.path] = mtime
tree.append(entry.name[: -len(matching_suffix)])
elif entry.is_dir():
scan = scan_directory(entry.path, suffixes, directoriesFirst=False)
if len(scan) != 0:
tree.append((entry.name, scan))
return tree
def listModels(model_type):
nonlocal models_scanned
model_extensions = MODEL_EXTENSIONS.get(model_type, [])
models_dir = os.path.join(app.MODELS_DIR, model_type)
if not os.path.exists(models_dir):
os.makedirs(models_dir)
try:
models["options"][model_type] = scan_directory(models_dir, model_extensions)
except MaliciousModelException as e:
models["scan-error"] = e
# custom models
listModels(model_type="stable-diffusion")
listModels(model_type="vae")
listModels(model_type="hypernetwork")
listModels(model_type="gfpgan")
if models_scanned > 0:
log.info(f"[green]Scanned {models_scanned} models. Nothing infected[/]")
# legacy
custom_weight_path = os.path.join(app.SD_DIR, "custom-model.ckpt")
if os.path.exists(custom_weight_path):
models["options"]["stable-diffusion"].append("custom-model")
return models

View File

@ -0,0 +1,177 @@
import queue
import time
import json
import pprint
from easydiffusion import device_manager
from easydiffusion.types import TaskData, Response, Image as ResponseImage, UserInitiatedStop, GenerateImageRequest
from easydiffusion.utils import get_printable_request, save_images_to_disk, log
from sdkit import Context
from sdkit.generate import generate_images
from sdkit.filter import apply_filters
from sdkit.utils import img_to_buffer, img_to_base64_str, latent_samples_to_images, gc
context = Context() # thread-local
"""
runtime data (bound locally to this thread), for e.g. device, references to loaded models, optimization flags etc
"""
def init(device):
"""
Initializes the fields that will be bound to this runtime's context, and sets the current torch device
"""
context.stop_processing = False
context.temp_images = {}
context.partial_x_samples = None
device_manager.device_init(context, device)
def make_images(
req: GenerateImageRequest, task_data: TaskData, data_queue: queue.Queue, task_temp_images: list, step_callback
):
context.stop_processing = False
print_task_info(req, task_data)
images, seeds = make_images_internal(req, task_data, data_queue, task_temp_images, step_callback)
res = Response(req, task_data, images=construct_response(images, seeds, task_data, base_seed=req.seed))
res = res.json()
data_queue.put(json.dumps(res))
log.info("Task completed")
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
):
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)
if task_data.save_to_disk_path is not None:
save_images_to_disk(images, filtered_images, req, task_data)
seeds = [*range(req.seed, req.seed + len(images))]
if task_data.show_only_filtered_image or filtered_images is images:
return filtered_images, seeds
else:
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,
stream_image_progress_interval: int,
):
context.temp_images.clear()
callback = make_step_callback(req, task_data, data_queue, task_temp_images, step_callback, stream_image_progress, stream_image_progress_interval)
try:
if req.init_image is not None:
req.sampler_name = "ddim"
images = generate_images(context, callback=callback, **req.dict())
user_stopped = False
except UserInitiatedStop:
images = []
user_stopped = True
if context.partial_x_samples is not None:
images = latent_samples_to_images(context, context.partial_x_samples)
finally:
if hasattr(context, "partial_x_samples") and context.partial_x_samples is not None:
del context.partial_x_samples
context.partial_x_samples = None
return images, user_stopped
def filter_images(task_data: TaskData, images: list, user_stopped):
if user_stopped:
return images
filters_to_apply = []
if task_data.block_nsfw:
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)
def construct_response(images: list, seeds: list, task_data: TaskData, base_seed: int):
return [
ResponseImage(
data=img_to_base64_str(img, task_data.output_format, task_data.output_quality),
seed=seed,
)
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,
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)
last_callback_time = -1
def update_temp_img(x_samples, task_temp_images: list):
partial_images = []
images = latent_samples_to_images(context, x_samples)
for i, img in enumerate(images):
buf = img_to_buffer(img, output_format="JPEG")
context.temp_images[f"{task_data.request_id}/{i}"] = buf
task_temp_images[i] = buf
partial_images.append({"path": f"/image/tmp/{task_data.request_id}/{i}"})
del images
return partial_images
def on_image_step(x_samples, i):
nonlocal last_callback_time
context.partial_x_samples = x_samples
step_time = time.time() - last_callback_time if last_callback_time != -1 else -1
last_callback_time = time.time()
progress = {"step": i, "step_time": step_time, "total_steps": n_steps}
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)
data_queue.put(json.dumps(progress))
step_callback()
if context.stop_processing:
raise UserInitiatedStop("User requested that we stop processing")
return on_image_step

285
ui/easydiffusion/server.py Normal file
View File

@ -0,0 +1,285 @@
"""server.py: FastAPI SD-UI Web Host.
Notes:
async endpoints always run on the main thread. Without they run on the thread pool.
"""
import os
import traceback
import datetime
from typing import List, Union
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
from pydantic import BaseModel
from easydiffusion import app, model_manager, task_manager
from easydiffusion.types import TaskData, GenerateImageRequest, MergeRequest
from easydiffusion.utils import log
log.info(f"started in {app.SD_DIR}")
log.info(f"started at {datetime.datetime.now():%x %X}")
server_api = FastAPI()
NOCACHE_HEADERS = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
class NoCacheStaticFiles(StaticFiles):
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"]
):
response_headers.update(NOCACHE_HEADERS)
return False
return super().is_not_modified(response_headers, request_headers)
class SetAppConfigRequest(BaseModel):
update_branch: str = None
render_devices: Union[List[str], List[int], str, int] = None
model_vae: str = None
ui_open_browser_on_start: bool = None
listen_to_network: bool = None
listen_port: int = None
def init():
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:
server_api.mount(
f"/plugins/{dir_prefix}", NoCacheStaticFiles(directory=plugins_dir), name=f"plugins-{dir_prefix}"
)
@server_api.post("/app_config")
async def set_app_config(req: SetAppConfigRequest):
return set_app_config_internal(req)
@server_api.get("/get/{key:path}")
def read_web_data(key: str = None):
return read_web_data_internal(key)
@server_api.get("/ping") # Get server and optionally session status.
def ping(session_id: str = None):
return ping_internal(session_id)
@server_api.post("/render")
def render(req: dict):
return render_internal(req)
@server_api.post("/model/merge")
def model_merge(req: dict):
print(req)
return model_merge_internal(req)
@server_api.get("/image/stream/{task_id:int}")
def stream(task_id: int):
return stream_internal(task_id)
@server_api.get("/image/stop")
def stop(task: int):
return stop_internal(task)
@server_api.get("/image/tmp/{task_id:int}/{img_id:int}")
def get_image(task_id: int, img_id: int):
return get_image_internal(task_id, img_id)
@server_api.get("/")
def read_root():
return FileResponse(os.path.join(app.SD_UI_DIR, "index.html"), headers=NOCACHE_HEADERS)
@server_api.on_event("shutdown")
def shutdown_event(): # Signal render thread to close on shutdown
task_manager.current_state_error = SystemExit("Application shutting down.")
# API implementations
def set_app_config_internal(req: SetAppConfigRequest):
config = app.getConfig()
if req.update_branch is not None:
config["update_branch"] = req.update_branch
if req.render_devices is not None:
update_render_devices_in_config(config, req.render_devices)
if req.ui_open_browser_on_start is not None:
if "ui" not in config:
config["ui"] = {}
config["ui"]["open_browser_on_start"] = req.ui_open_browser_on_start
if req.listen_to_network is not None:
if "net" not in config:
config["net"] = {}
config["net"]["listen_to_network"] = bool(req.listen_to_network)
if req.listen_port is not None:
if "net" not in config:
config["net"] = {}
config["net"]["listen_port"] = int(req.listen_port)
try:
app.setConfig(config)
if req.render_devices:
app.update_render_threads()
return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
except Exception as e:
log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
def update_render_devices_in_config(config, render_devices):
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}")
if render_devices.startswith("cuda:"):
render_devices = render_devices.split(",")
config["render_devices"] = render_devices
def read_web_data_internal(key: str = None):
if not key: # /get without parameters, stable-diffusion easter egg.
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)
elif key == "system_info":
config = app.getConfig()
output_dir = config.get("force_save_path", os.path.join(os.path.expanduser("~"), app.OUTPUT_DIRNAME))
system_info = {
"devices": task_manager.get_devices(),
"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
response = {"status": str(task_manager.current_state)}
if session_id:
session = task_manager.get_cached_session(session_id, update_ttl=True)
response["tasks"] = {id(t): t.status for t in session.tasks}
response["devices"] = task_manager.get_devices()
return JSONResponse(response, headers=NOCACHE_HEADERS)
def render_internal(req: dict):
try:
# separate out the request data into rendering and task-specific data
render_req: GenerateImageRequest = GenerateImageRequest.parse_obj(req)
task_data: TaskData = TaskData.parse_obj(req)
# Overwrite user specified save path
config = app.getConfig()
if "force_save_path" in config:
task_data.save_to_disk_path = config["force_save_path"]
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
new_task = task_manager.render(render_req, task_data)
response = {
"status": str(task_manager.current_state),
"queue": len(task_manager.tasks_queue),
"stream": f"/image/stream/{id(new_task)}",
"task": id(new_task),
}
return JSONResponse(response, headers=NOCACHE_HEADERS)
except ChildProcessError as e: # Render thread is dead
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.
raise HTTPException(status_code=503, detail=str(e)) # HTTP503 Service Unavailable
except Exception as e:
log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
def model_merge_internal(req: dict):
try:
from sdkit.train import merge_models
from easydiffusion.utils.save_utils import filename_regex
mergeReq: MergeRequest = MergeRequest.parse_obj(req)
merge_models(
model_manager.resolve_model_to_use(mergeReq.model0, "stable-diffusion"),
model_manager.resolve_model_to_use(mergeReq.model1, "stable-diffusion"),
mergeReq.ratio,
os.path.join(app.MODELS_DIR, "stable-diffusion", filename_regex.sub("_", mergeReq.out_path)),
mergeReq.use_fp16,
)
return JSONResponse({"status": "OK"}, headers=NOCACHE_HEADERS)
except Exception as e:
log.error(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
def stream_internal(task_id: int):
# TODO Move to WebSockets ??
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 (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.response:
# log.info(f'Session {session_id} sending cached response')
return JSONResponse(task.response, headers=NOCACHE_HEADERS)
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)}')
return StreamingResponse(task.read_buffer_generator(), media_type="application/json")
def stop_internal(task: int):
if not task:
if (
task_manager.current_state == task_manager.ServerStates.Online
or task_manager.current_state == task_manager.ServerStates.Unavailable
):
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 = 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 isinstance(task.error, StopAsyncIteration):
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):
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.temp_images[img_id]:
raise HTTPException(status_code=425, detail="Too Early, task data is not available yet.") # HTTP425 Too Early
try:
img_data = task.temp_images[img_id]
img_data.seek(0)
return StreamingResponse(img_data, media_type="image/jpeg")
except KeyError as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@ -0,0 +1,562 @@
"""task_manager.py: manage tasks dispatching and render threads.
Notes:
render_threads should be the only hard reference held by the manager to the threads.
Use weak_thread_data to store all other data using weak keys.
This will allow for garbage collection after the thread dies.
"""
import json
import traceback
TASK_TTL = 15 * 60 # seconds, Discard last session's task timeout
import torch
import queue, threading, time, weakref
from typing import Any, Hashable
from easydiffusion import device_manager
from easydiffusion.types import TaskData, GenerateImageRequest
from easydiffusion.utils import log
from sdkit.utils import gc
THREAD_NAME_PREFIX = ""
ERR_LOCK_FAILED = " failed to acquire lock within timeout."
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.
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 ServerStates:
class Init(Symbol):
pass
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):
task_data.request_id = id(self)
self.render_request: GenerateImageRequest = req # Initial Request
self.task_data: TaskData = task_data
self.response: Any = None # Copy of the last reponse
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.error: Exception = None
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
async def read_buffer_generator(self):
try:
while not self.buffer_queue.empty():
res = self.buffer_queue.get(block=False)
self.buffer_queue.task_done()
yield res
except queue.Empty as e:
yield
@property
def status(self):
if self.lock.locked():
return "running"
if isinstance(self.error, StopAsyncIteration):
return "stopped"
if self.error:
return "error"
if not self.buffer_queue.empty():
return "buffer"
if self.response:
return "completed"
return "pending"
@property
def is_pending(self):
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.
class DataCache:
def __init__(self):
self._base = dict()
self._lock: threading.Lock = threading.Lock()
def _get_ttl_time(self, ttl: int) -> int:
return int(time.time()) + ttl
def _is_expired(self, timestamp: int) -> bool:
return int(time.time()) >= timestamp
def clean(self) -> None:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.clean" + ERR_LOCK_FAILED)
try:
# Create a list of expired keys to delete
to_delete = []
for key in self._base:
ttl, _ = self._base[key]
if self._is_expired(ttl):
to_delete.append(key)
# Remove Items
for key in to_delete:
(_, val) = self._base[key]
if isinstance(val, RenderTask):
log.debug(f"RenderTask {key} expired. Data removed.")
elif isinstance(val, SessionState):
log.debug(f"Session {key} expired. Data removed.")
else:
log.debug(f"Key {key} expired. Data removed.")
del self._base[key]
finally:
self._lock.release()
def clear(self) -> None:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.clear" + ERR_LOCK_FAILED)
try:
self._base.clear()
finally:
self._lock.release()
def delete(self, key: Hashable) -> bool:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.delete" + ERR_LOCK_FAILED)
try:
if key not in self._base:
return False
del self._base[key]
return True
finally:
self._lock.release()
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)
try:
if key in self._base:
_, value = self._base.get(key)
self._base[key] = (self._get_ttl_time(ttl), value)
return True
return False
finally:
self._lock.release()
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)
try:
self._base[key] = (self._get_ttl_time(ttl), value)
except Exception as e:
log.error(traceback.format_exc())
return False
else:
return True
finally:
self._lock.release()
def tryGet(self, key: Hashable) -> Any:
if not self._lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("DataCache.tryGet" + ERR_LOCK_FAILED)
try:
ttl, value = self._base.get(key, (None, None))
if ttl is not None and self._is_expired(ttl):
log.debug(f"Session {key} expired. Discarding data.")
del self._base[key]
return None
return value
finally:
self._lock.release()
manager_lock = threading.RLock()
render_threads = []
current_state = ServerStates.Init
current_state_error: Exception = None
tasks_queue = []
session_cache = DataCache()
task_cache = DataCache()
weak_thread_data = weakref.WeakKeyDictionary()
idle_event: threading.Event = threading.Event()
class SessionState:
def __init__(self, id: str):
self._id = id
self._tasks_ids = []
@property
def id(self):
return self._id
@property
def tasks(self):
tasks = []
for task_id in self._tasks_ids:
task = task_cache.tryGet(task_id)
if task:
tasks.append(task)
return tasks
def put(self, task, ttl=TASK_TTL):
task_id = id(task)
self._tasks_ids.append(task_id)
if not task_cache.put(task_id, task, ttl):
return False
while len(self._tasks_ids) > len(render_threads) * 2:
self._tasks_ids.pop(0)
return True
def thread_get_next_task():
from easydiffusion import renderer
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.")
return None
if len(tasks_queue) <= 0:
manager_lock.release()
return None
task = None
try: # Select a render task.
for queued_task in tasks_queue:
if queued_task.render_device and renderer.context.device != queued_task.render_device:
# Is asking for a specific render device.
if is_alive(queued_task.render_device) > 0:
continue # requested device alive, skip current one.
else:
# Requested device is not active, return error to UI.
queued_task.error = Exception(queued_task.render_device + " is not currently active.")
task = queued_task
break
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.
continue # Skip Tasks, don't run on CPU unless there is nothing else or user asked for it.
task = queued_task
break
if task is not None:
del tasks_queue[tasks_queue.index(task)]
return task
finally:
manager_lock.release()
def thread_render(device):
global current_state, current_state_error
from easydiffusion import renderer, model_manager
try:
renderer.init(device)
weak_thread_data[threading.current_thread()] = {
"device": renderer.context.device,
"device_name": renderer.context.device_name,
"alive": True,
}
current_state = ServerStates.LoadingModel
model_manager.load_default_models(renderer.context)
current_state = ServerStates.Online
except Exception as e:
log.error(traceback.format_exc())
weak_thread_data[threading.current_thread()] = {"error": e, "alive": False}
return
while True:
session_cache.clean()
task_cache.clean()
if not weak_thread_data[threading.current_thread()]["alive"]:
log.info(f"Shutting down thread for device {renderer.context.device}")
model_manager.unload_all(renderer.context)
return
if isinstance(current_state_error, SystemExit):
current_state = ServerStates.Unavailable
return
task = thread_get_next_task()
if task is None:
idle_event.clear()
idle_event.wait(timeout=1)
continue
if task.error is not None:
log.error(task.error)
task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response))
continue
if current_state_error:
task.error = current_state_error
task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response))
continue
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.")
try:
def step_callback():
global current_state_error
if (
isinstance(current_state_error, SystemExit)
or isinstance(current_state_error, StopAsyncIteration)
or isinstance(task.error, StopAsyncIteration)
):
renderer.context.stop_processing = True
if isinstance(current_state_error, StopAsyncIteration):
task.error = current_state_error
current_state_error = None
log.info(f"Session {task.task_data.session_id} sent cancel signal for task {id(task)}")
current_state = ServerStates.LoadingModel
model_manager.resolve_model_paths(task.task_data)
model_manager.reload_models_if_necessary(renderer.context, task.task_data)
current_state = ServerStates.Rendering
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.
task_cache.keep(id(task), TASK_TTL)
session_cache.keep(task.task_data.session_id, TASK_TTL)
except Exception as e:
task.error = str(e)
task.response = {"status": "failed", "detail": str(task.error)}
task.buffer_queue.put(json.dumps(task.response))
log.error(traceback.format_exc())
finally:
gc(renderer.context)
task.lock.release()
task_cache.keep(id(task), TASK_TTL)
session_cache.keep(task.task_data.session_id, TASK_TTL)
if isinstance(task.error, StopAsyncIteration):
log.info(f"Session {task.task_data.session_id} task {id(task)} cancelled!")
elif task.error is not None:
log.info(f"Session {task.task_data.session_id} task {id(task)} failed!")
else:
log.info(
f"Session {task.task_data.session_id} task {id(task)} completed by {renderer.context.device_name}."
)
current_state = ServerStates.Online
def get_cached_task(task_id: str, update_ttl: bool = False):
# By calling keep before tryGet, wont discard if was expired.
if update_ttl and not task_cache.keep(task_id, TASK_TTL):
# Failed to keep task, already gone.
return None
return task_cache.tryGet(task_id)
def get_cached_session(session_id: str, update_ttl: bool = False):
if update_ttl:
session_cache.keep(session_id, TASK_TTL)
session = session_cache.tryGet(session_id)
if not session:
session = SessionState(session_id)
session_cache.put(session_id, session, TASK_TTL)
return session
def get_devices():
devices = {
"all": {},
"active": {},
}
def get_device_info(device):
if device == "cpu":
return {"name": device_manager.get_processor_name()}
mem_free, mem_total = torch.cuda.mem_get_info(device)
mem_free /= float(10**9)
mem_total /= float(10**9)
return {
"name": torch.cuda.get_device_name(device),
"mem_free": mem_free,
"mem_total": mem_total,
"max_vram_usage_level": device_manager.get_max_vram_usage_level(device),
}
# list the compatible devices
gpu_count = torch.cuda.device_count()
for device in range(gpu_count):
device = f"cuda:{device}"
if not device_manager.is_device_compatible(device):
continue
devices["all"].update({device: get_device_info(device)})
devices["all"].update({"cpu": get_device_info("cpu")})
# list the activated devices
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("get_devices" + ERR_LOCK_FAILED)
try:
for rthread in render_threads:
if not rthread.is_alive():
continue
weak_data = weak_thread_data.get(rthread)
if not weak_data or not "device" in weak_data or not "device_name" in weak_data:
continue
device = weak_data["device"]
devices["active"].update({device: get_device_info(device)})
finally:
manager_lock.release()
return devices
def is_alive(device=None):
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("is_alive" + ERR_LOCK_FAILED)
nbr_alive = 0
try:
for rthread in render_threads:
if device is not None:
weak_data = weak_thread_data.get(rthread)
if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
continue
thread_device = weak_data["device"]
if thread_device != device:
continue
if rthread.is_alive():
nbr_alive += 1
return nbr_alive
finally:
manager_lock.release()
def start_render_thread(device):
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("start_render_thread" + ERR_LOCK_FAILED)
log.info(f"Start new Rendering Thread on device: {device}")
try:
rthread = threading.Thread(target=thread_render, kwargs={"device": device})
rthread.daemon = True
rthread.name = THREAD_NAME_PREFIX + device
rthread.start()
render_threads.append(rthread)
finally:
manager_lock.release()
timeout = DEVICE_START_TIMEOUT
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]:
log.error(f"{rthread}, {device}, error: {weak_thread_data[rthread]['error']}")
return False
if timeout <= 0:
return False
timeout -= 1
time.sleep(1)
return True
def stop_render_thread(device):
try:
device_manager.validate_device_id(device, log_prefix="stop_render_thread")
except:
log.error(traceback.format_exc())
return False
if not manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT):
raise Exception("stop_render_thread" + ERR_LOCK_FAILED)
log.info(f"Stopping Rendering Thread on device: {device}")
try:
thread_to_remove = None
for rthread in render_threads:
weak_data = weak_thread_data.get(rthread)
if weak_data is None or not "device" in weak_data or weak_data["device"] is None:
continue
thread_device = weak_data["device"]
if thread_device == device:
weak_data["alive"] = False
thread_to_remove = rthread
break
if thread_to_remove is not None:
render_threads.remove(rthread)
return True
finally:
manager_lock.release()
return False
def update_render_threads(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_stop: {devices_to_stop}")
for device in devices_to_stop:
if is_alive(device) <= 0:
log.debug(f"{device} is not alive")
continue
if not stop_render_thread(device):
log.warn(f"{device} could not stop render thread")
for device in devices_to_start:
if is_alive(device) >= 1:
log.debug(f"{device} already registered.")
continue
if not start_render_thread(device):
log.warn(f"{device} failed to start.")
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'
)
log.debug(f"active devices: {get_devices()['active']}")
def shutdown_event(): # Signal render thread to close on shutdown
global current_state_error
current_state_error = SystemExit("Application shutting down.")
def render(render_req: GenerateImageRequest, task_data: TaskData):
current_thread_count = is_alive()
if current_thread_count <= 0: # Render thread is dead
raise ChildProcessError("Rendering thread has died.")
# Alive, check if task in cache
session = get_cached_session(task_data.session_id, update_ttl=True)
pending_tasks = list(filter(lambda t: t.is_pending, session.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}."
)
new_task = RenderTask(render_req, task_data)
if session.put(new_task, TASK_TTL):
# Use twice the normal timeout for adding user requests.
# Tries to force session.put to fail before tasks_queue.put would.
if manager_lock.acquire(blocking=True, timeout=LOCK_TIMEOUT * 2):
try:
tasks_queue.append(new_task)
idle_event.set()
return new_task
finally:
manager_lock.release()
raise RuntimeError("Failed to add task to cache.")

103
ui/easydiffusion/types.py Normal file
View File

@ -0,0 +1,103 @@
from pydantic import BaseModel
from typing import Any
class GenerateImageRequest(BaseModel):
prompt: str = ""
negative_prompt: str = ""
seed: int = 42
width: int = 512
height: int = 512
num_outputs: int = 1
num_inference_steps: int = 50
guidance_scale: float = 7.5
init_image: Any = None
init_image_mask: Any = None
prompt_strength: float = 0.8
preserve_init_image_color_profile = False
sampler_name: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
hypernetwork_strength: float = 0
class TaskData(BaseModel):
request_id: str = None
session_id: str = "session"
save_to_disk_path: str = None
vram_usage_level: str = "balanced" # or "low" or "medium"
use_face_correction: str = None # or "GFPGANv1.3"
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
upscale_amount: int = 4 # or 2
use_stable_diffusion_model: str = "sd-v1-4"
# use_stable_diffusion_config: str = "v1-inference"
use_vae_model: str = None
use_hypernetwork_model: str = None
show_only_filtered_image: bool = False
block_nsfw: bool = False
output_format: str = "jpeg" # or "png" or "webp"
output_quality: int = 75
metadata_output_format: str = "txt" # or "json"
stream_image_progress: bool = False
stream_image_progress_interval: int = 5
class MergeRequest(BaseModel):
model0: str = None
model1: str = None
ratio: float = None
out_path: str = "mix"
use_fp16 = True
class Image:
data: str # base64
seed: int
is_nsfw: bool
path_abs: str = None
def __init__(self, data, seed):
self.data = data
self.seed = seed
def json(self):
return {
"data": self.data,
"seed": self.seed,
"path_abs": self.path_abs,
}
class Response:
render_request: GenerateImageRequest
task_data: TaskData
images: list
def __init__(self, render_request: GenerateImageRequest, task_data: TaskData, images: list):
self.render_request = render_request
self.task_data = task_data
self.images = images
def json(self):
del self.render_request.init_image
del self.render_request.init_image_mask
res = {
"status": "succeeded",
"render_request": self.render_request.dict(),
"task_data": self.task_data.dict(),
"output": [],
}
for image in self.images:
res["output"].append(image.json())
return res
class UserInitiatedStop(Exception):
pass

View File

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

View File

@ -0,0 +1,132 @@
import os
import time
import base64
import re
from easydiffusion.types import TaskData, GenerateImageRequest
from sdkit.utils import save_images, save_dicts
filename_regex = re.compile("[^a-zA-Z0-9._-]")
# keep in sync with `ui/media/js/dnd.js`
TASK_TEXT_MAPPING = {
"prompt": "Prompt",
"width": "Width",
"height": "Height",
"seed": "Seed",
"num_inference_steps": "Steps",
"guidance_scale": "Guidance Scale",
"prompt_strength": "Prompt Strength",
"use_face_correction": "Use Face Correction",
"use_upscale": "Use Upscaling",
"upscale_amount": "Upscale By",
"sampler_name": "Sampler",
"negative_prompt": "Negative Prompt",
"use_stable_diffusion_model": "Stable Diffusion model",
"use_vae_model": "VAE model",
"use_hypernetwork_model": "Hypernetwork model",
"hypernetwork_strength": "Hypernetwork Strength",
}
def save_images_to_disk(images: list, filtered_images: list, req: GenerateImageRequest, task_data: TaskData):
now = time.time()
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)
make_filename = make_filename_callback(req, now=now)
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,
)
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:
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,
)
def get_metadata_entries_for_request(req: GenerateImageRequest, task_data: TaskData):
metadata = get_printable_request(req)
metadata.update(
{
"use_stable_diffusion_model": task_data.use_stable_diffusion_model,
"use_vae_model": task_data.use_vae_model,
"use_hypernetwork_model": task_data.use_hypernetwork_model,
"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 task_data.use_hypernetwork_model is None:
del metadata["hypernetwork_strength"]
# if text, format it in the text format expected by the UI
is_txt_format = task_data.metadata_output_format.lower() == "txt"
if is_txt_format:
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)]
for i, entry in enumerate(entries):
entry["Seed" if is_txt_format else "seed"] = req.seed + i
return entries
def get_printable_request(req: GenerateImageRequest):
metadata = req.dict()
del metadata["init_image"]
del metadata["init_image_mask"]
if req.init_image is None:
del metadata["prompt_strength"]
return metadata
def make_filename_callback(req: GenerateImageRequest, suffix=None, now=None):
if now is None:
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]
name = f"{prompt_flattened}_{img_id}"
name = name if suffix is None else f"{name}_{suffix}"
return name
return make_filename

View File

@ -1,138 +1,167 @@
<!DOCTYPE html>
<html>
<head>
<title>Easy Diffusion</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#673AB6">
<link rel="icon" type="image/png" href="/media/images/favicon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="/media/images/favicon-32x32.png" sizes="32x32">
<link rel="stylesheet" href="/media/css/fonts.css?v=1">
<link rel="stylesheet" href="/media/css/themes.css?v=1">
<link rel="stylesheet" href="/media/css/main.css?v=3">
<link rel="stylesheet" href="/media/css/auto-save.css?v=2">
<link rel="stylesheet" href="/media/css/modifier-thumbnails.css?v=2">
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css?v=1">
<link rel="stylesheet" href="/media/css/drawingboard.min.css">
<link rel="stylesheet" href="/media/css/jquery-confirm.min.css">
<link rel="stylesheet" href="/media/css/fonts.css">
<link rel="stylesheet" href="/media/css/themes.css">
<link rel="stylesheet" href="/media/css/main.css">
<link rel="stylesheet" href="/media/css/auto-save.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/image-editor.css">
<link rel="stylesheet" href="/media/css/searchable-models.css">
<link rel="manifest" href="/media/manifest.webmanifest">
<script src="/media/js/jquery-3.6.1.min.js"></script>
<script src="/media/js/drawingboard.min.js"></script>
<script src="/media/js/jquery-confirm.min.js"></script>
<script src="/media/js/marked.min.js"></script>
</head>
<body>
<div id="container">
<div id="top-nav">
<div id="logo">
<h1>Stable Diffusion UI <small>v2.3.5 <span id="updateBranchLabel"></span></small></h1>
<h1>
Easy Diffusion
<small>v2.5.22 <span id="updateBranchLabel"></span></small>
</h1>
</div>
<ul id="top-nav-items">
<li class="dropdown">
<span><i class="fa fa-comments icon"></i> Help & Community</span>
<ul id="community-links" class="dropdown-content">
<li><a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/" target="_blank"><i class="fa-solid fa-book fa-fw"></i> User guide</a></li>
<li><a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" target="_blank"><i class="fa-solid fa-circle-question fa-fw"></i> Usual problems and solutions</a></li>
<li><a href="https://discord.com/invite/u9yhsFmEkB" target="_blank"><i class="fa-brands fa-discord fa-fw"></i> Discord user community</a></li>
<li><a href="https://www.reddit.com/r/StableDiffusionUI/" target="_blank"><i class="fa-brands fa-reddit fa-fw"></i> Reddit community</a></li>
<li><a href="https://github.com/cmdr2/stable-diffusion-ui" target="_blank"><i class="fa-brands fa-github fa-fw"></i> Source code on GitHub</a></li>
</ul>
</li>
<li class="dropdown">
<div id="server-status">
<div id="server-status-color"></div>
<span id="server-status-msg">Stable Diffusion is starting..</span>
</div>
<div id="tab-container" class="tab-container">
<span id="tab-main" class="tab active">
<span><i class="fa fa-image icon"></i> Generate</span>
</span>
<span id="tab-settings" class="tab">
<span><i class="fa fa-gear icon"></i> Settings</span>
<div id="system-settings" class="panel-box settings-box dropdown-content">
<ul id="system-settings-entries">
<li><b class="settings-subheader">System Settings</b></li>
<br/>
<li><label for="theme">Theme: </label><select id="theme" name="theme"><option value="theme-default">Default</option></select></li>
<li><input id="save_to_disk" name="save_to_disk" type="checkbox"> <label for="save_to_disk">Automatically save to <input id="diskPath" name="diskPath" size="40" disabled></label></li>
<li><input id="sound_toggle" name="sound_toggle" type="checkbox" checked> <label for="sound_toggle">Play sound on task completion</label></li>
<li><input id="turbo" name="turbo" type="checkbox" checked> <label for="turbo">Turbo mode <small>(generates images faster, but uses an additional 1 GB of GPU memory)</small></label></li>
<li><input id="use_cpu" name="use_cpu" type="checkbox"> <label for="use_cpu">Use CPU instead of GPU <small>(warning: this will be *very* slow)</small></label></li>
<li><input id="use_full_precision" name="use_full_precision" type="checkbox"> <label for="use_full_precision">Use full precision <small>(for GPU-only. warning: this will consume more VRAM)</small></label></li>
<li>
<input id="auto_save_settings" name="auto_save_settings" checked type="checkbox">
<label for="auto_save_settings">Automatically save settings <small>(settings restored on browser load)</small></label>
<br/>
<button id="configureSettingsSaveBtn">Configure</button>
</li>
<!-- <li><input id="allow_nsfw" name="allow_nsfw" type="checkbox"> <label for="allow_nsfw">Allow NSFW Content (You confirm you are above 18 years of age)</label></li> -->
<br/>
<li><input id="use_beta_channel" name="use_beta_channel" type="checkbox"> <label for="use_beta_channel">🔥Beta channel. Get the latest features immediately (but could be less stable). Please restart the program after changing this.</label></li>
</ul>
</div>
</li>
</ul>
</span>
<span id="tab-about" class="tab">
<span><i class="fa fa-comments icon"></i> Help & Community</span>
</span>
</div>
</div>
<div class="flex-container">
<div id="editor" class="col-fixed-10">
<div id="server-status">
<div id="server-status-color"></div>
<span id="server-status-msg">Stable Diffusion is starting..</span>
</div>
<div id="tab-content-wrapper">
<div id="tab-content-main" class="tab-content active flex-container">
<div id="editor">
<div id="editor-inputs">
<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>
<input id="prompt_from_file" name="prompt_from_file" type="file" /> <!-- hidden -->
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle">Negative Prompt <small>(optional)</small></label>
<label for="negative_prompt" class="collapsible" id="negative_prompt_handle">
Negative Prompt
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-prompts#negative-prompts" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top">Click to learn more about Negative Prompts</span></i></a>
<small>(optional)</small>
</label>
<div class="collapsible-content">
<input id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)">
<textarea id="negative_prompt" name="negative_prompt" placeholder="list the things to remove from the image (e.g. fog, green)"></textarea>
</div>
</div>
<div id="editor-inputs-init-image" class="row">
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label> <input id="init_image" name="init_image" type="file" /><br/>
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label>
<div id="init_image_preview_container" class="image_preview_container">
<div id="init_image_wrapper">
<img id="init_image_preview" src="" />
<span id="init_image_size_box"></span>
<button class="init_image_clear image_clear_btn">X</button>
<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>
</div>
<div id="init_image_buttons">
<div class="button">
<i class="fa-regular fa-folder-open"></i>
Browse
<input id="init_image" name="init_image" type="file" />
</div>
<div id="init_image_button_draw" class="button">
<i class="fa-solid fa-pencil"></i>
Draw
</div>
<div id="inpaint_button_container">
<div id="init_image_button_inpaint" class="button">
<i class="fa-solid fa-paintbrush"></i>
Inpaint
</div>
<input id="enable_mask" name="enable_mask" type="checkbox">
</div>
</div>
<br/>
<input id="enable_mask" name="enable_mask" type="checkbox"> <label for="enable_mask">In-Painting (beta) <small>(select the area which the AI will paint into)</small></label>
<div id="inpaintingEditor"></div>
</div>
<div id="apply_color_correction_setting" class="pl-5"><input id="apply_color_correction" name="apply_color_correction" type="checkbox"> <label for="apply_color_correction">Preserve color profile <small>(helps during inpainting)</small></label></div>
</div>
<div id="editor-inputs-tags-container" class="row">
<label>Image Modifiers: <small>(click an Image Modifier to remove it)</small></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>
<button id="makeImage">Make Image</button>
<button id="stopImage" class="secondaryButton">Stop All</button>
<button id="makeImage" class="primaryButton">Make Image</button>
<div id="render-buttons">
<button id="stopImage" class="secondaryButton">Stop All</button>
<button id="pause"><i class="fa-solid fa-pause"></i> Pause All</button>
<button id="resume"><i class="fa-solid fa-play"></i> Resume</button>
</div>
</div>
<div class="line-separator">&nbsp;</div>
<span class="line-separator"></span>
<div id="editor-settings" class="panel-box settings-box">
<div id="editor-settings" class="settings-box panel-box">
<h4 class="collapsible">
Image Settings
<i id="reset-image-settings" class="fa-solid fa-arrow-rotate-left">
<span class="simple-tooltip right">
<i id="reset-image-settings" class="fa-solid fa-arrow-rotate-left section-button">
<span class="simple-tooltip top-left">
Reset Image Settings
</span>
</i>
</h4>
<ul id="editor-settings-entries" class="collapsible-content">
<li><table>
<div id="editor-settings-entries" class="collapsible-content">
<div><table>
<tr><b class="settings-subheader">Image Settings</b></tr>
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="30000" 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="stable_diffusion_model">Model:</label></td><td>
<select id="stable_diffusion_model" name="stable_diffusion_model">
<!-- <option value="sd-v1-4" selected>sd-v1-4</option> -->
</select>
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td class="model-input">
<input id="stable_diffusion_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<button id="reload-models" class="secondaryButton reloadModels"><i class='fa-solid fa-rotate'></i></button>
<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>
<tr id="samplerSelection" class="pl-5"><td><label for="sampler">Sampler:</label></td><td>
<select id="sampler" name="sampler">
<option value="plms">plms</option>
<option value="ddim">ddim</option>
<option value="heun">heun</option>
<option value="euler">euler</option>
<option value="euler_a" selected>euler_a</option>
<option value="dpm2">dpm2</option>
<option value="dpm2_a">dpm2_a</option>
<option value="lms">lms</option>
<!-- <tr id="modelConfigSelection" class="pl-5"><td><label for="model_config">Model Config:</i></label></td><td>
<select id="model_config" name="model_config">
</select>
</td></tr> -->
<tr class="pl-5"><td><label for="vae_model">Custom VAE:</i></label></td><td>
<input id="vae_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<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>
<tr id="samplerSelection" class="pl-5"><td><label for="sampler_name">Sampler:</label></td><td>
<select id="sampler_name" name="sampler_name">
<option value="plms">PLMS</option>
<option value="ddim">DDIM</option>
<option value="heun">Heun</option>
<option value="euler">Euler</option>
<option value="euler_a" selected>Euler Ancestral</option>
<option value="dpm2">DPM2</option>
<option value="dpm2_a">DPM2 Ancestral</option>
<option value="lms">LMS</option>
<option value="dpm_solver_stability">DPM Solver (Stability AI)</option>
<option value="dpmpp_2s_a">DPM++ 2s Ancestral</option>
<option value="dpmpp_2m">DPM++ 2m</option>
<option value="dpmpp_sde">DPM++ SDE</option>
<option value="dpm_fast">DPM Fast</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>
<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>
<tr class="pl-5"><td><label>Image Size: </label></td><td>
<select id="width" name="width" value="512">
@ -181,35 +210,57 @@
<label for="height"><small>(height)</small></label>
</td></tr>
<tr class="pl-5"><td><label for="num_inference_steps">Inference Steps:</label></td><td> <input id="num_inference_steps" name="num_inference_steps" size="4" value="25" 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="10" 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></span>
<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 class="pl-5"><td><label for="hypernetwork_model">Hypernetwork:</i></label></td><td>
<input id="hypernetwork_model" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
</td></tr>
<tr id="hypernetwork_strength_container" class="pl-5">
<td><label for="hypernetwork_strength_slider">Hypernetwork Strength:</label></td>
<td> <input id="hypernetwork_strength_slider" name="hypernetwork_strength_slider" class="editor-slider" value="100" type="range" min="0" max="100"> <input id="hypernetwork_strength" name="hypernetwork_strength" size="4" pattern="^[0-9\.]+$" onkeypress="preventNonNumericalInput(event)"><br/></td>
</tr>
<tr class="pl-5"><td><label for="output_format">Output Format:</label></td><td>
<select id="output_format" name="output_format">
<option value="jpeg" selected>jpeg</option>
<option value="png">png</option>
<option value="webp">webp</option>
</select>
</td></tr>
</li></table>
<br/>
<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)">
</td></tr>
</table></div>
<div><ul>
<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, slightly slower image creation)</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="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</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">
<input id="use_upscale" name="use_upscale" type="checkbox"> <label for="use_upscale">Upscale image by 4x with </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">
<option value="2">2x</option>
<option value="4" selected>4x</option>
</select>
with
<select id="upscale_model" name="upscale_model">
<option value="RealESRGAN_x4plus" selected>RealESRGAN_x4plus</option>
<option value="RealESRGAN_x4plus_anime_6B">RealESRGAN_x4plus_anime_6B</option>
</select>
</li>
<li class="pl-5"><input id="show_only_filtered_image" name="show_only_filtered_image" type="checkbox" checked> <label for="show_only_filtered_image">Show only the corrected/upscaled image</label></li>
</ul>
</ul></div>
</div>
</div>
<div id="editor-modifiers" class="panel-box">
<button id="modifier-settings-btn" title="Add custom modifiers"><i class="fa fa-gear"></i></button>
<h4 class="collapsible">Image Modifiers (art styles, tags etc)</h4>
<h4 class="collapsible">
Image Modifiers (art styles, tags etc)
<i id="modifier-settings-btn" class="fa-solid fa-gear section-button">
<span class="simple-tooltip left">
Add Custom Modifiers
</span>
</i>
</h4>
<div id="editor-modifiers-entries" class="collapsible-content">
<div id="editor-modifiers-entries-toolbar">
<label for="preview-image">Image Style:</label>
@ -232,25 +283,116 @@
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! :)
</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 id="tab-content-settings" class="tab-content">
<div id="system-settings" class="tab-content-inner">
<h1>System Settings</h1>
<div class="parameters-table"></div>
<br/>
<button id="save-system-settings-btn" class="primaryButton">Save</button>
<br/><br/>
<div>
<h3><i class="fa fa-microchip icon"></i> System Info</h3>
<div id="system-info">
<table>
<tr><td><label>Processor:</label></td><td id="system-info-cpu" class="value"></td></tr>
<tr><td><label>Compatible Graphics Cards (all):</label></td><td id="system-info-gpus-all" class="value"></td></tr>
<tr><td></td><td>&nbsp;</td></tr>
<tr><td><label>Used for rendering 🔥:</label></td><td id="system-info-rendering-devices" class="value"></td></tr>
<tr><td><label>Server Addresses <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">You can access Stable Diffusion UI from other devices using these addresses</span></i> :</label></td><td id="system-info-server-hosts" class="value"></td></tr>
</table>
</div>
</div>
</div>
</div>
<div id="tab-content-about" class="tab-content">
<div class="tab-content-inner">
<div class="float-container">
<div class="float-child">
<h1>Help</h1>
<ul id="help-links">
<li><span class="help-section">Using the software</span>
<ul>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/How-To-Use" target="_blank"><i class="fa-solid fa-book fa-fw"></i> How to use</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/UI-Overview" target="_blank"><i class="fa-solid fa-list fa-fw"></i> UI Overview</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Writing-Prompts" target="_blank"><i class="fa-solid fa-pen-to-square fa-fw"></i> Writing prompts</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Inpainting" target="_blank"><i class="fa-solid fa-paintbrush fa-fw"></i> Inpainting</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Run-on-Multiple-GPUs" target="_blank"><i class="fa-solid fa-paintbrush fa-fw"></i> Run on Multiple GPUs</a>
</ul>
<li><span class="help-section">Installation</span>
<ul>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Troubleshooting" target="_blank"><i class="fa-solid fa-circle-question fa-fw"></i> Troubleshooting</a>
</ul>
<li><span class="help-section">Downloadable Content</span>
<ul>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Custom-Models" target="_blank"><i class="fa-solid fa-images fa-fw"></i> Custom Models</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/UI-Plugins" target="_blank"><i class="fa-solid fa-puzzle-piece fa-fw"></i> UI Plugins</a>
<li> <a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/VAE-Variational-Auto-Encoder" target="_blank"><i class="fa-solid fa-hand-sparkles fa-fw"></i> VAE Variational Auto Encoder</a>
</ul>
</ul>
</div>
<div class="float-child">
<h1>Community</h1>
<ul id="community-links">
<li><a href="https://discord.com/invite/u9yhsFmEkB" target="_blank"><i class="fa-brands fa-discord fa-fw"></i> Discord user community</a></li>
<li><a href="https://www.reddit.com/r/StableDiffusionUI/" target="_blank"><i class="fa-brands fa-reddit fa-fw"></i> Reddit community</a></li>
<li><a href="https://github.com/cmdr2/stable-diffusion-ui" target="_blank"><i class="fa-brands fa-github fa-fw"></i> Source code on GitHub</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div id="save-settings-config" style="display:none">
<div id="save-settings-config" class="popup">
<div>
<span id="save-settings-config-close-btn">X</span>
<i class="close-button fa-solid fa-xmark"></i>
<h1>Save Settings Configuration</h1>
<p>Select which settings should be remembered when restarting the browser</p>
<table id="save-settings-config-table">
<table id="save-settings-config-table" class="form-table">
</table>
</div>
</div>
<div id="modifier-settings-config" style="display:none">
<div id="modifier-settings-config" class="popup">
<div>
<span id="modifier-settings-config-close-btn">X</span>
<i class="close-button fa-solid fa-xmark"></i>
<h1>Modifier Settings</h1>
<p>Set your custom modifiers (one per line)</p>
<textarea id="custom-modifiers-input" placeholder="Enter your custom modifiers, one-per-line"></textarea>
@ -258,9 +400,41 @@
</div>
</div>
<div class="line-separator">&nbsp;</div>
<div id="image-editor" class="popup image-editor-popup">
<div>
<i class="close-button fa-solid fa-xmark"></i>
<h1>Image Editor</h1>
<div class="flex-container">
<div class="editor-controls-left"></div>
<div class="editor-controls-center">
<div></div>
</div>
<div class="editor-controls-right">
<div></div>
</div>
</div>
</div>
</div>
<div id="footer" class="panel-box">
<div id="image-inpainter" class="popup image-editor-popup">
<div>
<i class="close-button fa-solid fa-xmark"></i>
<h1>Inpainter</h1>
<div class="flex-container">
<div class="editor-controls-left"></div>
<div class="editor-controls-center">
<div></div>
</div>
<div class="editor-controls-right">
<div></div>
</div>
</div>
</div>
</div>
<div id="footer-spacer"></div>
<div id="footer">
<div class="line-separator">&nbsp;</div>
<p>If you found this project useful and want to help keep it alive, please <a href="https://ko-fi.com/cmdr2_stablediffusion_ui" target="_blank"><img src="/media/images/kofi.png" id="coffeeButton"></a> to help cover the cost of development and maintenance! Thank you for your support!</p>
<p>Please feel free to join the <a href="https://discord.com/invite/u9yhsFmEkB" target="_blank">discord community</a> or <a href="https://github.com/cmdr2/stable-diffusion-ui/issues" target="_blank">file an issue</a> if you have any problems or suggestions in using this interface.</p>
<div id="footer-legal">
@ -271,25 +445,34 @@
</div>
</div>
</body>
<script src="media/js/utils.js"></script>
<script src="media/js/engine.js"></script>
<script src="media/js/parameters.js"></script>
<script src="media/js/plugins.js"></script>
<script src="media/js/plugins.js?v=1"></script>
<script src="media/js/utils.js?v=5"></script>
<script src="media/js/inpainting-editor.js?v=1"></script>
<script src="media/js/image-modifiers.js?v=3"></script>
<script src="media/js/auto-save.js?v=2.3"></script>
<script src="media/js/main.js?v=5"></script>
<script src="media/js/themes.js?v=2"></script>
<script src="media/js/image-modifiers.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/themes.js"></script>
<script src="media/js/dnd.js"></script>
<script src="media/js/image-editor.js"></script>
<script>
async function init() {
await initSettings()
await getModels()
await getDiskPath()
await getAppConfig()
await loadModifiers()
await loadUIPlugins()
await loadModifiers()
await getSystemInfo()
setInterval(healthCheck, HEALTH_PING_INTERVAL * 1000)
healthCheck()
SD.init({
events: {
statusChange: setServerStatus
, idle: onIdle
}
})
playSound()
}

10
ui/main.py Normal file
View File

@ -0,0 +1,10 @@
from easydiffusion import model_manager, app, server
from easydiffusion.server import server_api # required for uvicorn
# Init the app
model_manager.init()
app.init()
server.init()
# start the browser ui
app.open_browser()

View File

@ -6,69 +6,76 @@
display: none;
}
#save-settings-config {
position: absolute;
background: rgba(32, 33, 36, 50%);
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
z-index: 1000;
}
#save-settings-config > div {
background: var(--background-color3);
max-width: 600px;
margin: auto;
margin-top: 50px;
border-radius: 6px;
padding: 30px;
text-align: center;
}
#save-settings-config-table {
.form-table {
margin: auto;
}
#save-settings-config-table th {
.form-table th {
padding-top: 15px;
padding-bottom: 5px;
}
#save-settings-config-table td:first-child,
#save-settings-config-table th:first-child {
.form-table td:first-child > *,
.form-table th:first-child > * {
float: right;
white-space: nowrap;
}
#save-settings-config-table td:last-child,
#save-settings-config-table th:last-child {
.form-table td:last-child > *,
.form-table th:last-child > * {
float: left;
}
#save-settings-config-table td small {
.parameters-table {
display: flex;
flex-direction: column;
gap: 1px;
}
.parameters-table > div {
background: var(--background-color2);
display: flex;
padding: 0px 4px;
}
.parameters-table > div > div {
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.parameters-table small {
color: rgb(153, 153, 153);
}
#save-settings-config-close-btn {
float: right;
cursor: pointer;
padding: 10px;
transform: translate(50%, -50%) scaleX(130%);
.parameters-table > div > div:nth-child(1) {
font-size: 20px;
width: 45px;
}
#reset-image-settings {
cursor: pointer;
float: right;
padding: 8px;
opacity: 1;
transition: opacity 0.5;
.parameters-table > div > div:nth-child(2) {
flex: 1;
flex-direction: column;
text-align: left;
justify-content: center;
align-items: start;
gap: 4px;
}
.collapsible:not(.active) #reset-image-settings {
display: none;
.parameters-table > div > div:nth-child(3) {
text-align: right;
}
#reset-image-settings.hidden {
opacity: 0;
pointer-events: none;
.parameters-table > div:first-child {
border-radius: 12px 12px 0px 0px;
}
.parameters-table > div:last-child {
border-radius: 0px 0px 12px 12px;
}
.parameters-table .fa-fire {
color: #F7630C;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,216 @@
.editor-controls-left {
padding-left: 32px;
text-align: left;
padding-bottom: 20px;
max-width: min-content;
}
.editor-options-container {
display: flex;
row-gap: 10px;
}
.editor-options-container > * {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.editor-options-container > * > * {
position: inherit;
width: 32px;
height: 32px;
border-radius: 16px;
background: var(--background-color3);
cursor: pointer;
transition: opacity 0.25s;
}
.editor-options-container > * > *:hover {
opacity: 0.75;
}
.editor-options-container > * > *.active {
border: 1px solid #3584e4;
}
.image_editor_opacity .editor-options-container > * > *:not(.active) {
border: 1px solid var(--background-color3);
}
.image_editor_color .editor-options-container {
flex-wrap: wrap;
}
.image_editor_color .editor-options-container > * {
flex: 20%;
}
.image_editor_color .editor-options-container > * > * {
position: relative;
}
.image_editor_color .editor-options-container > * > *.active::before {
content: "\f00c";
display: var(--fa-display,inline-block);
font-style: normal;
font-variant: normal;
line-height: 1;
text-rendering: auto;
font-family: var(--fa-style-family, "Font Awesome 6 Free");
font-weight: var(--fa-style, 900);
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(125%);
color: black;
}
.image_editor_color .editor-options-container > *:first-child {
flex: 100%;
}
.image_editor_color .editor-options-container > *:first-child > * {
width: 100%;
}
.image_editor_color .editor-options-container > *:first-child > * > input {
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.image_editor_color .editor-options-container > *:first-child > * > span {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
opacity: 0.5;
}
.image_editor_color .editor-options-container > *:first-child > *.active > span {
opacity: 0;
}
.image_editor_tool .editor-options-container {
flex-wrap: wrap;
}
.image_editor_tool .editor-options-container > * {
padding: 2px;
flex: 50%;
}
.editor-controls-center {
/* background: var(--background-color2); */
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.editor-controls-center > div {
position: relative;
background: black;
}
.editor-controls-center canvas {
position: absolute;
left: 0;
top: 0;
}
.editor-controls-right {
padding: 32px;
display: flex;
flex-direction: column;
}
.editor-controls-right > div:last-child {
flex: 1;
display: flex;
flex-direction: column;
min-width: 200px;
gap: 5px;
justify-content: end;
}
.image-editor-button {
width: 100%;
height: 32px;
border-radius: 16px;
background: var(--background-color3);
}
.editor-controls-right .image-editor-button {
margin-bottom: 4px;
}
#init_image_button_inpaint .input-toggle {
position: absolute;
left: 16px;
}
#init_image_button_inpaint .input-toggle input:not(:checked) ~ label {
pointer-events: none;
}
.image-editor-popup {
--popup-margin: 16px;
--popup-padding: 24px;
}
.image-editor-popup > div {
margin: var(--popup-margin);
padding: var(--popup-padding);
min-height: calc(100vh - (2 * var(--popup-margin)));
max-width: none;
min-width: fit-content;
}
.image-editor-popup h1 {
position: absolute;
top: 32px;
left: 50%;
transform: translateX(-50%);
}
@media screen and (max-width: 700px) {
.image-editor-popup > div {
margin: 0px;
padding: 0px;
}
.image-editor-popup h1 {
position: relative;
transform: none;
left: auto;
}
}
.image-editor-popup > div > div {
min-height: calc(100vh - (2 * var(--popup-margin)) - (2 * var(--popup-padding)));
}
.inpainter .image_editor_color {
display: none;
}
.inpainter .editor-canvas-background {
opacity: 0.75;
}
#init_image_preview_container .button {
display: flex;
padding: 6px;
height: 24px;
box-shadow: 2px 2px 1px 1px #00000088;
}
#init_image_preview_container .button:hover {
background: var(--background-color4)
}
.image-editor-popup .button {
display: flex;
}
.image-editor-popup h4 {
text-align: left;
}

9
ui/media/css/jquery-confirm.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -217,32 +217,6 @@
#modifier-settings-btn {
float: right;
}
#modifier-settings-config {
position: fixed;
background: rgba(32, 33, 36, 50%);
top: 0px;
left: 0px;
width: 100vw;
height: 100vh;
z-index: 1000;
}
#modifier-settings-config > div {
background: var(--background-color2);
max-width: 600px;
margin: auto;
margin-top: 100px;
border-radius: 6px;
padding: 30px;
text-align: center;
}
#modifier-settings-config-close-btn {
float: right;
cursor: pointer;
padding: 10px;
transform: translate(50%, -50%) scaleX(130%);
}
#modifier-settings-config textarea {
width: 90%;
height: 150px;

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

@ -1,34 +1,47 @@
:root {
--background-color1: rgb(32, 33, 36); /* main parts of the page */
--background-color2: rgb(44, 45, 48); /* main panels */
--background-color3: rgb(47, 49, 53);
--background-color4: rgb(18, 18, 19); /* settings dropdowns */
--main-hue: 222;
--main-saturation: 4%;
--value-base: 13%;
--value-step: 5%;
--background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base));
--background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1 * var(--value-step))));
--background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.5 * var(--value-step))));
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1.5 * var(--value-step))));
--accent-hue: 266;
--accent-hue: 267;
--accent-lightness: 36%;
--accent-lightness-hover: 40%;
--text-color: #eee;
--input-text-color: black;
--input-background-color: #e9e9ed;
--input-border-color: #8f8f9d;
--input-text-color: #eee;
--input-background-color: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (0.7 * var(--value-step))));
--input-border-color: var(--background-color4);
--button-text-color: var(--input-text-color);
--button-color: #e9e9ed;
--button-border: 1px solid #8f8f9d;
--button-color: var(--input-background-color);
--button-border: none;
/* other */
--input-border-radius: 4px;
--input-border-size: 1px;
--accent-color: hsl(var(--accent-hue), 100%, var(--accent-lightness));
--accent-color-hover: hsl(var(--accent-hue), 100%, var(--accent-lightness-hover));
--make-image-border: 2px solid hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) - 21%));
--accent-text-color: rgb(255, 221, 255);
--primary-button-border: none;
--input-switch-padding: 1px;
--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. */
--theme-color-fallback: #673AB6;
}
.theme-light {
--background-color1: white;
--background-color2: #dddddd;
--background-color2: #ececec;
--background-color3: #e7e9eb;
--background-color4: #cccccc;
@ -37,6 +50,13 @@
--input-text-color: black;
--input-background-color: #f8f9fa;
--input-border-color: grey;
--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 {
@ -47,37 +67,37 @@
--accent-hue: 235;
--accent-lightness: 65%;
--make-image-border: none;
--button-color: var(--accent-color);
--button-border: none;
--input-text-color: #ccc;
--input-border-size: 2px;
--input-background-color: #202225;
--input-border-color: var(--input-background-color);
--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 {
--main-hue: 222;
--main-saturation: 18%;
--value-base: 19%;
--value-base: 18%;
--value-step: 3%;
--background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base));
--background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1 * var(--value-step))));
--background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step))));
--accent-hue: 212;
--make-image-border: none;
--button-color: var(--accent-color);
--button-border: none;
--input-border-size: 1px;
--input-background-color: var(--background-color3);
--input-text-color: #ccc;
--input-border-color: var(--background-color4);
--accent-hue: 212;
--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;
}
@ -90,16 +110,13 @@
--background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (1 * var(--value-step))));
--background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step))));
--make-image-border: none;
--button-color: var(--accent-color);
--button-border: none;
--input-border-size: 1px;
--input-background-color: var(--background-color3);
--input-text-color: #ccc;
--input-border-color: var(--background-color4);
--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 {
@ -110,17 +127,12 @@
--background-color1: hsl(var(--main-hue), var(--main-saturation), var(--value-base));
--background-color2: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1 * var(--value-step))));
--background-color3: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (2 * var(--value-step))));
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (3 * var(--value-step))));
--make-image-border: none;
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) + (1.4 * var(--value-step))));
--button-color: var(--accent-color);
--button-border: none;
--input-border-size: 0px;
--input-background-color: var(--background-color3);
--input-text-color: #ccc;
--input-border-color: var(--background-color4);
--input-border-size: 0px;
--theme-color-fallback: #000000;
}
.theme-wild {
@ -134,13 +146,38 @@
--background-color4: hsl(var(--main-hue), var(--main-saturation), calc(var(--value-base) - (3 * var(--value-step))));
--accent-hue: 212;
--make-image-border: none;
--button-color: var(--accent-color);
--button-border: none;
--input-border-size: 1px;
--input-background-color: hsl(222, var(--main-saturation), calc(var(--value-base) - (2 * var(--value-step))));
--input-text-color: red;
--input-border-color: green;
--input-text-color: #FF0000;
--input-border-color: #005E05;
--tertiary-color: white;
--accent-text-color: #f7fbff;
}
.theme-gnomie {
--background-color1: #242424;
--background-color2: #353535;
--background-color3: #494949;
--background-color4: #000000;
--accent-hue: 213;
--accent-lightness: 55%;
--accent-color: #2168bf;
--input-border-radius: 6px;
--input-text-color: #ffffff;
--input-background-color: #2a2a2a;
--input-border-size: 0px;
--input-border-color: var(--input-background-color);
--theme-color-fallback: #2168bf;
}
.theme-gnomie .panel-box {
border: none;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
border-radius: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 576" width="24" height="24">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
<path style="filter: drop-shadow(0px 0px 20px white)" d="M290.7 57.4 57.4 290.7c-25 25-25 65.5 0 90.5l80 80c12 12 28.3 18.7 45.3 18.7H512c17.7 0 32-14.3 32-32s-14.3-32-32-32H387.9l130.7-130.6c25-25 25-65.5 0-90.5L381.3 57.4c-25-25-65.5-25-90.5 0zm6.7 358.6H182.6l-80-80 124.7-124.7 137.4 137.4-67.3 67.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
<path style="filter: drop-shadow(0px 0px 20px white)" d="M341.6 29.2 240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4 101.5-101.6c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6v42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480h42.4c21.2 0 41.6-8.4 56.6-23.4l120.7-120.7-45.3-45.3-120.7 120.7c-3 3-7.1 4.7-11.3 4.7H96v-36.1c0-4.2 1.7-8.3 4.7-11.3l120.7-120.7-45.3-45.3L55.4 323.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 775 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 576" width="24" height="24">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
<path style="filter: drop-shadow(0px 0px 20px white)" d="M118.6 9.4c-12.5-12.5-32.7-12.5-45.2 0s-12.5 32.8 0 45.3l81.3 81.3-92.1 92.1c-37.5 37.5-37.5 98.3 0 135.8l117.5 117.5c37.5 37.5 98.3 37.5 135.8 0l190.4-190.5c28.1-28.1 28.1-73.7 0-101.8L354.9 37.7c-28.1-28.1-73.7-28.1-101.8 0l-53.1 53-81.4-81.3zM200 181.3l49.4 49.4c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L245.3 136l53.1-53.1c3.1-3.1 8.2-3.1 11.3 0l151.4 151.4c3.1 3.1 3.1 8.2 0 11.3L418.7 288H99.5c1.4-5.4 4.2-10.4 8.4-14.6l92.1-92.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="24" height="24">
<!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc.-->
<path style="filter: drop-shadow(0px 0px 20px white)" d="m410.3 231 11.3-11.3-33.9-33.9-62.1-62.1-33.9-33.9-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2l199.2-199.2 22.6-22.7zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9l-78.2 23 23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1v32c0 8.8 7.2 16 16 16h32zM362.7 18.7l-14.4 14.5-22.6 22.6-11.4 11.3 33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5l-39.3-39.4c-25-25-65.5-25-90.5 0zm-47.4 168-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 934 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 973 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

View File

@ -13,17 +13,24 @@ const SETTINGS_IDS_LIST = [
"num_outputs_total",
"num_outputs_parallel",
"stable_diffusion_model",
"sampler",
"vae_model",
"hypernetwork_model",
"sampler_name",
"width",
"height",
"num_inference_steps",
"guidance_scale",
"prompt_strength",
"hypernetwork_strength",
"output_format",
"output_quality",
"negative_prompt",
"stream_image_progress",
"use_face_correction",
"gfpgan_model",
"use_upscale",
"upscale_amount",
"block_nsfw",
"show_only_filtered_image",
"upscale_model",
"preview-image",
@ -32,10 +39,14 @@ const SETTINGS_IDS_LIST = [
"save_to_disk",
"diskPath",
"sound_toggle",
"turbo",
"use_cpu",
"use_full_precision",
"auto_save_settings"
"vram_usage_level",
"confirm_dangerous_actions",
"metadata_output_format",
"auto_save_settings",
"apply_color_correction",
"process_order_toggle",
"thumbnail_size",
"auto_scroll"
]
const IGNORE_BY_DEFAULT = [
@ -52,7 +63,12 @@ const SETTINGS_SECTIONS = [ // gets the "keys" property filled in with an ordere
async function initSettings() {
SETTINGS_IDS_LIST.forEach(id => {
var element = document.getElementById(id)
var label = document.querySelector(`label[for='${element.id}']`)
if (!element) {
console.error(`Missing settings element ${id}`)
}
if (id in SETTINGS) { // don't create it again
return
}
SETTINGS[id] = {
key: id,
element: element,
@ -68,7 +84,8 @@ async function initSettings() {
SETTINGS_SECTIONS.forEach(section => {
var name = section.name
var element = document.getElementById(section.id)
var children = Array.from(element.querySelectorAll(unsorted_settings_ids.map(id => `#${id}`).join(",")))
var unsorted_ids = unsorted_settings_ids.map(id => `#${id}`).join(",")
var children = unsorted_ids == "" ? [] : Array.from(element.querySelectorAll(unsorted_ids));
section.keys = []
children.forEach(e => {
section.keys.push(e.id)
@ -79,6 +96,9 @@ async function initSettings() {
}
function getSetting(element) {
if (element.dataset && 'path' in element.dataset) {
return element.dataset.path
}
if (typeof element === "string" || element instanceof String) {
element = SETTINGS[element].element
}
@ -88,6 +108,10 @@ function getSetting(element) {
return 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) {
element = SETTINGS[element].element
}
@ -121,15 +145,16 @@ function loadSettings() {
var saved_settings_text = localStorage.getItem(SETTINGS_KEY)
if (saved_settings_text) {
var saved_settings = JSON.parse(saved_settings_text)
if (saved_settings.find(s => s.key == "auto_save_settings").value == false) {
if (saved_settings.find(s => s.key == "auto_save_settings")?.value == false) {
setSetting("auto_save_settings", false)
return
}
CURRENTLY_LOADING_SETTINGS = true
saved_settings.map(saved_setting => {
saved_settings.forEach(saved_setting => {
var setting = SETTINGS[saved_setting.key]
if (setting === undefined) {
return
if (!setting) {
console.warn(`Attempted to load setting ${saved_setting.key}, but no setting found`);
return null;
}
setting.ignore = saved_setting.ignore
if (!setting.ignore) {
@ -209,22 +234,25 @@ function fillSaveSettingsConfigTable() {
})
})
})
prettifyInputs(saveSettingsConfigTable)
}
document.getElementById("save-settings-config-close-btn").addEventListener('click', () => {
saveSettingsConfigOverlay.style.display = 'none'
// configureSettingsSaveBtn
var autoSaveSettings = document.getElementById("auto_save_settings")
var configSettingsButton = document.createElement("button")
configSettingsButton.textContent = "Configure"
configSettingsButton.style.margin = "0px 5px"
autoSaveSettings.insertAdjacentElement("beforebegin", configSettingsButton)
autoSaveSettings.addEventListener("change", () => {
configSettingsButton.style.display = autoSaveSettings.checked ? "block" : "none"
})
document.getElementById("configureSettingsSaveBtn").addEventListener('click', () => {
configSettingsButton.addEventListener('click', () => {
fillSaveSettingsConfigTable()
saveSettingsConfigOverlay.style.display = 'block'
})
saveSettingsConfigOverlay.addEventListener('click', (event) => {
if (event.target.id == saveSettingsConfigOverlay.id) {
saveSettingsConfigOverlay.style.display = 'none'
}
})
document.getElementById("save-settings-config-close-btn").addEventListener('click', () => {
saveSettingsConfigOverlay.style.display = 'none'
saveSettingsConfigOverlay.classList.add("active")
})
resetImageSettingsButton.addEventListener('click', event => {
loadDefaultSettingsSection("editor-settings");
@ -245,10 +273,12 @@ function tryLoadOldSettings() {
var saved_settings = JSON.parse(saved_settings_text)
Object.keys(saved_settings.should_save).forEach(key => {
key = key in old_map ? old_map[key] : key
if (!(key in SETTINGS)) return
SETTINGS[key].ignore = !saved_settings.should_save[key]
});
Object.keys(saved_settings.values).forEach(key => {
key = key in old_map ? old_map[key] : key
if (!(key in SETTINGS)) return
var setting = SETTINGS[key]
if (!setting.ignore) {
setting.value = saved_settings.values[key]
@ -263,8 +293,6 @@ function tryLoadOldSettings() {
"soundEnabled": "sound_toggle",
"saveToDisk": "save_to_disk",
"useCPU": "use_cpu",
"useFullPrecision": "use_full_precision",
"useTurboMode": "turbo",
"diskPath": "diskPath",
"useFaceCorrection": "use_face_correction",
"useUpscaling": "use_upscale",
@ -276,9 +304,11 @@ function tryLoadOldSettings() {
Object.keys(individual_settings_map).forEach(localStorageKey => {
var localStorageValue = localStorage.getItem(localStorageKey);
if (localStorageValue !== null) {
var setting = SETTINGS[individual_settings_map[localStorageKey]]
if (setting == null || setting == undefined) {
return
let key = individual_settings_map[localStorageKey]
var setting = SETTINGS[key]
if (!setting) {
console.warn(`Attempted to map old setting ${key}, but no setting found`);
return null;
}
if (setting.element.type == "checkbox" && (typeof localStorageValue === "string" || localStorageValue instanceof String)) {
localStorageValue = localStorageValue == "true"

619
ui/media/js/dnd.js Normal file
View File

@ -0,0 +1,619 @@
"use strict" // Opt in to a restricted variant of JavaScript
const EXT_REGEX = /(?:\.([^.]+))?$/
const TEXT_EXTENSIONS = ['txt', 'json']
const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'bmp', 'tiff', 'tif', 'tga', 'webp']
function parseBoolean(stringValue) {
if (typeof stringValue === 'boolean') {
return stringValue
}
if (typeof stringValue === 'number') {
return stringValue !== 0
}
if (typeof stringValue !== 'string') {
return false
}
switch(stringValue?.toLowerCase()?.trim()) {
case "true":
case "yes":
case "on":
case "1":
return true;
case "false":
case "no":
case "off":
case "0":
case "none":
case null:
case undefined:
return false;
}
try {
return Boolean(JSON.parse(stringValue));
} catch {
return Boolean(stringValue)
}
}
const TASK_MAPPING = {
prompt: { name: 'Prompt',
setUI: (prompt) => {
promptField.value = prompt
},
readUI: () => promptField.value,
parse: (val) => val
},
negative_prompt: { name: 'Negative Prompt',
setUI: (negative_prompt) => {
negativePromptField.value = negative_prompt
},
readUI: () => negativePromptField.value,
parse: (val) => val
},
active_tags: { name: "Image Modifiers",
setUI: (active_tags) => {
refreshModifiersState(active_tags)
},
readUI: () => activeTags.map(x => x.name),
parse: (val) => val
},
inactive_tags: { name: "Inactive Image Modifiers",
setUI: (inactive_tags) => {
refreshInactiveTags(inactive_tags)
},
readUI: () => activeTags.filter(tag => tag.inactive === true).map(x => x.name),
parse: (val) => val
},
width: { name: 'Width',
setUI: (width) => {
const oldVal = widthField.value
widthField.value = width
if (!widthField.value) {
widthField.value = oldVal
}
},
readUI: () => parseInt(widthField.value),
parse: (val) => parseInt(val)
},
height: { name: 'Height',
setUI: (height) => {
const oldVal = heightField.value
heightField.value = height
if (!heightField.value) {
heightField.value = oldVal
}
},
readUI: () => parseInt(heightField.value),
parse: (val) => parseInt(val)
},
seed: { name: 'Seed',
setUI: (seed) => {
if (!seed) {
randomSeedField.checked = true
seedField.disabled = true
seedField.value = 0
return
}
randomSeedField.checked = false
seedField.disabled = false
seedField.value = seed
},
readUI: () => parseInt(seedField.value), // just return the value the user is seeing in the UI
parse: (val) => parseInt(val)
},
num_inference_steps: { name: 'Steps',
setUI: (num_inference_steps) => {
numInferenceStepsField.value = num_inference_steps
},
readUI: () => parseInt(numInferenceStepsField.value),
parse: (val) => parseInt(val)
},
guidance_scale: { name: 'Guidance Scale',
setUI: (guidance_scale) => {
guidanceScaleField.value = guidance_scale
updateGuidanceScaleSlider()
},
readUI: () => parseFloat(guidanceScaleField.value),
parse: (val) => parseFloat(val)
},
prompt_strength: { name: 'Prompt Strength',
setUI: (prompt_strength) => {
promptStrengthField.value = prompt_strength
updatePromptStrengthSlider()
},
readUI: () => parseFloat(promptStrengthField.value),
parse: (val) => parseFloat(val)
},
init_image: { name: 'Initial Image',
setUI: (init_image) => {
initImagePreview.src = init_image
},
readUI: () => initImagePreview.src,
parse: (val) => val
},
mask: { name: 'Mask',
setUI: (mask) => {
setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter)
imageInpainter.setImg(mask)
}, 250)
maskSetting.checked = Boolean(mask)
},
readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined),
parse: (val) => val
},
preserve_init_image_color_profile: { name: 'Preserve Color Profile',
setUI: (preserve_init_image_color_profile) => {
applyColorCorrectionField.checked = parseBoolean(preserve_init_image_color_profile)
},
readUI: () => applyColorCorrectionField.checked,
parse: (val) => parseBoolean(val)
},
use_face_correction: { name: 'Use Face Correction',
setUI: (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 ? gfpganModelField.value : undefined),
parse: (val) => val
},
use_upscale: { name: 'Use Upscaling',
setUI: (use_upscale) => {
const oldVal = upscaleModelField.value
upscaleModelField.value = getModelPath(use_upscale, ['.pth'])
if (upscaleModelField.value) { // Is a valid value for the field.
useUpscalingField.checked = true
upscaleModelField.disabled = false
upscaleAmountField.disabled = false
} else { // Not a valid value, restore the old value and disable the filter.
upscaleModelField.disabled = true
upscaleAmountField.disabled = true
upscaleModelField.value = oldVal
useUpscalingField.checked = false
}
},
readUI: () => (useUpscalingField.checked ? upscaleModelField.value : undefined),
parse: (val) => val
},
upscale_amount: { name: 'Upscale By',
setUI: (upscale_amount) => {
upscaleAmountField.value = upscale_amount
},
readUI: () => upscaleAmountField.value,
parse: (val) => val
},
sampler_name: { name: 'Sampler',
setUI: (sampler_name) => {
samplerField.value = sampler_name
},
readUI: () => samplerField.value,
parse: (val) => val
},
use_stable_diffusion_model: { name: 'Stable Diffusion model',
setUI: (use_stable_diffusion_model) => {
const oldVal = stableDiffusionModelField.value
use_stable_diffusion_model = getModelPath(use_stable_diffusion_model, ['.ckpt', '.safetensors'])
stableDiffusionModelField.value = use_stable_diffusion_model
if (!stableDiffusionModelField.value) {
stableDiffusionModelField.value = oldVal
}
},
readUI: () => stableDiffusionModelField.value,
parse: (val) => val
},
use_vae_model: { name: 'VAE model',
setUI: (use_vae_model) => {
const oldVal = vaeModelField.value
use_vae_model = (use_vae_model === undefined || use_vae_model === null || use_vae_model === 'None' ? '' : use_vae_model)
if (use_vae_model !== '') {
use_vae_model = getModelPath(use_vae_model, ['.vae.pt', '.ckpt'])
use_vae_model = use_vae_model !== '' ? use_vae_model : oldVal
}
vaeModelField.value = use_vae_model
},
readUI: () => vaeModelField.value,
parse: (val) => val
},
use_hypernetwork_model: { name: 'Hypernetwork model',
setUI: (use_hypernetwork_model) => {
const oldVal = hypernetworkModelField.value
use_hypernetwork_model = (use_hypernetwork_model === undefined || use_hypernetwork_model === null || use_hypernetwork_model === 'None' ? '' : use_hypernetwork_model)
if (use_hypernetwork_model !== '') {
use_hypernetwork_model = getModelPath(use_hypernetwork_model, ['.pt'])
use_hypernetwork_model = use_hypernetwork_model !== '' ? use_hypernetwork_model : oldVal
}
hypernetworkModelField.value = use_hypernetwork_model
hypernetworkModelField.dispatchEvent(new Event('change'))
},
readUI: () => hypernetworkModelField.value,
parse: (val) => val
},
hypernetwork_strength: { name: 'Hypernetwork Strength',
setUI: (hypernetwork_strength) => {
hypernetworkStrengthField.value = hypernetwork_strength
updateHypernetworkStrengthSlider()
},
readUI: () => parseFloat(hypernetworkStrengthField.value),
parse: (val) => parseFloat(val)
},
num_outputs: { name: 'Parallel Images',
setUI: (num_outputs) => {
numOutputsParallelField.value = num_outputs
},
readUI: () => parseInt(numOutputsParallelField.value),
parse: (val) => val
},
use_cpu: { name: 'Use CPU',
setUI: (use_cpu) => {
useCPUField.checked = use_cpu
},
readUI: () => useCPUField.checked,
parse: (val) => val
},
stream_image_progress: { name: 'Stream Image Progress',
setUI: (stream_image_progress) => {
streamImageProgressField.checked = (parseInt(numOutputsTotalField.value) > 50 ? false : stream_image_progress)
},
readUI: () => streamImageProgressField.checked,
parse: (val) => Boolean(val)
},
show_only_filtered_image: { name: 'Show only the corrected/upscaled image',
setUI: (show_only_filtered_image) => {
showOnlyFilteredImageField.checked = show_only_filtered_image
},
readUI: () => showOnlyFilteredImageField.checked,
parse: (val) => Boolean(val)
},
output_format: { name: 'Output Format',
setUI: (output_format) => {
outputFormatField.value = output_format
},
readUI: () => outputFormatField.value,
parse: (val) => val
},
save_to_disk_path: { name: 'Save to disk path',
setUI: (save_to_disk_path) => {
saveToDiskField.checked = Boolean(save_to_disk_path)
diskPathField.value = save_to_disk_path
},
readUI: () => diskPathField.value,
parse: (val) => val
}
}
function restoreTaskToUI(task, fieldsToSkip) {
fieldsToSkip = fieldsToSkip || []
if ('numOutputsTotal' in task) {
numOutputsTotalField.value = task.numOutputsTotal
}
if ('seed' in task) {
randomSeedField.checked = false
seedField.value = task.seed
}
if (!('reqBody' in task)) {
return
}
for (const key in TASK_MAPPING) {
if (key in task.reqBody && !fieldsToSkip.includes(key)) {
TASK_MAPPING[key].setUI(task.reqBody[key])
}
}
// properly reset fields not present in the task
if (!('use_hypernetwork_model' in task.reqBody)) {
hypernetworkModelField.value = ""
hypernetworkModelField.dispatchEvent(new Event("change"))
}
// restore the original prompt if provided (e.g. use settings), fallback to prompt as needed (e.g. copy/paste or d&d)
promptField.value = task.reqBody.original_prompt
if (!('original_prompt' in task.reqBody)) {
promptField.value = task.reqBody.prompt
}
// properly reset checkboxes
if (!('use_face_correction' in task.reqBody)) {
useFaceCorrectionField.checked = false
gfpganModelField.disabled = true
}
if (!('use_upscale' in task.reqBody)) {
useUpscalingField.checked = false
}
if (!('mask' in task.reqBody) && maskSetting.checked) {
maskSetting.checked = false
maskSetting.dispatchEvent(new Event("click"))
}
upscaleModelField.disabled = !useUpscalingField.checked
upscaleAmountField.disabled = !useUpscalingField.checked
// hide/show source picture as needed
if (IMAGE_REGEX.test(initImagePreview.src) && task.reqBody.init_image == undefined) {
// hide source image
initImageClearBtn.dispatchEvent(new Event("click"))
}
else if (task.reqBody.init_image !== undefined) {
// listen for inpainter loading event, which happens AFTER the main image loads (which reloads the inpainter)
initImagePreview.addEventListener('load', function() {
if (Boolean(task.reqBody.mask)) {
imageInpainter.setImg(task.reqBody.mask)
maskSetting.checked = true
}
}, { once: true })
initImagePreview.src = task.reqBody.init_image
}
}
function readUI() {
const reqBody = {}
for (const key in TASK_MAPPING) {
reqBody[key] = TASK_MAPPING[key].readUI()
}
return {
'numOutputsTotal': parseInt(numOutputsTotalField.value),
'seed': TASK_MAPPING['seed'].readUI(),
'reqBody': reqBody
}
}
function getModelPath(filename, extensions)
{
if (typeof filename !== "string") {
return
}
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) {
filename = filename.slice(pathIdx)
}
extensions.forEach(ext => {
if (filename.endsWith(ext)) {
filename = filename.slice(0, filename.length - ext.length)
}
})
return filename
}
const TASK_TEXT_MAPPING = {
prompt: 'Prompt',
width: 'Width',
height: 'Height',
seed: 'Seed',
num_inference_steps: 'Steps',
guidance_scale: 'Guidance Scale',
prompt_strength: 'Prompt Strength',
use_face_correction: 'Use Face Correction',
use_upscale: 'Use Upscaling',
upscale_amount: 'Upscale By',
sampler_name: 'Sampler',
negative_prompt: 'Negative Prompt',
use_stable_diffusion_model: 'Stable Diffusion model',
use_hypernetwork_model: 'Hypernetwork model',
hypernetwork_strength: 'Hypernetwork Strength'
}
function parseTaskFromText(str) {
const taskReqBody = {}
const lines = str.split('\n')
if (lines.length === 0) {
return
}
// Prompt
let knownKeyOnFirstLine = false
for (let key in TASK_TEXT_MAPPING) {
if (lines[0].startsWith(TASK_TEXT_MAPPING[key] + ':')) {
knownKeyOnFirstLine = true
break
}
}
if (!knownKeyOnFirstLine) {
taskReqBody.prompt = lines[0]
console.log('Prompt:', taskReqBody.prompt)
}
for (const key in TASK_TEXT_MAPPING) {
if (key in taskReqBody) {
continue
}
const name = TASK_TEXT_MAPPING[key];
let val = undefined
const reName = new RegExp(`${name}\\ *:\\ *(.*)(?:\\r\\n|\\r|\\n)*`, 'igm')
const match = reName.exec(str);
if (match) {
str = str.slice(0, match.index) + str.slice(match.index + match[0].length)
val = match[1]
}
if (val !== undefined) {
taskReqBody[key] = TASK_MAPPING[key].parse(val.trim())
console.log(TASK_MAPPING[key].name + ':', taskReqBody[key])
if (!str) {
break
}
}
}
if (Object.keys(taskReqBody).length <= 0) {
return undefined
}
const task = { reqBody: taskReqBody }
if ('seed' in taskReqBody) {
task.seed = taskReqBody.seed
}
return task
}
async function parseContent(text) {
text = text.trim();
if (text.startsWith('{') && text.endsWith('}')) {
try {
const task = JSON.parse(text)
if (!('reqBody' in task)) { // support the format saved to the disk, by the UI
task.reqBody = Object.assign({}, task)
}
restoreTaskToUI(task)
return true
} catch (e) {
console.warn(`JSON text content couldn't be parsed.`, e)
}
return false
}
// Normal txt file.
const task = parseTaskFromText(text)
if (text.toLowerCase().includes('seed:') && task) { // only parse valid task content
restoreTaskToUI(task)
return true
} else {
console.warn(`Raw text content couldn't be parsed.`)
return false
}
}
async function readFile(file, i) {
console.log(`Event %o reading file[${i}]:${file.name}...`)
const fileContent = (await file.text()).trim()
return await parseContent(fileContent)
}
function dropHandler(ev) {
console.log('Content dropped...')
let items = []
if (ev?.dataTransfer?.items) { // Use DataTransferItemList interface
items = Array.from(ev.dataTransfer.items)
items = items.filter(item => item.kind === 'file')
items = items.map(item => item.getAsFile())
} else if (ev?.dataTransfer?.files) { // Use DataTransfer interface
items = Array.from(ev.dataTransfer.files)
}
items.forEach(item => {item.file_ext = EXT_REGEX.exec(item.name.toLowerCase())[1]})
let text_items = items.filter(item => TEXT_EXTENSIONS.includes(item.file_ext))
let image_items = items.filter(item => IMAGE_EXTENSIONS.includes(item.file_ext))
if (image_items.length > 0 && ev.target == initImageSelector) {
return // let the event bubble up, so that the Init Image filepicker can receive this
}
ev.preventDefault() // Prevent default behavior (Prevent file/content from being opened)
text_items.forEach(readFile)
}
function dragOverHandler(ev) {
console.log('Content in drop zone')
// Prevent default behavior (Prevent file/content from being opened)
ev.preventDefault()
ev.dataTransfer.dropEffect = "copy"
let img = new Image()
img.src = '//' + location.host + '/media/images/favicon-32x32.png'
ev.dataTransfer.setDragImage(img, 16, 16)
}
document.addEventListener("drop", dropHandler)
document.addEventListener("dragover", dragOverHandler)
const TASK_REQ_NO_EXPORT = [
"use_cpu",
"save_to_disk_path"
]
const resetSettings = document.getElementById('reset-image-settings')
function checkReadTextClipboardPermission (result) {
if (result.state != "granted" && result.state != "prompt") {
return
}
// PASTE ICON
const pasteIcon = document.createElement('i')
pasteIcon.className = 'fa-solid fa-paste section-button'
pasteIcon.innerHTML = `<span class="simple-tooltip top-left">Paste Image Settings</span>`
pasteIcon.addEventListener('click', async (event) => {
event.stopPropagation()
// Add css class 'active'
pasteIcon.classList.add('active')
// In 350 ms remove the 'active' class
asyncDelay(350).then(() => pasteIcon.classList.remove('active'))
// Retrieve clipboard content and try to parse it
const text = await navigator.clipboard.readText();
await parseContent(text)
})
resetSettings.parentNode.insertBefore(pasteIcon, resetSettings)
}
navigator.permissions.query({ name: "clipboard-read" }).then(checkReadTextClipboardPermission, (reason) => console.log('clipboard-read is not available. %o', reason))
document.addEventListener('paste', async (event) => {
if (event.target) {
const targetTag = event.target.tagName.toLowerCase()
// Disable when targeting input elements.
if (targetTag === 'input' || targetTag === 'textarea') {
return
}
}
const paste = (event.clipboardData || window.clipboardData).getData('text')
const selection = window.getSelection()
if (selection.toString().trim().length <= 0 && await parseContent(paste)) {
event.preventDefault()
return
}
})
// Adds a copy and a paste icon if the browser grants permission to write to clipboard.
function checkWriteToClipboardPermission (result) {
if (result.state != "granted" && result.state != "prompt") {
return
}
// COPY ICON
const copyIcon = document.createElement('i')
copyIcon.className = 'fa-solid fa-clipboard section-button'
copyIcon.innerHTML = `<span class="simple-tooltip top-left">Copy Image Settings</span>`
copyIcon.addEventListener('click', (event) => {
event.stopPropagation()
// Add css class 'active'
copyIcon.classList.add('active')
// In 350 ms remove the 'active' class
asyncDelay(350).then(() => copyIcon.classList.remove('active'))
const uiState = readUI()
TASK_REQ_NO_EXPORT.forEach((key) => delete uiState.reqBody[key])
if (uiState.reqBody.init_image && !IMAGE_REGEX.test(uiState.reqBody.init_image)) {
delete uiState.reqBody.init_image
delete uiState.reqBody.prompt_strength
}
navigator.clipboard.writeText(JSON.stringify(uiState, undefined, 4))
})
resetSettings.parentNode.insertBefore(copyIcon, resetSettings)
}
// Determine which access we have to the clipboard. Clipboard access is only available on localhost or via TLS.
navigator.permissions.query({ name: "clipboard-write" }).then(checkWriteToClipboardPermission, (e) => {
if (e instanceof TypeError && typeof navigator?.clipboard?.writeText === 'function') {
// Fix for firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1560373
checkWriteToClipboardPermission({state:"granted"})
}
})

File diff suppressed because one or more lines are too long

1311
ui/media/js/engine.js Normal file

File diff suppressed because it is too large Load Diff

837
ui/media/js/image-editor.js Normal file
View File

@ -0,0 +1,837 @@
var editorControlsLeft = document.getElementById("image-editor-controls-left")
const IMAGE_EDITOR_MAX_SIZE = 800
const IMAGE_EDITOR_BUTTONS = [
{
name: "Cancel",
icon: "fa-regular fa-circle-xmark",
handler: editor => {
editor.hide()
}
},
{
name: "Save",
icon: "fa-solid fa-floppy-disk",
handler: editor => {
editor.saveImage()
}
}
]
const defaultToolBegin = (editor, ctx, x, y, is_overlay = false) => {
ctx.beginPath()
ctx.moveTo(x, y)
}
const defaultToolMove = (editor, ctx, x, y, is_overlay = false) => {
ctx.lineTo(x, y)
if (is_overlay) {
ctx.clearRect(0, 0, editor.width, editor.height)
ctx.stroke()
}
}
const defaultToolEnd = (editor, ctx, x, y, is_overlay = false) => {
ctx.stroke()
if (is_overlay) {
ctx.clearRect(0, 0, editor.width, editor.height)
}
}
const toolDoNothing = (editor, ctx, x, y, is_overlay = false) => {}
const IMAGE_EDITOR_TOOLS = [
{
id: "draw",
name: "Draw",
icon: "fa-solid fa-pencil",
cursor: "url(/media/images/fa-pencil.svg) 0 24, pointer",
begin: defaultToolBegin,
move: defaultToolMove,
end: defaultToolEnd
},
{
id: "erase",
name: "Erase",
icon: "fa-solid fa-eraser",
cursor: "url(/media/images/fa-eraser.svg) 0 14, pointer",
begin: defaultToolBegin,
move: (editor, ctx, x, y, is_overlay = false) => {
ctx.lineTo(x, y)
if (is_overlay) {
ctx.clearRect(0, 0, editor.width, editor.height)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1
ctx.filter = "none"
ctx.drawImage(editor.canvas_current, 0, 0)
editor.setBrush(editor.layers.overlay)
ctx.stroke()
editor.canvas_current.style.opacity = 0
}
},
end: (editor, ctx, x, y, is_overlay = false) => {
ctx.stroke()
if (is_overlay) {
ctx.clearRect(0, 0, editor.width, editor.height)
editor.canvas_current.style.opacity = ""
}
},
setBrush: (editor, layer) => {
layer.ctx.globalCompositeOperation = "destination-out"
}
},
{
id: "fill",
name: "Fill",
icon: "fa-solid fa-fill",
cursor: "url(/media/images/fa-fill.svg) 20 6, pointer",
begin: (editor, ctx, x, y, is_overlay = false) => {
if (!is_overlay) {
var color = hexToRgb(ctx.fillStyle)
color.a = parseInt(ctx.globalAlpha * 255) // layer.ctx.globalAlpha
flood_fill(editor, ctx, parseInt(x), parseInt(y), color)
}
},
move: toolDoNothing,
end: toolDoNothing
},
{
id: "colorpicker",
name: "Picker",
icon: "fa-solid fa-eye-dropper",
cursor: "url(/media/images/fa-eye-dropper.svg) 0 24, pointer",
begin: (editor, ctx, x, y, is_overlay = false) => {
if (!is_overlay) {
var img_rgb = editor.layers.background.ctx.getImageData(x, y, 1, 1).data
var drawn_rgb = editor.ctx_current.getImageData(x, y, 1, 1).data
var drawn_opacity = drawn_rgb[3] / 255
editor.custom_color_input.value = rgbToHex({
r: (drawn_rgb[0] * drawn_opacity) + (img_rgb[0] * (1 - drawn_opacity)),
g: (drawn_rgb[1] * drawn_opacity) + (img_rgb[1] * (1 - drawn_opacity)),
b: (drawn_rgb[2] * drawn_opacity) + (img_rgb[2] * (1 - drawn_opacity)),
})
editor.custom_color_input.dispatchEvent(new Event("change"))
}
},
move: toolDoNothing,
end: toolDoNothing
}
]
const IMAGE_EDITOR_ACTIONS = [
{
id: "fill_all",
name: "Fill all",
icon: "fa-solid fa-paint-roller",
handler: (editor) => {
editor.ctx_current.globalCompositeOperation = "source-over"
editor.ctx_current.rect(0, 0, editor.width, editor.height)
editor.ctx_current.fill()
editor.setBrush()
},
trackHistory: true
},
{
id: "clear",
name: "Clear",
icon: "fa-solid fa-xmark",
handler: (editor) => {
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
},
trackHistory: true
},
{
id: "undo",
name: "Undo",
icon: "fa-solid fa-rotate-left",
handler: (editor) => {
editor.history.undo()
},
trackHistory: false
},
{
id: "redo",
name: "Redo",
icon: "fa-solid fa-rotate-right",
handler: (editor) => {
editor.history.redo()
},
trackHistory: false
}
]
var IMAGE_EDITOR_SECTIONS = [
{
name: "tool",
title: "Tool",
default: "draw",
options: Array.from(IMAGE_EDITOR_TOOLS.map(t => t.id)),
initElement: (element, option) => {
var tool_info = IMAGE_EDITOR_TOOLS.find(t => t.id == option)
element.className = "image-editor-button button"
var sub_element = document.createElement("div")
var icon = document.createElement("i")
tool_info.icon.split(" ").forEach(c => icon.classList.add(c))
sub_element.appendChild(icon)
sub_element.append(tool_info.name)
element.appendChild(sub_element)
}
},
{
name: "color",
title: "Color",
default: "#f1c232",
options: [
"custom",
"#ea9999", "#e06666", "#cc0000", "#990000", "#660000",
"#f9cb9c", "#f6b26b", "#e69138", "#b45f06", "#783f04",
"#ffe599", "#ffd966", "#f1c232", "#bf9000", "#7f6000",
"#b6d7a8", "#93c47d", "#6aa84f", "#38761d", "#274e13",
"#a4c2f4", "#6d9eeb", "#3c78d8", "#1155cc", "#1c4587",
"#b4a7d6", "#8e7cc3", "#674ea7", "#351c75", "#20124d",
"#d5a6bd", "#c27ba0", "#a64d79", "#741b47", "#4c1130",
"#ffffff", "#c0c0c0", "#838383", "#525252", "#000000",
],
initElement: (element, option) => {
if (option == "custom") {
var input = document.createElement("input")
input.type = "color"
element.appendChild(input)
var span = document.createElement("span")
span.textContent = "Custom"
span.onclick = function(e) {
input.click()
}
element.appendChild(span)
}
else {
element.style.background = option
}
},
getCustom: editor => {
var input = editor.popup.querySelector(".image_editor_color input")
return input.value
}
},
{
name: "brush_size",
title: "Brush Size",
default: 48,
options: [ 6, 12, 16, 24, 30, 40, 48, 64 ],
initElement: (element, option) => {
element.parentElement.style.flex = option
element.style.width = option + "px"
element.style.height = option + "px"
element.style['margin-right'] = '2px'
element.style["border-radius"] = (option / 2).toFixed() + "px"
}
},
{
name: "opacity",
title: "Opacity",
default: 0,
options: [ 0, 0.2, 0.4, 0.6, 0.8 ],
initElement: (element, option) => {
element.style.background = `repeating-conic-gradient(rgba(0, 0, 0, ${option}) 0% 25%, rgba(255, 255, 255, ${option}) 0% 50%) 50% / 10px 10px`
}
},
{
name: "sharpness",
title: "Sharpness",
default: 0,
options: [ 0, 0.05, 0.1, 0.2, 0.3 ],
initElement: (element, option) => {
var size = 32
var blur_amount = parseInt(option * size)
var sub_element = document.createElement("div")
sub_element.style.background = `var(--background-color3)`
sub_element.style.filter = `blur(${blur_amount}px)`
sub_element.style.width = `${size - 2}px`
sub_element.style.height = `${size - 2}px`
sub_element.style['border-radius'] = `${size}px`
element.style.background = "none"
element.appendChild(sub_element)
}
}
]
class EditorHistory {
constructor(editor) {
this.editor = editor
this.events = [] // stack of all events (actions/edits)
this.current_edit = null
this.rewind_index = 0 // how many events back into the history we've rewound to. (current state is just after event at index 'length - this.rewind_index - 1')
}
push(event) {
// probably add something here eventually to save state every x events
if (this.rewind_index != 0) {
this.events = this.events.slice(0, 0 - this.rewind_index)
this.rewind_index = 0
}
var snapshot_frequency = 20 // (every x edits, take a snapshot of the current drawing state, for faster rewinding)
if (this.events.length > 0 && this.events.length % snapshot_frequency == 0) {
event.snapshot = this.editor.layers.drawing.ctx.getImageData(0, 0, this.editor.width, this.editor.height)
}
this.events.push(event)
}
pushAction(action) {
this.push({
type: "action",
id: action
});
}
editBegin(x, y) {
this.current_edit = {
type: "edit",
id: this.editor.getOptionValue("tool"),
options: Object.assign({}, this.editor.options),
points: [ { x: x, y: y } ]
}
}
editMove(x, y) {
if (this.current_edit) {
this.current_edit.points.push({ x: x, y: y })
}
}
editEnd(x, y) {
if (this.current_edit) {
this.push(this.current_edit)
this.current_edit = null
}
}
clear() {
this.events = []
}
undo() {
this.rewindTo(this.rewind_index + 1)
}
redo() {
this.rewindTo(this.rewind_index - 1)
}
rewindTo(new_rewind_index) {
if (new_rewind_index < 0 || new_rewind_index > this.events.length) {
return; // do nothing if target index is out of bounds
}
var ctx = this.editor.layers.drawing.ctx
ctx.clearRect(0, 0, this.editor.width, this.editor.height)
var target_index = this.events.length - 1 - new_rewind_index
var snapshot_index = target_index
while (snapshot_index > -1) {
if (this.events[snapshot_index].snapshot) {
break
}
snapshot_index--
}
if (snapshot_index != -1) {
ctx.putImageData(this.events[snapshot_index].snapshot, 0, 0);
}
for (var i = (snapshot_index + 1); i <= target_index; i++) {
var event = this.events[i]
if (event.type == "action") {
var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == event.id)
action.handler(this.editor)
}
else if (event.type == "edit") {
var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == event.id)
this.editor.setBrush(this.editor.layers.drawing, event.options)
var first_point = event.points[0]
tool.begin(this.editor, ctx, first_point.x, first_point.y)
for (var point_i = 1; point_i < event.points.length; point_i++) {
tool.move(this.editor, ctx, event.points[point_i].x, event.points[point_i].y)
}
var last_point = event.points[event.points.length - 1]
tool.end(this.editor, ctx, last_point.x, last_point.y)
}
}
// re-set brush to current settings
this.editor.setBrush(this.editor.layers.drawing)
this.rewind_index = new_rewind_index
}
}
class ImageEditor {
constructor(popup, inpainter = false) {
this.inpainter = inpainter
this.popup = popup
this.history = new EditorHistory(this)
if (inpainter) {
this.popup.classList.add("inpainter")
}
this.drawing = false
this.temp_previous_tool = null // used for the ctrl-colorpicker functionality
this.container = popup.querySelector(".editor-controls-center > div")
this.layers = {}
var layer_names = [
"background",
"drawing",
"overlay"
]
layer_names.forEach(name => {
let canvas = document.createElement("canvas")
canvas.className = `editor-canvas-${name}`
this.container.appendChild(canvas)
this.layers[name] = {
name: name,
canvas: canvas,
ctx: canvas.getContext("2d")
}
})
// add mouse handlers
this.container.addEventListener("mousedown", this.mouseHandler.bind(this))
this.container.addEventListener("mouseup", this.mouseHandler.bind(this))
this.container.addEventListener("mousemove", this.mouseHandler.bind(this))
this.container.addEventListener("mouseout", this.mouseHandler.bind(this))
this.container.addEventListener("mouseenter", this.mouseHandler.bind(this))
this.container.addEventListener("touchstart", this.mouseHandler.bind(this))
this.container.addEventListener("touchmove", this.mouseHandler.bind(this))
this.container.addEventListener("touchcancel", this.mouseHandler.bind(this))
this.container.addEventListener("touchend", this.mouseHandler.bind(this))
// initialize editor controls
this.options = {}
this.optionElements = {}
IMAGE_EDITOR_SECTIONS.forEach(section => {
section.id = `image_editor_${section.name}`
var sectionElement = document.createElement("div")
sectionElement.className = section.id
var title = document.createElement("h4")
title.innerText = section.title
sectionElement.appendChild(title)
var optionsContainer = document.createElement("div")
optionsContainer.classList.add("editor-options-container")
this.optionElements[section.name] = []
section.options.forEach((option, index) => {
var optionHolder = document.createElement("div")
var optionElement = document.createElement("div")
optionHolder.appendChild(optionElement)
section.initElement(optionElement, option)
optionElement.addEventListener("click", target => this.selectOption(section.name, index))
optionsContainer.appendChild(optionHolder)
this.optionElements[section.name].push(optionElement)
})
this.selectOption(section.name, section.options.indexOf(section.default))
sectionElement.appendChild(optionsContainer)
this.popup.querySelector(".editor-controls-left").appendChild(sectionElement)
})
this.custom_color_input = this.popup.querySelector(`input[type="color"]`)
this.custom_color_input.addEventListener("change", () => {
this.custom_color_input.parentElement.style.background = this.custom_color_input.value
this.selectOption("color", 0)
})
if (this.inpainter) {
this.selectOption("color", IMAGE_EDITOR_SECTIONS.find(s => s.name == "color").options.indexOf("#ffffff"))
this.selectOption("opacity", IMAGE_EDITOR_SECTIONS.find(s => s.name == "opacity").options.indexOf(0.4))
}
// initialize the right-side controls
var buttonContainer = document.createElement("div")
IMAGE_EDITOR_BUTTONS.forEach(button => {
var element = document.createElement("div")
var icon = document.createElement("i")
element.className = "image-editor-button button"
icon.className = button.icon
element.appendChild(icon)
element.append(button.name)
buttonContainer.appendChild(element)
element.addEventListener("click", event => button.handler(this))
})
var actionsContainer = document.createElement("div")
var actionsTitle = document.createElement("h4")
actionsTitle.textContent = "Actions"
actionsContainer.appendChild(actionsTitle);
IMAGE_EDITOR_ACTIONS.forEach(action => {
var element = document.createElement("div")
var icon = document.createElement("i")
element.className = "image-editor-button button"
icon.className = action.icon
element.appendChild(icon)
element.append(action.name)
actionsContainer.appendChild(element)
element.addEventListener("click", event => this.runAction(action.id))
})
this.popup.querySelector(".editor-controls-right").appendChild(actionsContainer)
this.popup.querySelector(".editor-controls-right").appendChild(buttonContainer)
this.keyHandlerBound = this.keyHandler.bind(this)
this.setSize(512, 512)
}
show() {
this.popup.classList.add("active")
document.addEventListener("keydown", this.keyHandlerBound)
document.addEventListener("keyup", this.keyHandlerBound)
}
hide() {
this.popup.classList.remove("active")
document.removeEventListener("keydown", this.keyHandlerBound)
document.removeEventListener("keyup", this.keyHandlerBound)
}
setSize(width, height) {
if (width == this.width && height == this.height) {
return
}
if (width > height) {
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
var multiplier = max_size / width
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
else {
var max_size = Math.min(parseInt(window.innerHeight * 0.9), height, 768)
var multiplier = max_size / height
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
this.width = parseInt(width)
this.height = parseInt(height)
this.container.style.width = width + "px"
this.container.style.height = height + "px"
Object.values(this.layers).forEach(layer => {
layer.canvas.width = width
layer.canvas.height = height
})
if (this.inpainter) {
this.saveImage() // We've reset the size of the image so inpainting is different
}
this.setBrush()
this.history.clear()
}
get tool() {
var tool_id = this.getOptionValue("tool")
return IMAGE_EDITOR_TOOLS.find(t => t.id == tool_id);
}
loadTool() {
this.drawing = false
this.container.style.cursor = this.tool.cursor;
}
setImage(url, width, height) {
this.setSize(width, height)
this.layers.background.ctx.clearRect(0, 0, this.width, this.height)
if (!(url && this.inpainter)) {
this.layers.drawing.ctx.clearRect(0, 0, this.width, this.height)
}
if (url) {
var image = new Image()
image.onload = () => {
this.layers.background.ctx.drawImage(image, 0, 0, this.width, this.height)
}
image.src = url
}
else {
this.layers.background.ctx.fillStyle = "#ffffff"
this.layers.background.ctx.beginPath()
this.layers.background.ctx.rect(0, 0, this.width, this.height)
this.layers.background.ctx.fill()
}
this.history.clear()
}
saveImage() {
if (!this.inpainter) {
// This is not an inpainter, so save the image as the new img2img input
this.layers.background.ctx.drawImage(this.layers.drawing.canvas, 0, 0, this.width, this.height)
var base64 = this.layers.background.canvas.toDataURL()
initImagePreview.src = base64 // this will trigger the rest of the app to use it
}
else {
// This is an inpainter, so make sure the toggle is set accordingly
var is_blank = !this.layers.drawing.ctx
.getImageData(0, 0, this.width, this.height).data
.some(channel => channel !== 0)
maskSetting.checked = !is_blank
}
this.hide()
}
getImg() { // a drop-in replacement of the drawingboard version
return this.layers.drawing.canvas.toDataURL()
}
setImg(dataUrl) { // a drop-in replacement of the drawingboard version
var image = new Image()
image.onload = () => {
var ctx = this.layers.drawing.ctx;
ctx.clearRect(0, 0, this.width, this.height)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1
ctx.filter = "none"
ctx.drawImage(image, 0, 0, this.width, this.height)
this.setBrush(this.layers.drawing)
}
image.src = dataUrl
}
runAction(action_id) {
var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == action_id)
if (action.trackHistory) {
this.history.pushAction(action_id)
}
action.handler(this)
}
setBrush(layer = null, options = null) {
if (options == null) {
options = this.options
}
if (layer) {
layer.ctx.lineCap = "round"
layer.ctx.lineJoin = "round"
layer.ctx.lineWidth = options.brush_size
layer.ctx.fillStyle = options.color
layer.ctx.strokeStyle = options.color
var sharpness = parseInt(options.sharpness * options.brush_size)
layer.ctx.filter = sharpness == 0 ? `none` : `blur(${sharpness}px)`
layer.ctx.globalAlpha = (1 - options.opacity)
layer.ctx.globalCompositeOperation = "source-over"
var tool = IMAGE_EDITOR_TOOLS.find(t => t.id == options.tool)
if (tool && tool.setBrush) {
tool.setBrush(editor, layer)
}
}
else {
Object.values([ "drawing", "overlay" ]).map(name => this.layers[name]).forEach(l => {
this.setBrush(l)
})
}
}
get ctx_overlay() {
return this.layers.overlay.ctx
}
get ctx_current() { // the idea is this will help support having custom layers and editing each one
return this.layers.drawing.ctx
}
get canvas_current() {
return this.layers.drawing.canvas
}
keyHandler(event) { // handles keybinds like ctrl+z, ctrl+y
if (!this.popup.classList.contains("active")) {
document.removeEventListener("keydown", this.keyHandlerBound)
document.removeEventListener("keyup", this.keyHandlerBound)
return // this catches if something else closes the window but doesnt properly unbind the key handler
}
// keybindings
if (event.type == "keydown") {
if ((event.key == "z" || event.key == "Z") && event.ctrlKey) {
if (!event.shiftKey) {
this.history.undo()
}
else {
this.history.redo()
}
}
if (event.key == "y" && event.ctrlKey) {
this.history.redo()
}
if (event.key === "Escape") {
this.hide()
}
}
// dropper ctrl holding handler stuff
var dropper_active = this.temp_previous_tool != null;
if (dropper_active && !event.ctrlKey) {
this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == this.temp_previous_tool))
this.temp_previous_tool = null
}
else if (!dropper_active && event.ctrlKey) {
this.temp_previous_tool = this.getOptionValue("tool")
this.selectOption("tool", IMAGE_EDITOR_TOOLS.findIndex(t => t.id == "colorpicker"))
}
}
mouseHandler(event) {
var bbox = this.layers.overlay.canvas.getBoundingClientRect()
var x = (event.clientX || 0) - bbox.left
var y = (event.clientY || 0) - bbox.top
var type = event.type;
var touchmap = {
touchstart: "mousedown",
touchmove: "mousemove",
touchend: "mouseup",
touchcancel: "mouseup"
}
if (type in touchmap) {
type = touchmap[type]
if (event.touches && event.touches[0]) {
var touch = event.touches[0]
var x = (touch.clientX || 0) - bbox.left
var y = (touch.clientY || 0) - bbox.top
}
}
event.preventDefault()
// do drawing-related stuff
if (type == "mousedown" || (type == "mouseenter" && event.buttons == 1)) {
this.drawing = true
this.tool.begin(this, this.ctx_current, x, y)
this.tool.begin(this, this.ctx_overlay, x, y, true)
this.history.editBegin(x, y)
}
if (type == "mouseup" || type == "mousemove") {
if (this.drawing) {
if (x > 0 && y > 0) {
this.tool.move(this, this.ctx_current, x, y)
this.tool.move(this, this.ctx_overlay, x, y, true)
this.history.editMove(x, y)
}
}
}
if (type == "mouseup" || type == "mouseout") {
if (this.drawing) {
this.drawing = false
this.tool.end(this, this.ctx_current, x, y)
this.tool.end(this, this.ctx_overlay, x, y, true)
this.history.editEnd(x, y)
}
}
}
getOptionValue(section_name) {
var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name)
return this.options && section_name in this.options ? this.options[section_name] : section.default
}
selectOption(section_name, option_index) {
var section = IMAGE_EDITOR_SECTIONS.find(s => s.name == section_name)
var value = section.options[option_index]
this.options[section_name] = value == "custom" ? section.getCustom(this) : value
this.optionElements[section_name].forEach(element => element.classList.remove("active"))
this.optionElements[section_name][option_index].classList.add("active")
// change the editor
this.setBrush()
if (section.name == "tool") {
this.loadTool()
}
}
}
const imageEditor = new ImageEditor(document.getElementById("image-editor"))
const imageInpainter = new ImageEditor(document.getElementById("image-inpainter"), true)
imageEditor.setImage(null, 512, 512)
imageInpainter.setImage(null, 512, 512)
document.getElementById("init_image_button_draw").addEventListener("click", () => {
imageEditor.show()
})
document.getElementById("init_image_button_inpaint").addEventListener("click", () => {
imageInpainter.show()
})
img2imgUnload() // no init image when the app starts
function rgbToHex(rgb) {
function componentToHex(c) {
var hex = parseInt(c).toString(16)
return hex.length == 1 ? "0" + hex : hex
}
return "#" + componentToHex(rgb.r) + componentToHex(rgb.g) + componentToHex(rgb.b)
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
function pixelCompare(int1, int2) {
return Math.abs(int1 - int2) < 4
}
// adapted from https://ben.akrin.com/canvas_fill/fill_04.html
function flood_fill(editor, the_canvas_context, x, y, color) {
pixel_stack = [{x:x, y:y}] ;
pixels = the_canvas_context.getImageData( 0, 0, editor.width, editor.height ) ;
var linear_cords = ( y * editor.width + x ) * 4 ;
var original_color = {r:pixels.data[linear_cords],
g:pixels.data[linear_cords+1],
b:pixels.data[linear_cords+2],
a:pixels.data[linear_cords+3]} ;
var opacity = color.a / 255;
var new_color = {
r: parseInt((color.r * opacity) + (original_color.r * (1 - opacity))),
g: parseInt((color.g * opacity) + (original_color.g * (1 - opacity))),
b: parseInt((color.b * opacity) + (original_color.b * (1 - opacity)))
}
if ((pixelCompare(new_color.r, original_color.r) &&
pixelCompare(new_color.g, original_color.g) &&
pixelCompare(new_color.b, original_color.b)))
{
return; // This color is already the color we want, so do nothing
}
var max_stack_size = editor.width * editor.height;
while( pixel_stack.length > 0 && pixel_stack.length < max_stack_size ) {
new_pixel = pixel_stack.shift() ;
x = new_pixel.x ;
y = new_pixel.y ;
linear_cords = ( y * editor.width + x ) * 4 ;
while( y-->=0 &&
(pixelCompare(pixels.data[linear_cords], original_color.r) &&
pixelCompare(pixels.data[linear_cords+1], original_color.g) &&
pixelCompare(pixels.data[linear_cords+2], original_color.b))) {
linear_cords -= editor.width * 4 ;
}
linear_cords += editor.width * 4 ;
y++ ;
var reached_left = false ;
var reached_right = false ;
while( y++<editor.height &&
(pixelCompare(pixels.data[linear_cords], original_color.r) &&
pixelCompare(pixels.data[linear_cords+1], original_color.g) &&
pixelCompare(pixels.data[linear_cords+2], original_color.b))) {
pixels.data[linear_cords] = new_color.r ;
pixels.data[linear_cords+1] = new_color.g ;
pixels.data[linear_cords+2] = new_color.b ;
pixels.data[linear_cords+3] = 255 ;
if( x>0 ) {
if( pixelCompare(pixels.data[linear_cords-4], original_color.r) &&
pixelCompare(pixels.data[linear_cords-4+1], original_color.g) &&
pixelCompare(pixels.data[linear_cords-4+2], original_color.b)) {
if( !reached_left ) {
pixel_stack.push( {x:x-1, y:y} ) ;
reached_left = true ;
}
} else if( reached_left ) {
reached_left = false ;
}
}
if( x<editor.width-1 ) {
if( pixelCompare(pixels.data[linear_cords+4], original_color.r) &&
pixelCompare(pixels.data[linear_cords+4+1], original_color.g) &&
pixelCompare(pixels.data[linear_cords+4+2], original_color.b)) {
if( !reached_right ) {
pixel_stack.push( {x:x+1,y:y} ) ;
reached_right = true ;
}
} else if( reached_right ) {
reached_right = false ;
}
}
linear_cords += editor.width * 4 ;
}
}
the_canvas_context.putImageData( pixels, 0, 0 ) ;
}

View File

@ -16,7 +16,7 @@ const modifierThumbnailPath = 'media/modifier-thumbnails'
const activeCardClass = 'modifier-card-active'
const CUSTOM_MODIFIERS_KEY = "customModifiers"
function createModifierCard(name, previews) {
function createModifierCard(name, previews, removeBy) {
const modifierCard = document.createElement('div')
modifierCard.className = 'modifier-card'
modifierCard.innerHTML = `
@ -44,10 +44,10 @@ function createModifierCard(name, previews) {
}
const maxLabelLength = 30
const nameWithoutBy = name.replace('by ', '')
const cardLabel = removeBy ? name.replace('by ', '') : name
if(nameWithoutBy.length <= maxLabelLength) {
label.querySelector('p').innerText = nameWithoutBy
if(cardLabel.length <= maxLabelLength) {
label.querySelector('p').innerText = cardLabel
} else {
const tooltipText = document.createElement('span')
tooltipText.className = 'tooltip-text'
@ -56,13 +56,14 @@ function createModifierCard(name, previews) {
label.classList.add('tooltip')
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
}
function createModifierGroup(modifierGroup, initiallyExpanded) {
function createModifierGroup(modifierGroup, initiallyExpanded, removeBy) {
const title = modifierGroup.category
const modifiers = modifierGroup.modifiers
@ -75,25 +76,23 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
if (initiallyExpanded === true) {
titleEl.className += ' active'
modifiersEl.style.display = 'block'
}
modifiers.forEach(modObj => {
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') {
modifiersEl.appendChild(modifierCard)
const trimmedName = trimModifiers(modifierName)
modifierCard.addEventListener('click', () => {
if (activeTags.map(x => x.name).includes(modifierName)) {
if (activeTags.map(x => trimModifiers(x.name)).includes(trimmedName)) {
// remove modifier from active array
activeTags = activeTags.filter(x => x.name != modifierName)
modifierCard.classList.remove(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
activeTags = activeTags.filter(x => trimModifiers(x.name) != trimmedName)
toggleCardState(trimmedName, false)
} else {
// add modifier to active array
activeTags.push({
@ -102,13 +101,11 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
'originElement': modifierCard,
'previews': modifierPreviews
})
modifierCard.classList.add(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-'
toggleCardState(trimmedName, true)
}
refreshTagsList()
document.dispatchEvent(new Event('refreshImageModifiers'))
})
}
})
@ -118,6 +115,7 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
modifiersEl.appendChild(brk)
let e = document.createElement('div')
e.className = 'modifier-category'
e.appendChild(titleEl)
e.appendChild(modifiersEl)
@ -126,6 +124,10 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
return e
}
function trimModifiers(tag) {
return tag.replace(/^\(+|\)+$/g, '').replace(/^\[+|\]+$/g, '')
}
async function loadModifiers() {
try {
let res = await fetch('/get/modifiers')
@ -137,7 +139,7 @@ async function loadModifiers() {
res.reverse()
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)
@ -147,6 +149,84 @@ async function loadModifiers() {
}
loadCustomModifiers()
document.dispatchEvent(new Event('loadImageModifiers'))
}
function refreshModifiersState(newTags) {
// clear existing modifiers
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName // pick the full modifier name
if (activeTags.map(x => x.name).includes(modifierName)) {
modifierCard.classList.remove(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
}
})
activeTags = []
// set new modifiers
newTags.forEach(tag => {
let found = false
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(modifierCard => {
const modifierName = modifierCard.querySelector('.modifier-card-label p').dataset.fullName
const shortModifierName = modifierCard.querySelector('.modifier-card-label p').innerText
if (trimModifiers(tag) == trimModifiers(modifierName)) {
// 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
const imageModifierCard = modifierCard.cloneNode(true)
imageModifierCard.querySelector('.modifier-card-label p').innerText = shortModifierName
activeTags.push({
'name': modifierName,
'element': imageModifierCard,
'originElement': modifierCard
})
}
modifierCard.classList.add(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '-'
found = true
}
})
if (found == false) { // custom tag went missing, create one here
let modifierCard = createModifierCard(tag, undefined, false) // create a modifier card for the missing tag, no image
modifierCard.addEventListener('click', () => {
if (activeTags.map(x => x.name).includes(tag)) {
// remove modifier from active array
activeTags = activeTags.filter(x => x.name != tag)
modifierCard.classList.remove(activeCardClass)
modifierCard.querySelector('.modifier-card-image-overlay').innerText = '+'
}
refreshTagsList()
})
activeTags.push({
'name': tag,
'element': modifierCard,
'originElement': undefined // no origin element for missing tags
})
}
})
refreshTagsList()
}
function refreshInactiveTags(inactiveTags) {
// update inactive tags
if (inactiveTags !== undefined && inactiveTags.length > 0) {
activeTags.forEach (tag => {
if (inactiveTags.find(element => element === tag.name) !== undefined) {
tag.inactive = true
}
})
}
// update cards
let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay')
overlays.forEach (i => {
let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText
if (inactiveTags.find(element => element === modifierName) !== undefined) {
i.parentElement.classList.add('modifier-toggle-inactive')
}
})
}
function refreshTagsList() {
@ -166,15 +246,15 @@ function refreshTagsList() {
editorModifierTagsList.appendChild(tag.element)
tag.element.addEventListener('click', () => {
let idx = activeTags.indexOf(tag)
let idx = activeTags.findIndex(o => { return o.name === tag.name })
if (idx !== -1) {
activeTags[idx].originElement.classList.remove(activeCardClass)
activeTags[idx].originElement.querySelector('.modifier-card-image-overlay').innerText = '+'
toggleCardState(activeTags[idx].name, false)
activeTags.splice(idx, 1)
refreshTagsList()
}
document.dispatchEvent(new Event('refreshImageModifiers'))
})
})
@ -183,6 +263,23 @@ function refreshTagsList() {
editorModifierTagsList.appendChild(brk)
}
function toggleCardState(modifierName, makeActive) {
document.querySelector('#editor-modifiers').querySelectorAll('.modifier-card').forEach(card => {
const name = card.querySelector('.modifier-card-label').innerText
if ( trimModifiers(modifierName) == trimModifiers(name)
|| trimModifiers(modifierName) == 'by ' + trimModifiers(name)) {
if(makeActive) {
card.classList.add(activeCardClass)
card.querySelector('.modifier-card-image-overlay').innerText = '-'
}
else{
card.classList.remove(activeCardClass)
card.querySelector('.modifier-card-image-overlay').innerText = '+'
}
}
})
}
function changePreviewImages(val) {
const previewImages = document.querySelectorAll('.modifier-card-image-container img')
@ -245,16 +342,9 @@ function resizeModifierCards(val) {
modifierCardSizeSlider.onchange = () => resizeModifierCards(modifierCardSizeSlider.value)
previewImageField.onchange = () => changePreviewImages(previewImageField.value)
modifierSettingsBtn.addEventListener('click', function() {
modifierSettingsOverlay.style.display = 'block'
})
document.getElementById("modifier-settings-config-close-btn").addEventListener('click', () => {
modifierSettingsOverlay.style.display = 'none'
})
modifierSettingsOverlay.addEventListener('click', (event) => {
if (event.target.id == modifierSettingsOverlay.id) {
modifierSettingsOverlay.style.display = 'none'
}
modifierSettingsBtn.addEventListener('click', function(e) {
modifierSettingsOverlay.classList.add("active")
e.stopPropagation()
})
function saveCustomModifiers() {
@ -264,31 +354,7 @@ function saveCustomModifiers() {
}
function loadCustomModifiers() {
let customModifiers = localStorage.getItem(CUSTOM_MODIFIERS_KEY, '')
customModifiersTextBox.value = customModifiers
if (customModifiersGroupElement !== undefined) {
customModifiersGroupElement.remove()
}
if (customModifiers && customModifiers.trim() !== '') {
customModifiers = customModifiers.split('\n')
customModifiers = customModifiers.filter(m => m.trim() !== '')
customModifiers = customModifiers.map(function(m) {
return {
"modifier": m
}
})
let customGroup = {
'category': 'Custom Modifiers',
'modifiers': customModifiers
}
customModifiersGroupElement = createModifierGroup(customGroup, true)
createCollapsibles(customModifiersGroupElement)
}
PLUGINS['MODIFIERS_LOAD'].forEach(fn=>fn.loader.call())
}
customModifiersTextBox.addEventListener('change', saveCustomModifiers)

View File

@ -1,41 +0,0 @@
const INPAINTING_EDITOR_SIZE = 450
let inpaintingEditorContainer = document.querySelector('#inpaintingEditor')
let inpaintingEditor = new DrawingBoard.Board('inpaintingEditor', {
color: "#ffffff",
background: false,
size: 30,
webStorage: false,
controls: [{'DrawingMode': {'filler': false}}, 'Size', 'Navigation']
})
let inpaintingEditorCanvasBackground = document.querySelector('.drawing-board-canvas-wrapper')
function resizeInpaintingEditor(widthValue, heightValue) {
if (widthValue === heightValue) {
widthValue = INPAINTING_EDITOR_SIZE
heightValue = INPAINTING_EDITOR_SIZE
} else if (widthValue > heightValue) {
heightValue = (heightValue / widthValue) * INPAINTING_EDITOR_SIZE
widthValue = INPAINTING_EDITOR_SIZE
} else {
widthValue = (widthValue / heightValue) * INPAINTING_EDITOR_SIZE
heightValue = INPAINTING_EDITOR_SIZE
}
if (inpaintingEditor.opts.aspectRatio === (widthValue / heightValue).toFixed(3)) {
// Same ratio, don't reset the canvas.
return
}
inpaintingEditor.opts.aspectRatio = (widthValue / heightValue).toFixed(3)
inpaintingEditorContainer.style.width = widthValue + 'px'
inpaintingEditorContainer.style.height = heightValue + 'px'
inpaintingEditor.opts.enlargeYourContainer = true
inpaintingEditor.opts.size = inpaintingEditor.ctx.lineWidth
inpaintingEditor.resize()
inpaintingEditor.ctx.lineCap = "round"
inpaintingEditor.ctx.lineJoin = "round"
inpaintingEditor.ctx.lineWidth = inpaintingEditor.opts.size
inpaintingEditor.setColor(inpaintingEditor.opts.color)
}

10
ui/media/js/jquery-confirm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

6
ui/media/js/marked.min.js vendored Normal file

File diff suppressed because one or more lines are too long

479
ui/media/js/parameters.js Normal file
View File

@ -0,0 +1,479 @@
/**
* Enum of parameter types
* @readonly
* @enum {string}
*/
var ParameterType = {
checkbox: "checkbox",
select: "select",
select_multiple: "select_multiple",
slider: "slider",
custom: "custom",
};
/**
* JSDoc style
* @typedef {object} Parameter
* @property {string} id
* @property {ParameterType} type
* @property {string} label
* @property {?string} note
* @property {number|boolean|string} default
*/
/** @type {Array.<Parameter>} */
var PARAMETERS = [
{
id: "theme",
type: ParameterType.select,
label: "Theme",
default: "theme-default",
note: "customize the look and feel of the ui",
options: [ // Note: options expanded dynamically
{
value: "theme-default",
label: "Default"
}
],
icon: "fa-palette"
},
{
id: "save_to_disk",
type: ParameterType.checkbox,
label: "Auto-Save Images",
note: "automatically saves images to the specified location",
icon: "fa-download",
default: false,
},
{
id: "diskPath",
type: ParameterType.custom,
label: "Save Location",
render: (parameter) => {
return `<input id="${parameter.id}" name="${parameter.id}" size="30" disabled>`
}
},
{
id: "metadata_output_format",
type: ParameterType.select,
label: "Metadata format",
note: "will be saved to disk in this format",
default: "txt",
options: [
{
value: "none",
label: "none"
},
{
value: "txt",
label: "txt"
},
{
value: "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",
type: ParameterType.checkbox,
label: "Enable Sound",
note: "plays a sound on task completion",
icon: "fa-volume-low",
default: true,
},
{
id: "process_order_toggle",
type: ParameterType.checkbox,
label: "Process newest jobs first",
note: "reverse the normal processing order",
icon: "fa-arrow-down-short-wide",
default: false,
},
{
id: "ui_open_browser_on_start",
type: ParameterType.checkbox,
label: "Open browser on startup",
note: "starts the default browser on startup",
icon: "fa-window-restore",
default: true,
},
{
id: "vram_usage_level",
type: ParameterType.select,
label: "GPU Memory Usage",
note: "Faster performance requires more GPU memory (VRAM)<br/><br/>" +
"<b>Balanced:</b> nearly as fast as High, much lower VRAM usage<br/>" +
"<b>High:</b> fastest, maximum GPU memory usage</br>" +
"<b>Low:</b> slowest, recommended for GPUs with 3 to 4 GB memory",
icon: "fa-forward",
default: "balanced",
options: [
{value: "balanced", label: "Balanced"},
{value: "high", label: "High"},
{value: "low", label: "Low"}
],
},
{
id: "use_cpu",
type: ParameterType.checkbox,
label: "Use CPU (not GPU)",
note: "warning: this will be *very* slow",
icon: "fa-microchip",
default: false,
},
{
id: "auto_pick_gpus",
type: ParameterType.checkbox,
label: "Automatically pick the GPUs (experimental)",
default: false,
},
{
id: "use_gpus",
type: ParameterType.select_multiple,
label: "GPUs to use (experimental)",
note: "to process in parallel",
default: false,
},
{
id: "auto_save_settings",
type: ParameterType.checkbox,
label: "Auto-Save Settings",
note: "restores settings on browser load",
icon: "fa-gear",
default: true,
},
{
id: "confirm_dangerous_actions",
type: ParameterType.checkbox,
label: "Confirm dangerous actions",
note: "Actions that might lead to data loss must either be clicked with the shift key pressed, or confirmed in an 'Are you sure?' dialog",
icon: "fa-check-double",
default: true,
},
{
id: "listen_to_network",
type: ParameterType.checkbox,
label: "Make Stable Diffusion available on your network",
note: "Other devices on your network can access this web page",
icon: "fa-network-wired",
default: true,
},
{
id: "listen_port",
type: ParameterType.custom,
label: "Network port",
note: "Port that this server listens to. The '9000' part in 'http://localhost:9000'",
icon: "fa-anchor",
render: (parameter) => {
return `<input id="${parameter.id}" name="${parameter.id}" size="6" value="9000" onkeypress="preventNonNumericalInput(event)">`
}
},
{
id: "use_beta_channel",
type: ParameterType.checkbox,
label: "Beta channel",
note: "Get the latest features immediately (but could be less stable). Please restart the program after changing this.",
icon: "fa-fire",
default: false,
},
];
function getParameterSettingsEntry(id) {
let parameter = PARAMETERS.filter(p => p.id === id)
if (parameter.length === 0) {
return
}
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) {
switch (parameter.type) {
case ParameterType.checkbox:
var is_checked = parameter.default ? " checked" : "";
return `<input id="${parameter.id}" name="${parameter.id}"${is_checked} type="checkbox">`
case ParameterType.select:
case ParameterType.select_multiple:
var options = (parameter.options || []).map(option => `<option value="${option.value}">${option.label}</option>`).join("")
var multiple = (parameter.type == ParameterType.select_multiple ? 'multiple' : '')
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:
return parameter.render(parameter)
default:
console.error(`Invalid type for parameter ${parameter.id}`);
return "ERROR: Invalid Type"
}
}
let parametersTable = document.querySelector("#system-settings .parameters-table")
/* fill in the system settings popup table */
function initParameters() {
PARAMETERS.forEach(parameter => {
var element = getParameterElement(parameter)
var note = parameter.note ? `<small>${parameter.note}</small>` : "";
var icon = parameter.icon ? `<i class="fa ${parameter.icon}"></i>` : "";
var newrow = document.createElement('div')
newrow.innerHTML = `
<div>${icon}</div>
<div><label for="${parameter.id}">${parameter.label}</label>${note}</div>
<div>${element}</div>`
parametersTable.appendChild(newrow)
parameter.settingsEntry = newrow
})
}
initParameters()
let vramUsageLevelField = document.querySelector('#vram_usage_level')
let useCPUField = document.querySelector('#use_cpu')
let autoPickGPUsField = document.querySelector('#auto_pick_gpus')
let useGPUsField = document.querySelector('#use_gpus')
let saveToDiskField = document.querySelector('#save_to_disk')
let diskPathField = document.querySelector('#diskPath')
let metadataOutputFormatField = document.querySelector('#metadata_output_format')
let listenToNetworkField = document.querySelector("#listen_to_network")
let listenPortField = document.querySelector("#listen_port")
let useBetaChannelField = document.querySelector("#use_beta_channel")
let uiOpenBrowserOnStartField = document.querySelector("#ui_open_browser_on_start")
let confirmDangerousActionsField = document.querySelector("#confirm_dangerous_actions")
let saveSettingsBtn = document.querySelector('#save-system-settings-btn')
async function changeAppConfig(configDelta) {
try {
let res = await fetch('/app_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(configDelta)
})
res = await res.json()
console.log('set config status response', res)
} catch (e) {
console.log('set config status error', e)
}
}
async function getAppConfig() {
try {
let res = await fetch('/get/app_config')
const config = await res.json()
if (config.update_branch === 'beta') {
useBetaChannelField.checked = true
document.querySelector("#updateBranchLabel").innerText = "(beta)"
}
if (config.ui && config.ui.open_browser_on_start === false) {
uiOpenBrowserOnStartField.checked = false
}
if (config.net && config.net.listen_to_network === false) {
listenToNetworkField.checked = false
}
if (config.net && config.net.listen_port !== undefined) {
listenPortField.value = config.net.listen_port
}
console.log('get config status response', config)
} catch (e) {
console.log('get config status error', e)
}
}
saveToDiskField.addEventListener('change', function(e) {
diskPathField.disabled = !this.checked
metadataOutputFormatField.disabled = !this.checked
})
function getCurrentRenderDeviceSelection() {
let selectedGPUs = $('#use_gpus').val()
if (useCPUField.checked && !autoPickGPUsField.checked) {
return 'cpu'
}
if (autoPickGPUsField.checked || selectedGPUs.length == 0) {
return 'auto'
}
return selectedGPUs.join(',')
}
useCPUField.addEventListener('click', function() {
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus')
if (this.checked) {
gpuSettingEntry.style.display = 'none'
autoPickGPUSettingEntry.style.display = 'none'
autoPickGPUsField.setAttribute('data-old-value', autoPickGPUsField.checked)
autoPickGPUsField.checked = false
} else if (useGPUsField.options.length >= MIN_GPUS_TO_SHOW_SELECTION) {
gpuSettingEntry.style.display = ''
autoPickGPUSettingEntry.style.display = ''
let oldVal = autoPickGPUsField.getAttribute('data-old-value')
if (oldVal === null || oldVal === undefined) { // the UI started with CPU selected by default
autoPickGPUsField.checked = true
} else {
autoPickGPUsField.checked = (oldVal === 'true')
}
gpuSettingEntry.style.display = (autoPickGPUsField.checked ? 'none' : '')
}
})
useGPUsField.addEventListener('click', function() {
let selectedGPUs = $('#use_gpus').val()
autoPickGPUsField.checked = (selectedGPUs.length === 0)
})
autoPickGPUsField.addEventListener('click', function() {
if (this.checked) {
$('#use_gpus').val([])
}
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
gpuSettingEntry.style.display = (this.checked ? 'none' : '')
})
async function setDiskPath(defaultDiskPath, force=false) {
var diskPath = getSetting("diskPath")
if (force || diskPath == '' || diskPath == undefined || diskPath == "undefined") {
setSetting("diskPath", defaultDiskPath)
}
}
function setDeviceInfo(devices) {
let cpu = devices.all.cpu.name
let allGPUs = Object.keys(devices.all).filter(d => d != 'cpu')
let activeGPUs = Object.keys(devices.active)
function ID_TO_TEXT(d) {
let info = devices.all[d]
if ("mem_free" in info && "mem_total" in info) {
return `${info.name} <small>(${d}) (${info.mem_free.toFixed(1)}Gb free / ${info.mem_total.toFixed(1)} Gb total)</small>`
} else {
return `${info.name} <small>(${d}) (no memory info)</small>`
}
}
allGPUs = allGPUs.map(ID_TO_TEXT)
activeGPUs = activeGPUs.map(ID_TO_TEXT)
let systemInfoEl = document.querySelector('#system-info')
systemInfoEl.querySelector('#system-info-cpu').innerText = cpu
systemInfoEl.querySelector('#system-info-gpus-all').innerHTML = allGPUs.join('</br>')
systemInfoEl.querySelector('#system-info-rendering-devices').innerHTML = activeGPUs.join('</br>')
}
function setHostInfo(hosts) {
let port = listenPortField.value
hosts = hosts.map(addr => `http://${addr}:${port}/`).map(url => `<div><a href="${url}">${url}</a></div>`)
document.querySelector('#system-info-server-hosts').innerHTML = hosts.join('')
}
async function getSystemInfo() {
try {
const res = await SD.getSystemInfo()
let devices = res['devices']
let allDeviceIds = Object.keys(devices['all']).filter(d => d !== 'cpu')
let activeDeviceIds = Object.keys(devices['active']).filter(d => d !== 'cpu')
if (activeDeviceIds.length === 0) {
useCPUField.checked = true
}
if (allDeviceIds.length < MIN_GPUS_TO_SHOW_SELECTION || useCPUField.checked) {
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
gpuSettingEntry.style.display = 'none'
let autoPickGPUSettingEntry = getParameterSettingsEntry('auto_pick_gpus')
autoPickGPUSettingEntry.style.display = 'none'
}
if (allDeviceIds.length === 0) {
useCPUField.checked = true
useCPUField.disabled = true // no compatible GPUs, so make the CPU mandatory
}
autoPickGPUsField.checked = (devices['config'] === 'auto')
useGPUsField.innerHTML = ''
allDeviceIds.forEach(device => {
let deviceName = devices['all'][device]['name']
let deviceOption = `<option value="${device}">${deviceName} (${device})</option>`
useGPUsField.insertAdjacentHTML('beforeend', deviceOption)
})
if (autoPickGPUsField.checked) {
let gpuSettingEntry = getParameterSettingsEntry('use_gpus')
gpuSettingEntry.style.display = 'none'
} else {
$('#use_gpus').val(activeDeviceIds)
}
setDeviceInfo(devices)
setHostInfo(res['hosts'])
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) {
console.log('error fetching devices', e)
}
}
saveSettingsBtn.addEventListener('click', function() {
if (listenPortField.value == '') {
alert('The network port field must not be empty.')
return
}
if (listenPortField.value < 1 || listenPortField.value > 65535) {
alert('The network port must be a number from 1 to 65535')
return
}
let updateBranch = (useBetaChannelField.checked ? 'beta' : 'main')
changeAppConfig({
'render_devices': getCurrentRenderDeviceSelection(),
'update_branch': updateBranch,
'ui_open_browser_on_start': uiOpenBrowserOnStartField.checked,
'listen_to_network': listenToNetworkField.checked,
'listen_port': listenPortField.value
})
saveSettingsBtn.classList.add('active')
asyncDelay(300).then(() => saveSettingsBtn.classList.remove('active'))
})

View File

@ -24,23 +24,50 @@ const PLUGINS = {
* }
* })
*/
IMAGE_INFO_BUTTONS: []
IMAGE_INFO_BUTTONS: [],
GET_PROMPTS_HOOK: [],
MODIFIERS_LOAD: [],
TASK_CREATE: [],
OUTPUTS_FORMATS: new ServiceContainer(
function png() { 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) {
const service = ServiceContainer.prototype.register.apply(this, args)
if (typeof outputFormatField !== 'undefined') {
const newOption = document.createElement("option")
newOption.setAttribute("value", service.name)
newOption.innerText = service.name
outputFormatField.appendChild(newOption)
}
return service
}
function loadScript(url) {
const script = document.createElement('script')
const promiseSrc = new PromiseSource()
script.addEventListener('error', () => promiseSrc.reject(new Error(`Script "${url}" couldn't be loaded.`)))
script.addEventListener('load', () => promiseSrc.resolve(url))
script.src = url + '?t=' + Date.now()
console.log('loading script', url)
document.head.appendChild(script)
return promiseSrc.promise
}
async function loadUIPlugins() {
try {
let res = await fetch('/get/ui_plugins')
if (res.status === 200) {
res = await res.json()
res.forEach(pluginPath => {
let script = document.createElement('script')
script.src = pluginPath + '?t=' + Date.now()
console.log('loading plugin', pluginPath)
document.head.appendChild(script)
})
const res = await fetch('/get/ui_plugins')
if (!res.ok) {
console.error(`Error HTTP${res.status} while loading plugins list. - ${res.statusText}`)
return
}
const plugins = await res.json()
const loadingPromises = plugins.map(loadScript)
return await Promise.allSettled(loadingPromises)
} catch (e) {
console.log('error fetching plugin paths', e)
}

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

@ -13,8 +13,15 @@ function initTheme() {
.filter(sheet => sheet.href?.startsWith(window.location.origin))
.flatMap(sheet => Array.from(sheet.cssRules))
.forEach(rule => {
var selector = rule.selectorText; // TODO: also do selector == ":root", re-run un-set props
if (selector && selector.startsWith(".theme-")) {
var selector = rule.selectorText;
if (selector && selector.startsWith(".theme-") && !selector.includes(" ")) {
if (DEFAULT_THEME) { // re-add props that dont change (css needs this so they update correctly)
Array.from(DEFAULT_THEME.rule.style)
.filter(cssVariable => !Array.from(rule.style).includes(cssVariable))
.forEach(cssVariable => {
rule.style.setProperty(cssVariable, DEFAULT_THEME.rule.style.getPropertyValue(cssVariable));
});
}
var theme_key = selector.substring(1);
THEMES.push({
key: theme_key,
@ -60,14 +67,16 @@ function themeFieldChanged() {
body.style = "";
var theme = THEMES.find(t => t.key == theme_key);
let borderColor = undefined
if (theme) {
// refresh variables incase they are back referencing
Array.from(DEFAULT_THEME.rule.style)
.filter(cssVariable => !Array.from(theme.rule.style).includes(cssVariable))
.forEach(cssVariable => {
body.style.setProperty(cssVariable, DEFAULT_THEME.rule.style.getPropertyValue(cssVariable));
});
borderColor = theme.rule.style.getPropertyValue('--input-border-color').trim()
if (!borderColor.startsWith('#')) {
borderColor = theme.rule.style.getPropertyValue('--theme-color-fallback')
}
} else {
borderColor = DEFAULT_THEME.rule.style.getPropertyValue('--theme-color-fallback')
}
document.querySelector('meta[name="theme-color"]').setAttribute("content", borderColor)
}
themeField.addEventListener('change', themeFieldChanged);

View File

@ -1,41 +1,51 @@
"use strict";
// https://gomakethings.com/finding-the-next-and-previous-sibling-elements-that-match-a-selector-with-vanilla-js/
function getNextSibling(elem, selector) {
// Get the next sibling element
var sibling = elem.nextElementSibling
// Get the next sibling element
let sibling = elem.nextElementSibling
// If there's no selector, return the first sibling
if (!selector) return sibling
// If there's no selector, return the first sibling
if (!selector) {
return sibling
}
// If the sibling matches our selector, use it
// If not, jump to the next sibling and continue the loop
while (sibling) {
if (sibling.matches(selector)) return sibling
sibling = sibling.nextElementSibling
}
// If the sibling matches our selector, use it
// If not, jump to the next sibling and continue the loop
while (sibling) {
if (sibling.matches(selector)) {
return sibling
}
sibling = sibling.nextElementSibling
}
}
/* Panel Stuff */
// true = open
var COLLAPSIBLES_INITIALIZED = false;
let COLLAPSIBLES_INITIALIZED = false;
const COLLAPSIBLES_KEY = "collapsibles";
const COLLAPSIBLE_PANELS = []; // filled in by createCollapsibles with all the elements matching .collapsible
// on-init call this for any panels that are marked open
function toggleCollapsible(element) {
var collapsibleHeader = element.querySelector(".collapsible");
var handle = element.querySelector(".collapsible-handle");
const collapsibleHeader = element.querySelector(".collapsible");
const handle = element.querySelector(".collapsible-handle");
collapsibleHeader.classList.toggle("active")
let content = getNextSibling(collapsibleHeader, '.collapsible-content')
if (content.style.display === "block") {
if (!collapsibleHeader.classList.contains("active")) {
content.style.display = "none"
handle.innerHTML = '&#x2795;' // plus
if (handle != null) { // render results don't have a handle
handle.innerHTML = '&#x2795;' // plus
}
} else {
content.style.display = "block"
handle.innerHTML = '&#x2796;' // minus
if (handle != null) { // render results don't have a handle
handle.innerHTML = '&#x2796;' // minus
}
}
document.dispatchEvent(new CustomEvent('collapsibleClick', { detail: collapsibleHeader }))
if (COLLAPSIBLES_INITIALIZED && COLLAPSIBLE_PANELS.includes(element)) {
saveCollapsibles()
@ -43,16 +53,16 @@ function toggleCollapsible(element) {
}
function saveCollapsibles() {
var values = {}
let values = {}
COLLAPSIBLE_PANELS.forEach(element => {
var value = element.querySelector(".collapsible").className.indexOf("active") !== -1
let value = element.querySelector(".collapsible").className.indexOf("active") !== -1
values[element.id] = value
})
localStorage.setItem(COLLAPSIBLES_KEY, JSON.stringify(values))
}
function createCollapsibles(node) {
var save = false
let save = false
if (!node) {
node = document
save = true
@ -65,7 +75,7 @@ function createCollapsibles(node) {
let handle = document.createElement('span')
handle.className = 'collapsible-handle'
if (c.className.indexOf('active') !== -1) {
if (c.classList.contains("active")) {
handle.innerHTML = '&#x2796;' // minus
} else {
handle.innerHTML = '&#x2795;' // plus
@ -77,7 +87,7 @@ function createCollapsibles(node) {
})
})
if (save) {
var saved = localStorage.getItem(COLLAPSIBLES_KEY)
let saved = localStorage.getItem(COLLAPSIBLES_KEY)
if (!saved) {
saved = tryLoadOldCollapsibles();
}
@ -85,9 +95,9 @@ function createCollapsibles(node) {
saveCollapsibles()
saved = localStorage.getItem(COLLAPSIBLES_KEY)
}
var values = JSON.parse(saved)
let values = JSON.parse(saved)
COLLAPSIBLE_PANELS.forEach(element => {
var value = element.querySelector(".collapsible").className.indexOf("active") !== -1
let value = element.querySelector(".collapsible").className.indexOf("active") !== -1
if (values[element.id] != value) {
toggleCollapsible(element)
}
@ -97,17 +107,17 @@ function createCollapsibles(node) {
}
function tryLoadOldCollapsibles() {
var old_map = {
const old_map = {
"advancedPanelOpen": "editor-settings",
"modifiersPanelOpen": "editor-modifiers",
"negativePromptPanelOpen": "editor-inputs-prompt"
};
if (localStorage.getItem(Object.keys(old_map)[0])) {
var result = {};
let result = {};
Object.keys(old_map).forEach(key => {
var value = localStorage.getItem(key);
const value = localStorage.getItem(key);
if (value !== null) {
result[old_map[key]] = value == true || value == "true"
result[old_map[key]] = (value == true || value == "true")
localStorage.removeItem(key)
}
});
@ -146,17 +156,17 @@ function millisecondsToStr(milliseconds) {
return (number > 1) ? 's' : ''
}
var temp = Math.floor(milliseconds / 1000)
var hours = Math.floor((temp %= 86400) / 3600)
var s = ''
let temp = Math.floor(milliseconds / 1000)
let hours = Math.floor((temp %= 86400) / 3600)
let s = ''
if (hours) {
s += hours + ' hour' + numberEnding(hours) + ' '
}
var minutes = Math.floor((temp %= 3600) / 60)
let minutes = Math.floor((temp %= 3600) / 60)
if (minutes) {
s += minutes + ' minute' + numberEnding(minutes) + ' '
}
var seconds = temp % 60
let seconds = temp % 60
if (!hours && minutes < 4 && seconds) {
s += seconds + ' second' + numberEnding(seconds)
}
@ -174,7 +184,7 @@ function BraceExpander() {
function bracePair(tkns, iPosn, iNest, lstCommas) {
if (iPosn >= tkns.length || iPosn < 0) return null;
var t = tkns[iPosn],
let t = tkns[iPosn],
n = (t === '{') ? (
iNest + 1
) : (t === '}' ? (
@ -194,7 +204,7 @@ function BraceExpander() {
function andTree(dctSofar, tkns) {
if (!tkns.length) return [dctSofar, []];
var dctParse = dctSofar ? dctSofar : {
let dctParse = dctSofar ? dctSofar : {
fn: and,
args: []
},
@ -227,14 +237,14 @@ function BraceExpander() {
// Parse of a PARADIGM subtree
function orTree(dctSofar, tkns, lstCommas) {
if (!tkns.length) return [dctSofar, []];
var iLast = lstCommas.length;
let iLast = lstCommas.length;
return {
fn: or,
args: splitsAt(
lstCommas, tkns
).map(function (x, i) {
var ts = x.slice(
let ts = x.slice(
1, i === iLast ? (
-1
) : void 0
@ -252,7 +262,7 @@ function BraceExpander() {
// List of unescaped braces and commas, and remaining strings
function tokens(str) {
// Filter function excludes empty splitting artefacts
var toS = function (x) {
let toS = function (x) {
return x.toString();
};
@ -266,7 +276,7 @@ function BraceExpander() {
// PARSE TREE OPERATOR (1 of 2)
// Each possible head * each possible tail
function and(args) {
var lng = args.length,
let lng = args.length,
head = lng ? args[0] : null,
lstHead = "string" === typeof head ? (
[head]
@ -326,7 +336,7 @@ function BraceExpander() {
// s -> [s]
this.expand = function(s) {
// BRACE EXPRESSION PARSED
var dctParse = andTree(null, tokens(s))[0];
let dctParse = andTree(null, tokens(s))[0];
// ABSTRACT SYNTAX TREE LOGGED
// console.log(pp(dctParse));
@ -337,12 +347,76 @@ function BraceExpander() {
}
/** Pause the execution of an async function until timer elapse.
* @Returns a promise that will resolve after the specified timeout.
*/
function asyncDelay(timeout) {
return new Promise(function(resolve, reject) {
setTimeout(resolve, timeout, true)
})
}
function PromiseSource() {
const srcPromise = new Promise((resolve, reject) => {
Object.defineProperties(this, {
resolve: { value: resolve, writable: false }
, reject: { value: reject, writable: false }
})
})
Object.defineProperties(this, {
promise: {value: makeQuerablePromise(srcPromise), writable: false}
})
}
/** A debounce is a higher-order function, which is a function that returns another function
* that, as long as it continues to be invoked, will not be triggered.
* The function will be called after it stops being called for N milliseconds.
* If `immediate` is passed, trigger the function on the leading edge, instead of the trailing.
* @Returns a promise that will resolve to func return value.
*/
function debounce (func, wait, immediate) {
if (typeof wait === "undefined") {
wait = 40
}
if (typeof wait !== "number") {
throw new Error("wait is not an number.")
}
let timeout = null
let lastPromiseSrc = new PromiseSource()
const applyFn = function(context, args) {
let result = undefined
try {
result = func.apply(context, args)
} catch (err) {
lastPromiseSrc.reject(err)
}
if (result instanceof Promise) {
result.then(lastPromiseSrc.resolve, lastPromiseSrc.reject)
} else {
lastPromiseSrc.resolve(result)
}
}
return function(...args) {
const callNow = Boolean(immediate && !timeout)
const context = this;
if (timeout) {
clearTimeout(timeout)
}
timeout = setTimeout(function () {
if (!immediate) {
applyFn(context, args)
}
lastPromiseSrc = new PromiseSource()
timeout = null
}, wait)
if (callNow) {
applyFn(context, args)
}
return lastPromiseSrc.promise
}
}
function preventNonNumericalInput(e) {
e = e || window.event;
let charCode = (typeof e.which == "undefined") ? e.keyCode : e.which;
@ -354,3 +428,252 @@ function preventNonNumericalInput(e) {
e.preventDefault();
}
}
/** Returns the global object for the current execution environement.
* @Returns window in a browser, global in node and self in a ServiceWorker.
* @Notes Allows unit testing and use of the engine outside of a browser.
*/
function getGlobal() {
if (typeof globalThis === 'object') {
return globalThis
} else if (typeof global === 'object') {
return global
} else if (typeof self === 'object') {
return self
}
try {
return Function('return this')()
} catch {
// If the Function constructor fails, we're in a browser with eval disabled by CSP headers.
return window
} // Returns undefined if global can't be found.
}
/** Check if x is an Array or a TypedArray.
* @Returns true if x is an Array or a TypedArray, false otherwise.
*/
function isArrayOrTypedArray(x) {
return Boolean(typeof x === 'object' && (Array.isArray(x) || (ArrayBuffer.isView(x) && !(x instanceof DataView))))
}
function makeQuerablePromise(promise) {
if (typeof promise !== 'object') {
throw new Error('promise is not an object.')
}
if (!(promise instanceof Promise)) {
throw new Error('Argument is not a promise.')
}
// Don't modify a promise that's been already modified.
if ('isResolved' in promise || 'isRejected' in promise || 'isPending' in promise) {
return promise
}
let isPending = true
let isRejected = false
let rejectReason = undefined
let isResolved = false
let resolvedValue = undefined
const qurPro = promise.then(
function(val){
isResolved = true
isPending = false
resolvedValue = val
return val
}
, function(reason) {
rejectReason = reason
isRejected = true
isPending = false
throw reason
}
)
Object.defineProperties(qurPro, {
'isResolved': {
get: () => isResolved
}
, 'resolvedValue': {
get: () => resolvedValue
}
, 'isPending': {
get: () => isPending
}
, 'isRejected': {
get: () => isRejected
}
, 'rejectReason': {
get: () => rejectReason
}
})
return qurPro
}
/* inserts custom html to allow prettifying of inputs */
function prettifyInputs(root_element) {
root_element.querySelectorAll(`input[type="checkbox"]`).forEach(element => {
if (element.style.display === "none") {
return
}
var parent = element.parentNode;
if (!parent.classList.contains("input-toggle")) {
var wrapper = document.createElement("div");
wrapper.classList.add("input-toggle");
parent.replaceChild(wrapper, element);
wrapper.appendChild(element);
var label = document.createElement("label");
label.htmlFor = element.id;
wrapper.appendChild(label);
}
})
}
class GenericEventSource {
#events = {};
#types = []
constructor(...eventsTypes) {
if (Array.isArray(eventsTypes) && eventsTypes.length === 1 && Array.isArray(eventsTypes[0])) {
eventsTypes = eventsTypes[0]
}
this.#types.push(...eventsTypes)
}
get eventTypes() {
return this.#types
}
/** Add a new event listener
*/
addEventListener(name, handler) {
if (!this.#types.includes(name)) {
throw new Error('Invalid event name.')
}
if (this.#events.hasOwnProperty(name)) {
this.#events[name].push(handler)
} else {
this.#events[name] = [handler]
}
}
/** Remove the event listener
*/
removeEventListener(name, handler) {
if (!this.#events.hasOwnProperty(name)) {
return
}
const index = this.#events[name].indexOf(handler)
if (index != -1) {
this.#events[name].splice(index, 1)
}
}
fireEvent(name, ...args) {
if (!this.#types.includes(name)) {
throw new Error(`Event ${String(name)} missing from Events.types`)
}
if (!this.#events.hasOwnProperty(name)) {
return Promise.resolve()
}
if (!args || !args.length) {
args = []
}
const evs = this.#events[name]
if (evs.length <= 0) {
return Promise.resolve()
}
return Promise.allSettled(evs.map((callback) => {
try {
return Promise.resolve(callback.apply(SD, args))
} catch (ex) {
return Promise.reject(ex)
}
}))
}
}
class ServiceContainer {
#services = new Map()
#singletons = new Map()
constructor(...servicesParams) {
servicesParams.forEach(this.register.bind(this))
}
get services () {
return this.#services
}
get singletons() {
return this.#singletons
}
register(params) {
if (ServiceContainer.isConstructor(params)) {
if (typeof params.name !== 'string') {
throw new Error('params.name is not a string.')
}
params = {name:params.name, definition:params}
}
if (typeof params !== 'object') {
throw new Error('params is not an object.')
}
[ 'name',
'definition',
].forEach((key) => {
if (!(key in params)) {
console.error('Invalid service %o registration.', params)
throw new Error(`params.${key} is not defined.`)
}
})
const opts = {definition: params.definition}
if ('dependencies' in params) {
if (Array.isArray(params.dependencies)) {
params.dependencies.forEach((dep) => {
if (typeof dep !== 'string') {
throw new Error('dependency name is not a string.')
}
})
opts.dependencies = params.dependencies
} else {
throw new Error('params.dependencies is not an array.')
}
}
if (params.singleton) {
opts.singleton = true
}
this.#services.set(params.name, opts)
return Object.assign({name: params.name}, opts)
}
get(name) {
const ctorInfos = this.#services.get(name)
if (!ctorInfos) {
return
}
if(!ServiceContainer.isConstructor(ctorInfos.definition)) {
return ctorInfos.definition
}
if(!ctorInfos.singleton) {
return this._createInstance(ctorInfos)
}
const singletonInstance = this.#singletons.get(name)
if(singletonInstance) {
return singletonInstance
}
const newSingletonInstance = this._createInstance(ctorInfos)
this.#singletons.set(name, newSingletonInstance)
return newSingletonInstance
}
_getResolvedDependencies(service) {
let classDependencies = []
if(service.dependencies) {
classDependencies = service.dependencies.map(this.get.bind(this))
}
return classDependencies
}
_createInstance(service) {
if (!ServiceContainer.isClass(service.definition)) {
// Call as normal function.
return service.definition(...this._getResolvedDependencies(service))
}
// Use new
return new service.definition(...this._getResolvedDependencies(service))
}
static isClass(definition) {
return typeof definition === 'function' && Boolean(definition.prototype) && definition.prototype.constructor === definition
}
static isConstructor(definition) {
return typeof definition === 'function'
}
}

View File

@ -0,0 +1,8 @@
{
"name": "Stable Diffusion UI",
"display": "standalone",
"display_override": [
"window-controls-overlay"
],
"theme_color": "#000000"
}

View File

@ -0,0 +1,28 @@
(function () {
"use strict"
let autoScroll = document.querySelector("#auto_scroll")
// observe for changes in the preview pane
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.target.className == 'img-batch') {
Autoscroll(mutation.target)
}
})
})
observer.observe(document.getElementById('preview'), {
childList: true,
subtree: true
})
function Autoscroll(target) {
if (autoScroll.checked && target !== null) {
const img = target.querySelector('img')
img.addEventListener('load', function() {
img.closest('.imageTaskContainer').scrollIntoView()
}, { once: true })
}
}
})()

View File

@ -0,0 +1,95 @@
(function () { "use strict"
if (typeof editorModifierTagsList !== 'object') {
console.error('editorModifierTagsList missing...')
return
}
const styleSheet = document.createElement("style");
styleSheet.textContent = `
.modifier-card-tiny.drag-sort-active {
background: transparent;
border: 2px dashed white;
opacity:0.2;
}
`;
document.head.appendChild(styleSheet);
// observe for changes in tag list
const observer = new MutationObserver(function (mutations) {
// mutations.forEach(function (mutation) {
if (editorModifierTagsList.childNodes.length > 0) {
ModifierDragAndDrop(editorModifierTagsList)
}
// })
})
observer.observe(editorModifierTagsList, {
childList: true
})
let current
function ModifierDragAndDrop(target) {
let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay')
overlays.forEach (i => {
i.parentElement.draggable = true;
i.parentElement.ondragstart = (e) => {
current = i
i.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].innerText = ''
i.parentElement.draggable = true
i.parentElement.classList.add('drag-sort-active')
for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) {
if (item.parentElement.parentElement.getElementsByClassName('modifier-card-overlay')[0] != current) {
item.parentElement.parentElement.getElementsByClassName('modifier-card-image-overlay')[0].style.opacity = 0
if(item.parentElement.getElementsByClassName('modifier-card-image').length > 0) {
item.parentElement.getElementsByClassName('modifier-card-image')[0].style.filter = 'none'
}
item.parentElement.parentElement.style.transform = 'none'
item.parentElement.parentElement.style.boxShadow = 'none'
}
item.innerText = ''
}
}
i.ondragenter = (e) => {
e.preventDefault()
if (i != current) {
let currentPos = 0, droppedPos = 0;
for (let it = 0; it < overlays.length; it++) {
if (current == overlays[it]) { currentPos = it; }
if (i == overlays[it]) { droppedPos = it; }
}
if (i.parentElement != current.parentElement) {
let currentPos = 0, droppedPos = 0
for (let it = 0; it < overlays.length; it++) {
if (current == overlays[it]) { currentPos = it }
if (i == overlays[it]) { droppedPos = it }
}
if (currentPos < droppedPos) {
current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement.nextSibling).getElementsByClassName('modifier-card-overlay')[0]
} else {
current = i.parentElement.parentNode.insertBefore(current.parentElement, i.parentElement).getElementsByClassName('modifier-card-overlay')[0]
}
// update activeTags
const tag = activeTags.splice(currentPos, 1)
activeTags.splice(droppedPos, 0, tag[0])
document.dispatchEvent(new Event('refreshImageModifiers'))
}
}
};
i.ondragover = (e) => {
e.preventDefault()
}
i.parentElement.ondragend = (e) => {
i.parentElement.classList.remove('drag-sort-active')
for(let item of document.querySelector('#editor-inputs-tags-list').getElementsByClassName('modifier-card-image-overlay')) {
item.style.opacity = ''
item.innerText = '-'
}
}
})
}
})()

View File

@ -0,0 +1,66 @@
(function () { "use strict"
if (typeof editorModifierTagsList !== 'object') {
console.error('editorModifierTagsList missing...')
return
}
// observe for changes in tag list
const observer = new MutationObserver(function (mutations) {
// mutations.forEach(function (mutation) {
if (editorModifierTagsList.childNodes.length > 0) {
ModifierMouseWheel(editorModifierTagsList)
}
// })
})
observer.observe(editorModifierTagsList, {
childList: true
})
function ModifierMouseWheel(target) {
let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay')
overlays.forEach (i => {
i.onwheel = (e) => {
if (e.ctrlKey == true) {
e.preventDefault()
const delta = Math.sign(event.deltaY)
let s = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText
if (delta < 0) {
// wheel scrolling up
if (s.substring(0, 1) == '[' && s.substring(s.length-1) == ']') {
s = s.substring(1, s.length - 1)
}
else
{
if (s.substring(0, 10) !== '('.repeat(10) && s.substring(s.length-10) !== ')'.repeat(10)) {
s = '(' + s + ')'
}
}
}
else{
// wheel scrolling down
if (s.substring(0, 1) == '(' && s.substring(s.length-1) == ')') {
s = s.substring(1, s.length - 1)
}
else
{
if (s.substring(0, 10) !== '['.repeat(10) && s.substring(s.length-10) !== ']'.repeat(10)) {
s = '[' + s + ']'
}
}
}
i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].innerText = s
// update activeTags
for (let it = 0; it < overlays.length; it++) {
if (i == overlays[it]) {
activeTags[it].name = s
break
}
}
document.dispatchEvent(new Event('refreshImageModifiers'))
}
}
})
}
})()

View File

@ -0,0 +1,3 @@
Custom plugins in this folder will be shipped to all the users by default.
This allows UI features to be built as plugins (testing our Plugins API, and keeping our core lean and modular).

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v4.5.0</title>
<link rel="shortcut icon" type="image/png" href="./jasmine/jasmine_favicon.png">
<link rel="stylesheet" href="./jasmine/jasmine.css">
<script src="./jasmine/jasmine.js"></script>
<script src="./jasmine/jasmine-html.js"></script>
<script src="./jasmine/boot0.js"></script>
<!-- optional: include a file here that configures the Jasmine env -->
<script src="./jasmine/boot1.js"></script>
<!-- include source files here... -->
<script src="/media/js/utils.js?v=4"></script>
<script src="/media/js/engine.js?v=1"></script>
<!-- <script src="./engine.js?v=1"></script> -->
<script src="/media/js/plugins.js?v=1"></script>
<!-- include spec files here... -->
<script src="./jasmineSpec.js"></script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,31 @@
(function() {
PLUGINS['MODIFIERS_LOAD'].push({
loader: function() {
let customModifiers = localStorage.getItem(CUSTOM_MODIFIERS_KEY, '')
customModifiersTextBox.value = customModifiers
if (customModifiersGroupElement !== undefined) {
customModifiersGroupElement.remove()
}
if (customModifiers && customModifiers.trim() !== '') {
customModifiers = customModifiers.split('\n')
customModifiers = customModifiers.filter(m => m.trim() !== '')
customModifiers = customModifiers.map(function(m) {
return {
"modifier": m
}
})
let customGroup = {
'category': 'Custom Modifiers',
'modifiers': customModifiers
}
customModifiersGroupElement = createModifierGroup(customGroup, true)
createCollapsibles(customModifiersGroupElement)
}
}
})
})()

View File

@ -0,0 +1,64 @@
/*
Copyright (c) 2008-2022 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
This file starts the process of "booting" Jasmine. It initializes Jasmine,
makes its globals available, and creates the env. This file should be loaded
after `jasmine.js` and `jasmine_html.js`, but before `boot1.js` or any project
source files or spec files are loaded.
*/
(function() {
const jasmineRequire = window.jasmineRequire || require('./jasmine.js');
/**
* ## Require &amp; Instantiate
*
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
*/
const jasmine = jasmineRequire.core(jasmineRequire),
global = jasmine.getGlobal();
global.jasmine = jasmine;
/**
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
*/
jasmineRequire.html(jasmine);
/**
* Create the Jasmine environment. This is used to run all specs in a project.
*/
const env = jasmine.getEnv();
/**
* ## The Global Interface
*
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
*/
const jasmineInterface = jasmineRequire.interface(jasmine, env);
/**
* Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
*/
for (const property in jasmineInterface) {
global[property] = jasmineInterface[property];
}
})();

View File

@ -0,0 +1,132 @@
/*
Copyright (c) 2008-2022 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/**
This file finishes 'booting' Jasmine, performing all of the necessary
initialization before executing the loaded environment and all of a project's
specs. This file should be loaded after `boot0.js` but before any project
source files or spec files are loaded. Thus this file can also be used to
customize Jasmine for a project.
If a project is using Jasmine via the standalone distribution, this file can
be customized directly. If you only wish to configure the Jasmine env, you
can load another file that calls `jasmine.getEnv().configure({...})`
after `boot0.js` is loaded and before this file is loaded.
*/
(function() {
const env = jasmine.getEnv();
/**
* ## Runner Parameters
*
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
*/
const queryString = new jasmine.QueryString({
getWindowLocation: function() {
return window.location;
}
});
const filterSpecs = !!queryString.getParam('spec');
const config = {
stopOnSpecFailure: queryString.getParam('stopOnSpecFailure'),
stopSpecOnExpectationFailure: queryString.getParam(
'stopSpecOnExpectationFailure'
),
hideDisabled: queryString.getParam('hideDisabled')
};
const random = queryString.getParam('random');
if (random !== undefined && random !== '') {
config.random = random;
}
const seed = queryString.getParam('seed');
if (seed) {
config.seed = seed;
}
/**
* ## Reporters
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
*/
const htmlReporter = new jasmine.HtmlReporter({
env: env,
navigateWithNewParam: function(key, value) {
return queryString.navigateWithNewParam(key, value);
},
addToExistingQueryString: function(key, value) {
return queryString.fullStringWithNewParam(key, value);
},
getContainer: function() {
return document.body;
},
createElement: function() {
return document.createElement.apply(document, arguments);
},
createTextNode: function() {
return document.createTextNode.apply(document, arguments);
},
timer: new jasmine.Timer(),
filterSpecs: filterSpecs
});
/**
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
*/
env.addReporter(jsApiReporter);
env.addReporter(htmlReporter);
/**
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
*/
const specFilter = new jasmine.HtmlSpecFilter({
filterString: function() {
return queryString.getParam('spec');
}
});
config.specFilter = function(spec) {
return specFilter.matches(spec.getFullName());
};
env.configure(config);
/**
* ## Execution
*
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
*/
const currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
env.execute();
};
})();

View File

@ -0,0 +1,964 @@
/*
Copyright (c) 2008-2022 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// eslint-disable-next-line no-var
var jasmineRequire = window.jasmineRequire || require('./jasmine.js');
jasmineRequire.html = function(j$) {
j$.ResultsNode = jasmineRequire.ResultsNode();
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.QueryString = jasmineRequire.QueryString();
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
};
jasmineRequire.HtmlReporter = function(j$) {
function ResultsStateBuilder() {
this.topResults = new j$.ResultsNode({}, '', null);
this.currentParent = this.topResults;
this.specsExecuted = 0;
this.failureCount = 0;
this.pendingSpecCount = 0;
}
ResultsStateBuilder.prototype.suiteStarted = function(result) {
this.currentParent.addChild(result, 'suite');
this.currentParent = this.currentParent.last();
};
ResultsStateBuilder.prototype.suiteDone = function(result) {
this.currentParent.updateResult(result);
if (this.currentParent !== this.topResults) {
this.currentParent = this.currentParent.parent;
}
if (result.status === 'failed') {
this.failureCount++;
}
};
ResultsStateBuilder.prototype.specStarted = function(result) {};
ResultsStateBuilder.prototype.specDone = function(result) {
this.currentParent.addChild(result, 'spec');
if (result.status !== 'excluded') {
this.specsExecuted++;
}
if (result.status === 'failed') {
this.failureCount++;
}
if (result.status == 'pending') {
this.pendingSpecCount++;
}
};
ResultsStateBuilder.prototype.jasmineDone = function(result) {
if (result.failedExpectations) {
this.failureCount += result.failedExpectations.length;
}
};
function HtmlReporter(options) {
function config() {
return (options.env && options.env.configuration()) || {};
}
const getContainer = options.getContainer;
const createElement = options.createElement;
const createTextNode = options.createTextNode;
const navigateWithNewParam = options.navigateWithNewParam || function() {};
const addToExistingQueryString =
options.addToExistingQueryString || defaultQueryString;
const filterSpecs = options.filterSpecs;
let htmlReporterMain;
let symbols;
const deprecationWarnings = [];
const failures = [];
this.initialize = function() {
clearPrior();
htmlReporterMain = createDom(
'div',
{ className: 'jasmine_html-reporter' },
createDom(
'div',
{ className: 'jasmine-banner' },
createDom('a', {
className: 'jasmine-title',
href: 'http://jasmine.github.io/',
target: '_blank'
}),
createDom('span', { className: 'jasmine-version' }, j$.version)
),
createDom('ul', { className: 'jasmine-symbol-summary' }),
createDom('div', { className: 'jasmine-alert' }),
createDom(
'div',
{ className: 'jasmine-results' },
createDom('div', { className: 'jasmine-failures' })
)
);
getContainer().appendChild(htmlReporterMain);
};
let totalSpecsDefined;
this.jasmineStarted = function(options) {
totalSpecsDefined = options.totalSpecsDefined || 0;
};
const summary = createDom('div', { className: 'jasmine-summary' });
const stateBuilder = new ResultsStateBuilder();
this.suiteStarted = function(result) {
stateBuilder.suiteStarted(result);
};
this.suiteDone = function(result) {
stateBuilder.suiteDone(result);
if (result.status === 'failed') {
failures.push(failureDom(result));
}
addDeprecationWarnings(result, 'suite');
};
this.specStarted = function(result) {
stateBuilder.specStarted(result);
};
this.specDone = function(result) {
stateBuilder.specDone(result);
if (noExpectations(result)) {
const noSpecMsg = "Spec '" + result.fullName + "' has no expectations.";
if (result.status === 'failed') {
console.error(noSpecMsg);
} else {
console.warn(noSpecMsg);
}
}
if (!symbols) {
symbols = find('.jasmine-symbol-summary');
}
symbols.appendChild(
createDom('li', {
className: this.displaySpecInCorrectFormat(result),
id: 'spec_' + result.id,
title: result.fullName
})
);
if (result.status === 'failed') {
failures.push(failureDom(result));
}
addDeprecationWarnings(result, 'spec');
};
this.displaySpecInCorrectFormat = function(result) {
return noExpectations(result) && result.status === 'passed'
? 'jasmine-empty'
: this.resultStatus(result.status);
};
this.resultStatus = function(status) {
if (status === 'excluded') {
return config().hideDisabled
? 'jasmine-excluded-no-display'
: 'jasmine-excluded';
}
return 'jasmine-' + status;
};
this.jasmineDone = function(doneResult) {
stateBuilder.jasmineDone(doneResult);
const banner = find('.jasmine-banner');
const alert = find('.jasmine-alert');
const order = doneResult && doneResult.order;
alert.appendChild(
createDom(
'span',
{ className: 'jasmine-duration' },
'finished in ' + doneResult.totalTime / 1000 + 's'
)
);
banner.appendChild(optionsMenu(config()));
if (stateBuilder.specsExecuted < totalSpecsDefined) {
const skippedMessage =
'Ran ' +
stateBuilder.specsExecuted +
' of ' +
totalSpecsDefined +
' specs - run all';
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
const skippedLink =
(window.location.pathname || '') +
addToExistingQueryString('spec', '');
alert.appendChild(
createDom(
'span',
{ className: 'jasmine-bar jasmine-skipped' },
createDom(
'a',
{ href: skippedLink, title: 'Run all specs' },
skippedMessage
)
)
);
}
let statusBarMessage = '';
let statusBarClassName = 'jasmine-overall-result jasmine-bar ';
const globalFailures =
(doneResult && doneResult.failedExpectations) || [];
const failed = stateBuilder.failureCount + globalFailures.length > 0;
if (totalSpecsDefined > 0 || failed) {
statusBarMessage +=
pluralize('spec', stateBuilder.specsExecuted) +
', ' +
pluralize('failure', stateBuilder.failureCount);
if (stateBuilder.pendingSpecCount) {
statusBarMessage +=
', ' + pluralize('pending spec', stateBuilder.pendingSpecCount);
}
}
if (doneResult.overallStatus === 'passed') {
statusBarClassName += ' jasmine-passed ';
} else if (doneResult.overallStatus === 'incomplete') {
statusBarClassName += ' jasmine-incomplete ';
statusBarMessage =
'Incomplete: ' +
doneResult.incompleteReason +
', ' +
statusBarMessage;
} else {
statusBarClassName += ' jasmine-failed ';
}
let seedBar;
if (order && order.random) {
seedBar = createDom(
'span',
{ className: 'jasmine-seed-bar' },
', randomized with seed ',
createDom(
'a',
{
title: 'randomized with seed ' + order.seed,
href: seedHref(order.seed)
},
order.seed
)
);
}
alert.appendChild(
createDom(
'span',
{ className: statusBarClassName },
statusBarMessage,
seedBar
)
);
const errorBarClassName = 'jasmine-bar jasmine-errored';
const afterAllMessagePrefix = 'AfterAll ';
for (let i = 0; i < globalFailures.length; i++) {
alert.appendChild(
createDom(
'span',
{ className: errorBarClassName },
globalFailureMessage(globalFailures[i])
)
);
}
function globalFailureMessage(failure) {
if (failure.globalErrorType === 'load') {
const prefix = 'Error during loading: ' + failure.message;
if (failure.filename) {
return (
prefix + ' in ' + failure.filename + ' line ' + failure.lineno
);
} else {
return prefix;
}
} else if (failure.globalErrorType === 'afterAll') {
return afterAllMessagePrefix + failure.message;
} else {
return failure.message;
}
}
addDeprecationWarnings(doneResult);
for (let i = 0; i < deprecationWarnings.length; i++) {
const children = [];
let context;
switch (deprecationWarnings[i].runnableType) {
case 'spec':
context = '(in spec: ' + deprecationWarnings[i].runnableName + ')';
break;
case 'suite':
context = '(in suite: ' + deprecationWarnings[i].runnableName + ')';
break;
default:
context = '';
}
deprecationWarnings[i].message.split('\n').forEach(function(line) {
children.push(line);
children.push(createDom('br'));
});
children[0] = 'DEPRECATION: ' + children[0];
children.push(context);
if (deprecationWarnings[i].stack) {
children.push(createExpander(deprecationWarnings[i].stack));
}
alert.appendChild(
createDom(
'span',
{ className: 'jasmine-bar jasmine-warning' },
children
)
);
}
const results = find('.jasmine-results');
results.appendChild(summary);
summaryList(stateBuilder.topResults, summary);
if (failures.length) {
alert.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-spec-list' },
createDom('span', {}, 'Spec List | '),
createDom(
'a',
{ className: 'jasmine-failures-menu', href: '#' },
'Failures'
)
)
);
alert.appendChild(
createDom(
'span',
{ className: 'jasmine-menu jasmine-bar jasmine-failure-list' },
createDom(
'a',
{ className: 'jasmine-spec-list-menu', href: '#' },
'Spec List'
),
createDom('span', {}, ' | Failures ')
)
);
find('.jasmine-failures-menu').onclick = function() {
setMenuModeTo('jasmine-failure-list');
return false;
};
find('.jasmine-spec-list-menu').onclick = function() {
setMenuModeTo('jasmine-spec-list');
return false;
};
setMenuModeTo('jasmine-failure-list');
const failureNode = find('.jasmine-failures');
for (let i = 0; i < failures.length; i++) {
failureNode.appendChild(failures[i]);
}
}
};
return this;
function failureDom(result) {
const failure = createDom(
'div',
{ className: 'jasmine-spec-detail jasmine-failed' },
failureDescription(result, stateBuilder.currentParent),
createDom('div', { className: 'jasmine-messages' })
);
const messages = failure.childNodes[1];
for (let i = 0; i < result.failedExpectations.length; i++) {
const expectation = result.failedExpectations[i];
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-result-message' },
expectation.message
)
);
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-stack-trace' },
expectation.stack
)
);
}
if (result.failedExpectations.length === 0) {
messages.appendChild(
createDom(
'div',
{ className: 'jasmine-result-message' },
'Spec has no expectations'
)
);
}
if (result.debugLogs) {
messages.appendChild(debugLogTable(result.debugLogs));
}
return failure;
}
function debugLogTable(debugLogs) {
const tbody = createDom('tbody');
debugLogs.forEach(function(entry) {
tbody.appendChild(
createDom(
'tr',
{},
createDom('td', {}, entry.timestamp.toString()),
createDom('td', {}, entry.message)
)
);
});
return createDom(
'div',
{ className: 'jasmine-debug-log' },
createDom(
'div',
{ className: 'jasmine-debug-log-header' },
'Debug logs'
),
createDom(
'table',
{},
createDom(
'thead',
{},
createDom(
'tr',
{},
createDom('th', {}, 'Time (ms)'),
createDom('th', {}, 'Message')
)
),
tbody
)
);
}
function summaryList(resultsTree, domParent) {
let specListNode;
for (let i = 0; i < resultsTree.children.length; i++) {
const resultNode = resultsTree.children[i];
if (filterSpecs && !hasActiveSpec(resultNode)) {
continue;
}
if (resultNode.type === 'suite') {
const suiteListNode = createDom(
'ul',
{ className: 'jasmine-suite', id: 'suite-' + resultNode.result.id },
createDom(
'li',
{
className:
'jasmine-suite-detail jasmine-' + resultNode.result.status
},
createDom(
'a',
{ href: specHref(resultNode.result) },
resultNode.result.description
)
)
);
summaryList(resultNode, suiteListNode);
domParent.appendChild(suiteListNode);
}
if (resultNode.type === 'spec') {
if (domParent.getAttribute('class') !== 'jasmine-specs') {
specListNode = createDom('ul', { className: 'jasmine-specs' });
domParent.appendChild(specListNode);
}
let specDescription = resultNode.result.description;
if (noExpectations(resultNode.result)) {
specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
}
if (
resultNode.result.status === 'pending' &&
resultNode.result.pendingReason !== ''
) {
specDescription =
specDescription +
' PENDING WITH MESSAGE: ' +
resultNode.result.pendingReason;
}
specListNode.appendChild(
createDom(
'li',
{
className: 'jasmine-' + resultNode.result.status,
id: 'spec-' + resultNode.result.id
},
createDom(
'a',
{ href: specHref(resultNode.result) },
specDescription
)
)
);
}
}
}
function optionsMenu(config) {
const optionsMenuDom = createDom(
'div',
{ className: 'jasmine-run-options' },
createDom('span', { className: 'jasmine-trigger' }, 'Options'),
createDom(
'div',
{ className: 'jasmine-payload' },
createDom(
'div',
{ className: 'jasmine-stop-on-failure' },
createDom('input', {
className: 'jasmine-fail-fast',
id: 'jasmine-fail-fast',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-fail-fast' },
'stop execution on spec failure'
)
),
createDom(
'div',
{ className: 'jasmine-throw-failures' },
createDom('input', {
className: 'jasmine-throw',
id: 'jasmine-throw-failures',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-throw-failures' },
'stop spec on expectation failure'
)
),
createDom(
'div',
{ className: 'jasmine-random-order' },
createDom('input', {
className: 'jasmine-random',
id: 'jasmine-random-order',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-random-order' },
'run tests in random order'
)
),
createDom(
'div',
{ className: 'jasmine-hide-disabled' },
createDom('input', {
className: 'jasmine-disabled',
id: 'jasmine-hide-disabled',
type: 'checkbox'
}),
createDom(
'label',
{ className: 'jasmine-label', for: 'jasmine-hide-disabled' },
'hide disabled tests'
)
)
)
);
const failFastCheckbox = optionsMenuDom.querySelector(
'#jasmine-fail-fast'
);
failFastCheckbox.checked = config.stopOnSpecFailure;
failFastCheckbox.onclick = function() {
navigateWithNewParam('stopOnSpecFailure', !config.stopOnSpecFailure);
};
const throwCheckbox = optionsMenuDom.querySelector(
'#jasmine-throw-failures'
);
throwCheckbox.checked = config.stopSpecOnExpectationFailure;
throwCheckbox.onclick = function() {
navigateWithNewParam(
'stopSpecOnExpectationFailure',
!config.stopSpecOnExpectationFailure
);
};
const randomCheckbox = optionsMenuDom.querySelector(
'#jasmine-random-order'
);
randomCheckbox.checked = config.random;
randomCheckbox.onclick = function() {
navigateWithNewParam('random', !config.random);
};
const hideDisabled = optionsMenuDom.querySelector(
'#jasmine-hide-disabled'
);
hideDisabled.checked = config.hideDisabled;
hideDisabled.onclick = function() {
navigateWithNewParam('hideDisabled', !config.hideDisabled);
};
const optionsTrigger = optionsMenuDom.querySelector('.jasmine-trigger'),
optionsPayload = optionsMenuDom.querySelector('.jasmine-payload'),
isOpen = /\bjasmine-open\b/;
optionsTrigger.onclick = function() {
if (isOpen.test(optionsPayload.className)) {
optionsPayload.className = optionsPayload.className.replace(
isOpen,
''
);
} else {
optionsPayload.className += ' jasmine-open';
}
};
return optionsMenuDom;
}
function failureDescription(result, suite) {
const wrapper = createDom(
'div',
{ className: 'jasmine-description' },
createDom(
'a',
{ title: result.description, href: specHref(result) },
result.description
)
);
let suiteLink;
while (suite && suite.parent) {
wrapper.insertBefore(createTextNode(' > '), wrapper.firstChild);
suiteLink = createDom(
'a',
{ href: suiteHref(suite) },
suite.result.description
);
wrapper.insertBefore(suiteLink, wrapper.firstChild);
suite = suite.parent;
}
return wrapper;
}
function suiteHref(suite) {
const els = [];
while (suite && suite.parent) {
els.unshift(suite.result.description);
suite = suite.parent;
}
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
return (
(window.location.pathname || '') +
addToExistingQueryString('spec', els.join(' '))
);
}
function addDeprecationWarnings(result, runnableType) {
if (result && result.deprecationWarnings) {
for (let i = 0; i < result.deprecationWarnings.length; i++) {
const warning = result.deprecationWarnings[i].message;
deprecationWarnings.push({
message: warning,
stack: result.deprecationWarnings[i].stack,
runnableName: result.fullName,
runnableType: runnableType
});
}
}
}
function createExpander(stackTrace) {
const expandLink = createDom('a', { href: '#' }, 'Show stack trace');
const root = createDom(
'div',
{ className: 'jasmine-expander' },
expandLink,
createDom(
'div',
{ className: 'jasmine-expander-contents jasmine-stack-trace' },
stackTrace
)
);
expandLink.addEventListener('click', function(e) {
e.preventDefault();
if (root.classList.contains('jasmine-expanded')) {
root.classList.remove('jasmine-expanded');
expandLink.textContent = 'Show stack trace';
} else {
root.classList.add('jasmine-expanded');
expandLink.textContent = 'Hide stack trace';
}
});
return root;
}
function find(selector) {
return getContainer().querySelector('.jasmine_html-reporter ' + selector);
}
function clearPrior() {
const oldReporter = find('');
if (oldReporter) {
getContainer().removeChild(oldReporter);
}
}
function createDom(type, attrs, childrenArrayOrVarArgs) {
const el = createElement(type);
let children;
if (j$.isArray_(childrenArrayOrVarArgs)) {
children = childrenArrayOrVarArgs;
} else {
children = [];
for (let i = 2; i < arguments.length; i++) {
children.push(arguments[i]);
}
}
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (typeof child === 'string') {
el.appendChild(createTextNode(child));
} else {
if (child) {
el.appendChild(child);
}
}
}
for (const attr in attrs) {
if (attr == 'className') {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
}
function pluralize(singular, count) {
const word = count == 1 ? singular : singular + 's';
return '' + count + ' ' + word;
}
function specHref(result) {
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
return (
(window.location.pathname || '') +
addToExistingQueryString('spec', result.fullName)
);
}
function seedHref(seed) {
// include window.location.pathname to fix issue with karma-jasmine-html-reporter in angular: see https://github.com/jasmine/jasmine/issues/1906
return (
(window.location.pathname || '') +
addToExistingQueryString('seed', seed)
);
}
function defaultQueryString(key, value) {
return '?' + key + '=' + value;
}
function setMenuModeTo(mode) {
htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
}
function noExpectations(result) {
const allExpectations =
result.failedExpectations.length + result.passedExpectations.length;
return (
allExpectations === 0 &&
(result.status === 'passed' || result.status === 'failed')
);
}
function hasActiveSpec(resultNode) {
if (resultNode.type == 'spec' && resultNode.result.status != 'excluded') {
return true;
}
if (resultNode.type == 'suite') {
for (let i = 0, j = resultNode.children.length; i < j; i++) {
if (hasActiveSpec(resultNode.children[i])) {
return true;
}
}
}
}
}
return HtmlReporter;
};
jasmineRequire.HtmlSpecFilter = function() {
function HtmlSpecFilter(options) {
const filterString =
options &&
options.filterString() &&
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const filterPattern = new RegExp(filterString);
this.matches = function(specName) {
return filterPattern.test(specName);
};
}
return HtmlSpecFilter;
};
jasmineRequire.ResultsNode = function() {
function ResultsNode(result, type, parent) {
this.result = result;
this.type = type;
this.parent = parent;
this.children = [];
this.addChild = function(result, type) {
this.children.push(new ResultsNode(result, type, this));
};
this.last = function() {
return this.children[this.children.length - 1];
};
this.updateResult = function(result) {
this.result = result;
};
}
return ResultsNode;
};
jasmineRequire.QueryString = function() {
function QueryString(options) {
this.navigateWithNewParam = function(key, value) {
options.getWindowLocation().search = this.fullStringWithNewParam(
key,
value
);
};
this.fullStringWithNewParam = function(key, value) {
const paramMap = queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
};
this.getParam = function(key) {
return queryStringToParamMap()[key];
};
return this;
function toQueryString(paramMap) {
const qStrPairs = [];
for (const prop in paramMap) {
qStrPairs.push(
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
);
}
return '?' + qStrPairs.join('&');
}
function queryStringToParamMap() {
const paramStr = options.getWindowLocation().search.substring(1);
let params = [];
const paramMap = {};
if (paramStr.length > 0) {
params = paramStr.split('&');
for (let i = 0; i < params.length; i++) {
const p = params[i].split('=');
let value = decodeURIComponent(p[1]);
if (value === 'true' || value === 'false') {
value = JSON.parse(value);
}
paramMap[decodeURIComponent(p[0])] = value;
}
}
return paramMap;
}
}
return QueryString;
};

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,412 @@
"use strict"
const JASMINE_SESSION_ID = `jasmine-${String(Date.now()).slice(8)}`
beforeEach(function () {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 15 * 60 * 1000 // Test timeout after 15 minutes
jasmine.addMatchers({
toBeOneOf: function () {
return {
compare: function (actual, expected) {
return {
pass: expected.includes(actual)
}
}
}
}
})
})
describe('stable-diffusion-ui', function() {
beforeEach(function() {
expect(typeof SD).toBe('object')
expect(typeof SD.serverState).toBe('object')
expect(typeof SD.serverState.status).toBe('string')
})
it('should be able to reach the backend', async function() {
expect(SD.serverState.status).toBe(SD.ServerStates.unavailable)
SD.sessionId = JASMINE_SESSION_ID
await SD.init()
expect(SD.isServerAvailable()).toBeTrue()
})
it('enfore the current task state', function() {
const task = new SD.Task()
expect(task.status).toBe(SD.TaskStatus.init)
expect(task.isPending).toBeTrue()
task._setStatus(SD.TaskStatus.pending)
expect(task.status).toBe(SD.TaskStatus.pending)
expect(task.isPending).toBeTrue()
expect(function() {
task._setStatus(SD.TaskStatus.init)
}).toThrowError()
task._setStatus(SD.TaskStatus.waiting)
expect(task.status).toBe(SD.TaskStatus.waiting)
expect(task.isPending).toBeTrue()
expect(function() {
task._setStatus(SD.TaskStatus.pending)
}).toThrowError()
task._setStatus(SD.TaskStatus.processing)
expect(task.status).toBe(SD.TaskStatus.processing)
expect(task.isPending).toBeTrue()
expect(function() {
task._setStatus(SD.TaskStatus.pending)
}).toThrowError()
task._setStatus(SD.TaskStatus.failed)
expect(task.status).toBe(SD.TaskStatus.failed)
expect(task.isPending).toBeFalse()
expect(function() {
task._setStatus(SD.TaskStatus.processing)
}).toThrowError()
expect(function() {
task._setStatus(SD.TaskStatus.completed)
}).toThrowError()
})
it('should be able to run tasks', async function() {
expect(typeof SD.Task.run).toBe('function')
const promiseGenerator = (function*(val) {
expect(val).toBe('start')
expect(yield 1 + 1).toBe(4)
expect(yield 2 + 2).toBe(8)
yield asyncDelay(500)
expect(yield 3 + 3).toBe(12)
expect(yield 4 + 4).toBe(16)
return 8 + 8
})('start')
const callback = function({value, done}) {
return {value: 2 * value, done}
}
expect(await SD.Task.run(promiseGenerator, {callback})).toBe(32)
})
it('should be able to queue tasks', async function() {
expect(typeof SD.Task.enqueue).toBe('function')
const promiseGenerator = (function*(val) {
expect(val).toBe('start')
expect(yield 1 + 1).toBe(4)
expect(yield 2 + 2).toBe(8)
yield asyncDelay(500)
expect(yield 3 + 3).toBe(12)
expect(yield 4 + 4).toBe(16)
return 8 + 8
})('start')
const callback = function({value, done}) {
return {value: 2 * value, done}
}
const gen = SD.Task.asGenerator({generator: promiseGenerator, callback})
expect(await SD.Task.enqueue(gen)).toBe(32)
})
it('should be able to chain handlers', async function() {
expect(typeof SD.Task.enqueue).toBe('function')
const promiseGenerator = (function*(val) {
expect(val).toBe('start')
expect(yield {test: '1'}).toEqual({test: '1', foo: 'bar'})
expect(yield 2 + 2).toEqual(8)
yield asyncDelay(500)
expect(yield 3 + 3).toEqual(12)
expect(yield {test: 4}).toEqual({test: 8, foo: 'bar'})
return {test: 8}
})('start')
const gen1 = SD.Task.asGenerator({generator: promiseGenerator, callback: function({value, done}) {
if (typeof value === "object") {
value['foo'] = 'bar'
}
return {value, done}
}})
const gen2 = SD.Task.asGenerator({generator: gen1, callback: function({value, done}) {
if (typeof value === 'number') {
value = 2 * value
}
if (typeof value === 'object' && typeof value.test === 'number') {
value.test = 2 * value.test
}
return {value, done}
}})
expect(await SD.Task.enqueue(gen2)).toEqual({test:32, foo: 'bar'})
})
describe('ServiceContainer', function() {
it('should be able to register providers', function() {
const cont = new ServiceContainer(
function foo() {
this.bar = ''
},
function bar() {
return () => 0
},
{ name: 'zero', definition: 0 },
{ name: 'ctx', definition: () => Object.create(null), singleton: true },
{ name: 'test',
definition: (ctx, missing, one, foo) => {
expect(ctx).toEqual({ran: true})
expect(one).toBe(1)
expect(typeof foo).toBe('object')
expect(foo.bar).toBeDefined()
expect(typeof missing).toBe('undefined')
return {foo: 'bar'}
}, dependencies: ['ctx', 'missing', 'one', 'foo']
}
)
const fooObj = cont.get('foo')
expect(typeof fooObj).toBe('object')
fooObj.ran = true
const ctx = cont.get('ctx')
expect(ctx).toEqual({})
ctx.ran = true
const bar = cont.get('bar')
expect(typeof bar).toBe('function')
expect(bar()).toBe(0)
cont.register({name: 'one', definition: 1})
const test = cont.get('test')
expect(typeof test).toBe('object')
expect(test.foo).toBe('bar')
})
})
it('should be able to stream data in chunks', async function() {
expect(SD.isServerAvailable()).toBeTrue()
const nbr_steps = 15
let res = await fetch('/render', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
"prompt": "a photograph of an astronaut riding a horse",
"negative_prompt": "",
"width": 128,
"height": 128,
"seed": Math.floor(Math.random() * 10000000),
"sampler": "plms",
"use_stable_diffusion_model": "sd-v1-4",
"num_inference_steps": nbr_steps,
"guidance_scale": 7.5,
"numOutputsParallel": 1,
"stream_image_progress": true,
"show_only_filtered_image": true,
"output_format": "jpeg",
"session_id": JASMINE_SESSION_ID,
}),
})
expect(res.ok).toBeTruthy()
const renderRequest = await res.json()
expect(typeof renderRequest.stream).toBe('string')
expect(renderRequest.task).toBeDefined()
// Wait for server status to update.
await SD.waitUntil(() => {
console.log('Waiting for %s to be received...', renderRequest.task)
return (!SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)])
}, 250, 10 * 60 * 1000)
// Wait for task to start on server.
await SD.waitUntil(() => {
console.log('Waiting for %s to start...', renderRequest.task)
return !SD.serverState.tasks || SD.serverState.tasks[String(renderRequest.task)] !== 'pending'
}, 250)
const reader = new SD.ChunkedStreamReader(renderRequest.stream)
const parseToString = reader.parse
reader.parse = function(value) {
value = parseToString.call(this, value)
if (!value || value.length <= 0) {
return
}
return reader.readStreamAsJSON(value.join(''))
}
reader.onNext = function({done, value}) {
console.log(value)
if (typeof value === 'object' && 'status' in value) {
done = true
}
return {done, value}
}
let lastUpdate = undefined
let stepCount = 0
let complete = false
//for await (const stepUpdate of reader) {
for await (const stepUpdate of reader.open()) {
console.log('ChunkedStreamReader received ', stepUpdate)
lastUpdate = stepUpdate
if (complete) {
expect(stepUpdate.status).toBe('succeeded')
expect(stepUpdate.output).toHaveSize(1)
} else {
expect(stepUpdate.total_steps).toBe(nbr_steps)
expect(stepUpdate.step).toBe(stepCount)
if (stepUpdate.step === stepUpdate.total_steps) {
complete = true
} else {
stepCount++
}
}
}
for(let i=1; i <= 5; ++i) {
res = await fetch(renderRequest.stream)
expect(res.ok).toBeTruthy()
const cachedResponse = await res.json()
console.log('Cache test %s received %o', i, cachedResponse)
expect(lastUpdate).toEqual(cachedResponse)
}
})
describe('should be able to make renders', function() {
beforeEach(function() {
expect(SD.isServerAvailable()).toBeTrue()
})
it('basic inline request', async function() {
let stepCount = 0
let complete = false
const result = await SD.render({
"prompt": "a photograph of an astronaut riding a horse",
"width": 128,
"height": 128,
"num_inference_steps": 10,
"show_only_filtered_image": false,
//"use_face_correction": 'GFPGANv1.3',
"use_upscale": "RealESRGAN_x4plus",
"session_id": JASMINE_SESSION_ID,
}, function(event) {
console.log(this, event)
if ('update' in event) {
const stepUpdate = event.update
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
expect(stepUpdate.status).toBe('succeeded')
expect(stepUpdate.output).toHaveSize(2)
} else {
expect(stepUpdate.step).toBe(stepCount)
if (stepUpdate.step === stepUpdate.total_steps) {
complete = true
} else {
stepCount++
}
}
}
})
console.log(result)
expect(result.status).toBe('succeeded')
expect(result.output).toHaveSize(2)
})
it('post and reader request', async function() {
const renderTask = new SD.RenderTask({
"prompt": "a photograph of an astronaut riding a horse",
"width": 128,
"height": 128,
"seed": SD.MAX_SEED_VALUE,
"num_inference_steps": 10,
"session_id": JASMINE_SESSION_ID,
})
expect(renderTask.status).toBe(SD.TaskStatus.init)
const timeout = -1
const renderRequest = await renderTask.post(timeout)
expect(typeof renderRequest.stream).toBe('string')
expect(renderTask.status).toBe(SD.TaskStatus.waiting)
expect(renderTask.streamUrl).toBe(renderRequest.stream)
await renderTask.waitUntil({state: SD.TaskStatus.processing, callback: () => console.log('Waiting for render task to start...') })
expect(renderTask.status).toBe(SD.TaskStatus.processing)
let stepCount = 0
let complete = false
//for await (const stepUpdate of renderTask.reader) {
for await (const stepUpdate of renderTask.reader.open()) {
console.log(stepUpdate)
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
expect(stepUpdate.status).toBe('succeeded')
expect(stepUpdate.output).toHaveSize(1)
} else {
expect(stepUpdate.step).toBe(stepCount)
if (stepUpdate.step === stepUpdate.total_steps) {
complete = true
} else {
stepCount++
}
}
}
expect(renderTask.status).toBe(SD.TaskStatus.completed)
expect(renderTask.result.status).toBe('succeeded')
expect(renderTask.result.output).toHaveSize(1)
})
it('queued request', async function() {
let stepCount = 0
let complete = false
const renderTask = new SD.RenderTask({
"prompt": "a photograph of an astronaut riding a horse",
"width": 128,
"height": 128,
"num_inference_steps": 10,
"show_only_filtered_image": false,
//"use_face_correction": 'GFPGANv1.3',
"use_upscale": "RealESRGAN_x4plus",
"session_id": JASMINE_SESSION_ID,
})
await renderTask.enqueue(function(event) {
console.log(this, event)
if ('update' in event) {
const stepUpdate = event.update
if (complete || (stepUpdate.status && stepUpdate.step === stepUpdate.total_steps)) {
expect(stepUpdate.status).toBe('succeeded')
expect(stepUpdate.output).toHaveSize(2)
} else {
expect(stepUpdate.step).toBe(stepCount)
if (stepUpdate.step === stepUpdate.total_steps) {
complete = true
} else {
stepCount++
}
}
}
})
console.log(renderTask.result)
expect(renderTask.result.status).toBe('succeeded')
expect(renderTask.result.output).toHaveSize(2)
})
})
describe('# Special cases', function() {
it('should throw an exception on set for invalid sessionId', function() {
expect(function() {
SD.sessionId = undefined
}).toThrowError("Can't set sessionId to undefined.")
})
})
})
const loadCompleted = window.onload
let loadEvent = undefined
window.onload = function(evt) {
loadEvent = evt
}
if (!PLUGINS.SELFTEST) {
PLUGINS.SELFTEST = {}
}
loadUIPlugins().then(function() {
console.log('loadCompleted', loadEvent)
describe('@Plugins', function() {
it('exposes hooks to overide', function() {
expect(typeof PLUGINS.IMAGE_INFO_BUTTONS).toBe('object')
expect(typeof PLUGINS.TASK_CREATE).toBe('object')
})
describe('supports selftests', function() { // Hook to allow plugins to define tests.
const pluginsTests = Object.keys(PLUGINS.SELFTEST).filter((key) => PLUGINS.SELFTEST.hasOwnProperty(key))
if (!pluginsTests || pluginsTests.length <= 0) {
it('but nothing loaded...', function() {
expect(true).toBeTruthy()
})
return
}
for (const pTest of pluginsTests) {
describe(pTest, function() {
const testFn = PLUGINS.SELFTEST[pTest]
return Promise.resolve(testFn.call(jasmine, pTest))
})
}
})
})
loadCompleted.call(window, loadEvent)
})

View File

@ -0,0 +1,458 @@
(function() {
"use strict"
///////////////////// Function section
function smoothstep(x) {
return x * x * (3 - 2 * x)
}
function smootherstep(x) {
return x * x * x * (x * (x * 6 - 15) + 10)
}
function smootheststep(x) {
let y = -20 * Math.pow(x, 7)
y += 70 * Math.pow(x, 6)
y -= 84 * Math.pow(x, 5)
y += 35 * Math.pow(x, 4)
return y
}
function getCurrentTime() {
const now = new Date();
let hours = now.getHours();
let minutes = now.getMinutes();
let seconds = now.getSeconds();
hours = hours < 10 ? `0${hours}` : hours;
minutes = minutes < 10 ? `0${minutes}` : minutes;
seconds = seconds < 10 ? `0${seconds}` : seconds;
return `${hours}:${minutes}:${seconds}`;
}
function addLogMessage(message) {
const logContainer = document.getElementById('merge-log');
logContainer.innerHTML += `<i>${getCurrentTime()}</i> ${message}<br>`;
// Scroll to the bottom of the log
logContainer.scrollTop = logContainer.scrollHeight;
document.querySelector('#merge-log-container').style.display = 'block'
}
function addLogSeparator() {
const logContainer = document.getElementById('merge-log');
logContainer.innerHTML += '<hr>'
logContainer.scrollTop = logContainer.scrollHeight;
}
function drawDiagram(fn) {
const SIZE = 300
const canvas = document.getElementById('merge-canvas');
canvas.height = canvas.width = SIZE
const ctx = canvas.getContext('2d');
// Draw coordinate system
ctx.scale(1, -1);
ctx.translate(0, -canvas.height);
ctx.lineWidth = 1;
ctx.beginPath();
ctx.strokeStyle = 'white'
ctx.moveTo(0,0); ctx.lineTo(0,SIZE); ctx.lineTo(SIZE,SIZE); ctx.lineTo(SIZE,0); ctx.lineTo(0,0); ctx.lineTo(SIZE,SIZE);
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([1,2])
const n = SIZE / 10
for (let i=n; i<SIZE; i+=n) {
ctx.moveTo(0,i)
ctx.lineTo(SIZE,i)
ctx.moveTo(i,0)
ctx.lineTo(i,SIZE)
}
ctx.stroke()
ctx.beginPath()
ctx.setLineDash([])
ctx.beginPath();
ctx.strokeStyle = 'black'
ctx.lineWidth = 3;
// Plot function
const numSamples = 20;
for (let i = 0; i <= numSamples; i++) {
const x = i / numSamples;
const y = fn(x);
const canvasX = x * SIZE;
const canvasY = y * SIZE;
if (i === 0) {
ctx.moveTo(canvasX, canvasY);
} else {
ctx.lineTo(canvasX, canvasY);
}
}
ctx.stroke()
// Plot alpha values (yellow boxes)
let start = parseFloat( document.querySelector('#merge-start').value )
let step = parseFloat( document.querySelector('#merge-step').value )
let iterations = document.querySelector('#merge-count').value>>0
ctx.beginPath()
ctx.fillStyle = "yellow"
for (let i=0; i< iterations; i++) {
const alpha = ( start + i * step ) / 100
const x = alpha*SIZE
const y = fn(alpha) * SIZE
if (x <= SIZE) {
ctx.rect(x-3,y-3,6,6)
ctx.fill()
} else {
ctx.strokeStyle = 'red'
ctx.moveTo(0,0); ctx.lineTo(0,SIZE); ctx.lineTo(SIZE,SIZE); ctx.lineTo(SIZE,0); ctx.lineTo(0,0); ctx.lineTo(SIZE,SIZE);
ctx.stroke()
addLogMessage('<i>Warning: maximum ratio is &#8805; 100%</i>')
}
}
}
function updateChart() {
let fn = (x) => x
switch (document.querySelector('#merge-interpolation').value) {
case 'SmoothStep':
fn = smoothstep
break
case 'SmootherStep':
fn = smootherstep
break
case 'SmoothestStep':
fn = smootheststep
break
}
drawDiagram(fn)
}
/////////////////////// Tab implementation
document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', `
<span id="tab-merge" class="tab">
<span><i class="fa fa-code-merge icon"></i> Merge models</span>
</span>
`)
document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', `
<div id="tab-content-merge" class="tab-content">
<div id="merge" class="tab-content-inner">
Loading..
</div>
</div>
`)
const tabMerge = document.querySelector('#tab-merge')
if (tabMerge) {
linkTabContents(tabMerge)
}
const merge = document.querySelector('#merge')
if (!merge) {
// merge tab not found, dont exec plugin code.
return
}
document.querySelector('body').insertAdjacentHTML('beforeend', `
<style>
#tab-content-merge .tab-content-inner {
max-width: 100%;
padding: 10pt;
}
.merge-container {
margin-left: 15%;
margin-right: 15%;
text-align: left;
display: inline-grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
"merge-input merge-config"
"merge-buttons merge-buttons";
}
.merge-container p {
margin-top: 3pt;
margin-bottom: 3pt;
}
.merge-config .tab-content {
background: var(--background-color1);
border-radius: 3pt;
}
.merge-config .tab-content-inner {
text-align: left;
}
.merge-input {
grid-area: merge-input;
padding-left:1em;
}
.merge-config {
grid-area: merge-config;
padding:1em;
}
.merge-config input {
margin-bottom: 3px;
}
.merge-config select {
margin-bottom: 3px;
}
.merge-buttons {
grid-area: merge-buttons;
padding:1em;
text-align: center;
}
#merge-button {
padding: 8px;
width:20em;
}
div#merge-log {
height:150px;
overflow-x:hidden;
overflow-y:scroll;
background:var(--background-color1);
border-radius: 3pt;
}
div#merge-log i {
color: hsl(var(--accent-hue), 100%, calc(2*var(--accent-lightness)));
font-family: monospace;
}
.disabled {
background: var(--background-color4);
color: var(--text-color);
}
#merge-type-tabs {
border-bottom: 1px solid black;
}
#merge-log-container {
display: none;
}
.merge-container #merge-warning {
color: rgb(153, 153, 153);
}
</style>
`)
merge.innerHTML = `
<div class="merge-container panel-box">
<div class="merge-input">
<p><label for="#mergeModelA">Select Model A:</label></p>
<input id="mergeModelA" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<p><label for="#mergeModelB">Select Model B:</label></p>
<input id="mergeModelB" type="text" spellcheck="false" autocomplete="off" class="model-filter" data-path="" />
<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>
<br/>
<table>
<tr>
<td><label for="#merge-filename">Output file name:</label></td>
<td><input id="merge-filename" size=24> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Base name of the output file.<br>Mix ratio and file suffix will be appended to this.</span></i></td>
</tr>
<tr>
<td><label for="#merge-fp">Output precision:</label></td>
<td><select id="merge-fp">
<option value="fp16">fp16 (smaller file size)</option>
<option value="fp32">fp32 (larger file size)</option>
</select>
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Image generation uses fp16, so it's a good choice.<br>Use fp32 if you want to use the result models for more mixes</span></i>
</td>
</tr>
<tr>
<td><label for="#merge-format">Output file format:</label></td>
<td><select id="merge-format">
<option value="safetensors">Safetensors (recommended)</option>
<option value="ckpt">CKPT/Pickle (legacy format)</option>
</select>
</td>
</tr>
</table>
<br/>
<div id="merge-log-container">
<p><label for="#merge-log">Log messages:</label></p>
<div id="merge-log"></div>
</div>
</div>
<div class="merge-config">
<div class="tab-container">
<span id="tab-merge-opts-single" class="tab active">
<span>Make a single file</small></span>
</span>
<span id="tab-merge-opts-batch" class="tab">
<span>Make multiple variations</small></span>
</span>
</div>
<div>
<div id="tab-content-merge-opts-single" class="tab-content active">
<div class="tab-content-inner">
<small>Saves a single merged model file, at the specified merge ratio.</small><br/><br/>
<label for="#single-merge-ratio-slider">Merge ratio:</label>
<input id="single-merge-ratio-slider" name="single-merge-ratio-slider" class="editor-slider" value="50" type="range" min="1" max="1000">
<input id="single-merge-ratio" size=2 value="5">%
<i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Model A's contribution to the mix. The rest will be from Model B.</span></i>
</div>
</div>
<div id="tab-content-merge-opts-batch" class="tab-content">
<div class="tab-content-inner">
<small>Saves multiple variations of the model, at different merge ratios.<br/>Each variation will be saved as a separate file.</small><br/><br/>
<table>
<tr><td><label for="#merge-count">Number of variations:</label></td>
<td> <input id="merge-count" size=2 value="5"></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Number of models to create</span></i></td></tr>
<tr><td><label for="#merge-start">Starting merge ratio:</label></td>
<td> <input id="merge-start" size=2 value="5">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Smallest share of model A in the mix</span></i></td></tr>
<tr><td><label for="#merge-step">Increment each step:</label></td>
<td> <input id="merge-step" size=2 value="10">%</td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Share of model A added into the mix per step</span></i></td></tr>
<tr><td><label for="#merge-interpolation">Interpolation model:</label></td>
<td> <select id="merge-interpolation">
<option>Exact</option>
<option>SmoothStep</option>
<option>SmootherStep</option>
<option>SmoothestStep</option>
</select></td>
<td> <i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip top-left">Sigmoid function to be applied to the model share before mixing</span></i></td></tr>
</table>
<br/>
<small>Preview of variation ratios:</small><br/>
<canvas id="merge-canvas" width="400" height="400"></canvas>
</div>
</div>
</div>
</div>
<div class="merge-buttons">
<button id="merge-button" class="primaryButton">Merge models</button>
</div>
</div>`
const tabSettingsSingle = document.querySelector('#tab-merge-opts-single')
const tabSettingsBatch = document.querySelector('#tab-merge-opts-batch')
linkTabContents(tabSettingsSingle)
linkTabContents(tabSettingsBatch)
console.log('Activate')
let mergeModelAField = new ModelDropdown(document.querySelector('#mergeModelA'), 'stable-diffusion')
let mergeModelBField = new ModelDropdown(document.querySelector('#mergeModelB'), 'stable-diffusion')
updateChart()
// slider
const singleMergeRatioField = document.querySelector('#single-merge-ratio')
const singleMergeRatioSlider = document.querySelector('#single-merge-ratio-slider')
function updateSingleMergeRatio() {
singleMergeRatioField.value = singleMergeRatioSlider.value / 10
singleMergeRatioField.dispatchEvent(new Event("change"))
}
function updateSingleMergeRatioSlider() {
if (singleMergeRatioField.value < 0) {
singleMergeRatioField.value = 0
} else if (singleMergeRatioField.value > 100) {
singleMergeRatioField.value = 100
}
singleMergeRatioSlider.value = singleMergeRatioField.value * 10
singleMergeRatioSlider.dispatchEvent(new Event("change"))
}
singleMergeRatioSlider.addEventListener('input', updateSingleMergeRatio)
singleMergeRatioField.addEventListener('input', updateSingleMergeRatioSlider)
updateSingleMergeRatio()
document.querySelector('.merge-config').addEventListener('change', updateChart)
document.querySelector('#merge-button').addEventListener('click', async function(e) {
// Build request template
let model0 = document.querySelector('#mergeModelA').value
let model1 = document.querySelector('#mergeModelB').value
let request = { model0: model0, model1: model1 }
request['use_fp16'] = document.querySelector('#merge-fp').value == 'fp16'
let iterations = document.querySelector('#merge-count').value>>0
let start = parseFloat( document.querySelector('#merge-start').value )
let step = parseFloat( document.querySelector('#merge-step').value )
if (isTabActive(tabSettingsSingle)) {
start = parseFloat(singleMergeRatioField.value)
step = 0
iterations = 1
addLogMessage(`merge ratio = ${start}%`)
} else {
addLogMessage(`start = ${start}%`)
addLogMessage(`step = ${step}%`)
}
if (start + (iterations-1) * step >= 100) {
addLogMessage('<i>Aborting: maximum ratio is &#8805; 100%</i>')
addLogMessage('Reduce the number of variations or the step size')
addLogSeparator()
document.querySelector('#merge-count').focus()
return
}
if (document.querySelector('#merge-filename').value == "") {
addLogMessage('<i>Aborting: No output file name specified</i>')
addLogSeparator()
document.querySelector('#merge-filename').focus()
return
}
// Disable merge button
e.target.disabled=true
e.target.classList.add('disabled')
let cursor = $("body").css("cursor");
let label = document.querySelector('#merge-button').innerHTML
$("body").css("cursor", "progress");
document.querySelector('#merge-button').innerHTML = 'Merging models ...'
addLogMessage("Merging models")
addLogMessage("Model A: "+model0)
addLogMessage("Model B: "+model1)
// Batch main loop
for (let i=0; i<iterations; i++) {
let alpha = ( start + i * step ) / 100
switch (document.querySelector('#merge-interpolation').value) {
case 'SmoothStep':
alpha = smoothstep(alpha)
break
case 'SmootherStep':
alpha = smootherstep(alpha)
break
case 'SmoothestStep':
alpha = smootheststep(alpha)
break
}
addLogMessage(`merging batch job ${i+1}/${iterations}, alpha = ${alpha.toFixed(5)}...`)
request['out_path'] = document.querySelector('#merge-filename').value
request['out_path'] += '-' + alpha.toFixed(5) + '.' + document.querySelector('#merge-format').value
addLogMessage(`&nbsp;&nbsp;filename: ${request['out_path']}`)
request['ratio'] = alpha
let res = await fetch('/model/merge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request) })
const data = await res.json();
addLogMessage(JSON.stringify(data))
}
addLogMessage("<b>Done.</b> The models have been saved to your <tt>models/stable-diffusion</tt> folder.")
addLogSeparator()
// Re-enable merge button
$("body").css("cursor", cursor);
document.querySelector('#merge-button').innerHTML = label
e.target.disabled=false
e.target.classList.remove('disabled')
// Update model list
stableDiffusionModelField.innerHTML = ''
vaeModelField.innerHTML = ''
hypernetworkModelField.innerHTML = ''
await getModels()
})
})()

View File

@ -0,0 +1,53 @@
(function () {
"use strict"
var styleSheet = document.createElement("style");
styleSheet.textContent = `
.modifier-card-tiny.modifier-toggle-inactive {
background: transparent;
border: 2px dashed red;
opacity:0.2;
}
`;
document.head.appendChild(styleSheet);
// observe for changes in tag list
var observer = new MutationObserver(function (mutations) {
// mutations.forEach(function (mutation) {
if (editorModifierTagsList.childNodes.length > 0) {
ModifierToggle()
}
// })
})
observer.observe(editorModifierTagsList, {
childList: true
})
function ModifierToggle() {
let overlays = document.querySelector('#editor-inputs-tags-list').querySelectorAll('.modifier-card-overlay')
overlays.forEach (i => {
i.oncontextmenu = (e) => {
e.preventDefault()
if (i.parentElement.classList.contains('modifier-toggle-inactive')) {
i.parentElement.classList.remove('modifier-toggle-inactive')
}
else
{
i.parentElement.classList.add('modifier-toggle-inactive')
}
// refresh activeTags
let modifierName = i.parentElement.getElementsByClassName('modifier-card-label')[0].getElementsByTagName("p")[0].dataset.fullName
activeTags = activeTags.map(obj => {
if (trimModifiers(obj.name) === trimModifiers(modifierName)) {
return {...obj, inactive: (obj.element.classList.contains('modifier-toggle-inactive'))};
}
return obj;
});
document.dispatchEvent(new Event('refreshImageModifiers'))
}
})
}
})()

View File

@ -0,0 +1,64 @@
(function() {
// Register selftests when loaded by jasmine.
if (typeof PLUGINS?.SELFTEST === 'object') {
PLUGINS.SELFTEST["release-notes"] = function() {
it('should be able to fetch CHANGES.md', async function() {
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/main/CHANGES.md`)
expect(releaseNotes.status).toBe(200)
})
}
}
document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', `
<span id="tab-news" class="tab">
<span><i class="fa fa-bolt icon"></i> What's new?</span>
</span>
`)
document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', `
<div id="tab-content-news" class="tab-content">
<div id="news" class="tab-content-inner">
Loading..
</div>
</div>
`)
const tabNews = document.querySelector('#tab-news')
if (tabNews) {
linkTabContents(tabNews)
}
const news = document.querySelector('#news')
if (!news) {
// news tab not found, dont exec plugin code.
return
}
document.querySelector('body').insertAdjacentHTML('beforeend', `
<style>
#tab-content-news .tab-content-inner {
max-width: 100%;
text-align: left;
padding: 10pt;
}
</style>
`)
loadScript('/media/js/marked.min.js').then(async function() {
let appConfig = await fetch('/get/app_config')
if (!appConfig.ok) {
console.error('[release-notes] Failed to get app_config.')
return
}
appConfig = await appConfig.json()
const updateBranch = appConfig.update_branch || 'main'
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`)
if (!releaseNotes.ok) {
console.error('[release-notes] Failed to get CHANGES.md.')
return
}
releaseNotes = await releaseNotes.text()
news.innerHTML = marked.parse(releaseNotes)
})
})()

View File

@ -0,0 +1,25 @@
/* SD-UI Selftest Plugin.js
*/
(function() { "use strict"
const ID_PREFIX = "selftest-plugin"
const links = document.getElementById("community-links")
if (!links) {
console.error('%s the ID "community-links" cannot be found.', ID_PREFIX)
return
}
// Add link to Jasmine SpecRunner
const pluginLink = document.createElement('li')
const options = {
'stopSpecOnExpectationFailure': "true",
'stopOnSpecFailure': 'false',
'random': 'false',
'hideDisabled': 'false'
}
const optStr = Object.entries(options).map(([key, val]) => `${key}=${val}`).join('&')
pluginLink.innerHTML = `<a id="${ID_PREFIX}-starttest" href="${location.protocol}/plugins/core/SpecRunner.html?${optStr}" target="_blank"><i class="fa-solid fa-vial-circle-check"></i> Start SelfTest</a>`
links.appendChild(pluginLink)
console.log('%s loaded!', ID_PREFIX)
})()

View File

@ -1,107 +0,0 @@
import json
class Request:
session_id: str = "session"
prompt: str = ""
negative_prompt: str = ""
init_image: str = None # base64
mask: str = None # base64
num_outputs: int = 1
num_inference_steps: int = 50
guidance_scale: float = 7.5
width: int = 512
height: int = 512
seed: int = 42
prompt_strength: float = 0.8
sampler: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
# allow_nsfw: bool = False
precision: str = "autocast" # or "full"
save_to_disk_path: str = None
turbo: bool = True
use_cpu: bool = False
use_full_precision: bool = False
use_face_correction: str = None # or "GFPGANv1.3"
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
use_stable_diffusion_model: str = "sd-v1-4"
show_only_filtered_image: bool = False
output_format: str = "jpeg" # or "png"
stream_progress_updates: bool = False
stream_image_progress: bool = False
def json(self):
return {
"session_id": self.session_id,
"prompt": self.prompt,
"negative_prompt": self.negative_prompt,
"num_outputs": self.num_outputs,
"num_inference_steps": self.num_inference_steps,
"guidance_scale": self.guidance_scale,
"width": self.width,
"height": self.height,
"seed": self.seed,
"prompt_strength": self.prompt_strength,
"sampler": self.sampler,
"use_face_correction": self.use_face_correction,
"use_upscale": self.use_upscale,
"use_stable_diffusion_model": self.use_stable_diffusion_model,
"output_format": self.output_format,
}
def to_string(self):
return f'''
session_id: {self.session_id}
prompt: {self.prompt}
negative_prompt: {self.negative_prompt}
seed: {self.seed}
num_inference_steps: {self.num_inference_steps}
sampler: {self.sampler}
guidance_scale: {self.guidance_scale}
w: {self.width}
h: {self.height}
precision: {self.precision}
save_to_disk_path: {self.save_to_disk_path}
turbo: {self.turbo}
use_cpu: {self.use_cpu}
use_full_precision: {self.use_full_precision}
use_face_correction: {self.use_face_correction}
use_upscale: {self.use_upscale}
use_stable_diffusion_model: {self.use_stable_diffusion_model}
show_only_filtered_image: {self.show_only_filtered_image}
output_format: {self.output_format}
stream_progress_updates: {self.stream_progress_updates}
stream_image_progress: {self.stream_image_progress}'''
class Image:
data: str # base64
seed: int
is_nsfw: bool
path_abs: str = None
def __init__(self, data, seed):
self.data = data
self.seed = seed
def json(self):
return {
"data": self.data,
"seed": self.seed,
"path_abs": self.path_abs,
}
class Response:
request: Request
images: list
def json(self):
res = {
"status": 'succeeded',
"request": self.request.json(),
"output": [],
}
for image in self.images:
res["output"].append(image.json())
return res

View File

@ -1,332 +0,0 @@
diff --git a/optimizedSD/ddpm.py b/optimizedSD/ddpm.py
index b967b55..35ef520 100644
--- a/optimizedSD/ddpm.py
+++ b/optimizedSD/ddpm.py
@@ -22,7 +22,7 @@ from ldm.util import exists, default, instantiate_from_config
from ldm.modules.diffusionmodules.util import make_beta_schedule
from ldm.modules.diffusionmodules.util import make_ddim_sampling_parameters, make_ddim_timesteps, noise_like
from ldm.modules.diffusionmodules.util import make_beta_schedule, extract_into_tensor, noise_like
-from samplers import CompVisDenoiser, get_ancestral_step, to_d, append_dims,linear_multistep_coeff
+from .samplers import CompVisDenoiser, get_ancestral_step, to_d, append_dims,linear_multistep_coeff
def disabled_train(self):
"""Overwrite model.train with this function to make sure train/eval mode
@@ -506,6 +506,8 @@ class UNet(DDPM):
x_latent = noise if x0 is None else x0
# sampling
+ if sampler in ('ddim', 'dpm2', 'heun', 'dpm2_a', 'lms') and not hasattr(self, 'ddim_timesteps'):
+ self.make_schedule(ddim_num_steps=S, ddim_eta=eta, verbose=False)
if sampler == "plms":
self.make_schedule(ddim_num_steps=S, ddim_eta=eta, verbose=False)
@@ -528,39 +530,46 @@ class UNet(DDPM):
elif sampler == "ddim":
samples = self.ddim_sampling(x_latent, conditioning, S, unconditional_guidance_scale=unconditional_guidance_scale,
unconditional_conditioning=unconditional_conditioning,
- mask = mask,init_latent=x_T,use_original_steps=False)
+ mask = mask,init_latent=x_T,use_original_steps=False,
+ callback=callback, img_callback=img_callback)
elif sampler == "euler":
self.make_schedule(ddim_num_steps=S, ddim_eta=eta, verbose=False)
samples = self.euler_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
elif sampler == "euler_a":
self.make_schedule(ddim_num_steps=S, ddim_eta=eta, verbose=False)
samples = self.euler_ancestral_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
elif sampler == "dpm2":
samples = self.dpm_2_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
elif sampler == "heun":
samples = self.heun_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
elif sampler == "dpm2_a":
samples = self.dpm_2_ancestral_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
elif sampler == "lms":
samples = self.lms_sampling(self.alphas_cumprod,x_latent, S, conditioning, unconditional_conditioning=unconditional_conditioning,
- unconditional_guidance_scale=unconditional_guidance_scale)
+ unconditional_guidance_scale=unconditional_guidance_scale,
+ img_callback=img_callback)
+
+ yield from samples
if(self.turbo):
self.model1.to("cpu")
self.model2.to("cpu")
- return samples
-
@torch.no_grad()
def plms_sampling(self, cond,b, img,
ddim_use_original_steps=False,
@@ -599,10 +608,10 @@ class UNet(DDPM):
old_eps.append(e_t)
if len(old_eps) >= 4:
old_eps.pop(0)
- if callback: callback(i)
- if img_callback: img_callback(pred_x0, i)
+ if callback: yield from callback(i)
+ if img_callback: yield from img_callback(pred_x0, i)
- return img
+ yield from img_callback(img, len(iterator)-1)
@torch.no_grad()
def p_sample_plms(self, x, c, t, index, repeat_noise=False, use_original_steps=False, quantize_denoised=False,
@@ -706,7 +715,8 @@ class UNet(DDPM):
@torch.no_grad()
def ddim_sampling(self, x_latent, cond, t_start, unconditional_guidance_scale=1.0, unconditional_conditioning=None,
- mask = None,init_latent=None,use_original_steps=False):
+ mask = None,init_latent=None,use_original_steps=False,
+ callback=None, img_callback=None):
timesteps = self.ddim_timesteps
timesteps = timesteps[:t_start]
@@ -730,10 +740,13 @@ class UNet(DDPM):
unconditional_guidance_scale=unconditional_guidance_scale,
unconditional_conditioning=unconditional_conditioning)
+ if callback: yield from callback(i)
+ if img_callback: yield from img_callback(x_dec, i)
+
if mask is not None:
- return x0 * mask + (1. - mask) * x_dec
+ x_dec = x0 * mask + (1. - mask) * x_dec
- return x_dec
+ yield from img_callback(x_dec, len(iterator)-1)
@torch.no_grad()
@@ -779,13 +792,16 @@ class UNet(DDPM):
@torch.no_grad()
- def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
+ def euler_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None,callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
+ img_callback=None):
"""Implements Algorithm 2 (Euler steps) from Karras et al. (2022)."""
extra_args = {} if extra_args is None else extra_args
cvd = CompVisDenoiser(ac)
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running Euler Sampling with {len(sigmas) - 1} timesteps")
+
s_in = x.new_ones([x.shape[0]]).half()
for i in trange(len(sigmas) - 1, disable=disable):
gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
@@ -807,13 +823,18 @@ class UNet(DDPM):
d = to_d(x, sigma_hat, denoised)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
+
+ if img_callback: yield from img_callback(x, i)
+
dt = sigmas[i + 1] - sigma_hat
# Euler method
x = x + d * dt
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
@torch.no_grad()
- def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None):
+ def euler_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None,
+ img_callback=None):
"""Ancestral sampling with Euler method steps."""
extra_args = {} if extra_args is None else extra_args
@@ -822,6 +843,8 @@ class UNet(DDPM):
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running Euler Ancestral Sampling with {len(sigmas) - 1} timesteps")
+
s_in = x.new_ones([x.shape[0]]).half()
for i in trange(len(sigmas) - 1, disable=disable):
@@ -837,17 +860,22 @@ class UNet(DDPM):
sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1])
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
+
+ if img_callback: yield from img_callback(x, i)
+
d = to_d(x, sigmas[i], denoised)
# Euler method
dt = sigma_down - sigmas[i]
x = x + d * dt
x = x + torch.randn_like(x) * sigma_up
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
@torch.no_grad()
- def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
+ def heun_sampling(self, ac, x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
+ img_callback=None):
"""Implements Algorithm 2 (Heun steps) from Karras et al. (2022)."""
extra_args = {} if extra_args is None else extra_args
@@ -855,6 +883,8 @@ class UNet(DDPM):
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running Heun Sampling with {len(sigmas) - 1} timesteps")
+
s_in = x.new_ones([x.shape[0]]).half()
for i in trange(len(sigmas) - 1, disable=disable):
@@ -876,6 +906,9 @@ class UNet(DDPM):
d = to_d(x, sigma_hat, denoised)
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigma_hat, 'denoised': denoised})
+
+ if img_callback: yield from img_callback(x, i)
+
dt = sigmas[i + 1] - sigma_hat
if sigmas[i + 1] == 0:
# Euler method
@@ -895,11 +928,13 @@ class UNet(DDPM):
d_2 = to_d(x_2, sigmas[i + 1], denoised_2)
d_prime = (d + d_2) / 2
x = x + d_prime * dt
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
@torch.no_grad()
- def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.):
+ def dpm_2_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1,extra_args=None, callback=None, disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'), s_noise=1.,
+ img_callback=None):
"""A sampler inspired by DPM-Solver-2 and Algorithm 2 from Karras et al. (2022)."""
extra_args = {} if extra_args is None else extra_args
@@ -907,6 +942,8 @@ class UNet(DDPM):
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running DPM2 Sampling with {len(sigmas) - 1} timesteps")
+
s_in = x.new_ones([x.shape[0]]).half()
for i in trange(len(sigmas) - 1, disable=disable):
gamma = min(s_churn / (len(sigmas) - 1), 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
@@ -924,7 +961,7 @@ class UNet(DDPM):
e_t_uncond, e_t = (x_in + eps * c_out).chunk(2)
denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)
-
+ if img_callback: yield from img_callback(x, i)
d = to_d(x, sigma_hat, denoised)
# Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule
@@ -945,11 +982,13 @@ class UNet(DDPM):
d_2 = to_d(x_2, sigma_mid, denoised_2)
x = x + d_2 * dt_2
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
@torch.no_grad()
- def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None):
+ def dpm_2_ancestral_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None,
+ img_callback=None):
"""Ancestral sampling with DPM-Solver inspired second-order steps."""
extra_args = {} if extra_args is None else extra_args
@@ -957,6 +996,8 @@ class UNet(DDPM):
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running DPM2 Ancestral Sampling with {len(sigmas) - 1} timesteps")
+
s_in = x.new_ones([x.shape[0]]).half()
for i in trange(len(sigmas) - 1, disable=disable):
@@ -973,6 +1014,9 @@ class UNet(DDPM):
sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1])
if callback is not None:
callback({'x': x, 'i': i, 'sigma': sigmas[i], 'sigma_hat': sigmas[i], 'denoised': denoised})
+
+ if img_callback: yield from img_callback(x, i)
+
d = to_d(x, sigmas[i], denoised)
# Midpoint method, where the midpoint is chosen according to a rho=3 Karras schedule
sigma_mid = ((sigmas[i] ** (1 / 3) + sigma_down ** (1 / 3)) / 2) ** 3
@@ -993,11 +1037,13 @@ class UNet(DDPM):
d_2 = to_d(x_2, sigma_mid, denoised_2)
x = x + d_2 * dt_2
x = x + torch.randn_like(x) * sigma_up
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
@torch.no_grad()
- def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4):
+ def lms_sampling(self,ac,x, S, cond, unconditional_conditioning = None, unconditional_guidance_scale = 1, extra_args=None, callback=None, disable=None, order=4,
+ img_callback=None):
extra_args = {} if extra_args is None else extra_args
s_in = x.new_ones([x.shape[0]])
@@ -1005,6 +1051,8 @@ class UNet(DDPM):
sigmas = cvd.get_sigmas(S)
x = x*sigmas[0]
+ print(f"Running LMS Sampling with {len(sigmas) - 1} timesteps")
+
ds = []
for i in trange(len(sigmas) - 1, disable=disable):
@@ -1017,6 +1065,7 @@ class UNet(DDPM):
e_t_uncond, e_t = (x_in + eps * c_out).chunk(2)
denoised = e_t_uncond + unconditional_guidance_scale * (e_t - e_t_uncond)
+ if img_callback: yield from img_callback(x, i)
d = to_d(x, sigmas[i], denoised)
ds.append(d)
@@ -1027,4 +1076,5 @@ class UNet(DDPM):
cur_order = min(i + 1, order)
coeffs = [linear_multistep_coeff(cur_order, sigmas.cpu(), i, j) for j in range(cur_order)]
x = x + sum(coeff * d for coeff, d in zip(coeffs, reversed(ds)))
- return x
+
+ yield from img_callback(x, len(sigmas)-1)
diff --git a/optimizedSD/openaimodelSplit.py b/optimizedSD/openaimodelSplit.py
index abc3098..7a32ffe 100644
--- a/optimizedSD/openaimodelSplit.py
+++ b/optimizedSD/openaimodelSplit.py
@@ -13,7 +13,7 @@ from ldm.modules.diffusionmodules.util import (
normalization,
timestep_embedding,
)
-from splitAttention import SpatialTransformer
+from .splitAttention import SpatialTransformer
class AttentionPool2d(nn.Module):

View File

@ -1,13 +0,0 @@
diff --git a/environment.yaml b/environment.yaml
index 7f25da8..306750f 100644
--- a/environment.yaml
+++ b/environment.yaml
@@ -23,6 +23,8 @@ dependencies:
- torch-fidelity==0.3.0
- transformers==4.19.2
- torchmetrics==0.6.0
+ - pywavelets==1.3.0
+ - pandas==1.4.4
- kornia==0.6
- -e git+https://github.com/CompVis/taming-transformers.git@master#egg=taming-transformers
- -e git+https://github.com/openai/CLIP.git@main#egg=clip

View File

@ -1,698 +0,0 @@
import json
import os, re
import traceback
import torch
import numpy as np
from omegaconf import OmegaConf
from PIL import Image, ImageOps
from tqdm import tqdm, trange
from itertools import islice
from einops import rearrange
import time
from pytorch_lightning import seed_everything
from torch import autocast
from contextlib import nullcontext
from einops import rearrange, repeat
from ldm.util import instantiate_from_config
from optimizedSD.optimUtils import split_weighted_subprompts
from transformers import logging
from gfpgan import GFPGANer
from basicsr.archs.rrdbnet_arch import RRDBNet
from realesrgan import RealESRGANer
import uuid
logging.set_verbosity_error()
# consts
config_yaml = "optimizedSD/v1-inference.yaml"
filename_regex = re.compile('[^a-zA-Z0-9]')
# api stuff
from . import Request, Response, Image as ResponseImage
import base64
from io import BytesIO
#from colorama import Fore
# local
stop_processing = False
temp_images = {}
ckpt_file = None
gfpgan_file = None
real_esrgan_file = None
model = None
modelCS = None
modelFS = None
model_gfpgan = None
model_real_esrgan = None
model_is_half = False
model_fs_is_half = False
device = None
unet_bs = 1
precision = 'autocast'
sampler_plms = None
sampler_ddim = None
has_valid_gpu = False
force_full_precision = False
try:
gpu = torch.cuda.current_device()
gpu_name = torch.cuda.get_device_name(gpu)
print('GPU detected: ', gpu_name)
force_full_precision = ('nvidia' in gpu_name.lower() or 'geforce' in gpu_name.lower()) and (' 1660' in gpu_name or ' 1650' in gpu_name) # otherwise these NVIDIA cards create green images
if force_full_precision:
print('forcing full precision on NVIDIA 16xx cards, to avoid green images. GPU detected: ', gpu_name)
mem_free, mem_total = torch.cuda.mem_get_info(gpu)
mem_total /= float(10**9)
if mem_total < 3.0:
print("GPUs with less than 3 GB of VRAM are not compatible with Stable Diffusion")
raise Exception()
has_valid_gpu = True
except:
print('WARNING: No compatible GPU found. Using the CPU, but this will be very slow!')
pass
def load_model_ckpt(ckpt_to_use, device_to_use='cuda', turbo=False, unet_bs_to_use=1, precision_to_use='autocast'):
global ckpt_file, model, modelCS, modelFS, model_is_half, device, unet_bs, precision, model_fs_is_half
device = device_to_use if has_valid_gpu else 'cpu'
precision = precision_to_use if not force_full_precision else 'full'
unet_bs = unet_bs_to_use
unload_model()
if device == 'cpu':
precision = 'full'
sd = load_model_from_config(f"{ckpt_to_use}.ckpt")
li, lo = [], []
for key, value in sd.items():
sp = key.split(".")
if (sp[0]) == "model":
if "input_blocks" in sp:
li.append(key)
elif "middle_block" in sp:
li.append(key)
elif "time_embed" in sp:
li.append(key)
else:
lo.append(key)
for key in li:
sd["model1." + key[6:]] = sd.pop(key)
for key in lo:
sd["model2." + key[6:]] = sd.pop(key)
config = OmegaConf.load(f"{config_yaml}")
model = instantiate_from_config(config.modelUNet)
_, _ = model.load_state_dict(sd, strict=False)
model.eval()
model.cdevice = device
model.unet_bs = unet_bs
model.turbo = turbo
modelCS = instantiate_from_config(config.modelCondStage)
_, _ = modelCS.load_state_dict(sd, strict=False)
modelCS.eval()
modelCS.cond_stage_model.device = device
modelFS = instantiate_from_config(config.modelFirstStage)
_, _ = modelFS.load_state_dict(sd, strict=False)
modelFS.eval()
del sd
if device != "cpu" and precision == "autocast":
model.half()
modelCS.half()
modelFS.half()
model_is_half = True
model_fs_is_half = True
else:
model_is_half = False
model_fs_is_half = False
ckpt_file = ckpt_to_use
print('loaded ', ckpt_file, 'to', device, 'precision', precision)
def unload_model():
global model, modelCS, modelFS
if model is not None:
del model
del modelCS
del modelFS
model = None
modelCS = None
modelFS = None
def load_model_gfpgan(gfpgan_to_use):
global gfpgan_file, model_gfpgan
if gfpgan_to_use is None:
return
gfpgan_file = gfpgan_to_use
model_path = gfpgan_to_use + ".pth"
if device == 'cpu':
model_gfpgan = GFPGANer(model_path=model_path, upscale=1, arch='clean', channel_multiplier=2, bg_upsampler=None, device=torch.device('cpu'))
else:
model_gfpgan = GFPGANer(model_path=model_path, upscale=1, arch='clean', channel_multiplier=2, bg_upsampler=None, device=torch.device('cuda'))
print('loaded ', gfpgan_to_use, 'to', device, 'precision', precision)
def load_model_real_esrgan(real_esrgan_to_use):
global real_esrgan_file, model_real_esrgan
if real_esrgan_to_use is None:
return
real_esrgan_file = real_esrgan_to_use
model_path = real_esrgan_to_use + ".pth"
RealESRGAN_models = {
'RealESRGAN_x4plus': RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4),
'RealESRGAN_x4plus_anime_6B': RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=6, num_grow_ch=32, scale=4)
}
model_to_use = RealESRGAN_models[real_esrgan_to_use]
if device == 'cpu':
model_real_esrgan = RealESRGANer(scale=2, model_path=model_path, model=model_to_use, pre_pad=0, half=False) # cpu does not support half
model_real_esrgan.device = torch.device('cpu')
model_real_esrgan.model.to('cpu')
else:
model_real_esrgan = RealESRGANer(scale=2, model_path=model_path, model=model_to_use, pre_pad=0, half=model_is_half)
model_real_esrgan.model.name = real_esrgan_to_use
print('loaded ', real_esrgan_to_use, 'to', device, 'precision', precision)
def get_base_path(disk_path, session_id, prompt, img_id, ext, suffix=None):
if disk_path is None: return None
if session_id is None: return None
if ext is None: raise Exception('Missing ext')
session_out_path = os.path.join(disk_path, session_id)
os.makedirs(session_out_path, exist_ok=True)
prompt_flattened = filename_regex.sub('_', prompt)[:50]
if suffix is not None:
return os.path.join(session_out_path, f"{prompt_flattened}_{img_id}_{suffix}.{ext}")
return os.path.join(session_out_path, f"{prompt_flattened}_{img_id}.{ext}")
def apply_filters(filter_name, image_data):
print(f'Applying filter {filter_name}...')
gc()
if filter_name == 'gfpgan':
_, _, output = model_gfpgan.enhance(image_data[:,:,::-1], has_aligned=False, only_center_face=False, paste_back=True)
image_data = output[:,:,::-1]
if filter_name == 'real_esrgan':
output, _ = model_real_esrgan.enhance(image_data[:,:,::-1])
image_data = output[:,:,::-1]
return image_data
def mk_img(req: Request):
try:
yield from do_mk_img(req)
except Exception as e:
print(traceback.format_exc())
gc()
if device != "cpu":
modelFS.to("cpu")
modelCS.to("cpu")
model.model1.to("cpu")
model.model2.to("cpu")
gc()
yield json.dumps({
"status": 'failed',
"detail": str(e)
})
def do_mk_img(req: Request):
global ckpt_file
global model, modelCS, modelFS, device
global model_gfpgan, model_real_esrgan
global stop_processing
stop_processing = False
res = Response()
res.request = req
res.images = []
temp_images.clear()
# custom model support:
# the req.use_stable_diffusion_model needs to be a valid path
# to the ckpt file (without the extension).
needs_model_reload = False
ckpt_to_use = ckpt_file
if ckpt_to_use != req.use_stable_diffusion_model:
ckpt_to_use = req.use_stable_diffusion_model
needs_model_reload = True
model.turbo = req.turbo
if req.use_cpu:
if device != 'cpu':
device = 'cpu'
if model_is_half:
load_model_ckpt(ckpt_to_use, device)
needs_model_reload = False
load_model_gfpgan(gfpgan_file)
load_model_real_esrgan(real_esrgan_file)
else:
if has_valid_gpu:
prev_device = device
device = 'cuda'
if (precision == 'autocast' and (req.use_full_precision or not model_is_half)) or \
(precision == 'full' and not req.use_full_precision and not force_full_precision):
load_model_ckpt(ckpt_to_use, device, req.turbo, unet_bs, ('full' if req.use_full_precision else 'autocast'))
needs_model_reload = False
if prev_device != device:
load_model_gfpgan(gfpgan_file)
load_model_real_esrgan(real_esrgan_file)
if needs_model_reload:
load_model_ckpt(ckpt_to_use, device, req.turbo, unet_bs, precision)
if req.use_face_correction != gfpgan_file:
load_model_gfpgan(req.use_face_correction)
if req.use_upscale != real_esrgan_file:
load_model_real_esrgan(req.use_upscale)
model.cdevice = device
modelCS.cond_stage_model.device = device
opt_prompt = req.prompt
opt_seed = req.seed
opt_n_iter = 1
opt_C = 4
opt_f = 8
opt_ddim_eta = 0.0
opt_init_img = req.init_image
print(req.to_string(), '\n device', device)
print('\n\n Using precision:', precision)
seed_everything(opt_seed)
batch_size = req.num_outputs
prompt = opt_prompt
assert prompt is not None
data = [batch_size * [prompt]]
if precision == "autocast" and device != "cpu":
precision_scope = autocast
else:
precision_scope = nullcontext
mask = None
if req.init_image is None:
handler = _txt2img
init_latent = None
t_enc = None
else:
handler = _img2img
init_image = load_img(req.init_image, req.width, req.height)
init_image = init_image.to(device)
if device != "cpu" and precision == "autocast":
init_image = init_image.half()
modelFS.to(device)
init_image = repeat(init_image, '1 ... -> b ...', b=batch_size)
init_latent = modelFS.get_first_stage_encoding(modelFS.encode_first_stage(init_image)) # move to latent space
if req.mask is not None:
mask = load_mask(req.mask, req.width, req.height, init_latent.shape[2], init_latent.shape[3], True).to(device)
mask = mask[0][0].unsqueeze(0).repeat(4, 1, 1).unsqueeze(0)
mask = repeat(mask, '1 ... -> b ...', b=batch_size)
if device != "cpu" and precision == "autocast":
mask = mask.half()
move_fs_to_cpu()
assert 0. <= req.prompt_strength <= 1., 'can only work with strength in [0.0, 1.0]'
t_enc = int(req.prompt_strength * req.num_inference_steps)
print(f"target t_enc is {t_enc} steps")
if req.save_to_disk_path is not None:
session_out_path = os.path.join(req.save_to_disk_path, req.session_id)
os.makedirs(session_out_path, exist_ok=True)
else:
session_out_path = None
seeds = ""
with torch.no_grad():
for n in trange(opt_n_iter, desc="Sampling"):
for prompts in tqdm(data, desc="data"):
with precision_scope("cuda"):
modelCS.to(device)
uc = None
if req.guidance_scale != 1.0:
uc = modelCS.get_learned_conditioning(batch_size * [req.negative_prompt])
if isinstance(prompts, tuple):
prompts = list(prompts)
subprompts, weights = split_weighted_subprompts(prompts[0])
if len(subprompts) > 1:
c = torch.zeros_like(uc)
totalWeight = sum(weights)
# normalize each "sub prompt" and add it
for i in range(len(subprompts)):
weight = weights[i]
# if not skip_normalize:
weight = weight / totalWeight
c = torch.add(c, modelCS.get_learned_conditioning(subprompts[i]), alpha=weight)
else:
c = modelCS.get_learned_conditioning(prompts)
modelFS.to(device)
partial_x_samples = None
last_callback_time = -1
def img_callback(x_samples, i):
nonlocal partial_x_samples, last_callback_time
partial_x_samples = x_samples
if req.stream_progress_updates:
n_steps = req.num_inference_steps if req.init_image is None else t_enc
step_time = time.time() - last_callback_time if last_callback_time != -1 else -1
last_callback_time = time.time()
progress = {"step": i, "total_steps": n_steps, "step_time": step_time}
if req.stream_image_progress and i % 5 == 0:
partial_images = []
for i in range(batch_size):
x_samples_ddim = modelFS.decode_first_stage(x_samples[i].unsqueeze(0))
x_sample = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0)
x_sample = 255.0 * rearrange(x_sample[0].cpu().numpy(), "c h w -> h w c")
x_sample = x_sample.astype(np.uint8)
img = Image.fromarray(x_sample)
buf = BytesIO()
img.save(buf, format='JPEG')
buf.seek(0)
del img, x_sample, x_samples_ddim
# don't delete x_samples, it is used in the code that called this callback
temp_images[str(req.session_id) + '/' + str(i)] = buf
partial_images.append({'path': f'/image/tmp/{req.session_id}/{i}'})
progress['output'] = partial_images
yield json.dumps(progress)
if stop_processing:
raise UserInitiatedStop("User requested that we stop processing")
# run the handler
try:
if handler == _txt2img:
x_samples = _txt2img(req.width, req.height, req.num_outputs, req.num_inference_steps, req.guidance_scale, None, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, req.sampler)
else:
x_samples = _img2img(init_latent, t_enc, batch_size, req.guidance_scale, c, uc, req.num_inference_steps, opt_ddim_eta, opt_seed, img_callback, mask)
yield from x_samples
x_samples = partial_x_samples
except UserInitiatedStop:
if partial_x_samples is None:
continue
x_samples = partial_x_samples
print("saving images")
for i in range(batch_size):
img_id = base64.b64encode(int(time.time()+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.
x_samples_ddim = modelFS.decode_first_stage(x_samples[i].unsqueeze(0))
x_sample = torch.clamp((x_samples_ddim + 1.0) / 2.0, min=0.0, max=1.0)
x_sample = 255.0 * rearrange(x_sample[0].cpu().numpy(), "c h w -> h w c")
x_sample = x_sample.astype(np.uint8)
img = Image.fromarray(x_sample)
has_filters = (req.use_face_correction is not None and req.use_face_correction.startswith('GFPGAN')) or \
(req.use_upscale is not None and req.use_upscale.startswith('RealESRGAN'))
return_orig_img = not has_filters or not req.show_only_filtered_image
if stop_processing:
return_orig_img = True
if req.save_to_disk_path is not None:
if return_orig_img:
img_out_path = get_base_path(req.save_to_disk_path, req.session_id, prompts[0], img_id, req.output_format)
save_image(img, img_out_path)
meta_out_path = get_base_path(req.save_to_disk_path, req.session_id, prompts[0], img_id, 'txt')
save_metadata(meta_out_path, req, prompts[0], opt_seed)
if return_orig_img:
img_data = img_to_base64_str(img, req.output_format)
res_image_orig = ResponseImage(data=img_data, seed=opt_seed)
res.images.append(res_image_orig)
if req.save_to_disk_path is not None:
res_image_orig.path_abs = img_out_path
del img
if has_filters and not stop_processing:
filters_applied = []
if req.use_face_correction:
x_sample = apply_filters('gfpgan', x_sample)
filters_applied.append(req.use_face_correction)
if req.use_upscale:
x_sample = apply_filters('real_esrgan', x_sample)
filters_applied.append(req.use_upscale)
if (len(filters_applied) > 0):
filtered_image = Image.fromarray(x_sample)
filtered_img_data = img_to_base64_str(filtered_image, req.output_format)
response_image = ResponseImage(data=filtered_img_data, seed=opt_seed)
res.images.append(response_image)
if req.save_to_disk_path is not None:
filtered_img_out_path = get_base_path(req.save_to_disk_path, req.session_id, prompts[0], img_id, req.output_format, "_".join(filters_applied))
save_image(filtered_image, filtered_img_out_path)
response_image.path_abs = filtered_img_out_path
del filtered_image
seeds += str(opt_seed) + ","
opt_seed += 1
move_fs_to_cpu()
gc()
del x_samples, x_samples_ddim, x_sample
print("memory_final = ", torch.cuda.memory_allocated() / 1e6)
print('Task completed')
yield json.dumps(res.json())
def save_image(img, img_out_path):
try:
img.save(img_out_path)
except:
print('could not save the file', traceback.format_exc())
def save_metadata(meta_out_path, req, prompt, opt_seed):
metadata = f"""{prompt}
Width: {req.width}
Height: {req.height}
Seed: {opt_seed}
Steps: {req.num_inference_steps}
Guidance Scale: {req.guidance_scale}
Prompt Strength: {req.prompt_strength}
Use Face Correction: {req.use_face_correction}
Use Upscaling: {req.use_upscale}
Sampler: {req.sampler}
Negative Prompt: {req.negative_prompt}
Stable Diffusion Model: {req.use_stable_diffusion_model + '.ckpt'}
"""
try:
with open(meta_out_path, 'w', encoding='utf-8') as f:
f.write(metadata)
except:
print('could not save the file', traceback.format_exc())
def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code, opt_C, opt_f, opt_ddim_eta, c, uc, opt_seed, img_callback, mask, sampler_name):
shape = [opt_n_samples, opt_C, opt_H // opt_f, opt_W // opt_f]
if device != "cpu":
mem = torch.cuda.memory_allocated() / 1e6
modelCS.to("cpu")
while torch.cuda.memory_allocated() / 1e6 >= mem:
time.sleep(1)
if sampler_name == 'ddim':
model.make_schedule(ddim_num_steps=opt_ddim_steps, ddim_eta=opt_ddim_eta, verbose=False)
samples_ddim = model.sample(
S=opt_ddim_steps,
conditioning=c,
seed=opt_seed,
shape=shape,
verbose=False,
unconditional_guidance_scale=opt_scale,
unconditional_conditioning=uc,
eta=opt_ddim_eta,
x_T=start_code,
img_callback=img_callback,
mask=mask,
sampler = sampler_name,
)
yield from samples_ddim
def _img2img(init_latent, t_enc, batch_size, opt_scale, c, uc, opt_ddim_steps, opt_ddim_eta, opt_seed, img_callback, mask):
# encode (scaled latent)
z_enc = model.stochastic_encode(
init_latent,
torch.tensor([t_enc] * batch_size).to(device),
opt_seed,
opt_ddim_eta,
opt_ddim_steps,
)
x_T = None if mask is None else init_latent
# decode it
samples_ddim = model.sample(
t_enc,
c,
z_enc,
unconditional_guidance_scale=opt_scale,
unconditional_conditioning=uc,
img_callback=img_callback,
mask=mask,
x_T=x_T,
sampler = 'ddim'
)
yield from samples_ddim
def move_fs_to_cpu():
if device != "cpu":
mem = torch.cuda.memory_allocated() / 1e6
modelFS.to("cpu")
while torch.cuda.memory_allocated() / 1e6 >= mem:
time.sleep(1)
def gc():
if device == 'cpu':
return
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
# internal
def chunk(it, size):
it = iter(it)
return iter(lambda: tuple(islice(it, size)), ())
def load_model_from_config(ckpt, verbose=False):
print(f"Loading model from {ckpt}")
pl_sd = torch.load(ckpt, map_location="cpu")
if "global_step" in pl_sd:
print(f"Global Step: {pl_sd['global_step']}")
sd = pl_sd["state_dict"]
return sd
# utils
class UserInitiatedStop(Exception):
pass
def load_img(img_str, w0, h0):
image = base64_str_to_img(img_str).convert("RGB")
w, h = image.size
print(f"loaded input image of size ({w}, {h}) from base64")
if h0 is not None and w0 is not None:
h, w = h0, w0
w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64
image = image.resize((w, h), resample=Image.Resampling.LANCZOS)
image = np.array(image).astype(np.float32) / 255.0
image = image[None].transpose(0, 3, 1, 2)
image = torch.from_numpy(image)
return 2.*image - 1.
def load_mask(mask_str, h0, w0, newH, newW, invert=False):
image = base64_str_to_img(mask_str).convert("RGB")
w, h = image.size
print(f"loaded input mask of size ({w}, {h})")
if invert:
print("inverted")
image = ImageOps.invert(image)
# where_0, where_1 = np.where(image == 0), np.where(image == 255)
# image[where_0], image[where_1] = 255, 0
if h0 is not None and w0 is not None:
h, w = h0, w0
w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64
print(f"New mask size ({w}, {h})")
image = image.resize((newW, newH), resample=Image.Resampling.LANCZOS)
image = np.array(image)
image = image.astype(np.float32) / 255.0
image = image[None].transpose(0, 3, 1, 2)
image = torch.from_numpy(image)
return image
# https://stackoverflow.com/a/61114178
def img_to_base64_str(img, output_format="PNG"):
buffered = BytesIO()
img.save(buffered, format=output_format)
buffered.seek(0)
img_byte = buffered.getvalue()
mime_type = "image/png" if output_format.lower() == "png" else "image/jpeg"
img_str = f"data:{mime_type};base64," + base64.b64encode(img_byte).decode()
return img_str
def base64_str_to_img(img_str):
mime_type = "image/png" if img_str.startswith("data:image/png;") else "image/jpeg"
img_str = img_str[len(f"data:{mime_type};base64,"):]
data = base64.b64decode(img_str)
buffered = BytesIO(data)
img = Image.open(buffered)
return img

View File

@ -1,299 +0,0 @@
import json
import traceback
TASK_TTL = 15 * 60 # seconds, Discard last session's task timeout
import queue, threading, time
from typing import Any, Generator, Hashable, Optional, Union
from pydantic import BaseModel
from sd_internal import Request, Response
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 Init(Symbol): pass
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: Request):
self.request: Request = req # Initial Request
self.response: Any = None # Copy of the last reponse
self.temp_images:[] = [None] * req.num_outputs * (1 if req.show_only_filtered_image else 2)
self.error: Exception = None
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
async def read_buffer_generator(self):
try:
while not self.buffer_queue.empty():
res = self.buffer_queue.get(block=False)
self.buffer_queue.task_done()
yield res
except queue.Empty as e: yield
# defaults from https://huggingface.co/blog/stable_diffusion
class ImageRequest(BaseModel):
session_id: str = "session"
prompt: str = ""
negative_prompt: str = ""
init_image: str = None # base64
mask: str = None # base64
num_outputs: int = 1
num_inference_steps: int = 50
guidance_scale: float = 7.5
width: int = 512
height: int = 512
seed: int = 42
prompt_strength: float = 0.8
sampler: str = None # "ddim", "plms", "heun", "euler", "euler_a", "dpm2", "dpm2_a", "lms"
# allow_nsfw: bool = False
save_to_disk_path: str = None
turbo: bool = True
use_cpu: bool = False
use_full_precision: bool = False
use_face_correction: str = None # or "GFPGANv1.3"
use_upscale: str = None # or "RealESRGAN_x4plus" or "RealESRGAN_x4plus_anime_6B"
use_stable_diffusion_model: str = "sd-v1-4"
show_only_filtered_image: bool = False
output_format: str = "jpeg" # or "png"
stream_progress_updates: bool = False
stream_image_progress: bool = False
# Temporary cache to allow to query tasks results for a short time after they are completed.
class TaskCache():
def __init__(self):
self._base = dict()
self._lock: threading.Lock = threading.RLock()
def _get_ttl_time(self, ttl: int) -> int:
return int(time.time()) + ttl
def _is_expired(self, timestamp: int) -> bool:
return int(time.time()) >= timestamp
def clean(self) -> None:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.clean failed to acquire lock within timeout.')
try:
# Create a list of expired keys to delete
to_delete = []
for key in self._base:
ttl, _ = self._base[key]
if self._is_expired(ttl):
to_delete.append(key)
# Remove Items
for key in to_delete:
del self._base[key]
print(f'Session {key} expired. Data removed.')
finally:
self._lock.release()
def clear(self) -> None:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.clear failed to acquire lock within timeout.')
try: self._base.clear()
finally: self._lock.release()
def delete(self, key: Hashable) -> bool:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.delete failed to acquire lock within timeout.')
try:
if key not in self._base:
return False
del self._base[key]
return True
finally:
self._lock.release()
def keep(self, key: Hashable, ttl: int) -> bool:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.keep failed to acquire lock within timeout.')
try:
if key in self._base:
_, value = self._base.get(key)
self._base[key] = (self._get_ttl_time(ttl), value)
return True
return False
finally:
self._lock.release()
def put(self, key: Hashable, value: Any, ttl: int) -> bool:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.put failed to acquire lock within timeout.')
try:
self._base[key] = (
self._get_ttl_time(ttl), value
)
except Exception as e:
print(str(e))
print(traceback.format_exc())
return False
else:
return True
finally:
self._lock.release()
def tryGet(self, key: Hashable) -> Any:
if not self._lock.acquire(blocking=True, timeout=10): raise Exception('TaskCache.tryGet failed to acquire lock within timeout.')
try:
ttl, value = self._base.get(key, (None, None))
if ttl is not None and self._is_expired(ttl):
print(f'Session {key} expired. Discarding data.')
self.delete(key)
return None
return value
finally:
self._lock.release()
current_state = ServerStates.Init
current_state_error:Exception = None
current_model_path = None
tasks_queue = queue.Queue()
task_cache = TaskCache()
default_model_to_load = None
def preload_model(file_path=None):
global current_state, current_state_error, current_model_path
if file_path == None:
file_path = default_model_to_load
if file_path == current_model_path:
return
current_state = ServerStates.LoadingModel
try:
from . import runtime
runtime.load_model_ckpt(ckpt_to_use=file_path)
current_model_path = file_path
current_state_error = None
current_state = ServerStates.Online
except Exception as e:
current_model_path = None
current_state_error = e
current_state = ServerStates.Unavailable
print(traceback.format_exc())
def thread_render():
global current_state, current_state_error, current_model_path
from . import runtime
current_state = ServerStates.Online
preload_model()
while True:
task_cache.clean()
if isinstance(current_state_error, SystemExit):
current_state = ServerStates.Unavailable
return
task = None
try:
task = tasks_queue.get(timeout=1)
except queue.Empty as e:
if isinstance(current_state_error, SystemExit):
current_state = ServerStates.Unavailable
return
else: continue
#if current_model_path != task.request.use_stable_diffusion_model:
# preload_model(task.request.use_stable_diffusion_model)
if current_state_error:
task.error = current_state_error
continue
print(f'Session {task.request.session_id} starting task {id(task)}')
try:
task.lock.acquire(blocking=False)
res = runtime.mk_img(task.request)
if current_model_path == task.request.use_stable_diffusion_model:
current_state = ServerStates.Rendering
else:
current_state = ServerStates.LoadingModel
except Exception as e:
task.error = e
task.lock.release()
tasks_queue.task_done()
print(traceback.format_exc())
continue
dataQueue = None
if task.request.stream_progress_updates:
dataQueue = task.buffer_queue
for result in res:
if current_state == ServerStates.LoadingModel:
current_state = ServerStates.Rendering
current_model_path = task.request.use_stable_diffusion_model
if isinstance(current_state_error, SystemExit) or isinstance(current_state_error, StopAsyncIteration) or isinstance(task.error, StopAsyncIteration):
runtime.stop_processing = True
if isinstance(current_state_error, StopAsyncIteration):
task.error = current_state_error
current_state_error = None
print(f'Session {task.request.session_id} sent cancel signal for task {id(task)}')
if dataQueue:
dataQueue.put(result)
if isinstance(result, str):
result = json.loads(result)
task.response = result
if 'output' in result:
for out_obj in result['output']:
if 'path' in out_obj:
img_id = out_obj['path'][out_obj['path'].rindex('/') + 1:]
task.temp_images[int(img_id)] = runtime.temp_images[out_obj['path'][11:]]
elif 'data' in out_obj:
task.temp_images[result['output'].index(out_obj)] = out_obj['data']
task_cache.keep(task.request.session_id, TASK_TTL)
# Task completed
task.lock.release()
tasks_queue.task_done()
task_cache.keep(task.request.session_id, TASK_TTL)
if isinstance(task.error, StopAsyncIteration):
print(f'Session {task.request.session_id} task {id(task)} cancelled!')
elif task.error is not None:
print(f'Session {task.request.session_id} task {id(task)} failed!')
else:
print(f'Session {task.request.session_id} task {id(task)} completed.')
current_state = ServerStates.Online
render_thread = threading.Thread(target=thread_render)
def start_render_thread():
# Start Rendering Thread
render_thread.daemon = True
render_thread.start()
def shutdown_event(): # Signal render thread to close on shutdown
global current_state_error
current_state_error = SystemExit('Application shutting down.')
def render(req : ImageRequest):
if not render_thread.is_alive(): # Render thread is dead
raise ChildProcessError('Rendering thread has died.')
# Alive, check if task in cache
task = task_cache.tryGet(req.session_id)
if task and not task.response and not task.error and not task.lock.locked():
# Unstarted task pending, deny queueing more than one.
raise ConnectionRefusedError(f'Session {req.session_id} has an already pending task.')
#
from . import runtime
r = Request()
r.session_id = req.session_id
r.prompt = req.prompt
r.negative_prompt = req.negative_prompt
r.init_image = req.init_image
r.mask = req.mask
r.num_outputs = req.num_outputs
r.num_inference_steps = req.num_inference_steps
r.guidance_scale = req.guidance_scale
r.width = req.width
r.height = req.height
r.seed = req.seed
r.prompt_strength = req.prompt_strength
r.sampler = req.sampler
# r.allow_nsfw = req.allow_nsfw
r.turbo = req.turbo
r.use_cpu = req.use_cpu
r.use_full_precision = req.use_full_precision
r.save_to_disk_path = req.save_to_disk_path
r.use_upscale: str = req.use_upscale
r.use_face_correction = req.use_face_correction
r.use_stable_diffusion_model = req.use_stable_diffusion_model
r.show_only_filtered_image = req.show_only_filtered_image
r.output_format = req.output_format
r.stream_progress_updates = True # the underlying implementation only supports streaming
r.stream_image_progress = req.stream_image_progress
if not req.stream_progress_updates:
r.stream_image_progress = False
new_task = RenderTask(r)
if task_cache.put(r.session_id, new_task, TASK_TTL):
tasks_queue.put(new_task, block=True, timeout=30)
return new_task
raise RuntimeError('Failed to add task to cache.')

View File

@ -1,299 +0,0 @@
import json
import traceback
import sys
import os
SD_DIR = os.getcwd()
print('started in ', SD_DIR)
SD_UI_DIR = os.getenv('SD_UI_PATH', None)
sys.path.append(os.path.dirname(SD_UI_DIR))
CONFIG_DIR = os.path.abspath(os.path.join(SD_UI_DIR, '..', 'scripts'))
MODELS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'models'))
UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'plugins', 'ui'))
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder
TASK_TTL = 15 * 60 # Discard last session's task timeout
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from starlette.responses import FileResponse, JSONResponse, StreamingResponse
from pydantic import BaseModel
import logging
import queue, threading, time
from typing import Any, Generator, Hashable, Optional, Union
from sd_internal import Request, Response, task_manager
app = FastAPI()
modifiers_cache = None
outpath = os.path.join(os.path.expanduser("~"), OUTPUT_DIRNAME)
os.makedirs(UI_PLUGINS_DIR, exist_ok=True)
# don't show access log entries for URLs that start with the given prefix
ACCESS_LOG_SUPPRESS_PATH_PREFIXES = ['/ping', '/image', '/modifier-thumbnails']
NOCACHE_HEADERS={"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
app.mount('/media', StaticFiles(directory=os.path.join(SD_UI_DIR, 'media')), name="media")
app.mount('/plugins', StaticFiles(directory=UI_PLUGINS_DIR), name="plugins")
class SetAppConfigRequest(BaseModel):
update_branch: str = "main"
# needs to support the legacy installations
def get_initial_model_to_load():
custom_weight_path = os.path.join(SD_DIR, 'custom-model.ckpt')
ckpt_to_use = "sd-v1-4" if not os.path.exists(custom_weight_path) else "custom-model"
ckpt_to_use = os.path.join(SD_DIR, ckpt_to_use)
config = getConfig()
if 'model' in config and 'stable-diffusion' in config['model']:
model_name = config['model']['stable-diffusion']
model_path = resolve_model_to_use(model_name)
if os.path.exists(model_path + '.ckpt'):
ckpt_to_use = model_path
else:
print('Could not find the configured custom model at:', model_path + '.ckpt', '. Using the default one:', ckpt_to_use + '.ckpt')
return ckpt_to_use
def resolve_model_to_use(model_name):
if model_name in ('sd-v1-4', 'custom-model'):
model_path = os.path.join(MODELS_DIR, 'stable-diffusion', model_name)
legacy_model_path = os.path.join(SD_DIR, model_name)
if not os.path.exists(model_path + '.ckpt') and os.path.exists(legacy_model_path + '.ckpt'):
model_path = legacy_model_path
else:
model_path = os.path.join(MODELS_DIR, 'stable-diffusion', model_name)
return model_path
@app.on_event("shutdown")
def shutdown_event(): # Signal render thread to close on shutdown
task_manager.current_state_error = SystemExit('Application shutting down.')
@app.get('/')
def read_root():
return FileResponse(os.path.join(SD_UI_DIR, 'index.html'), headers=NOCACHE_HEADERS)
@app.get('/ping') # Get server and optionally session status.
def ping(session_id:str=None):
if not task_manager.render_thread.is_alive(): # Render thread is dead.
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
response = {'status': str(task_manager.current_state)}
if session_id:
task = task_manager.task_cache.tryGet(session_id)
if task:
response['task'] = id(task)
if task.lock.locked():
response['session'] = 'running'
elif isinstance(task.error, StopAsyncIteration):
response['session'] = 'stopped'
elif task.error:
response['session'] = 'error'
elif not task.buffer_queue.empty():
response['session'] = 'buffer'
elif task.response:
response['session'] = 'completed'
else:
response['session'] = 'pending'
return JSONResponse(response, headers=NOCACHE_HEADERS)
def save_model_to_config(model_name):
config = getConfig()
if 'model' not in config:
config['model'] = {}
config['model']['stable-diffusion'] = model_name
setConfig(config)
@app.post('/render')
def render(req : task_manager.ImageRequest):
try:
save_model_to_config(req.use_stable_diffusion_model)
req.use_stable_diffusion_model = resolve_model_to_use(req.use_stable_diffusion_model)
new_task = task_manager.render(req)
response = {
'status': str(task_manager.current_state),
'queue': task_manager.tasks_queue.qsize(),
'stream': f'/image/stream/{req.session_id}/{id(new_task)}',
'task': id(new_task)
}
return JSONResponse(response, headers=NOCACHE_HEADERS)
except ChildProcessError as e: # Render thread is dead
raise HTTPException(status_code=500, detail=f'Rendering thread has died.') # HTTP500 Internal Server Error
except ConnectionRefusedError as e: # Unstarted task pending, deny queueing more than one.
raise HTTPException(status_code=503, detail=f'Session {req.session_id} has an already pending task.') # HTTP503 Service Unavailable
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get('/image/stream/{session_id:str}/{task_id:int}')
def stream(session_id:str, task_id:int):
#TODO Move to WebSockets ??
task = task_manager.task_cache.tryGet(session_id)
if not task: raise HTTPException(status_code=410, detail='No request received.') # HTTP410 Gone
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.response:
#print(f'Session {session_id} sending cached response')
return JSONResponse(task.response, headers=NOCACHE_HEADERS)
raise HTTPException(status_code=425, detail='Too Early, task not started yet.') # HTTP425 Too Early
#print(f'Session {session_id} opened live render stream {id(task.buffer_queue)}')
return StreamingResponse(task.read_buffer_generator(), media_type='application/json')
@app.get('/image/stop')
def stop(session_id:str=None):
if not session_id:
if task_manager.current_state == task_manager.ServerStates.Online or task_manager.current_state == task_manager.ServerStates.Unavailable:
raise HTTPException(status_code=409, detail='Not currently running any tasks.') # HTTP409 Conflict
task_manager.current_state_error = StopAsyncIteration('')
return {'OK'}
task = task_manager.task_cache.tryGet(session_id)
if not task: raise HTTPException(status_code=404, detail=f'Session {session_id} has no active task.') # HTTP404 Not Found
if isinstance(task.error, StopAsyncIteration): raise HTTPException(status_code=409, detail=f'Session {session_id} task is already stopped.') # HTTP409 Conflict
task.error = StopAsyncIteration('')
return {'OK'}
@app.get('/image/tmp/{session_id}/{img_id:int}')
def get_image(session_id, img_id):
task = task_manager.task_cache.tryGet(session_id)
if not task: raise HTTPException(status_code=410, detail=f'Session {session_id} has not submitted a task.') # HTTP410 Gone
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:
img_data = task.temp_images[img_id]
if isinstance(img_data, str):
return img_data
img_data.seek(0)
return StreamingResponse(img_data, media_type='image/jpeg')
except KeyError as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post('/app_config')
async def setAppConfig(req : SetAppConfigRequest):
try:
config = {
'update_branch': req.update_branch
}
config_json_str = json.dumps(config)
config_bat_str = f'@set update_branch={req.update_branch}'
config_sh_str = f'export update_branch={req.update_branch}'
config_json_path = os.path.join(CONFIG_DIR, 'config.json')
config_bat_path = os.path.join(CONFIG_DIR, 'config.bat')
config_sh_path = os.path.join(CONFIG_DIR, 'config.sh')
with open(config_json_path, 'w', encoding='utf-8') as f:
f.write(config_json_str)
with open(config_bat_path, 'w', encoding='utf-8') as f:
f.write(config_bat_str)
with open(config_sh_path, 'w', encoding='utf-8') as f:
f.write(config_sh_str)
return {'OK'}
except Exception as e:
print(traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
def getConfig(default_val={}):
try:
config_json_path = os.path.join(CONFIG_DIR, 'config.json')
if not os.path.exists(config_json_path):
return default_val
with open(config_json_path, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(str(e))
print(traceback.format_exc())
return default_val
def setConfig(config):
try:
config_json_path = os.path.join(CONFIG_DIR, 'config.json')
with open(config_json_path, 'w', encoding='utf-8') as f:
return json.dump(config, f)
except Exception as e:
print(str(e))
print(traceback.format_exc())
def getModels():
models = {
'active': {
'stable-diffusion': 'sd-v1-4',
},
'options': {
'stable-diffusion': ['sd-v1-4'],
},
}
# custom models
sd_models_dir = os.path.join(MODELS_DIR, 'stable-diffusion')
for file in os.listdir(sd_models_dir):
if file.endswith('.ckpt'):
model_name = os.path.splitext(file)[0]
models['options']['stable-diffusion'].append(model_name)
# legacy
custom_weight_path = os.path.join(SD_DIR, 'custom-model.ckpt')
if os.path.exists(custom_weight_path):
models['active']['stable-diffusion'] = 'custom-model'
models['options']['stable-diffusion'].append('custom-model')
config = getConfig()
if 'model' in config and 'stable-diffusion' in config['model']:
models['active']['stable-diffusion'] = config['model']['stable-diffusion']
return models
def getUIPlugins():
plugins = []
for file in os.listdir(UI_PLUGINS_DIR):
if file.endswith('.plugin.js'):
plugins.append(f'/plugins/{file}')
return plugins
@app.get('/get/{key:path}')
def read_web_data(key:str=None):
if not key: # /get without parameters, stable-diffusion easter egg.
raise HTTPException(status_code=418, detail="StableDiffusion is drawing a teapot!") # HTTP418 I'm a teapot
elif key == 'app_config':
config = getConfig(default_val=None)
if config is None:
raise HTTPException(status_code=500, detail="Config file is missing or unreadable")
return JSONResponse(config, headers=NOCACHE_HEADERS)
elif key == 'models':
return JSONResponse(getModels(), headers=NOCACHE_HEADERS)
elif key == 'modifiers': return FileResponse(os.path.join(SD_UI_DIR, 'modifiers.json'), headers=NOCACHE_HEADERS)
elif key == 'output_dir': return JSONResponse({ 'output_dir': outpath }, headers=NOCACHE_HEADERS)
elif key == 'ui_plugins': return JSONResponse(getUIPlugins(), headers=NOCACHE_HEADERS)
else:
raise HTTPException(status_code=404, detail=f'Request for unknown {key}') # HTTP404 Not Found
# don't log certain requests
class LogSuppressFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
path = record.getMessage()
for prefix in ACCESS_LOG_SUPPRESS_PATH_PREFIXES:
if path.find(prefix) != -1:
return False
return True
logging.getLogger('uvicorn.access').addFilter(LogSuppressFilter())
task_manager.default_model_to_load = get_initial_model_to_load()
task_manager.start_render_thread()
# start the browser ui
import webbrowser; webbrowser.open('http://localhost:9000')