Compare commits

...

37 Commits

Author SHA1 Message Date
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
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
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
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
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
e5c11ea214 Update custom-modifiers.plugin.js
Removing the redundant initialization of the array.
2022-11-23 03:00:19 -08:00
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
29 changed files with 1361 additions and 300 deletions

View File

@ -2,9 +2,10 @@
## v2.4 ## v2.4
### Major Changes ### Major Changes
- **Automatic scanning for malicious model files** - using `picklescan`. Thanks @JeLuf - **Automatic scanning for malicious model files** - using `picklescan`, and support for `safetensor` model format. Thanks @JeLuf
- **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 - **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 - **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
- **Image Editor** - for drawing simple images for guiding the AI. Thanks @mdiller
- **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller - **Cleaner UI design** - Show settings and help in new tabs, instead of dropdown popups (which were buggy). Thanks @mdiller
- **Progress bar.** 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 `{}, (), [], |` - **Custom Image Modifiers** - You can now save your custom image modifiers! Your saved modifiers can include special characters like `{}, (), [], |`
@ -21,8 +22,13 @@
- A `What's New?` tab in the UI - 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. - 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 - Show the network addresses of the server in the systems setting dialog
- Support loading models in the safetensor format, for improved safety
### Detailed changelog ### Detailed changelog
* 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 - 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 - 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 - Fix a bug where GFPGAN wouldn't work properly when multiple GPUs tried to run it at the same time. Thanks @madrang

View File

@ -44,7 +44,7 @@ if NOT DEFINED test_sd2 set test_sd2=N
@call git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a @call git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a
) )
if "%test_sd2%" == "Y" ( if "%test_sd2%" == "Y" (
@call git -c advice.detachedHead=false checkout 5d647c5459f4cd790672512222bc41903c01bb71 @call git -c advice.detachedHead=false checkout b1a80dfc75388914252ce363f923103185eaf48f
) )
@cd .. @cd ..
@ -182,6 +182,16 @@ call WHERE uvicorn > .tmp
) )
) )
@>nul 2>nul call python -c "import safetensors"
@if "%ERRORLEVEL%" NEQ "0" (
@echo. & echo SafeTensors not found. Installing
@call pip install safetensors || (
echo "Error installing the safetensors package 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!"
pause
exit /b
)
)
@>nul findstr /m "conda_sd_ui_deps_installed" ..\scripts\install_status.txt @>nul findstr /m "conda_sd_ui_deps_installed" ..\scripts\install_status.txt
@if "%ERRORLEVEL%" NEQ "0" ( @if "%ERRORLEVEL%" NEQ "0" (
@echo conda_sd_ui_deps_installed >> ..\scripts\install_status.txt @echo conda_sd_ui_deps_installed >> ..\scripts\install_status.txt

View File

@ -38,7 +38,7 @@ if [ -e "scripts/install_status.txt" ] && [ `grep -c sd_git_cloned scripts/insta
if [ "$test_sd2" == "N" ]; then if [ "$test_sd2" == "N" ]; then
git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a git -c advice.detachedHead=false checkout 7f32368ed1030a6e710537047bacd908adea183a
elif [ "$test_sd2" == "Y" ]; then elif [ "$test_sd2" == "Y" ]; then
git -c advice.detachedHead=false checkout 5d647c5459f4cd790672512222bc41903c01bb71 git -c advice.detachedHead=false checkout b1a80dfc75388914252ce363f923103185eaf48f
fi fi
cd .. cd ..
@ -150,6 +150,13 @@ else
pip install picklescan || fail "Picklescan installation failed." pip install picklescan || fail "Picklescan installation failed."
fi fi
if python -c "import safetensors" --help >/dev/null 2>&1; then
echo "SafeTensors is already installed."
else
echo "SafeTensors not found, installing."
pip install safetensors || fail "SafeTensors installation failed."
fi
mkdir -p "../models/stable-diffusion" mkdir -p "../models/stable-diffusion"

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

@ -12,12 +12,11 @@
<link rel="stylesheet" href="/media/css/auto-save.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/modifier-thumbnails.css">
<link rel="stylesheet" href="/media/css/fontawesome-all.min.css"> <link rel="stylesheet" href="/media/css/fontawesome-all.min.css">
<link rel="stylesheet" href="/media/css/drawingboard.min.css"> <link rel="stylesheet" href="/media/css/image-editor.css">
<link rel="stylesheet" href="/media/css/jquery-confirm.min.css"> <link rel="stylesheet" href="/media/css/jquery-confirm.min.css">
<link rel="manifest" href="/media/manifest.webmanifest"> <link rel="manifest" href="/media/manifest.webmanifest">
<script src="/media/js/jquery-3.6.1.min.js"></script> <script src="/media/js/jquery-3.6.1.min.js"></script>
<script src="/media/js/jquery-confirm.min.js"></script> <script src="/media/js/jquery-confirm.min.js"></script>
<script src="/media/js/drawingboard.min.js"></script>
<script src="/media/js/marked.min.js"></script> <script src="/media/js/marked.min.js"></script>
</head> </head>
<body> <body>
@ -26,7 +25,7 @@
<div id="logo"> <div id="logo">
<h1> <h1>
Stable Diffusion UI Stable Diffusion UI
<small>v2.4.17 <span id="updateBranchLabel"></span></small> <small>v2.4.18 <span id="updateBranchLabel"></span></small>
</h1> </h1>
</div> </div>
<div id="server-status"> <div id="server-status">
@ -65,7 +64,7 @@
</div> </div>
<div id="editor-inputs-init-image" class="row"> <div id="editor-inputs-init-image" class="row">
<label for="init_image">Initial Image (img2img) <small>(optional)</small> </label> <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_preview_container" class="image_preview_container">
<div id="init_image_wrapper"> <div id="init_image_wrapper">
@ -73,16 +72,26 @@
<span id="init_image_size_box"></span> <span id="init_image_size_box"></span>
<button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button> <button class="init_image_clear image_clear_btn"><i class="fa-solid fa-xmark"></i></button>
</div> </div>
<div id="init_image_buttons">
<br/> <div class="button">
<input id="enable_mask" name="enable_mask" type="checkbox"> <i class="fa-regular fa-folder-open"></i>
<label for="enable_mask"> Browse
In-Painting (beta) <input id="init_image" name="init_image" type="file" />
<a href="https://github.com/cmdr2/stable-diffusion-ui/wiki/Inpainting" target="_blank"><i class="fa-solid fa-circle-question help-btn"><span class="simple-tooltip right">Click to learn more about InPainting</span></i></a>
<small>(select the area which the AI will paint into)</small>
</label>
<div id="inpaintingEditor"></div>
</div> </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>
</div>
</div> </div>
<div id="editor-inputs-tags-container" class="row"> <div id="editor-inputs-tags-container" class="row">
@ -108,7 +117,7 @@
<div id="editor-settings-entries" class="collapsible-content"> <div id="editor-settings-entries" class="collapsible-content">
<div><table> <div><table>
<tr><b class="settings-subheader">Image Settings</b></tr> <tr><b class="settings-subheader">Image Settings</b></tr>
<tr class="pl-5"><td><label for="seed">Seed:</label></td><td><input id="seed" name="seed" size="10" value="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="num_outputs_total">Number of Images:</label></td><td><input id="num_outputs_total" name="num_outputs_total" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label><small>(total)</small></label> <input id="num_outputs_parallel" name="num_outputs_parallel" value="1" size="1" onkeypress="preventNonNumericalInput(event)"> <label for="num_outputs_parallel"><small>(in parallel)</small></label></td></tr>
<tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td> <tr class="pl-5"><td><label for="stable_diffusion_model">Model:</label></td><td>
<select id="stable_diffusion_model" name="stable_diffusion_model"> <select id="stable_diffusion_model" name="stable_diffusion_model">
@ -190,11 +199,14 @@
<option value="png">png</option> <option value="png">png</option>
</select> </select>
</td></tr> </td></tr>
<tr class="pl-5" id="output_quality_row"><td><label for="output_quality">JPEG Quality:</label></td><td>
<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> </table></div>
<div><ul> <div><ul>
<li><b class="settings-subheader">Render Settings</b></li> <li><b class="settings-subheader">Render Settings</b></li>
<li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, and slower image creation)</small></label></li> <li class="pl-5"><input id="stream_image_progress" name="stream_image_progress" type="checkbox"> <label for="stream_image_progress">Show a live preview <small>(uses more VRAM, slower images)</small></label></li>
<li class="pl-5"><input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes <small>(uses GFPGAN)</small></label></li> <li class="pl-5"><input id="use_face_correction" name="use_face_correction" type="checkbox"> <label for="use_face_correction">Fix incorrect faces and eyes <small>(uses GFPGAN)</small></label></li>
<li class="pl-5"> <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">Upscale image by 4x with </label>
@ -330,6 +342,38 @@
</div> </div>
</div> </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="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-spacer"></div>
<div id="footer"> <div id="footer">
<div class="line-separator">&nbsp;</div> <div class="line-separator">&nbsp;</div>
@ -343,24 +387,23 @@
</div> </div>
</div> </div>
</body> </body>
<script src="media/js/utils.js"></script> <script src="media/js/utils.js"></script>
<script src="media/js/parameters.js"></script> <script src="media/js/parameters.js"></script>
<script src="media/js/plugins.js"></script> <script src="media/js/plugins.js"></script>
<script src="media/js/inpainting-editor.js"></script>
<script src="media/js/image-modifiers.js"></script> <script src="media/js/image-modifiers.js"></script>
<script src="media/js/auto-save.js"></script> <script src="media/js/auto-save.js"></script>
<script src="media/js/main.js"></script> <script src="media/js/main.js"></script>
<script src="media/js/themes.js"></script> <script src="media/js/themes.js"></script>
<script src="media/js/dnd.js"></script> <script src="media/js/dnd.js"></script>
<script src="media/js/image-editor.js"></script>
<script> <script>
async function init() { async function init() {
await initSettings() await initSettings()
await getModels() await getModels()
await getDiskPath() await getDiskPath()
await getAppConfig() await getAppConfig()
await loadModifiers()
await loadUIPlugins() await loadUIPlugins()
await loadModifiers()
await getSystemInfo() await getSystemInfo()
setInterval(healthCheck, HEALTH_PING_INTERVAL * 1000) setInterval(healthCheck, HEALTH_PING_INTERVAL * 1000)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,211 @@
.editor-controls-left {
padding-left: 32px;
text-align: left;
padding-bottom: 20px;
}
.editor-options-container {
display: flex;
row-gap: 10px;
max-width: 210px;
}
.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: 2px 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);
}
#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;
}
.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;
}

View File

@ -44,9 +44,6 @@ code {
margin-top: 5px; margin-top: 5px;
display: block; display: block;
} }
.image_preview_container {
margin-top: 10pt;
}
.image_clear_btn { .image_clear_btn {
position: absolute; position: absolute;
transform: translate(30%, -30%); transform: translate(30%, -30%);
@ -275,32 +272,6 @@ img {
transform: translateY(25%); transform: translateY(25%);
} }
#inpaintingEditor {
width: 300pt;
height: 300pt;
margin-top: 5pt;
}
.drawing-board-canvas-wrapper {
background-size: 100% 100%;
}
.drawing-board-controls {
min-width: 273px;
}
.drawing-board-control > button {
background-color: #eee;
border-radius: 3pt;
}
.drawing-board-control-inner {
background-color: #eee;
border-radius: 3pt;
}
#inpaintingEditor canvas {
opacity: 0.6;
}
#enable_mask {
margin-top: 8pt;
}
#top-nav { #top-nav {
position: relative; position: relative;
background: var(--background-color4); background: var(--background-color4);
@ -484,8 +455,58 @@ img {
#prompt_from_file { #prompt_from_file {
display: none; display: none;
} }
#init_image_preview_container {
display: flex;
margin-top: 6px;
margin-bottom: 8px;
}
#init_image_preview_container:not(.has-image) #init_image_wrapper,
#init_image_preview_container:not(.has-image) #inpaint_button_container {
display: none;
}
#init_image_buttons {
display: flex;
gap: 8px;
}
#init_image_preview_container.has-image #init_image_buttons {
flex-direction: column;
padding-left: 8px;
}
#init_image_buttons .button {
position: relative;
height: 32px;
width: 150px;
}
#init_image_buttons .button > input {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
opacity: 0;
}
#inpaint_button_container {
display: flex;
align-items: center;
gap: 8px;
}
#init_image_wrapper {
grid-row: span 3;
position: relative;
width: fit-content;
max-height: 150px;
}
#init_image_preview { #init_image_preview {
max-width: 150px;
max-height: 150px; max-height: 150px;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -493,23 +514,18 @@ img {
border-radius: 6px; border-radius: 6px;
transition: all 1s ease-in-out; transition: all 1s ease-in-out;
} }
/*
#init_image_preview:hover { #init_image_preview:hover {
max-width: 500px; max-width: 500px;
max-height: 1000px; max-height: 1000px;
transition: all 1s 0.5s ease-in-out; transition: all 1s 0.5s ease-in-out;
} } */
#init_image_wrapper {
position: relative;
width: fit-content;
}
#init_image_size_box { #init_image_size_box {
position: absolute; position: absolute;
right: 0px; right: 0px;
bottom: 3px; bottom: 0px;
padding: 3px; padding: 3px;
background: black; background: black;
color: white; color: white;
@ -561,6 +577,10 @@ option {
cursor: pointer; cursor: pointer;
} }
input[type="file"] * {
cursor: pointer;
}
input, input,
select, select,
textarea { textarea {
@ -599,12 +619,26 @@ input[type="file"] {
} }
button, button,
input::file-selector-button { input::file-selector-button,
.button {
padding: 2px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: var(--input-border-radius);
background: var(--button-color); background: var(--button-color);
color: var(--button-text-color); color: var(--button-text-color);
border: var(--button-border); border: var(--button-border);
align-items: center;
justify-content: center;
cursor: pointer;
}
.button i {
margin-right: 8px;
}
button:hover,
.button:hover {
transition-duration: 0.1s;
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
} }
input::file-selector-button { input::file-selector-button {
@ -768,6 +802,8 @@ input::file-selector-button {
#promptsFromFileBtn { #promptsFromFileBtn {
font-size: 9pt; font-size: 9pt;
display: inline;
background-color: var(--accent-color);
} }
.section-button { .section-button {
@ -969,8 +1005,8 @@ input::file-selector-button {
display: none; display: none;
} }
#tab-content-wrapper { #tab-content-wrapper > * {
border-top: 8px solid var(--background-color1); padding-top: 8px;
} }
.tab-content-inner { .tab-content-inner {
@ -1007,10 +1043,6 @@ i.active {
float: right; float: right;
font-weight: bold; font-weight: bold;
} }
button:hover {
transition-duration: 0.1s;
background: hsl(var(--accent-hue), 100%, calc(var(--accent-lightness) + 6%));
}
button:active { button:active {
transition-duration: 0.1s; transition-duration: 0.1s;

View File

@ -19,7 +19,7 @@
--input-border-color: var(--background-color4); --input-border-color: var(--background-color4);
--button-text-color: var(--input-text-color); --button-text-color: var(--input-text-color);
--button-color: var(--accent-color); --button-color: var(--input-background-color);
--button-border: none; --button-border: none;
/* other */ /* other */
@ -42,7 +42,6 @@
--background-color4: #cccccc; --background-color4: #cccccc;
--text-color: black; --text-color: black;
--button-text-color: white;
--input-text-color: black; --input-text-color: black;
--input-background-color: #f8f9fa; --input-background-color: #f8f9fa;
@ -134,6 +133,7 @@
--input-border-color: #005E05; --input-border-color: #005E05;
} }
.theme-gnomie { .theme-gnomie {
--background-color1: #242424; --background-color1: #242424;
--background-color2: #353535; --background-color2: #353535;
@ -158,4 +158,3 @@
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25); box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.25);
border-radius: 10px; border-radius: 10px;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -21,6 +21,7 @@ const SETTINGS_IDS_LIST = [
"guidance_scale", "guidance_scale",
"prompt_strength", "prompt_strength",
"output_format", "output_format",
"output_quality",
"negative_prompt", "negative_prompt",
"stream_image_progress", "stream_image_progress",
"use_face_correction", "use_face_correction",

View File

@ -85,13 +85,14 @@ const TASK_MAPPING = {
if (!seed) { if (!seed) {
randomSeedField.checked = true randomSeedField.checked = true
seedField.disabled = true seedField.disabled = true
seedField.value = 0
return return
} }
randomSeedField.checked = false randomSeedField.checked = false
seedField.disabled = false seedField.disabled = false
seedField.value = seed seedField.value = seed
}, },
readUI: () => (randomSeedField.checked ? Math.floor(Math.random() * 10000000) : parseInt(seedField.value)), readUI: () => parseInt(seedField.value), // just return the value the user is seeing in the UI
parse: (val) => parseInt(val) parse: (val) => parseInt(val)
}, },
num_inference_steps: { name: 'Steps', num_inference_steps: { name: 'Steps',
@ -127,10 +128,12 @@ const TASK_MAPPING = {
}, },
mask: { name: 'Mask', mask: { name: 'Mask',
setUI: (mask) => { setUI: (mask) => {
inpaintingEditor.setImg(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) maskSetting.checked = Boolean(mask)
}, },
readUI: () => (maskSetting.checked ? inpaintingEditor.getImg() : undefined), readUI: () => (maskSetting.checked ? imageInpainter.getImg() : undefined),
parse: (val) => val parse: (val) => val
}, },
@ -289,18 +292,11 @@ function restoreTaskToUI(task, fieldsToSkip) {
// Show the source picture if present // Show the source picture if present
initImagePreview.src = (task.reqBody.init_image == undefined ? '' : task.reqBody.init_image) initImagePreview.src = (task.reqBody.init_image == undefined ? '' : task.reqBody.init_image)
if (IMAGE_REGEX.test(initImagePreview.src)) { if (IMAGE_REGEX.test(initImagePreview.src)) {
Boolean(task.reqBody.mask) ? inpaintingEditor.setImg(task.reqBody.mask) : inpaintingEditor.resetBackground() if (Boolean(task.reqBody.mask)) {
initImagePreviewContainer.style.display = 'block' setTimeout(() => { // add a delay to insure this happens AFTER the main image loads (which reloads the inpainter)
inpaintingEditorContainer.style.display = 'none' imageInpainter.setImg(task.reqBody.mask)
promptStrengthContainer.style.display = 'table-row' }, 250)
//samplerSelectionContainer.style.display = 'none' }
// maskSetting.checked = false
inpaintingEditorContainer.style.display = maskSetting.checked ? 'block' : 'none'
} else {
initImagePreviewContainer.style.display = 'none'
// inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'none'
// maskSetting.style.display = 'none'
} }
} }
function readUI() { function readUI() {

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,680 @@
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 IMAGE_EDITOR_TOOLS = [
{
id: "draw",
name: "Draw",
icon: "fa-solid fa-pencil",
cursor: "url(/media/images/fa-pencil.png) 0 24, pointer",
begin: defaultToolBegin,
move: defaultToolMove,
end: defaultToolEnd
},
{
id: "erase",
name: "Erase",
icon: "fa-solid fa-eraser",
cursor: "url(/media/images/fa-eraser.png) 0 18, 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: "colorpicker",
name: "Color Picker",
icon: "fa-solid fa-eye-dropper",
cursor: "url(/media/images/fa-eye-dropper.png) 0 24, pointer",
begin: (editor, ctx, x, y, is_overlay = false) => {
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: (editor, ctx, x, y, is_overlay = false) => {},
end: (editor, ctx, x, y, is_overlay = false) => {}
}
]
const IMAGE_EDITOR_ACTIONS = [
{
id: "clear",
name: "Clear",
icon: "fa-solid fa-xmark",
handler: (editor) => {
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
}
}
]
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: [ 16, 24, 32, 48, 64 ],
initElement: (element, option) => {
element.parentElement.style.flex = option
element.style.width = option + "px"
element.style.height = option + "px"
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 - 4}px`
sub_element.style.height = `${size - 4}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"))
}
// 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
}
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
if (width > height) {
var multiplier = max_size / width
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
else {
var multiplier = max_size / height
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
this.width = width
this.height = 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.drawing.ctx.clearRect(0, 0, this.width, this.height)
this.layers.background.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)
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()
}
}
// 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()
}
}
}
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)
}
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()
})

View File

@ -91,7 +91,7 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
if (activeTags.map(x => trimModifiers(x.name)).includes(trimmedName)) { if (activeTags.map(x => trimModifiers(x.name)).includes(trimmedName)) {
// remove modifier from active array // remove modifier from active array
activeTags = activeTags.filter(x => trimModifiers(x.name) != trimmedName) activeTags = activeTags.filter(x => trimModifiers(x.name) != trimmedName)
toggleCardState(modifierCard, false) toggleCardState(trimmedName, false)
} else { } else {
// add modifier to active array // add modifier to active array
activeTags.push({ activeTags.push({
@ -100,7 +100,7 @@ function createModifierGroup(modifierGroup, initiallyExpanded) {
'originElement': modifierCard, 'originElement': modifierCard,
'previews': modifierPreviews 'previews': modifierPreviews
}) })
toggleCardState(modifierCard, true) toggleCardState(trimmedName, true)
} }
refreshTagsList() refreshTagsList()
@ -219,10 +219,10 @@ function refreshTagsList() {
editorModifierTagsList.appendChild(tag.element) editorModifierTagsList.appendChild(tag.element)
tag.element.addEventListener('click', () => { 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 !== undefined) { if (idx !== -1) {
toggleCardState(activeTags[idx].originElement, false) toggleCardState(activeTags[idx].name, false)
activeTags.splice(idx, 1) activeTags.splice(idx, 1)
refreshTagsList() refreshTagsList()
@ -235,14 +235,21 @@ function refreshTagsList() {
editorModifierTagsList.appendChild(brk) editorModifierTagsList.appendChild(brk)
} }
function toggleCardState(card, makeActive) { function toggleCardState(modifierName, makeActive) {
if (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.classList.add(activeCardClass)
card.querySelector('.modifier-card-image-overlay').innerText = '-' card.querySelector('.modifier-card-image-overlay').innerText = '-'
} else { }
else{
card.classList.remove(activeCardClass) card.classList.remove(activeCardClass)
card.querySelector('.modifier-card-image-overlay').innerText = '+' card.querySelector('.modifier-card-image-overlay').innerText = '+'
} }
}
})
} }
function changePreviewImages(val) { function changePreviewImages(val) {
@ -319,31 +326,7 @@ function saveCustomModifiers() {
} }
function loadCustomModifiers() { function loadCustomModifiers() {
let customModifiers = localStorage.getItem(CUSTOM_MODIFIERS_KEY, '') PLUGINS['MODIFIERS_LOAD'].forEach(fn=>fn.loader.call())
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)
}
} }
customModifiersTextBox.addEventListener('change', saveCustomModifiers) 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)
}

View File

@ -16,6 +16,9 @@ let numOutputsParallelField = document.querySelector('#num_outputs_parallel')
let numInferenceStepsField = document.querySelector('#num_inference_steps') let numInferenceStepsField = document.querySelector('#num_inference_steps')
let guidanceScaleSlider = document.querySelector('#guidance_scale_slider') let guidanceScaleSlider = document.querySelector('#guidance_scale_slider')
let guidanceScaleField = document.querySelector('#guidance_scale') let guidanceScaleField = document.querySelector('#guidance_scale')
let outputQualitySlider = document.querySelector('#output_quality_slider')
let outputQualityField = document.querySelector('#output_quality')
let outputQualityRow = document.querySelector('#output_quality_row')
let randomSeedField = document.querySelector("#random_seed") let randomSeedField = document.querySelector("#random_seed")
let seedField = document.querySelector('#seed') let seedField = document.querySelector('#seed')
let widthField = document.querySelector('#width') let widthField = document.querySelector('#width')
@ -59,14 +62,6 @@ let serverStatusColor = document.querySelector('#server-status-color')
let serverStatusMsg = document.querySelector('#server-status-msg') let serverStatusMsg = document.querySelector('#server-status-msg')
document.querySelector('.drawing-board-control-navigation-back').innerHTML = '<i class="fa-solid fa-rotate-left"></i>'
document.querySelector('.drawing-board-control-navigation-forward').innerHTML = '<i class="fa-solid fa-rotate-right"></i>'
let maskResetButton = document.querySelector('.drawing-board-control-navigation-reset')
maskResetButton.innerHTML = 'Clear'
maskResetButton.style.fontWeight = 'normal'
maskResetButton.style.fontSize = '10pt'
let serverState = {'status': 'Offline', 'time': Date.now()} let serverState = {'status': 'Offline', 'time': Date.now()}
let bellPending = false let bellPending = false
@ -335,11 +330,7 @@ function onUseAsInputClick(req, img) {
initImageSelector.value = null initImageSelector.value = null
initImagePreview.src = imgData initImagePreview.src = imgData
initImagePreviewContainer.style.display = 'block'
inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'table-row'
maskSetting.checked = false maskSetting.checked = false
samplerSelectionContainer.style.display = 'none'
} }
function onDownloadImageClick(req, img) { function onDownloadImageClick(req, img) {
@ -387,11 +378,21 @@ function onMakeSimilarClick(req, img) {
function enqueueImageVariationTask(req, img, reqDiff) { function enqueueImageVariationTask(req, img, reqDiff) {
const imageSeed = img.getAttribute('data-seed') const imageSeed = img.getAttribute('data-seed')
const newTaskRequest = modifyCurrentRequest(req, reqDiff, { const newRequestBody = {
num_outputs: 1, // this can be user-configurable in the future num_outputs: 1, // this can be user-configurable in the future
seed: imageSeed seed: imageSeed
}) }
// If the user is editing pictures, stop modifyCurrentRequest from importing
// new values by setting the missing properties to undefined
if (!('init_image' in req) && !('init_image' in reqDiff)) {
newRequestBody.init_image = undefined
newRequestBody.mask = undefined
} else if (!('mask' in req) && !('mask' in reqDiff)) {
newRequestBody.mask = undefined
}
const newTaskRequest = modifyCurrentRequest(req, reqDiff, newRequestBody)
newTaskRequest.numOutputsTotal = 1 // this can be user-configurable in the future newTaskRequest.numOutputsTotal = 1 // this can be user-configurable in the future
newTaskRequest.batchCount = 1 newTaskRequest.batchCount = 1
@ -696,6 +697,12 @@ async function checkTasks() {
const genSeeds = Boolean(typeof task.reqBody.seed !== 'number' || (task.reqBody.seed === task.seed && task.numOutputsTotal > 1)) const genSeeds = Boolean(typeof task.reqBody.seed !== 'number' || (task.reqBody.seed === task.seed && task.numOutputsTotal > 1))
const startSeed = task.reqBody.seed || task.seed const startSeed = task.reqBody.seed || task.seed
// Update the seed *before* starting the processing so it's retained if user stops the task
if (randomSeedField.checked) {
seedField.value = task.seed
}
for (let i = 0; i < task.batchCount; i++) { for (let i = 0; i < task.batchCount; i++) {
let newTask = task let newTask = task
if (task.batchCount > 1) { if (task.batchCount > 1) {
@ -742,10 +749,6 @@ async function checkTasks() {
} }
} }
if (randomSeedField.checked) {
seedField.value = task.seed
}
currentTask = null currentTask = null
if (typeof requestIdleCallback === 'function') { if (typeof requestIdleCallback === 'function') {
@ -791,6 +794,7 @@ function getCurrentUserRequest() {
stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked), stream_image_progress: (numOutputsTotal > 50 ? false : streamImageProgressField.checked),
show_only_filtered_image: showOnlyFilteredImageField.checked, show_only_filtered_image: showOnlyFilteredImageField.checked,
output_format: outputFormatField.value, output_format: outputFormatField.value,
output_quality: outputQualityField.value,
original_prompt: promptField.value, original_prompt: promptField.value,
active_tags: (activeTags.map(x => x.name)) active_tags: (activeTags.map(x => x.name))
} }
@ -803,7 +807,7 @@ function getCurrentUserRequest() {
// newTask.reqBody.mask = maskImagePreview.src // newTask.reqBody.mask = maskImagePreview.src
// } // }
if (maskSetting.checked) { if (maskSetting.checked) {
newTask.reqBody.mask = inpaintingEditor.getImg() newTask.reqBody.mask = imageInpainter.getImg()
} }
newTask.reqBody.sampler = 'ddim' newTask.reqBody.sampler = 'ddim'
} else { } else {
@ -936,8 +940,9 @@ function getPrompts() {
prompts = prompts.map(prompt => prompt.trim()) prompts = prompts.map(prompt => prompt.trim())
prompts = prompts.filter(prompt => prompt !== '') prompts = prompts.filter(prompt => prompt !== '')
if (activeTags.length > 0) { const newTags = activeTags.filter(tag => tag.inactive === undefined || tag.inactive === false)
const promptTags = activeTags.map(x => x.name).join(", ") if (newTags.length > 0) {
const promptTags = newTags.map(x => x.name).join(", ")
prompts = prompts.map((prompt) => `${prompt}, ${promptTags}`) prompts = prompts.map((prompt) => `${prompt}, ${promptTags}`)
} }
@ -1089,13 +1094,14 @@ numOutputsTotalField.addEventListener('change', renameMakeImageButton)
numOutputsParallelField.addEventListener('change', renameMakeImageButton) numOutputsParallelField.addEventListener('change', renameMakeImageButton)
function onDimensionChange() { function onDimensionChange() {
if (!maskSetting.checked) {
return
}
let widthValue = parseInt(widthField.value) let widthValue = parseInt(widthField.value)
let heightValue = parseInt(heightField.value) let heightValue = parseInt(heightField.value)
if (!initImagePreviewContainer.classList.contains("has-image")) {
resizeInpaintingEditor(widthValue, heightValue) imageEditor.setImage(null, widthValue, heightValue)
}
else {
imageInpainter.setImage(initImagePreview.src, widthValue, heightValue)
}
} }
diskPathField.disabled = !saveToDiskField.checked diskPathField.disabled = !saveToDiskField.checked
@ -1114,6 +1120,7 @@ document.onkeydown = function(e) {
} }
} }
/********************* Guidance **************************/
function updateGuidanceScale() { function updateGuidanceScale() {
guidanceScaleField.value = guidanceScaleSlider.value / 10 guidanceScaleField.value = guidanceScaleSlider.value / 10
guidanceScaleField.dispatchEvent(new Event("change")) guidanceScaleField.dispatchEvent(new Event("change"))
@ -1134,6 +1141,7 @@ guidanceScaleSlider.addEventListener('input', updateGuidanceScale)
guidanceScaleField.addEventListener('input', updateGuidanceScaleSlider) guidanceScaleField.addEventListener('input', updateGuidanceScaleSlider)
updateGuidanceScale() updateGuidanceScale()
/********************* Prompt Strength *******************/
function updatePromptStrength() { function updatePromptStrength() {
promptStrengthField.value = promptStrengthSlider.value / 100 promptStrengthField.value = promptStrengthSlider.value / 100
promptStrengthField.dispatchEvent(new Event("change")) promptStrengthField.dispatchEvent(new Event("change"))
@ -1154,6 +1162,36 @@ promptStrengthSlider.addEventListener('input', updatePromptStrength)
promptStrengthField.addEventListener('input', updatePromptStrengthSlider) promptStrengthField.addEventListener('input', updatePromptStrengthSlider)
updatePromptStrength() updatePromptStrength()
/********************* JPEG Quality **********************/
function updateOutputQuality() {
outputQualityField.value = 0 | outputQualitySlider.value
outputQualityField.dispatchEvent(new Event("change"))
}
function updateOutputQualitySlider() {
if (outputQualityField.value < 10) {
outputQualityField.value = 10
} else if (outputQualityField.value > 95) {
outputQualityField.value = 95
}
outputQualitySlider.value = 0 | outputQualityField.value
outputQualitySlider.dispatchEvent(new Event("change"))
}
outputQualitySlider.addEventListener('input', updateOutputQuality)
outputQualityField.addEventListener('input', debounce(updateOutputQualitySlider))
updateOutputQuality()
outputFormatField.addEventListener('change', e => {
if (outputFormatField.value == 'jpeg') {
outputQualityRow.style.display='table-row'
} else {
outputQualityRow.style.display='none'
}
})
async function getModels() { async function getModels() {
try { try {
var sd_model_setting_key = "stable_diffusion_model" var sd_model_setting_key = "stable_diffusion_model"
@ -1208,7 +1246,7 @@ async function getModels() {
function checkRandomSeed() { function checkRandomSeed() {
if (randomSeedField.checked) { if (randomSeedField.checked) {
seedField.disabled = true seedField.disabled = true
seedField.value = "0" //seedField.value = "0" // This causes the seed to be lost if the user changes their mind after toggling the checkbox
} else { } else {
seedField.disabled = false seedField.disabled = false
} }
@ -1216,12 +1254,8 @@ function checkRandomSeed() {
randomSeedField.addEventListener('input', checkRandomSeed) randomSeedField.addEventListener('input', checkRandomSeed)
checkRandomSeed() checkRandomSeed()
function showInitImagePreview() { function loadImg2ImgFromFile() {
if (initImageSelector.files.length === 0) { if (initImageSelector.files.length === 0) {
initImagePreviewContainer.style.display = 'none'
// inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'none'
// maskSetting.style.display = 'none'
return return
} }
@ -1229,51 +1263,41 @@ function showInitImagePreview() {
let file = initImageSelector.files[0] let file = initImageSelector.files[0]
reader.addEventListener('load', function(event) { reader.addEventListener('load', function(event) {
// console.log(file.name, reader.result)
initImagePreview.src = reader.result initImagePreview.src = reader.result
initImagePreviewContainer.style.display = 'block'
inpaintingEditorContainer.style.display = 'none'
promptStrengthContainer.style.display = 'table-row'
samplerSelectionContainer.style.display = 'none'
// maskSetting.checked = false
}) })
if (file) { if (file) {
reader.readAsDataURL(file) reader.readAsDataURL(file)
} }
} }
initImageSelector.addEventListener('change', showInitImagePreview) initImageSelector.addEventListener('change', loadImg2ImgFromFile)
showInitImagePreview() loadImg2ImgFromFile()
function img2imgLoad() {
promptStrengthContainer.style.display = 'table-row'
samplerSelectionContainer.style.display = "none"
initImagePreviewContainer.classList.add("has-image")
initImagePreview.addEventListener('load', function() {
inpaintingEditorCanvasBackground.style.backgroundImage = "url('" + this.src + "')"
// maskSetting.style.display = 'block'
// inpaintingEditorContainer.style.display = 'block'
initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight initImageSizeBox.textContent = initImagePreview.naturalWidth + " x " + initImagePreview.naturalHeight
initImageSizeBox.style.display = 'block' imageEditor.setImage(this.src, initImagePreview.naturalWidth, initImagePreview.naturalHeight)
}) imageInpainter.setImage(this.src, parseInt(widthField.value), parseInt(heightField.value))
}
initImageClearBtn.addEventListener('click', function() { function img2imgUnload() {
initImageSelector.value = null initImageSelector.value = null
// maskImageSelector.value = null
initImagePreview.src = '' initImagePreview.src = ''
// maskImagePreview.src = ''
maskSetting.checked = false maskSetting.checked = false
initImagePreviewContainer.style.display = 'none' promptStrengthContainer.style.display = "none"
// inpaintingEditorContainer.style.display = 'none' samplerSelectionContainer.style.display = ""
// maskImagePreviewContainer.style.display = 'none' initImagePreviewContainer.classList.remove("has-image")
imageEditor.setImage(null, parseInt(widthField.value), parseInt(heightField.value))
// maskSetting.style.display = 'none' }
initImagePreview.addEventListener('load', img2imgLoad)
promptStrengthContainer.style.display = 'none' initImageClearBtn.addEventListener('click', img2imgUnload)
samplerSelectionContainer.style.display = 'table-row'
initImageSizeBox.style.display = 'none'
})
maskSetting.addEventListener('click', function() { maskSetting.addEventListener('click', function() {
inpaintingEditorContainer.style.display = (this.checked ? 'block' : 'none')
onDimensionChange() onDimensionChange()
}) })
@ -1313,9 +1337,22 @@ document.querySelectorAll('.popup').forEach(popup => {
} }
}) })
var tabElements = []; var tabElements = []
function selectTab(tab_id) {
let tabInfo = tabElements.find(t => t.tab.id == tab_id)
if (!tabInfo.tab.classList.contains("active")) {
tabElements.forEach(info => {
if (info.tab.classList.contains("active")) {
info.tab.classList.toggle("active")
info.content.classList.toggle("active")
}
})
tabInfo.tab.classList.toggle("active")
tabInfo.content.classList.toggle("active")
}
}
function linkTabContents(tab) { function linkTabContents(tab) {
var name = tab.id.replace("tab-", ""); var name = tab.id.replace("tab-", "")
var content = document.getElementById(`tab-content-${name}`) var content = document.getElementById(`tab-content-${name}`)
tabElements.push({ tabElements.push({
name: name, name: name,
@ -1323,18 +1360,7 @@ function linkTabContents(tab) {
content: content content: content
}) })
tab.addEventListener("click", event => { tab.addEventListener("click", event => selectTab(tab.id))
if (!tab.classList.contains("active")) {
tabElements.forEach(tabInfo => {
if (tabInfo.tab.classList.contains("active")) {
tabInfo.tab.classList.toggle("active")
tabInfo.content.classList.toggle("active")
}
})
tab.classList.toggle("active")
content.classList.toggle("active")
}
})
} }
document.querySelectorAll(".tab").forEach(linkTabContents) document.querySelectorAll(".tab").forEach(linkTabContents)

View File

@ -24,7 +24,8 @@ const PLUGINS = {
* } * }
* }) * })
*/ */
IMAGE_INFO_BUTTONS: [] IMAGE_INFO_BUTTONS: [],
MODIFIERS_LOAD: []
} }
async function loadUIPlugins() { async function loadUIPlugins() {

View File

@ -347,6 +347,16 @@ function asyncDelay(timeout) {
}) })
} }
/* Simple debounce function, placeholder for the one in engine.js for simple use cases */
function debounce(func, timeout = 300){
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
function preventNonNumericalInput(e) { function preventNonNumericalInput(e) {
e = e || window.event; e = e || window.event;
let charCode = (typeof e.which == "undefined") ? e.keyCode : e.which; let charCode = (typeof e.which == "undefined") ? e.keyCode : e.which;

View File

@ -17,8 +17,17 @@
prettifyInputs(document); prettifyInputs(document);
let autoScroll = document.querySelector("#auto_scroll") let autoScroll = document.querySelector("#auto_scroll")
SETTINGS_IDS_LIST.push("auto_scroll") /**
initSettings() * the use of initSettings() in the autoscroll plugin seems to be breaking the models dropdown and the save-to-disk folder field
* in the settings tab. They're both blank, because they're being re-initialized. Their earlier values came from the API call,
* but those values aren't stored in localStorage, since they aren't user-specified.
* So when initSettings() is called a second time, it overwrites the values with an empty string.
*
* We could either rework how new components can register themselves to be auto-saved, without having to call initSettings() again.
* Or we could move the autoscroll code into the main code, and include it in the list of fields in auto-save.js
*/
// SETTINGS_IDS_LIST.push("auto_scroll")
// initSettings()
// observe for changes in the preview pane // observe for changes in the preview pane
var observer = new MutationObserver(function (mutations) { var observer = new MutationObserver(function (mutations) {

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,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].innerText
activeTags = activeTags.map(obj => {
if (obj.name === modifierName) {
return {...obj, inactive: (obj.element.classList.contains('modifier-toggle-inactive'))};
}
return obj;
});
console.log(activeTags)
}
})
}
})()

View File

@ -25,6 +25,7 @@ class Request:
use_vae_model: str = None use_vae_model: str = None
show_only_filtered_image: bool = False show_only_filtered_image: bool = False
output_format: str = "jpeg" # or "png" output_format: str = "jpeg" # or "png"
output_quality: int = 75
stream_progress_updates: bool = False stream_progress_updates: bool = False
stream_image_progress: bool = False stream_image_progress: bool = False
@ -47,6 +48,7 @@ class Request:
"use_stable_diffusion_model": self.use_stable_diffusion_model, "use_stable_diffusion_model": self.use_stable_diffusion_model,
"use_vae_model": self.use_vae_model, "use_vae_model": self.use_vae_model,
"output_format": self.output_format, "output_format": self.output_format,
"output_quality": self.output_quality,
} }
def __str__(self): def __str__(self):
@ -70,6 +72,7 @@ class Request:
use_vae_model: {self.use_vae_model} use_vae_model: {self.use_vae_model}
show_only_filtered_image: {self.show_only_filtered_image} show_only_filtered_image: {self.show_only_filtered_image}
output_format: {self.output_format} output_format: {self.output_format}
output_quality: {self.output_quality}
stream_progress_updates: {self.stream_progress_updates} stream_progress_updates: {self.stream_progress_updates}
stream_image_progress: {self.stream_image_progress}''' stream_image_progress: {self.stream_image_progress}'''

View File

@ -29,6 +29,7 @@ from basicsr.archs.rrdbnet_arch import RRDBNet
from realesrgan import RealESRGANer from realesrgan import RealESRGANer
from threading import Lock from threading import Lock
from safetensors.torch import load_file
import uuid import uuid
@ -71,8 +72,6 @@ def thread_init(device):
thread_data.device_name = None thread_data.device_name = None
thread_data.unet_bs = 1 thread_data.unet_bs = 1
thread_data.precision = 'autocast' thread_data.precision = 'autocast'
thread_data.sampler_plms = None
thread_data.sampler_ddim = None
thread_data.turbo = False thread_data.turbo = False
thread_data.force_full_precision = False thread_data.force_full_precision = False
@ -98,7 +97,12 @@ def isSD2():
def load_model_ckpt(): def load_model_ckpt():
if not thread_data.ckpt_file: raise ValueError(f'Thread ckpt_file is undefined.') if not thread_data.ckpt_file: raise ValueError(f'Thread ckpt_file is undefined.')
if not os.path.exists(thread_data.ckpt_file + '.ckpt'): raise FileNotFoundError(f'Cannot find {thread_data.ckpt_file}.ckpt') if os.path.exists(thread_data.ckpt_file + '.ckpt'):
thread_data.ckpt_file += '.ckpt'
elif os.path.exists(thread_data.ckpt_file + '.safetensors'):
thread_data.ckpt_file += '.safetensors'
elif not os.path.exists(thread_data.ckpt_file):
raise FileNotFoundError(f'Cannot find {thread_data.ckpt_file}.ckpt or .safetensors')
if not thread_data.precision: if not thread_data.precision:
thread_data.precision = 'full' if thread_data.force_full_precision else 'autocast' thread_data.precision = 'full' if thread_data.force_full_precision else 'autocast'
@ -109,7 +113,7 @@ def load_model_ckpt():
if thread_data.device == 'cpu': if thread_data.device == 'cpu':
thread_data.precision = 'full' thread_data.precision = 'full'
print('loading', thread_data.ckpt_file + '.ckpt', 'to device', thread_data.device, 'using precision', thread_data.precision) print('loading', thread_data.ckpt_file, 'to device', thread_data.device, 'using precision', thread_data.precision)
if thread_data.test_sd2: if thread_data.test_sd2:
load_model_ckpt_sd2() load_model_ckpt_sd2()
@ -117,7 +121,7 @@ def load_model_ckpt():
load_model_ckpt_sd1() load_model_ckpt_sd1()
def load_model_ckpt_sd1(): def load_model_ckpt_sd1():
sd = load_model_from_config(thread_data.ckpt_file + '.ckpt') sd = load_model_from_config(thread_data.ckpt_file)
li, lo = [], [] li, lo = [], []
for key, value in sd.items(): for key, value in sd.items():
sp = key.split(".") sp = key.split(".")
@ -204,7 +208,7 @@ def load_model_ckpt_sd1():
thread_data.model_fs_is_half = False thread_data.model_fs_is_half = False
print(f'''loaded model print(f'''loaded model
model file: {thread_data.ckpt_file}.ckpt model file: {thread_data.ckpt_file}
model.device: {model.device} model.device: {model.device}
modelCS.device: {modelCS.cond_stage_model.device} modelCS.device: {modelCS.cond_stage_model.device}
modelFS.device: {thread_data.modelFS.device} modelFS.device: {thread_data.modelFS.device}
@ -215,7 +219,7 @@ def load_model_ckpt_sd2():
config = OmegaConf.load(config_file) config = OmegaConf.load(config_file)
verbose = False verbose = False
sd = load_model_from_config(thread_data.ckpt_file + '.ckpt') sd = load_model_from_config(thread_data.ckpt_file)
thread_data.model = instantiate_from_config(config.model) thread_data.model = instantiate_from_config(config.model)
m, u = thread_data.model.load_state_dict(sd, strict=False) m, u = thread_data.model.load_state_dict(sd, strict=False)
@ -230,6 +234,8 @@ def load_model_ckpt_sd2():
thread_data.model.eval() thread_data.model.eval()
del sd del sd
thread_data.model.cond_stage_model.device = torch.device(thread_data.device)
if thread_data.device != "cpu" and thread_data.precision == "autocast": if thread_data.device != "cpu" and thread_data.precision == "autocast":
thread_data.model.half() thread_data.model.half()
thread_data.model_is_half = True thread_data.model_is_half = True
@ -239,7 +245,7 @@ def load_model_ckpt_sd2():
thread_data.model_fs_is_half = False thread_data.model_fs_is_half = False
print(f'''loaded model print(f'''loaded model
model file: {thread_data.ckpt_file}.ckpt model file: {thread_data.ckpt_file}
using precision: {thread_data.precision}''') using precision: {thread_data.precision}''')
def unload_filters(): def unload_filters():
@ -401,7 +407,12 @@ def is_model_reload_necessary(req: Request):
# custom model support: # custom model support:
# the req.use_stable_diffusion_model needs to be a valid path # the req.use_stable_diffusion_model needs to be a valid path
# to the ckpt file (without the extension). # to the ckpt file (without the extension).
if not os.path.exists(req.use_stable_diffusion_model + '.ckpt'): raise FileNotFoundError(f'Cannot find {req.use_stable_diffusion_model}.ckpt') if os.path.exists(req.use_stable_diffusion_model + '.ckpt'):
req.use_stable_diffusion_model += '.ckpt'
elif os.path.exists(req.use_stable_diffusion_model + '.safetensors'):
req.use_stable_diffusion_model += '.safetensors'
elif not os.path.exists(req.use_stable_diffusion_model):
raise FileNotFoundError(f'Cannot find {req.use_stable_diffusion_model}.ckpt or .safetensors')
needs_model_reload = False needs_model_reload = False
if not thread_data.model or thread_data.ckpt_file != req.use_stable_diffusion_model or thread_data.vae_file != req.use_vae_model: if not thread_data.model or thread_data.ckpt_file != req.use_stable_diffusion_model or thread_data.vae_file != req.use_vae_model:
@ -664,12 +675,12 @@ def do_mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, ste
if req.save_to_disk_path is not None: if req.save_to_disk_path is not None:
if return_orig_img: 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) 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) save_image(img, img_out_path, req.output_format, req.output_quality)
meta_out_path = get_base_path(req.save_to_disk_path, req.session_id, prompts[0], img_id, 'txt') 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) save_metadata(meta_out_path, req, prompts[0], opt_seed)
if return_orig_img: if return_orig_img:
img_buffer = img_to_buffer(img, req.output_format) img_buffer = img_to_buffer(img, req.output_format, req.output_quality)
img_str = buffer_to_base64_str(img_buffer, req.output_format) img_str = buffer_to_base64_str(img_buffer, req.output_format)
res_image_orig = ResponseImage(data=img_str, seed=opt_seed) res_image_orig = ResponseImage(data=img_str, seed=opt_seed)
res.images.append(res_image_orig) res.images.append(res_image_orig)
@ -689,14 +700,14 @@ def do_mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, ste
filters_applied.append(req.use_upscale) filters_applied.append(req.use_upscale)
if (len(filters_applied) > 0): if (len(filters_applied) > 0):
filtered_image = Image.fromarray(img_data[i]) filtered_image = Image.fromarray(img_data[i])
filtered_buffer = img_to_buffer(filtered_image, req.output_format) filtered_buffer = img_to_buffer(filtered_image, req.output_format, req.output_quality)
filtered_img_data = buffer_to_base64_str(filtered_buffer, req.output_format) filtered_img_data = buffer_to_base64_str(filtered_buffer, req.output_format)
response_image = ResponseImage(data=filtered_img_data, seed=opt_seed) response_image = ResponseImage(data=filtered_img_data, seed=opt_seed)
res.images.append(response_image) res.images.append(response_image)
task_temp_images[i] = filtered_buffer task_temp_images[i] = filtered_buffer
if req.save_to_disk_path is not None: 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)) 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) save_image(filtered_image, filtered_img_out_path, req.output_format, req.output_quality)
response_image.path_abs = filtered_img_out_path response_image.path_abs = filtered_img_out_path
del filtered_image del filtered_image
# Filter Applied, move to next seed # Filter Applied, move to next seed
@ -717,8 +728,11 @@ def do_mk_img(req: Request, data_queue: queue.Queue, task_temp_images: list, ste
return res return res
def save_image(img, img_out_path): def save_image(img, img_out_path, output_format="", output_quality=75):
try: try:
if output_format.upper() == "JPEG":
img.save(img_out_path, quality=output_quality)
else:
img.save(img_out_path) img.save(img_out_path)
except: except:
print('could not save the file', traceback.format_exc()) print('could not save the file', traceback.format_exc())
@ -753,7 +767,7 @@ def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code,
if not thread_data.test_sd2: if not thread_data.test_sd2:
move_to_cpu(thread_data.modelCS) move_to_cpu(thread_data.modelCS)
if thread_data.test_sd2 and sampler_name not in ('plms', 'ddim'): if thread_data.test_sd2 and sampler_name not in ('plms', 'ddim', 'dpm2'):
raise Exception('Only plms and ddim samplers are supported right now, in SD 2.0') raise Exception('Only plms and ddim samplers are supported right now, in SD 2.0')
@ -768,18 +782,18 @@ def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code,
# x_T=start_code) # x_T=start_code)
if thread_data.test_sd2: if thread_data.test_sd2:
from ldm.models.diffusion.ddim import DDIMSampler
from ldm.models.diffusion.plms import PLMSSampler
shape = [opt_C, opt_H // opt_f, opt_W // opt_f]
if sampler_name == 'plms': if sampler_name == 'plms':
from ldm.models.diffusion.plms import PLMSSampler
sampler = PLMSSampler(thread_data.model) sampler = PLMSSampler(thread_data.model)
elif sampler_name == 'ddim': elif sampler_name == 'ddim':
from ldm.models.diffusion.ddim import DDIMSampler
sampler = DDIMSampler(thread_data.model) sampler = DDIMSampler(thread_data.model)
sampler.make_schedule(ddim_num_steps=opt_ddim_steps, ddim_eta=opt_ddim_eta, verbose=False) sampler.make_schedule(ddim_num_steps=opt_ddim_steps, ddim_eta=opt_ddim_eta, verbose=False)
elif sampler_name == 'dpm2':
from ldm.models.diffusion.dpm_solver import DPMSolverSampler
sampler = DPMSolverSampler(thread_data.model)
shape = [opt_C, opt_H // opt_f, opt_W // opt_f]
samples_ddim, intermediates = sampler.sample( samples_ddim, intermediates = sampler.sample(
S=opt_ddim_steps, S=opt_ddim_steps,
@ -869,13 +883,21 @@ def chunk(it, size):
def load_model_from_config(ckpt, verbose=False): def load_model_from_config(ckpt, verbose=False):
print(f"Loading model from {ckpt}") print(f"Loading model from {ckpt}")
if ckpt.endswith(".safetensors"):
print("Loading from safetensors")
pl_sd = load_file(ckpt, device="cpu")
else:
pl_sd = torch.load(ckpt, map_location="cpu") pl_sd = torch.load(ckpt, map_location="cpu")
if "global_step" in pl_sd: if "global_step" in pl_sd:
print(f"Global Step: {pl_sd['global_step']}") print(f"Global Step: {pl_sd['global_step']}")
sd = pl_sd["state_dict"]
return sd
# utils if "state_dict" in pl_sd:
return pl_sd["state_dict"]
else:
return pl_sd
class UserInitiatedStop(Exception): class UserInitiatedStop(Exception):
pass pass
@ -919,12 +941,15 @@ def load_mask(mask_str, h0, w0, newH, newW, invert=False):
return image return image
# https://stackoverflow.com/a/61114178 # https://stackoverflow.com/a/61114178
def img_to_base64_str(img, output_format="PNG"): def img_to_base64_str(img, output_format="PNG", output_quality=75):
buffered = img_to_buffer(img, output_format) buffered = img_to_buffer(img, output_format, quality=output_quality)
return buffer_to_base64_str(buffered, output_format) return buffer_to_base64_str(buffered, output_format)
def img_to_buffer(img, output_format="PNG"): def img_to_buffer(img, output_format="PNG", output_quality=75):
buffered = BytesIO() buffered = BytesIO()
if ( output_format.upper() == "JPEG" ):
img.save(buffered, format=output_format, quality=output_quality)
else:
img.save(buffered, format=output_format) img.save(buffered, format=output_format)
buffered.seek(0) buffered.seek(0)
return buffered return buffered

View File

@ -79,6 +79,7 @@ class ImageRequest(BaseModel):
use_vae_model: str = None use_vae_model: str = None
show_only_filtered_image: bool = False show_only_filtered_image: bool = False
output_format: str = "jpeg" # or "png" output_format: str = "jpeg" # or "png"
output_quality: int = 75
stream_progress_updates: bool = False stream_progress_updates: bool = False
stream_image_progress: bool = False stream_image_progress: bool = False
@ -95,6 +96,7 @@ class FilterRequest(BaseModel):
render_device: str = None render_device: str = None
use_full_precision: bool = False use_full_precision: bool = False
output_format: str = "jpeg" # or "png" output_format: str = "jpeg" # or "png"
output_quality: int = 75
# Temporary cache to allow to query tasks results for a short time after they are completed. # Temporary cache to allow to query tasks results for a short time after they are completed.
class TaskCache(): class TaskCache():
@ -504,6 +506,7 @@ def render(req : ImageRequest):
r.use_vae_model = req.use_vae_model r.use_vae_model = req.use_vae_model
r.show_only_filtered_image = req.show_only_filtered_image r.show_only_filtered_image = req.show_only_filtered_image
r.output_format = req.output_format r.output_format = req.output_format
r.output_quality = req.output_quality
r.stream_progress_updates = True # the underlying implementation only supports streaming r.stream_progress_updates = True # the underlying implementation only supports streaming
r.stream_image_progress = req.stream_image_progress r.stream_image_progress = req.stream_image_progress

View File

@ -24,7 +24,7 @@ USER_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_DIR, '..', 'plugins', 'ui'
CORE_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, 'plugins', 'ui')) CORE_UI_PLUGINS_DIR = os.path.abspath(os.path.join(SD_UI_DIR, 'plugins', 'ui'))
UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, 'core'), (USER_UI_PLUGINS_DIR, 'user')) UI_PLUGINS_SOURCES = ((CORE_UI_PLUGINS_DIR, 'core'), (USER_UI_PLUGINS_DIR, 'user'))
STABLE_DIFFUSION_MODEL_EXTENSIONS = ['.ckpt'] STABLE_DIFFUSION_MODEL_EXTENSIONS = ['.ckpt', '.safetensors']
VAE_MODEL_EXTENSIONS = ['.vae.pt', '.ckpt'] VAE_MODEL_EXTENSIONS = ['.vae.pt', '.ckpt']
OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder OUTPUT_DIRNAME = "Stable Diffusion UI" # in the user's home folder