Upload custom-hires-fix-mod-for-automatic1111-2.3 using SD-Hub
Browse files- custom-hires-fix-mod-for-automatic1111-2.3/README.md +29 -0
- custom-hires-fix-mod-for-automatic1111-2.3/config.yaml +17 -0
- custom-hires-fix-mod-for-automatic1111-2.3/scripts/__pycache__/custom_hires_fix.cpython-310.pyc +0 -0
- custom-hires-fix-mod-for-automatic1111-2.3/scripts/custom_hires_fix.py +1700 -0
custom-hires-fix-mod-for-automatic1111-2.3/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Custom Hires Fix (webui Extension)
|
| 2 |
+
## Webui Extension for customizing highres fix and improve details (currently separated from original highres fix)
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
#### Update 16.10.23:
|
| 6 |
+
- added ControlNet support: choose preprocessor/model in CN settings, but don't enable unit
|
| 7 |
+
- added Lora support: put Lora in extension prompt to enable Lora only for upscaling, put Lora in negative prompt to disable active Lora
|
| 8 |
+
|
| 9 |
+
#### Update 02.07.23:
|
| 10 |
+
- code rewritten again
|
| 11 |
+
- simplified settings
|
| 12 |
+
- fixed batch generation and image saving
|
| 13 |
+
|
| 14 |
+
#### Update 13.06.23:
|
| 15 |
+
- added gaussian noise instead of random
|
| 16 |
+
|
| 17 |
+
#### Update 29.05.23:
|
| 18 |
+
- added ToMe optomization in second pass, latest Auto1111 update required, controlled via "Token merging ratio for high-res pass" in settings
|
| 19 |
+
- added "Sharp" setting, should be used only with "Smoothness" if image is too blurry
|
| 20 |
+
|
| 21 |
+
#### Update 12.05.23:
|
| 22 |
+
- added smoothness for negative, completely fix ghosting/smears/dirt on flat colors with high denoising
|
| 23 |
+
|
| 24 |
+
#### Update 02.04.23:
|
| 25 |
+
###### Don't forget to clear ui-config.json!
|
| 26 |
+
- upscale separated from original high-res fix
|
| 27 |
+
- now works with img2img
|
| 28 |
+
- many fixes
|
| 29 |
+
|
custom-hires-fix-mod-for-automatic1111-2.3/config.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
width: 1536
|
| 2 |
+
height: 0
|
| 3 |
+
prompt: ''
|
| 4 |
+
negative_prompt: ''
|
| 5 |
+
steps: 15
|
| 6 |
+
first_upscaler: R-ESRGAN 4x+ Anime6B
|
| 7 |
+
second_upscaler: R-ESRGAN 4x+ Anime6B
|
| 8 |
+
first_latent: 0.3
|
| 9 |
+
second_latent: 0.1
|
| 10 |
+
strength: 1.25
|
| 11 |
+
filter: Noise sync (sharp)
|
| 12 |
+
filter_offset: 0
|
| 13 |
+
denoise_offset: 0.05
|
| 14 |
+
clip_skip: 0
|
| 15 |
+
sampler: Euler Dy
|
| 16 |
+
cn_ref: false
|
| 17 |
+
start_control_at: 0
|
custom-hires-fix-mod-for-automatic1111-2.3/scripts/__pycache__/custom_hires_fix.cpython-310.pyc
ADDED
|
Binary file (52.5 kB). View file
|
|
|
custom-hires-fix-mod-for-automatic1111-2.3/scripts/custom_hires_fix.py
ADDED
|
@@ -0,0 +1,1700 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import torch.nn.functional as F
|
| 3 |
+
import json
|
| 4 |
+
import hashlib
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from collections import OrderedDict
|
| 7 |
+
|
| 8 |
+
import gradio as gr
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from PIL import Image, ImageFilter
|
| 12 |
+
|
| 13 |
+
from modules import scripts, shared, processing, sd_schedulers, sd_samplers, script_callbacks, rng
|
| 14 |
+
from modules import images, devices, prompt_parser, sd_models, extra_networks
|
| 15 |
+
from typing import Optional
|
| 16 |
+
|
| 17 |
+
# Optional deps (best-effort)
|
| 18 |
+
def _safe_import(modname, pipname=None):
|
| 19 |
+
try:
|
| 20 |
+
__import__(modname)
|
| 21 |
+
return True
|
| 22 |
+
except Exception:
|
| 23 |
+
try:
|
| 24 |
+
import pip
|
| 25 |
+
if hasattr(pip, "main"):
|
| 26 |
+
pip.main(["install", pipname or modname])
|
| 27 |
+
else:
|
| 28 |
+
pip._internal.main(["install", pipname or modname])
|
| 29 |
+
__import__(modname)
|
| 30 |
+
return True
|
| 31 |
+
except Exception:
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
_safe_import("omegaconf")
|
| 35 |
+
_safe_import("kornia")
|
| 36 |
+
_safe_import("k_diffusion", "k-diffusion")
|
| 37 |
+
_safe_import("skimage")
|
| 38 |
+
_safe_import("cv2")
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from omegaconf import OmegaConf, DictConfig # type: ignore
|
| 42 |
+
except Exception: # graceful fallback if OmegaConf not available
|
| 43 |
+
class DictConfig(dict): # minimal stub
|
| 44 |
+
pass
|
| 45 |
+
class OmegaConf: # minimal stub
|
| 46 |
+
@staticmethod
|
| 47 |
+
def load(path):
|
| 48 |
+
return DictConfig()
|
| 49 |
+
@staticmethod
|
| 50 |
+
def create(obj):
|
| 51 |
+
return DictConfig(obj)
|
| 52 |
+
|
| 53 |
+
import kornia # type: ignore
|
| 54 |
+
import k_diffusion as K # type: ignore
|
| 55 |
+
|
| 56 |
+
# skimage helpers (optional)
|
| 57 |
+
try:
|
| 58 |
+
from skimage.exposure import match_histograms, equalize_adapthist # type: ignore
|
| 59 |
+
from skimage import color as skcolor # type: ignore
|
| 60 |
+
_SKIMAGE_OK = True
|
| 61 |
+
except Exception:
|
| 62 |
+
_SKIMAGE_OK = False
|
| 63 |
+
|
| 64 |
+
# OpenCV (optional)
|
| 65 |
+
try:
|
| 66 |
+
import cv2 # type: ignore
|
| 67 |
+
_CV2_OK = True
|
| 68 |
+
except Exception:
|
| 69 |
+
_CV2_OK = False
|
| 70 |
+
|
| 71 |
+
quote_swap = str.maketrans("\'\"", "\"\'")
|
| 72 |
+
config_path = (Path(__file__).parent.resolve() / "../config.yaml").resolve()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class CustomHiresFix(scripts.Script):
|
| 76 |
+
"""Two-stage img2img upscaling with optional latent mixing and prompt overrides.
|
| 77 |
+
|
| 78 |
+
Features:
|
| 79 |
+
- Ratio/width/height or Megapixels target (+ quick MP buttons)
|
| 80 |
+
- Compact preset panel (global presets)
|
| 81 |
+
- Separate steps for 1st/2nd pass
|
| 82 |
+
- Per-pass sampler + scheduler
|
| 83 |
+
- CFG base + optional delta on 2nd pass
|
| 84 |
+
- Reuse seed/noise on 2nd pass
|
| 85 |
+
- Conditioning cache (LRU) with capacity
|
| 86 |
+
- Second-pass prompt (append/replace)
|
| 87 |
+
- Per-pass LoRA weight scaling
|
| 88 |
+
- Seamless tiling (+ overlap)
|
| 89 |
+
- VAE tiling toggle (low VRAM)
|
| 90 |
+
- Color match to original (strength) with presets
|
| 91 |
+
- Post-FX presets: CLAHE (local contrast), Unsharp Mask
|
| 92 |
+
- PNG-info serialization + paste support
|
| 93 |
+
- Final ×4 upscale (optional), with its own upscaler and tiling overlap for seam-safe stitching.
|
| 94 |
+
"""
|
| 95 |
+
def __init__(self):
|
| 96 |
+
super().__init__()
|
| 97 |
+
# Load or init config
|
| 98 |
+
if config_path.exists():
|
| 99 |
+
try:
|
| 100 |
+
self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore
|
| 101 |
+
except Exception:
|
| 102 |
+
self.config = OmegaConf.create({}) # type: ignore
|
| 103 |
+
else:
|
| 104 |
+
self.config = OmegaConf.create({}) # type: ignore
|
| 105 |
+
|
| 106 |
+
# Runtime state
|
| 107 |
+
self.p = None
|
| 108 |
+
self.pp = None
|
| 109 |
+
self.cfg = 0.0
|
| 110 |
+
self.cond = None
|
| 111 |
+
self.uncond = None
|
| 112 |
+
self.width = None
|
| 113 |
+
self.height = None
|
| 114 |
+
self._orig_clip_skip = None
|
| 115 |
+
self._cn_units = []
|
| 116 |
+
self._use_cn = False
|
| 117 |
+
|
| 118 |
+
# Reuse state
|
| 119 |
+
self._saved_seeds = None
|
| 120 |
+
# subseeds may not exist in all pipelines; keep best-effort
|
| 121 |
+
self._saved_subseeds = None
|
| 122 |
+
self._saved_subseed_strength = None
|
| 123 |
+
self._saved_seed_resize_from_h = None
|
| 124 |
+
self._saved_seed_resize_from_w = None
|
| 125 |
+
self._first_noise = None
|
| 126 |
+
self._first_noise_shape = None
|
| 127 |
+
|
| 128 |
+
# Conditioning cache (LRU)
|
| 129 |
+
self._cond_cache: OrderedDict[str, tuple] = OrderedDict()
|
| 130 |
+
|
| 131 |
+
# VAE tiling state restore
|
| 132 |
+
self._orig_opt_vae_tiling = None
|
| 133 |
+
|
| 134 |
+
# Seamless tiling restore
|
| 135 |
+
self._orig_tiling = None
|
| 136 |
+
self._orig_tile_overlap = None
|
| 137 |
+
|
| 138 |
+
# Prompt override for second pass
|
| 139 |
+
self._override_prompt_second = None
|
| 140 |
+
|
| 141 |
+
# LoRA scaling factor per pass (used during _prepare_conditioning by pass context)
|
| 142 |
+
self._current_lora_factor = 1.0
|
| 143 |
+
# Scheduler restore state
|
| 144 |
+
self._orig_scheduler = None
|
| 145 |
+
self._orig_size = (None, None)
|
| 146 |
+
|
| 147 |
+
def _apply_token_merging(self, *, for_hr: bool = False, halve: bool = False):
|
| 148 |
+
"""Safely apply token merging ratio across webui versions."""
|
| 149 |
+
ratio_fn = getattr(self.p, "get_token_merging_ratio", None)
|
| 150 |
+
r: float = 0.0
|
| 151 |
+
if callable(ratio_fn):
|
| 152 |
+
try:
|
| 153 |
+
r = float(ratio_fn(for_hr=for_hr))
|
| 154 |
+
except TypeError:
|
| 155 |
+
r = float(ratio_fn())
|
| 156 |
+
except Exception:
|
| 157 |
+
r = 0.0
|
| 158 |
+
if halve:
|
| 159 |
+
r = r / 2.0
|
| 160 |
+
try:
|
| 161 |
+
sd_models.apply_token_merging(self.p.sd_model, r)
|
| 162 |
+
except Exception:
|
| 163 |
+
pass
|
| 164 |
+
|
| 165 |
+
def _set_scheduler_by_label(self, label_or_obj):
|
| 166 |
+
"""
|
| 167 |
+
Безопасно устанавливает планировщик по его видимому label.
|
| 168 |
+
На новых версиях — объект из sd_schedulers.schedulers,
|
| 169 |
+
на старых — откат к строке (как было).
|
| 170 |
+
"""
|
| 171 |
+
if not label_or_obj or label_or_obj == "Use same scheduler":
|
| 172 |
+
return
|
| 173 |
+
# Нормализуем к СТРОКЕ (ключу в schedulers_map)
|
| 174 |
+
if isinstance(label_or_obj, str):
|
| 175 |
+
label = label_or_obj
|
| 176 |
+
else:
|
| 177 |
+
label = getattr(label_or_obj, "label", getattr(label_or_obj, "name", str(label_or_obj)))
|
| 178 |
+
try:
|
| 179 |
+
# Если такого ключа нет, попробуем найти объект по label/name и взять его .label
|
| 180 |
+
sched_map = getattr(sd_schedulers, "schedulers_map", {})
|
| 181 |
+
if getattr(sched_map, "get", None) and sched_map.get(label) is None:
|
| 182 |
+
for s in getattr(sd_schedulers, "schedulers", []):
|
| 183 |
+
if getattr(s, "label", None) == label or getattr(s, "name", None) == label:
|
| 184 |
+
label = getattr(s, "label", label)
|
| 185 |
+
break
|
| 186 |
+
finally:
|
| 187 |
+
# ВАЖНО: всегда строка — иначе get_sigmas падает на unhashable
|
| 188 |
+
self.p.scheduler = label
|
| 189 |
+
|
| 190 |
+
# ---- A1111 Script API ----
|
| 191 |
+
def title(self):
|
| 192 |
+
return "Custom Hires Fix"
|
| 193 |
+
|
| 194 |
+
def show(self, is_img2img):
|
| 195 |
+
return scripts.AlwaysVisible
|
| 196 |
+
|
| 197 |
+
def ui(self, is_img2img):
|
| 198 |
+
visible_names = [x.name for x in sd_samplers.visible_samplers()]
|
| 199 |
+
sampler_names = ["Restart + DPM++ 3M SDE"] + visible_names
|
| 200 |
+
_scheds = getattr(sd_schedulers, "schedulers", [])
|
| 201 |
+
scheduler_names = ["Use same scheduler"] + [
|
| 202 |
+
getattr(x, "label", getattr(x, "name", str(x))) for x in _scheds]
|
| 203 |
+
|
| 204 |
+
with gr.Accordion(label="Custom Hires Fix", open=False) as enable_box:
|
| 205 |
+
enable = gr.Checkbox(label="Enable extension", value=bool(self.config.get("enable", False)))
|
| 206 |
+
|
| 207 |
+
# ---------- Compact preset panel ----------
|
| 208 |
+
with gr.Row():
|
| 209 |
+
quick_preset = gr.Dropdown(
|
| 210 |
+
["None", "Hi-Res Portrait", "Hi-Res Texture", "Hi-Res Illustration", "Hi-Res Product Shot"],
|
| 211 |
+
label="Quick preset",
|
| 212 |
+
value="None"
|
| 213 |
+
)
|
| 214 |
+
btn_apply_preset = gr.Button(value="Apply preset", variant="primary")
|
| 215 |
+
|
| 216 |
+
btn_mp_1 = gr.Button(value="MP 1.0")
|
| 217 |
+
btn_mp_2 = gr.Button(value="MP 2.0")
|
| 218 |
+
btn_mp_4 = gr.Button(value="MP 4.0")
|
| 219 |
+
btn_mp_8 = gr.Button(value="MP 8.0")
|
| 220 |
+
|
| 221 |
+
with gr.Row():
|
| 222 |
+
ratio = gr.Slider(minimum=0.0, maximum=4.0, step=0.05, label="Upscale by (ratio)",
|
| 223 |
+
value=float(self.config.get("ratio", 0.0)))
|
| 224 |
+
width = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize width to",
|
| 225 |
+
value=int(self.config.get("width", 0)))
|
| 226 |
+
height = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize height to",
|
| 227 |
+
value=int(self.config.get("height", 0)))
|
| 228 |
+
# --------- Size helpers ---------
|
| 229 |
+
with gr.Row():
|
| 230 |
+
long_edge = gr.Slider(minimum=0, maximum=8192, step=8,
|
| 231 |
+
label="Resize by long edge (0 = off)",
|
| 232 |
+
value=int(self.config.get("long_edge", 0)))
|
| 233 |
+
btn_swap_wh = gr.Button(value="Swap W↔H")
|
| 234 |
+
|
| 235 |
+
with gr.Row():
|
| 236 |
+
steps_first = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 1st pass",
|
| 237 |
+
value=int(self.config.get("steps_first", max(1, int(self.config.get("steps", 20))))))
|
| 238 |
+
steps_second = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 2nd pass",
|
| 239 |
+
value=int(self.config.get("steps_second", int(self.config.get("steps", 20)))))
|
| 240 |
+
|
| 241 |
+
# --------- Per-pass denoising ---------
|
| 242 |
+
with gr.Row():
|
| 243 |
+
denoise_first = gr.Slider(minimum=0.0, maximum=1.0, step=0.01,
|
| 244 |
+
label="Denoising strength — 1st pass",
|
| 245 |
+
value=float(self.config.get("denoise_first", 0.33)))
|
| 246 |
+
denoise_second = gr.Slider(minimum=0.0, maximum=1.0, step=0.01,
|
| 247 |
+
label="Denoising strength — 2nd pass",
|
| 248 |
+
value=float(self.config.get("denoise_second", 0.45)))
|
| 249 |
+
with gr.Row():
|
| 250 |
+
first_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
|
| 251 |
+
label="First upscaler", value=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
|
| 252 |
+
second_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
|
| 253 |
+
label="Second upscaler", value=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
|
| 254 |
+
|
| 255 |
+
with gr.Row():
|
| 256 |
+
first_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (first stage)",
|
| 257 |
+
value=float(self.config.get("first_latent", 0.3)))
|
| 258 |
+
second_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (second stage)",
|
| 259 |
+
value=float(self.config.get("second_latent", 0.1)))
|
| 260 |
+
|
| 261 |
+
with gr.Row():
|
| 262 |
+
filter_mode = gr.Dropdown(["Noise sync (sharp)", "Morphological (smooth)", "Combined (balanced)"],
|
| 263 |
+
label="Filter mode", value=self.config.get("filter_mode", "Noise sync (sharp)"))
|
| 264 |
+
strength = gr.Slider(minimum=0.5, maximum=4.0, step=0.1, label="Generation strength",
|
| 265 |
+
value=float(self.config.get("strength", 2.0)))
|
| 266 |
+
denoise_offset = gr.Slider(minimum=-0.1, maximum=0.2, step=0.01, label="Denoise offset",
|
| 267 |
+
value=float(self.config.get("denoise_offset", 0.05)))
|
| 268 |
+
# NEW: чекбокс включения адаптивной формы сигм
|
| 269 |
+
adaptive_sigma_enable = gr.Checkbox(label="Adaptive denoiser shaping (uses Filter/Strength)",
|
| 270 |
+
value=bool(self.config.get("adaptive_sigma_enable", False)))
|
| 271 |
+
|
| 272 |
+
with gr.Row():
|
| 273 |
+
prompt = gr.Textbox(label="Prompt override (1st pass)", placeholder="Leave empty to use main UI prompt",
|
| 274 |
+
value=self.config.get("prompt", ""))
|
| 275 |
+
negative_prompt = gr.Textbox(label="Negative prompt override", placeholder="Leave empty to use main UI negative prompt",
|
| 276 |
+
value=self.config.get("negative_prompt", ""))
|
| 277 |
+
|
| 278 |
+
with gr.Row():
|
| 279 |
+
second_pass_prompt = gr.Textbox(label="Second-pass prompt", placeholder="Append or replace on 2nd pass",
|
| 280 |
+
value=self.config.get("second_pass_prompt", ""))
|
| 281 |
+
second_pass_prompt_append = gr.Checkbox(label="Append instead of replace",
|
| 282 |
+
value=bool(self.config.get("second_pass_prompt_append", True)))
|
| 283 |
+
|
| 284 |
+
with gr.Accordion(label="Extra", open=False):
|
| 285 |
+
with gr.Row():
|
| 286 |
+
filter_offset = gr.Slider(minimum=-1.0, maximum=1.0, step=0.1, label="Filter offset",
|
| 287 |
+
value=float(self.config.get("filter_offset", 0.0)))
|
| 288 |
+
clip_skip = gr.Slider(minimum=0, maximum=12, step=1, label="CLIP skip (0 = keep)",
|
| 289 |
+
value=int(self.config.get("clip_skip", 0)))
|
| 290 |
+
|
| 291 |
+
# Per-pass sampler/scheduler
|
| 292 |
+
with gr.Row():
|
| 293 |
+
sampler_first = gr.Dropdown(sampler_names, label="Sampler — 1st pass",
|
| 294 |
+
value=self.config.get("sampler_first", sampler_names[0]))
|
| 295 |
+
sampler_second = gr.Dropdown(sampler_names, label="Sampler — 2nd pass",
|
| 296 |
+
value=self.config.get("sampler_second", self.config.get("sampler", sampler_names[0])))
|
| 297 |
+
with gr.Row():
|
| 298 |
+
scheduler_first = gr.Dropdown(
|
| 299 |
+
choices=scheduler_names, label="Schedule type — 1st pass",
|
| 300 |
+
value=self.config.get("scheduler_first", self.config.get("scheduler", scheduler_names[0]))
|
| 301 |
+
)
|
| 302 |
+
scheduler_second = gr.Dropdown(
|
| 303 |
+
choices=scheduler_names, label="Schedule type — 2nd pass",
|
| 304 |
+
value=self.config.get("scheduler_second", self.config.get("scheduler", scheduler_names[0]))
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
# Restore scheduler toggle
|
| 308 |
+
restore_scheduler_after = gr.Checkbox(
|
| 309 |
+
label="Restore scheduler after run",
|
| 310 |
+
value=bool(self.config.get("restore_scheduler_after", True))
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
with gr.Row():
|
| 314 |
+
cfg = gr.Slider(minimum=0, maximum=30, step=0.5, label="CFG Scale (base)",
|
| 315 |
+
value=float(self.config.get("cfg", 7.0)))
|
| 316 |
+
cfg_second_pass_boost = gr.Checkbox(label="Enable CFG delta on 2nd pass",
|
| 317 |
+
value=bool(self.config.get("cfg_second_pass_boost", True)))
|
| 318 |
+
cfg_second_pass_delta = gr.Slider(minimum=-5.0, maximum=5.0, step=0.5, label="CFG delta (2nd pass)",
|
| 319 |
+
value=float(self.config.get("cfg_second_pass_delta", 3.0)))
|
| 320 |
+
|
| 321 |
+
# Reuse seed/noise + Megapixels target
|
| 322 |
+
with gr.Row():
|
| 323 |
+
reuse_seed_noise = gr.Checkbox(label="Reuse seed/noise on 2nd pass",
|
| 324 |
+
value=bool(self.config.get("reuse_seed_noise", False)))
|
| 325 |
+
mp_target_enabled = gr.Checkbox(label="Enable Megapixels target",
|
| 326 |
+
value=bool(self.config.get("mp_target_enabled", False)))
|
| 327 |
+
mp_target = gr.Slider(minimum=0.3, maximum=16.0, step=0.1, label="Megapixels",
|
| 328 |
+
value=float(self.config.get("mp_target", 2.0)))
|
| 329 |
+
|
| 330 |
+
# Conditioning cache controls
|
| 331 |
+
with gr.Row():
|
| 332 |
+
cond_cache_enabled = gr.Checkbox(label="Enable conditioning cache (LRU)",
|
| 333 |
+
value=bool(self.config.get("cond_cache_enabled", True)))
|
| 334 |
+
cond_cache_max = gr.Slider(minimum=8, maximum=256, step=8, label="Conditioning cache size",
|
| 335 |
+
value=int(self.config.get("cond_cache_max", 64)))
|
| 336 |
+
|
| 337 |
+
# VAE tiling
|
| 338 |
+
with gr.Row():
|
| 339 |
+
vae_tiling_enabled = gr.Checkbox(label="Enable VAE tiling (low VRAM)",
|
| 340 |
+
value=bool(self.config.get("vae_tiling_enabled", False)))
|
| 341 |
+
|
| 342 |
+
# Seamless tiling
|
| 343 |
+
with gr.Row():
|
| 344 |
+
seamless_tiling_enabled = gr.Checkbox(label="Seamless tiling (texture)",
|
| 345 |
+
value=bool(self.config.get("seamless_tiling_enabled", False)))
|
| 346 |
+
tile_overlap = gr.Slider(minimum=0, maximum=64, step=1, label="Tile overlap (px)",
|
| 347 |
+
value=int(self.config.get("tile_overlap", 12)))
|
| 348 |
+
|
| 349 |
+
# LoRA scaling
|
| 350 |
+
with gr.Row():
|
| 351 |
+
lora_weight_first_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (1st pass)",
|
| 352 |
+
value=float(self.config.get("lora_weight_first_factor", 1.0)))
|
| 353 |
+
lora_weight_second_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (2nd pass)",
|
| 354 |
+
value=float(self.config.get("lora_weight_second_factor", 1.0)))
|
| 355 |
+
|
| 356 |
+
# Match colors presets & controls
|
| 357 |
+
with gr.Row():
|
| 358 |
+
match_colors_preset = gr.Dropdown(
|
| 359 |
+
["Off", "Subtle (0.3)", "Natural (0.5)", "Strong (0.8)"],
|
| 360 |
+
label="Match colors preset",
|
| 361 |
+
value=self.config.get("match_colors_preset", "Off")
|
| 362 |
+
)
|
| 363 |
+
match_colors_enabled = gr.Checkbox(label="Match colors to original",
|
| 364 |
+
value=bool(self.config.get("match_colors_enabled", False)))
|
| 365 |
+
match_colors_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label="Match strength",
|
| 366 |
+
value=float(self.config.get("match_colors_strength", 0.5)))
|
| 367 |
+
|
| 368 |
+
# Post-processing presets & controls
|
| 369 |
+
with gr.Row():
|
| 370 |
+
postfx_preset = gr.Dropdown(
|
| 371 |
+
["Off", "Soft clarity", "Portrait safe", "Texture boost", "Crisp detail"],
|
| 372 |
+
label="Post-FX preset",
|
| 373 |
+
value=self.config.get("postfx_preset", "Off")
|
| 374 |
+
)
|
| 375 |
+
clahe_enabled = gr.Checkbox(label="CLAHE (local contrast)",
|
| 376 |
+
value=bool(self.config.get("clahe_enabled", False)))
|
| 377 |
+
clahe_clip = gr.Slider(minimum=1.0, maximum=5.0, step=0.1, label="CLAHE clip limit",
|
| 378 |
+
value=float(self.config.get("clahe_clip", 2.0)))
|
| 379 |
+
clahe_tile_grid = gr.Slider(minimum=4, maximum=16, step=2, label="CLAHE tile grid",
|
| 380 |
+
value=int(self.config.get("clahe_tile_grid", 8)))
|
| 381 |
+
|
| 382 |
+
with gr.Row():
|
| 383 |
+
unsharp_enabled = gr.Checkbox(label="Unsharp Mask (sharpen)",
|
| 384 |
+
value=bool(self.config.get("unsharp_enabled", False)))
|
| 385 |
+
unsharp_radius = gr.Slider(minimum=0.5, maximum=5.0, step=0.1, label="Unsharp radius",
|
| 386 |
+
value=float(self.config.get("unsharp_radius", 1.5)))
|
| 387 |
+
unsharp_amount = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="Unsharp amount",
|
| 388 |
+
value=float(self.config.get("unsharp_amount", 0.75)))
|
| 389 |
+
unsharp_threshold = gr.Slider(minimum=0, maximum=10, step=1, label="Unsharp threshold",
|
| 390 |
+
value=int(self.config.get("unsharp_threshold", 0)))
|
| 391 |
+
|
| 392 |
+
with gr.Row():
|
| 393 |
+
cn_ref = gr.Checkbox(label="Use last image as ControlNet reference", value=bool(self.config.get("cn_ref", False)))
|
| 394 |
+
start_control_at = gr.Slider(minimum=0.0, maximum=0.7, step=0.01, label="CN start (enabled units)",
|
| 395 |
+
value=float(self.config.get("start_control_at", 0.0)))
|
| 396 |
+
|
| 397 |
+
# --- Final ×4 upscale controls ---
|
| 398 |
+
with gr.Row():
|
| 399 |
+
final_upscale_enable = gr.Checkbox(label="Final ×4 upscale (after 2nd pass)",
|
| 400 |
+
value=bool(self.config.get("final_upscale_enable", False)))
|
| 401 |
+
final_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
|
| 402 |
+
label="Final upscaler", value=self.config.get("final_upscaler", "R-ESRGAN 4x+"))
|
| 403 |
+
with gr.Row():
|
| 404 |
+
final_tile = gr.Slider(minimum=128, maximum=1024, step=32, label="Final tile size (px, pre-scale)",
|
| 405 |
+
value=int(self.config.get("final_tile", 512)))
|
| 406 |
+
final_tile_overlap = gr.Slider(minimum=0, maximum=64, step=2, label="Final tile overlap (px, pre-scale)",
|
| 407 |
+
value=int(self.config.get("final_tile_overlap", 16)))
|
| 408 |
+
|
| 409 |
+
# ---------- Preset logic (UI events) ----------
|
| 410 |
+
def _apply_match_preset(preset_name):
|
| 411 |
+
if preset_name == "Off":
|
| 412 |
+
return (gr.update(value=False), gr.update(value=0.5))
|
| 413 |
+
if preset_name == "Subtle (0.3)":
|
| 414 |
+
return (gr.update(value=True), gr.update(value=0.3))
|
| 415 |
+
if preset_name == "Natural (0.5)":
|
| 416 |
+
return (gr.update(value=True), gr.update(value=0.5))
|
| 417 |
+
if preset_name == "Strong (0.8)":
|
| 418 |
+
return (gr.update(value=True), gr.update(value=0.8))
|
| 419 |
+
return (gr.update(), gr.update())
|
| 420 |
+
|
| 421 |
+
def _apply_postfx_preset(preset_name):
|
| 422 |
+
# Returns: clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold
|
| 423 |
+
if preset_name == "Off":
|
| 424 |
+
return (gr.update(value=False), gr.update(value=2.0), gr.update(value=8),
|
| 425 |
+
gr.update(value=False), gr.update(value=1.5), gr.update(value=0.75), gr.update(value=0))
|
| 426 |
+
if preset_name == "Soft clarity":
|
| 427 |
+
return (gr.update(value=True), gr.update(value=1.8), gr.update(value=8),
|
| 428 |
+
gr.update(value=True), gr.update(value=1.2), gr.update(value=0.6), gr.update(value=0))
|
| 429 |
+
if preset_name == "Portrait safe":
|
| 430 |
+
return (gr.update(value=True), gr.update(value=1.6), gr.update(value=8),
|
| 431 |
+
gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2))
|
| 432 |
+
if preset_name == "Texture boost":
|
| 433 |
+
return (gr.update(value=True), gr.update(value=2.4), gr.update(value=8),
|
| 434 |
+
gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0))
|
| 435 |
+
if preset_name == "Crisp detail":
|
| 436 |
+
return (gr.update(value=True), gr.update(value=2.1), gr.update(value=8),
|
| 437 |
+
gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0))
|
| 438 |
+
return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
|
| 439 |
+
|
| 440 |
+
def _apply_quick_preset(name):
|
| 441 |
+
# Returns a large tuple of updates for several controls
|
| 442 |
+
out = [
|
| 443 |
+
gr.update(), # steps_first
|
| 444 |
+
gr.update(), # steps_second
|
| 445 |
+
gr.update(), # cfg_second_pass_boost
|
| 446 |
+
gr.update(), # cfg_second_pass_delta
|
| 447 |
+
gr.update(), # sampler_first
|
| 448 |
+
gr.update(), # sampler_second
|
| 449 |
+
gr.update(), # scheduler_first
|
| 450 |
+
gr.update(), # scheduler_second
|
| 451 |
+
gr.update(), # vae_tiling_enabled
|
| 452 |
+
gr.update(), # seamless_tiling_enabled
|
| 453 |
+
gr.update(), # tile_overlap
|
| 454 |
+
gr.update(), # match_colors_preset
|
| 455 |
+
gr.update(), # match_colors_enabled
|
| 456 |
+
gr.update(), # match_colors_strength
|
| 457 |
+
gr.update(), # postfx_preset
|
| 458 |
+
gr.update(), # clahe_enabled
|
| 459 |
+
gr.update(), # clahe_clip
|
| 460 |
+
gr.update(), # clahe_tile_grid
|
| 461 |
+
gr.update(), # unsharp_enabled
|
| 462 |
+
gr.update(), # unsharp_radius
|
| 463 |
+
gr.update(), # unsharp_amount
|
| 464 |
+
gr.update(), # unsharp_threshold
|
| 465 |
+
gr.update(), # reuse_seed_noise
|
| 466 |
+
gr.update(), # cond_cache_max
|
| 467 |
+
gr.update(), # lora_weight_first_factor
|
| 468 |
+
gr.update(), # lora_weight_second_factor
|
| 469 |
+
gr.update(), # mp_target_enabled
|
| 470 |
+
gr.update(), # mp_target
|
| 471 |
+
]
|
| 472 |
+
if name == "Hi-Res Portrait":
|
| 473 |
+
out = [
|
| 474 |
+
gr.update(value=18), gr.update(value=28),
|
| 475 |
+
gr.update(value=True), gr.update(value=2.5),
|
| 476 |
+
gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"),
|
| 477 |
+
gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
|
| 478 |
+
gr.update(value=True), gr.update(value=False), gr.update(value=12),
|
| 479 |
+
gr.update(value="Subtle (0.3)"), gr.update(value=True), gr.update(value=0.3),
|
| 480 |
+
gr.update(value="Portrait safe"), gr.update(value=True), gr.update(value=1.6), gr.update(value=8),
|
| 481 |
+
gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2),
|
| 482 |
+
gr.update(value=True), gr.update(value=64),
|
| 483 |
+
gr.update(value=1.0), gr.update(value=1.1),
|
| 484 |
+
gr.update(value=True), gr.update(value=2.0),
|
| 485 |
+
]
|
| 486 |
+
elif name == "Hi-Res Texture":
|
| 487 |
+
out = [
|
| 488 |
+
gr.update(value=14), gr.update(value=22),
|
| 489 |
+
gr.update(value=True), gr.update(value=3.0),
|
| 490 |
+
gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"),
|
| 491 |
+
gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
|
| 492 |
+
gr.update(value=True), gr.update(value=True), gr.update(value=12),
|
| 493 |
+
gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5),
|
| 494 |
+
gr.update(value="Texture boost"), gr.update(value=True), gr.update(value=2.2), gr.update(value=8),
|
| 495 |
+
gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0),
|
| 496 |
+
gr.update(value=True), gr.update(value=128),
|
| 497 |
+
gr.update(value=0.9), gr.update(value=1.25),
|
| 498 |
+
gr.update(value=True), gr.update(value=4.0),
|
| 499 |
+
]
|
| 500 |
+
elif name == "Hi-Res Illustration":
|
| 501 |
+
out = [
|
| 502 |
+
gr.update(value=16), gr.update(value=24),
|
| 503 |
+
gr.update(value=True), gr.update(value=2.0),
|
| 504 |
+
gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"),
|
| 505 |
+
gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
|
| 506 |
+
gr.update(value=True), gr.update(value=False), gr.update(value=8),
|
| 507 |
+
gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5),
|
| 508 |
+
gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.0), gr.update(value=8),
|
| 509 |
+
gr.update(value=True), gr.update(value=1.2), gr.update(value=0.9), gr.update(value=0),
|
| 510 |
+
gr.update(value=True), gr.update(value=64),
|
| 511 |
+
gr.update(value=0.85), gr.update(value=1.2),
|
| 512 |
+
gr.update(value=True), gr.update(value=2.0),
|
| 513 |
+
]
|
| 514 |
+
elif name == "Hi-Res Product Shot":
|
| 515 |
+
out = [
|
| 516 |
+
gr.update(value=18), gr.update(value=26),
|
| 517 |
+
gr.update(value=True), gr.update(value=2.0),
|
| 518 |
+
gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"),
|
| 519 |
+
gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
|
| 520 |
+
gr.update(value=True), gr.update(value=False), gr.update(value=8),
|
| 521 |
+
gr.update(value="Natural (0.5)"), gr.update(value=True), gr.update(value=0.5),
|
| 522 |
+
gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.1), gr.update(value=8),
|
| 523 |
+
gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0),
|
| 524 |
+
gr.update(value=True), gr.update(value=96),
|
| 525 |
+
gr.update(value=1.0), gr.update(value=1.15),
|
| 526 |
+
gr.update(value=True), gr.update(value=2.0),
|
| 527 |
+
]
|
| 528 |
+
return tuple(out)
|
| 529 |
+
|
| 530 |
+
# --- Привязка событий пресетов ---
|
| 531 |
+
match_colors_preset.change(
|
| 532 |
+
fn=_apply_match_preset,
|
| 533 |
+
inputs=[match_colors_preset],
|
| 534 |
+
outputs=[match_colors_enabled, match_colors_strength]
|
| 535 |
+
)
|
| 536 |
+
postfx_preset.change(
|
| 537 |
+
fn=_apply_postfx_preset,
|
| 538 |
+
inputs=[postfx_preset],
|
| 539 |
+
outputs=[clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold]
|
| 540 |
+
)
|
| 541 |
+
btn_apply_preset.click(
|
| 542 |
+
fn=_apply_quick_preset,
|
| 543 |
+
inputs=[quick_preset],
|
| 544 |
+
outputs=[
|
| 545 |
+
steps_first, steps_second,
|
| 546 |
+
cfg_second_pass_boost, cfg_second_pass_delta,
|
| 547 |
+
sampler_first, sampler_second,
|
| 548 |
+
scheduler_first, scheduler_second,
|
| 549 |
+
vae_tiling_enabled, seamless_tiling_enabled, tile_overlap,
|
| 550 |
+
match_colors_preset, match_colors_enabled, match_colors_strength,
|
| 551 |
+
postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
|
| 552 |
+
unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
|
| 553 |
+
reuse_seed_noise, cond_cache_max,
|
| 554 |
+
lora_weight_first_factor, lora_weight_second_factor,
|
| 555 |
+
mp_target_enabled, mp_target
|
| 556 |
+
]
|
| 557 |
+
)
|
| 558 |
+
# MP buttons
|
| 559 |
+
btn_mp_1.click(fn=lambda: (gr.update(value=True), gr.update(value=1.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
|
| 560 |
+
btn_mp_2.click(fn=lambda: (gr.update(value=True), gr.update(value=2.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
|
| 561 |
+
btn_mp_4.click(fn=lambda: (gr.update(value=True), gr.update(value=4.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
|
| 562 |
+
btn_mp_8.click(fn=lambda: (gr.update(value=True), gr.update(value=8.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
|
| 563 |
+
|
| 564 |
+
# Exclusivity helpers (STRICT two-mode: exact W×H OR pure ratio)
|
| 565 |
+
# Изменение любой стороны отключает ratio, НО не обнуляет вторую сторону — можно задать обе.
|
| 566 |
+
width.change(fn=lambda _: (gr.update(value=0.0), gr.update(value=False)),
|
| 567 |
+
inputs=width, outputs=[ratio, mp_target_enabled])
|
| 568 |
+
height.change(fn=lambda _: (gr.update(value=0.0), gr.update(value=False)),
|
| 569 |
+
inputs=height, outputs=[ratio, mp_target_enabled])
|
| 570 |
+
ratio.change(fn=lambda _: (gr.update(value=0), gr.update(value=0), gr.update(value=False)),
|
| 571 |
+
inputs=ratio, outputs=[width, height, mp_target_enabled])
|
| 572 |
+
# Long edge excludes ratio and explicit W/H
|
| 573 |
+
long_edge.change(fn=lambda _: (gr.update(value=0), gr.update(value=0), gr.update(value=0.0), gr.update(value=False)),
|
| 574 |
+
inputs=long_edge, outputs=[width, height, ratio, mp_target_enabled])
|
| 575 |
+
# Swap button
|
| 576 |
+
btn_swap_wh.click(fn=lambda w, h: (h, w), inputs=[width, height], outputs=[width, height])
|
| 577 |
+
|
| 578 |
+
# infotext paste support
|
| 579 |
+
def read_params(d, key, default=None):
|
| 580 |
+
try:
|
| 581 |
+
return d["Custom Hires Fix"].get(key, default)
|
| 582 |
+
except Exception:
|
| 583 |
+
return default
|
| 584 |
+
|
| 585 |
+
self.infotext_fields = [
|
| 586 |
+
(enable, lambda d: "Custom Hires Fix" in d),
|
| 587 |
+
(ratio, lambda d: read_params(d, "ratio", 0.0)),
|
| 588 |
+
(width, lambda d: read_params(d, "width", 0)),
|
| 589 |
+
(height, lambda d: read_params(d, "height", 0)),
|
| 590 |
+
(long_edge, lambda d: read_params(d, "long_edge", 0)),
|
| 591 |
+
(steps_first, lambda d: read_params(d, "steps_first", read_params(d, "steps", 20))),
|
| 592 |
+
(steps_second, lambda d: read_params(d, "steps_second", read_params(d, "steps", 20))),
|
| 593 |
+
(denoise_first, lambda d: read_params(d, "denoise_first", 0.33)),
|
| 594 |
+
(denoise_second, lambda d: read_params(d, "denoise_second", 0.45)),
|
| 595 |
+
(first_upscaler, lambda d: read_params(d, "first_upscaler")),
|
| 596 |
+
(second_upscaler, lambda d: read_params(d, "second_upscaler")),
|
| 597 |
+
(first_latent, lambda d: read_params(d, "first_latent", 0.0)),
|
| 598 |
+
(second_latent, lambda d: read_params(d, "second_latent", 0.0)),
|
| 599 |
+
(prompt, lambda d: read_params(d, "prompt", "")),
|
| 600 |
+
(negative_prompt, lambda d: read_params(d, "negative_prompt", "")),
|
| 601 |
+
(second_pass_prompt, lambda d: read_params(d, "second_pass_prompt", "")),
|
| 602 |
+
(second_pass_prompt_append, lambda d: read_params(d, "second_pass_prompt_append", True)),
|
| 603 |
+
(strength, lambda d: read_params(d, "strength", 0.0)),
|
| 604 |
+
(filter_mode, lambda d: read_params(d, "filter_mode")),
|
| 605 |
+
(denoise_offset, lambda d: read_params(d, "denoise_offset", 0.0)),
|
| 606 |
+
(filter_offset, lambda d: read_params(d, "filter_offset", 0.0)),
|
| 607 |
+
(adaptive_sigma_enable, lambda d: read_params(d, "adaptive_sigma_enable", False)),
|
| 608 |
+
(clip_skip, lambda d: read_params(d, "clip_skip", 0)),
|
| 609 |
+
# per-pass samplers/schedulers + legacy fallbacks
|
| 610 |
+
(sampler_first, lambda d: read_params(d, "sampler_first", read_params(d, "sampler", sampler_names[0]))),
|
| 611 |
+
(sampler_second, lambda d: read_params(d, "sampler_second", read_params(d, "sampler", sampler_names[0]))),
|
| 612 |
+
(scheduler_first, lambda d: read_params(d, "scheduler_first", read_params(d, "scheduler", scheduler_names[0]))),
|
| 613 |
+
(scheduler_second, lambda d: read_params(d, "scheduler_second", read_params(d, "scheduler", scheduler_names[0]))),
|
| 614 |
+
(restore_scheduler_after, lambda d: read_params(d, "restore_scheduler_after", True)),
|
| 615 |
+
# cfg/delta
|
| 616 |
+
(cfg, lambda d: read_params(d, "cfg", 7.0)),
|
| 617 |
+
(cfg_second_pass_boost, lambda d: read_params(d, "cfg_second_pass_boost", True)),
|
| 618 |
+
(cfg_second_pass_delta, lambda d: read_params(d, "cfg_second_pass_delta", 3.0)),
|
| 619 |
+
# flags
|
| 620 |
+
(reuse_seed_noise, lambda d: read_params(d, "reuse_seed_noise", False)),
|
| 621 |
+
(mp_target_enabled, lambda d: read_params(d, "mp_target_enabled", False)),
|
| 622 |
+
(mp_target, lambda d: read_params(d, "mp_target", 2.0)),
|
| 623 |
+
(cond_cache_enabled, lambda d: read_params(d, "cond_cache_enabled", True)),
|
| 624 |
+
(cond_cache_max, lambda d: read_params(d, "cond_cache_max", 64)),
|
| 625 |
+
(vae_tiling_enabled, lambda d: read_params(d, "vae_tiling_enabled", False)),
|
| 626 |
+
(seamless_tiling_enabled, lambda d: read_params(d, "seamless_tiling_enabled", False)),
|
| 627 |
+
(tile_overlap, lambda d: read_params(d, "tile_overlap", 12)),
|
| 628 |
+
(lora_weight_first_factor, lambda d: read_params(d, "lora_weight_first_factor", 1.0)),
|
| 629 |
+
(lora_weight_second_factor, lambda d: read_params(d, "lora_weight_second_factor", 1.0)),
|
| 630 |
+
(match_colors_preset, lambda d: read_params(d, "match_colors_preset", "Off")),
|
| 631 |
+
(match_colors_enabled, lambda d: read_params(d, "match_colors_enabled", False)),
|
| 632 |
+
(match_colors_strength, lambda d: read_params(d, "match_colors_strength", 0.5)),
|
| 633 |
+
(postfx_preset, lambda d: read_params(d, "postfx_preset", "Off")),
|
| 634 |
+
(clahe_enabled, lambda d: read_params(d, "clahe_enabled", False)),
|
| 635 |
+
(clahe_clip, lambda d: read_params(d, "clahe_clip", 2.0)),
|
| 636 |
+
(clahe_tile_grid, lambda d: read_params(d, "clahe_tile_grid", 8)),
|
| 637 |
+
(unsharp_enabled, lambda d: read_params(d, "unsharp_enabled", False)),
|
| 638 |
+
(unsharp_radius, lambda d: read_params(d, "unsharp_radius", 1.5)),
|
| 639 |
+
(unsharp_amount, lambda d: read_params(d, "unsharp_amount", 0.75)),
|
| 640 |
+
(unsharp_threshold, lambda d: read_params(d, "unsharp_threshold", 0)),
|
| 641 |
+
(cn_ref, lambda d: read_params(d, "cn_ref", False)),
|
| 642 |
+
(start_control_at, lambda d: read_params(d, "start_control_at", 0.0)),
|
| 643 |
+
# final upscale
|
| 644 |
+
(final_upscale_enable, lambda d: read_params(d, "final_upscale_enable", False)),
|
| 645 |
+
(final_upscaler, lambda d: read_params(d, "final_upscaler", "R-ESRGAN 4x+")),
|
| 646 |
+
(final_tile, lambda d: read_params(d, "final_tile", 512)),
|
| 647 |
+
(final_tile_overlap, lambda d: read_params(d, "final_tile_overlap", 16)),
|
| 648 |
+
]
|
| 649 |
+
|
| 650 |
+
return [
|
| 651 |
+
enable, quick_preset,
|
| 652 |
+
ratio, width, height, long_edge,
|
| 653 |
+
steps_first, steps_second, denoise_first, denoise_second,
|
| 654 |
+
first_upscaler, second_upscaler, first_latent, second_latent,
|
| 655 |
+
prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append,
|
| 656 |
+
strength, filter_mode, filter_offset, denoise_offset, adaptive_sigma_enable,
|
| 657 |
+
sampler_first, sampler_second, scheduler_first, scheduler_second,
|
| 658 |
+
restore_scheduler_after,
|
| 659 |
+
cfg, cfg_second_pass_boost, cfg_second_pass_delta,
|
| 660 |
+
reuse_seed_noise, mp_target_enabled, mp_target,
|
| 661 |
+
cond_cache_enabled, cond_cache_max,
|
| 662 |
+
vae_tiling_enabled,
|
| 663 |
+
seamless_tiling_enabled, tile_overlap,
|
| 664 |
+
lora_weight_first_factor, lora_weight_second_factor,
|
| 665 |
+
match_colors_preset, match_colors_enabled, match_colors_strength,
|
| 666 |
+
postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
|
| 667 |
+
unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
|
| 668 |
+
cn_ref, start_control_at,
|
| 669 |
+
# final upscale
|
| 670 |
+
final_upscale_enable, final_upscaler, final_tile, final_tile_overlap
|
| 671 |
+
]
|
| 672 |
+
|
| 673 |
+
# Capture base processing object and optional ControlNet state
|
| 674 |
+
def process(self, p, *args, **kwargs):
|
| 675 |
+
self.p = p
|
| 676 |
+
# Запомним исходные W/H, если есть
|
| 677 |
+
self._orig_size = (getattr(p, "width", None), getattr(p, "height", None))
|
| 678 |
+
self._cn_units = []
|
| 679 |
+
self._use_cn = False
|
| 680 |
+
self._first_noise = None
|
| 681 |
+
self._first_noise_shape = None
|
| 682 |
+
self._saved_seeds = None
|
| 683 |
+
self._saved_subseeds = None
|
| 684 |
+
self._saved_subseed_strength = None
|
| 685 |
+
self._saved_seed_resize_from_h = None
|
| 686 |
+
self._saved_seed_resize_from_w = None
|
| 687 |
+
self._override_prompt_second = None
|
| 688 |
+
|
| 689 |
+
# Try detect ControlNet (best-effort; path may vary across installs)
|
| 690 |
+
ext_candidates = [
|
| 691 |
+
"extensions.sd_webui_controlnet.scripts.external_code",
|
| 692 |
+
"extensions.sd-webui-controlnet.scripts.external_code",
|
| 693 |
+
"extensions-builtin.sd-webui-controlnet.scripts.external_code",
|
| 694 |
+
]
|
| 695 |
+
self._cn_ext = None
|
| 696 |
+
for mod in ext_candidates:
|
| 697 |
+
try:
|
| 698 |
+
self._cn_ext = __import__(mod, fromlist=["external_code"])
|
| 699 |
+
break
|
| 700 |
+
except Exception:
|
| 701 |
+
continue
|
| 702 |
+
if self._cn_ext:
|
| 703 |
+
try:
|
| 704 |
+
units = self._cn_ext.get_all_units_in_processing(p)
|
| 705 |
+
self._cn_units = list(units) if units else []
|
| 706 |
+
self._use_cn = len(self._cn_units) > 0
|
| 707 |
+
except Exception:
|
| 708 |
+
self._use_cn = False
|
| 709 |
+
|
| 710 |
+
# Log settings into PNG-info (single JSON block)
|
| 711 |
+
def before_process_batch(self, p, *args, **kwargs):
|
| 712 |
+
if not bool(self.config.get("enable", False)):
|
| 713 |
+
return
|
| 714 |
+
p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p)
|
| 715 |
+
|
| 716 |
+
def create_infotext(self, p, *args, **kwargs):
|
| 717 |
+
scale_val = 0
|
| 718 |
+
if int(self.config.get("width", 0)) and int(self.config.get("height", 0)):
|
| 719 |
+
scale_val = f"{int(self.config.get('width'))}x{int(self.config.get('height'))}"
|
| 720 |
+
elif float(self.config.get("ratio", 0)):
|
| 721 |
+
scale_val = float(self.config.get("ratio"))
|
| 722 |
+
|
| 723 |
+
payload = {
|
| 724 |
+
"scale": scale_val,
|
| 725 |
+
"ratio": float(self.config.get("ratio", 0.0)),
|
| 726 |
+
"width": int(self.config.get("width", 0) or 0),
|
| 727 |
+
"height": int(self.config.get("height", 0) or 0),
|
| 728 |
+
"long_edge": int(self.config.get("long_edge", 0)),
|
| 729 |
+
"denoise_first": float(self.config.get("denoise_first", 0.33)),
|
| 730 |
+
"denoise_second": float(self.config.get("denoise_second", 0.45)),
|
| 731 |
+
|
| 732 |
+
"steps_first": int(self.config.get("steps_first", int(self.config.get("steps", 20)))),
|
| 733 |
+
"steps_second": int(self.config.get("steps_second", int(self.config.get("steps", 20)))),
|
| 734 |
+
"steps": int(self.config.get("steps", int(self.config.get("steps_first", 20)))),
|
| 735 |
+
"first_upscaler": self.config.get("first_upscaler", ""),
|
| 736 |
+
"second_upscaler": self.config.get("second_upscaler", ""),
|
| 737 |
+
"first_latent": float(self.config.get("first_latent", 0.3)),
|
| 738 |
+
"second_latent": float(self.config.get("second_latent", 0.1)),
|
| 739 |
+
"prompt": self.config.get("prompt", ""),
|
| 740 |
+
"negative_prompt": self.config.get("negative_prompt", ""),
|
| 741 |
+
"second_pass_prompt": self.config.get("second_pass_prompt", ""),
|
| 742 |
+
"second_pass_prompt_append": bool(self.config.get("second_pass_prompt_append", True)),
|
| 743 |
+
"strength": float(self.config.get("strength", 2.0)),
|
| 744 |
+
"filter_mode": self.config.get("filter_mode", ""),
|
| 745 |
+
"filter_offset": float(self.config.get("filter_offset", 0.0)),
|
| 746 |
+
"denoise_offset": float(self.config.get("denoise_offset", 0.05)),
|
| 747 |
+
"adaptive_sigma_enable": bool(self.config.get("adaptive_sigma_enable", False)),
|
| 748 |
+
"clip_skip": int(self.config.get("clip_skip", 0)),
|
| 749 |
+
# per-pass sampler/scheduler (include legacy for context)
|
| 750 |
+
"sampler_first": self.config.get("sampler_first", ""),
|
| 751 |
+
"sampler_second": self.config.get("sampler_second", self.config.get("sampler", "")),
|
| 752 |
+
"scheduler_first": self.config.get("scheduler_first", self.config.get("scheduler", "")),
|
| 753 |
+
"scheduler_second": self.config.get("scheduler_second", self.config.get("scheduler", "")),
|
| 754 |
+
"restore_scheduler_after": bool(self.config.get("restore_scheduler_after", True)),
|
| 755 |
+
# cfg
|
| 756 |
+
"cfg": float(getattr(p, "cfg_scale", self.cfg)),
|
| 757 |
+
"cfg_second_pass_boost": bool(self.config.get("cfg_second_pass_boost", True)),
|
| 758 |
+
"cfg_second_pass_delta": float(self.config.get("cfg_second_pass_delta", 3.0)),
|
| 759 |
+
# flags
|
| 760 |
+
"reuse_seed_noise": bool(self.config.get("reuse_seed_noise", False)),
|
| 761 |
+
"mp_target_enabled": bool(self.config.get("mp_target_enabled", False)),
|
| 762 |
+
"mp_target": float(self.config.get("mp_target", 2.0)),
|
| 763 |
+
"cond_cache_enabled": bool(self.config.get("cond_cache_enabled", True)),
|
| 764 |
+
"cond_cache_max": int(self.config.get("cond_cache_max", 64)),
|
| 765 |
+
"vae_tiling_enabled": bool(self.config.get("vae_tiling_enabled", False)),
|
| 766 |
+
"seamless_tiling_enabled": bool(self.config.get("seamless_tiling_enabled", False)),
|
| 767 |
+
"tile_overlap": int(self.config.get("tile_overlap", 12)),
|
| 768 |
+
"lora_weight_first_factor": float(self.config.get("lora_weight_first_factor", 1.0)),
|
| 769 |
+
"lora_weight_second_factor": float(self.config.get("lora_weight_second_factor", 1.0)),
|
| 770 |
+
"match_colors_preset": self.config.get("match_colors_preset", "Off"),
|
| 771 |
+
"match_colors_enabled": bool(self.config.get("match_colors_enabled", False)),
|
| 772 |
+
"match_colors_strength": float(self.config.get("match_colors_strength", 0.5)),
|
| 773 |
+
"postfx_preset": self.config.get("postfx_preset", "Off"),
|
| 774 |
+
"clahe_enabled": bool(self.config.get("clahe_enabled", False)),
|
| 775 |
+
"clahe_clip": float(self.config.get("clahe_clip", 2.0)),
|
| 776 |
+
"clahe_tile_grid": int(self.config.get("clahe_tile_grid", 8)),
|
| 777 |
+
"unsharp_enabled": bool(self.config.get("unsharp_enabled", False)),
|
| 778 |
+
"unsharp_radius": float(self.config.get("unsharp_radius", 1.5)),
|
| 779 |
+
"unsharp_amount": float(self.config.get("unsharp_amount", 0.75)),
|
| 780 |
+
"unsharp_threshold": int(self.config.get("unsharp_threshold", 0)),
|
| 781 |
+
"cn_ref": bool(self.config.get("cn_ref", False)),
|
| 782 |
+
"start_control_at": float(self.config.get("start_control_at", 0.0)),
|
| 783 |
+
# final upscale
|
| 784 |
+
"final_upscale_enable": bool(self.config.get("final_upscale_enable", False)),
|
| 785 |
+
"final_upscaler": self.config.get("final_upscaler", "R-ESRGAN 4x+"),
|
| 786 |
+
"final_tile": int(self.config.get("final_tile", 512)),
|
| 787 |
+
"final_tile_overlap": int(self.config.get("final_tile_overlap", 16)),
|
| 788 |
+
}
|
| 789 |
+
return json.dumps(payload, ensure_ascii=False).translate(quote_swap)
|
| 790 |
+
|
| 791 |
+
# --- Main postprocess hook ---
|
| 792 |
+
def postprocess_image(self, p, pp,
|
| 793 |
+
enable, quick_preset,
|
| 794 |
+
ratio, width, height, long_edge,
|
| 795 |
+
steps_first, steps_second, denoise_first, denoise_second,
|
| 796 |
+
first_upscaler, second_upscaler, first_latent, second_latent,
|
| 797 |
+
prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append,
|
| 798 |
+
strength, filter_mode, filter_offset, denoise_offset, adaptive_sigma_enable,
|
| 799 |
+
sampler_first, sampler_second, scheduler_first, scheduler_second,
|
| 800 |
+
restore_scheduler_after,
|
| 801 |
+
cfg, cfg_second_pass_boost, cfg_second_pass_delta,
|
| 802 |
+
reuse_seed_noise, mp_target_enabled, mp_target,
|
| 803 |
+
cond_cache_enabled, cond_cache_max,
|
| 804 |
+
vae_tiling_enabled,
|
| 805 |
+
seamless_tiling_enabled, tile_overlap,
|
| 806 |
+
lora_weight_first_factor, lora_weight_second_factor,
|
| 807 |
+
match_colors_preset, match_colors_enabled, match_colors_strength,
|
| 808 |
+
postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
|
| 809 |
+
unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
|
| 810 |
+
cn_ref, start_control_at,
|
| 811 |
+
final_upscale_enable, final_upscaler, final_tile, final_tile_overlap):
|
| 812 |
+
if not enable:
|
| 813 |
+
return
|
| 814 |
+
|
| 815 |
+
# Save config chosen in UI
|
| 816 |
+
self.pp = pp
|
| 817 |
+
self.config["enable"] = bool(enable)
|
| 818 |
+
self.config["ratio"] = float(ratio)
|
| 819 |
+
self.config["width"] = int(width)
|
| 820 |
+
self.config["height"] = int(height)
|
| 821 |
+
self.config["long_edge"] = int(long_edge)
|
| 822 |
+
self.config["steps_first"] = int(steps_first)
|
| 823 |
+
self.config["steps_second"] = int(steps_second)
|
| 824 |
+
self.config["denoise_first"] = float(denoise_first)
|
| 825 |
+
self.config["denoise_second"] = float(denoise_second)
|
| 826 |
+
self.config["steps"] = int(steps_second) # legacy aggregate
|
| 827 |
+
self.config["first_upscaler"] = first_upscaler
|
| 828 |
+
self.config["second_upscaler"] = second_upscaler
|
| 829 |
+
self.config["first_latent"] = float(first_latent)
|
| 830 |
+
self.config["second_latent"] = float(second_latent)
|
| 831 |
+
self.config["prompt"] = prompt.strip()
|
| 832 |
+
self.config["negative_prompt"] = negative_prompt.strip()
|
| 833 |
+
self.config["second_pass_prompt"] = second_pass_prompt.strip()
|
| 834 |
+
self.config["second_pass_prompt_append"] = bool(second_pass_prompt_append)
|
| 835 |
+
self.config["strength"] = float(strength)
|
| 836 |
+
self.config["filter_mode"] = filter_mode
|
| 837 |
+
self.config["filter_offset"] = float(filter_offset)
|
| 838 |
+
self.config["denoise_offset"] = float(denoise_offset)
|
| 839 |
+
self.config["adaptive_sigma_enable"] = bool(adaptive_sigma_enable)
|
| 840 |
+
# per-pass sampler/scheduler
|
| 841 |
+
self.config["sampler_first"] = sampler_first
|
| 842 |
+
self.config["sampler_second"] = sampler_second
|
| 843 |
+
self.config["scheduler_first"] = scheduler_first
|
| 844 |
+
self.config["scheduler_second"] = scheduler_second
|
| 845 |
+
self.config["restore_scheduler_after"] = bool(restore_scheduler_after)
|
| 846 |
+
# cfg/delta
|
| 847 |
+
self.config["cfg"] = float(cfg)
|
| 848 |
+
self.config["cfg_second_pass_boost"] = bool(cfg_second_pass_boost)
|
| 849 |
+
self.config["cfg_second_pass_delta"] = float(cfg_second_pass_delta)
|
| 850 |
+
# flags & extras
|
| 851 |
+
self.config["reuse_seed_noise"] = bool(reuse_seed_noise)
|
| 852 |
+
self.config["mp_target_enabled"] = bool(mp_target_enabled)
|
| 853 |
+
self.config["mp_target"] = float(mp_target)
|
| 854 |
+
self.config["cond_cache_enabled"] = bool(cond_cache_enabled)
|
| 855 |
+
self.config["cond_cache_max"] = int(cond_cache_max)
|
| 856 |
+
self.config["vae_tiling_enabled"] = bool(vae_tiling_enabled)
|
| 857 |
+
self.config["seamless_tiling_enabled"] = bool(seamless_tiling_enabled)
|
| 858 |
+
self.config["tile_overlap"] = int(tile_overlap)
|
| 859 |
+
self.config["lora_weight_first_factor"] = float(lora_weight_first_factor)
|
| 860 |
+
self.config["lora_weight_second_factor"] = float(lora_weight_second_factor)
|
| 861 |
+
self.config["match_colors_preset"] = match_colors_preset
|
| 862 |
+
self.config["match_colors_enabled"] = bool(match_colors_enabled)
|
| 863 |
+
self.config["match_colors_strength"] = float(match_colors_strength)
|
| 864 |
+
self.config["postfx_preset"] = postfx_preset
|
| 865 |
+
self.config["clahe_enabled"] = bool(clahe_enabled)
|
| 866 |
+
self.config["clahe_clip"] = float(clahe_clip)
|
| 867 |
+
self.config["clahe_tile_grid"] = int(clahe_tile_grid)
|
| 868 |
+
self.config["unsharp_enabled"] = bool(unsharp_enabled)
|
| 869 |
+
self.config["unsharp_radius"] = float(unsharp_radius)
|
| 870 |
+
self.config["unsharp_amount"] = float(unsharp_amount)
|
| 871 |
+
self.config["unsharp_threshold"] = int(unsharp_threshold)
|
| 872 |
+
self.config["cn_ref"] = bool(cn_ref)
|
| 873 |
+
self.config["start_control_at"] = float(start_control_at)
|
| 874 |
+
# final upscale
|
| 875 |
+
self.config["final_upscale_enable"] = bool(final_upscale_enable)
|
| 876 |
+
self.config["final_upscaler"] = final_upscaler
|
| 877 |
+
self.config["final_tile"] = int(final_tile)
|
| 878 |
+
self.config["final_tile_overlap"] = int(final_tile_overlap)
|
| 879 |
+
self.cfg = float(cfg) if cfg else float(p.cfg_scale)
|
| 880 |
+
# Обновить PNG-info уже с актуальным self.config
|
| 881 |
+
p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p)
|
| 882 |
+
|
| 883 |
+
# Validate sizing:
|
| 884 |
+
# Если MP target выключен и long_edge=0 — строго ДВА режима:
|
| 885 |
+
# 1) точные W×H (ratio обязан быть 0),
|
| 886 |
+
# 2) или чистый ratio>0 при width=height=0.
|
| 887 |
+
if not self.config["mp_target_enabled"] and int(self.config.get("long_edge", 0)) == 0:
|
| 888 |
+
assert (
|
| 889 |
+
(int(width) > 0 and int(height) > 0 and float(ratio) == 0.0)
|
| 890 |
+
or
|
| 891 |
+
(int(width) == 0 and int(height) == 0 and float(ratio) > 0.0)
|
| 892 |
+
), "Strict sizing: set both width & height (ratio must be 0) OR set ratio>0 with width=height=0."
|
| 893 |
+
|
| 894 |
+
# Track extras activated during conditioning
|
| 895 |
+
self._activated_extras = []
|
| 896 |
+
|
| 897 |
+
# Preserve original batch size
|
| 898 |
+
self._orig_batch_size = getattr(self.p, 'batch_size', None)
|
| 899 |
+
|
| 900 |
+
# Apply CLIP skip for the run
|
| 901 |
+
self._orig_clip_skip = shared.opts.CLIP_stop_at_last_layers
|
| 902 |
+
if int(self.config.get("clip_skip", 0)) > 0:
|
| 903 |
+
shared.opts.CLIP_stop_at_last_layers = int(self.config.get("clip_skip", 0))
|
| 904 |
+
|
| 905 |
+
# Toggle VAE tiling for the run
|
| 906 |
+
self._set_vae_tiling(self.config["vae_tiling_enabled"])
|
| 907 |
+
|
| 908 |
+
# Toggle seamless tiling
|
| 909 |
+
# Save original scheduler (before first/second pass may change it)
|
| 910 |
+
self._orig_scheduler = getattr(self.p, "scheduler", None)
|
| 911 |
+
|
| 912 |
+
self._orig_tiling = getattr(self.p, "tiling", None)
|
| 913 |
+
self._orig_tile_overlap = getattr(self.p, "tile_overlap", None)
|
| 914 |
+
if bool(self.config.get("seamless_tiling_enabled", False)):
|
| 915 |
+
try:
|
| 916 |
+
self.p.tiling = True
|
| 917 |
+
if hasattr(self.p, "tile_overlap"):
|
| 918 |
+
self.p.tile_overlap = int(self.config.get("tile_overlap", 12))
|
| 919 |
+
except Exception:
|
| 920 |
+
pass
|
| 921 |
+
try:
|
| 922 |
+
with devices.autocast():
|
| 923 |
+
shared.state.nextjob()
|
| 924 |
+
x = self._first_pass(pp.image)
|
| 925 |
+
shared.state.nextjob()
|
| 926 |
+
x = self._second_pass(x)
|
| 927 |
+
# Final ×4 upscale (optional)
|
| 928 |
+
if bool(self.config.get("final_upscale_enable", False)):
|
| 929 |
+
x = self._final_upscale_4x(x)
|
| 930 |
+
self._apply_token_merging(for_hr=False)
|
| 931 |
+
# Post-FX chain is inside _second_pass; final upscale is pure upscaler
|
| 932 |
+
pp.image = x
|
| 933 |
+
finally:
|
| 934 |
+
# Restore options
|
| 935 |
+
shared.opts.CLIP_stop_at_last_layers = self._orig_clip_skip
|
| 936 |
+
self._restore_vae_tiling()
|
| 937 |
+
# Restore scheduler if requested (independent of tiling)
|
| 938 |
+
try:
|
| 939 |
+
if bool(self.config.get("restore_scheduler_after", True)) and getattr(self, "_orig_scheduler", None) is not None:
|
| 940 |
+
self.p.scheduler = self._orig_scheduler
|
| 941 |
+
except Exception:
|
| 942 |
+
pass
|
| 943 |
+
finally:
|
| 944 |
+
self._orig_scheduler = None
|
| 945 |
+
|
| 946 |
+
# Restore tiling if it existed
|
| 947 |
+
if self._orig_tiling is not None:
|
| 948 |
+
try:
|
| 949 |
+
self.p.tiling = self._orig_tiling
|
| 950 |
+
if hasattr(self.p, "tile_overlap") and self._orig_tile_overlap is not None:
|
| 951 |
+
self.p.tile_overlap = self._orig_tile_overlap
|
| 952 |
+
except Exception:
|
| 953 |
+
pass
|
| 954 |
+
try:
|
| 955 |
+
if getattr(self, "_orig_batch_size", None) is not None:
|
| 956 |
+
self.p.batch_size = self._orig_batch_size
|
| 957 |
+
except Exception:
|
| 958 |
+
pass
|
| 959 |
+
try:
|
| 960 |
+
# Deactivate any extras we activated during conditioning
|
| 961 |
+
for _extra in getattr(self, "_activated_extras", []) or []:
|
| 962 |
+
try:
|
| 963 |
+
extra_networks.deactivate(self.p, _extra)
|
| 964 |
+
except Exception:
|
| 965 |
+
pass
|
| 966 |
+
finally:
|
| 967 |
+
self._activated_extras = []
|
| 968 |
+
try:
|
| 969 |
+
# Сбросить override сигм для последующих шагов
|
| 970 |
+
self.p.sampler_noise_scheduler_override = None
|
| 971 |
+
except Exception:
|
| 972 |
+
pass
|
| 973 |
+
# Восстановить исходные размеры после возможного _enable_controlnet
|
| 974 |
+
try:
|
| 975 |
+
ow, oh = getattr(self, "_orig_size", (None, None))
|
| 976 |
+
if ow is not None:
|
| 977 |
+
self.p.width = ow
|
| 978 |
+
if oh is not None:
|
| 979 |
+
self.p.height = oh
|
| 980 |
+
except Exception:
|
| 981 |
+
pass
|
| 982 |
+
self._orig_size = (None, None)
|
| 983 |
+
# ---- Helpers ----
|
| 984 |
+
def _maybe_mp_resize(self, base_w, base_h, target_mp: float):
|
| 985 |
+
"""Compute size from megapixels while keeping aspect ratio; quantize to multiple of 8."""
|
| 986 |
+
aspect = base_w / base_h if base_h else 1.0
|
| 987 |
+
total_px = max(0.01, target_mp) * 1_000_000.0
|
| 988 |
+
w_float = math.sqrt(total_px * aspect)
|
| 989 |
+
h_float = w_float / aspect
|
| 990 |
+
w = max(8, int(round(w_float / 8) * 8))
|
| 991 |
+
h = max(8, int(round(h_float / 8) * 8))
|
| 992 |
+
return w, h
|
| 993 |
+
|
| 994 |
+
def _compute_denoise(self, base_key: str) -> float:
|
| 995 |
+
"""
|
| 996 |
+
Returns clamped denoising strength in [0,1] as (config[base_key] + denoise_offset).
|
| 997 |
+
Keeps backwards-compatibility with the previous "denoise_offset" knob.
|
| 998 |
+
"""
|
| 999 |
+
try:
|
| 1000 |
+
base = float(self.config.get(base_key, 0.5))
|
| 1001 |
+
except Exception:
|
| 1002 |
+
base = 0.5
|
| 1003 |
+
try:
|
| 1004 |
+
off = float(self.config.get("denoise_offset", 0.0))
|
| 1005 |
+
except Exception:
|
| 1006 |
+
off = 0.0
|
| 1007 |
+
val = max(0.0, min(1.0, base + off))
|
| 1008 |
+
return val
|
| 1009 |
+
|
| 1010 |
+
def _model_hash_for_cache(self):
|
| 1011 |
+
# best-effort model hash for cache key
|
| 1012 |
+
try:
|
| 1013 |
+
return getattr(shared.sd_model, "sd_model_hash", None) or getattr(shared.sd_model, "hash", None) or str(id(shared.sd_model))
|
| 1014 |
+
except Exception:
|
| 1015 |
+
return str(id(shared.sd_model))
|
| 1016 |
+
|
| 1017 |
+
def _cond_key(self, width, height, steps_for_cond, prompt: str, negative: str, clip_skip: int):
|
| 1018 |
+
h = hashlib.sha256()
|
| 1019 |
+
h.update((prompt or "").encode("utf-8"))
|
| 1020 |
+
h.update(b"::")
|
| 1021 |
+
h.update((negative or "").encode("utf-8"))
|
| 1022 |
+
key = f"{self._model_hash_for_cache()}|{width}x{height}|{steps_for_cond}|cs={clip_skip}|{h.hexdigest()}"
|
| 1023 |
+
return key
|
| 1024 |
+
|
| 1025 |
+
def _cond_cache_get(self, key: str):
|
| 1026 |
+
if not bool(self.config.get("cond_cache_enabled", True)):
|
| 1027 |
+
return None
|
| 1028 |
+
item = self._cond_cache.get(key)
|
| 1029 |
+
if item is not None:
|
| 1030 |
+
self._cond_cache.move_to_end(key)
|
| 1031 |
+
return item
|
| 1032 |
+
|
| 1033 |
+
def _cond_cache_put(self, key: str, value: tuple):
|
| 1034 |
+
if not bool(self.config.get("cond_cache_enabled", True)):
|
| 1035 |
+
return
|
| 1036 |
+
self._cond_cache[key] = value
|
| 1037 |
+
self._cond_cache.move_to_end(key)
|
| 1038 |
+
max_items = int(self.config.get("cond_cache_max", 64))
|
| 1039 |
+
while len(self._cond_cache) > max_items:
|
| 1040 |
+
self._cond_cache.popitem(last=False)
|
| 1041 |
+
|
| 1042 |
+
def _set_vae_tiling(self, enabled: bool):
|
| 1043 |
+
# Save original state if we have not yet
|
| 1044 |
+
if self._orig_opt_vae_tiling is None and hasattr(shared.opts, "sd_vae_tiling"):
|
| 1045 |
+
self._orig_opt_vae_tiling = bool(shared.opts.sd_vae_tiling)
|
| 1046 |
+
# Toggle option
|
| 1047 |
+
if hasattr(shared.opts, "sd_vae_tiling"):
|
| 1048 |
+
shared.opts.sd_vae_tiling = bool(enabled)
|
| 1049 |
+
# Try model-level toggle
|
| 1050 |
+
vae = getattr(shared.sd_model, "first_stage_model", None)
|
| 1051 |
+
if vae is not None:
|
| 1052 |
+
try:
|
| 1053 |
+
if enabled and hasattr(vae, "enable_tiling"):
|
| 1054 |
+
vae.enable_tiling()
|
| 1055 |
+
if not enabled and hasattr(vae, "disable_tiling"):
|
| 1056 |
+
vae.disable_tiling()
|
| 1057 |
+
except Exception:
|
| 1058 |
+
pass
|
| 1059 |
+
|
| 1060 |
+
def _restore_vae_tiling(self):
|
| 1061 |
+
if self._orig_opt_vae_tiling is not None and hasattr(shared.opts, "sd_vae_tiling"):
|
| 1062 |
+
shared.opts.sd_vae_tiling = self._orig_opt_vae_tiling
|
| 1063 |
+
vae = getattr(shared.sd_model, "first_stage_model", None)
|
| 1064 |
+
if vae is not None:
|
| 1065 |
+
try:
|
| 1066 |
+
if self._orig_opt_vae_tiling and hasattr(vae, "enable_tiling"):
|
| 1067 |
+
vae.enable_tiling()
|
| 1068 |
+
elif not self._orig_opt_vae_tiling and hasattr(vae, "disable_tiling"):
|
| 1069 |
+
vae.disable_tiling()
|
| 1070 |
+
except Exception:
|
| 1071 |
+
pass
|
| 1072 |
+
self._orig_opt_vae_tiling = None
|
| 1073 |
+
|
| 1074 |
+
def _scale_lora_in_prompt(self, text: str, factor: float) -> str:
|
| 1075 |
+
# Multiply existing <lora:name:weight>, or append weight if missing
|
| 1076 |
+
# Very lightweight parser to keep prompt untouched otherwise
|
| 1077 |
+
if factor is None or abs(factor - 1.0) < 1e-6:
|
| 1078 |
+
return text
|
| 1079 |
+
out = []
|
| 1080 |
+
i = 0
|
| 1081 |
+
while i < len(text):
|
| 1082 |
+
start = text.find("<lora:", i)
|
| 1083 |
+
if start == -1:
|
| 1084 |
+
out.append(text[i:])
|
| 1085 |
+
break
|
| 1086 |
+
out.append(text[i:start])
|
| 1087 |
+
end = text.find(">", start)
|
| 1088 |
+
if end == -1:
|
| 1089 |
+
out.append(text[start:])
|
| 1090 |
+
break
|
| 1091 |
+
token = text[start:end+1]
|
| 1092 |
+
parts = token[1:-1].split(":") # lora,name,weight?
|
| 1093 |
+
if len(parts) >= 2 and parts[0] == "lora":
|
| 1094 |
+
name = parts[1]
|
| 1095 |
+
if len(parts) >= 3:
|
| 1096 |
+
try:
|
| 1097 |
+
w = float(parts[2])
|
| 1098 |
+
except Exception:
|
| 1099 |
+
w = 1.0
|
| 1100 |
+
new_w = max(0.0, w * factor)
|
| 1101 |
+
new_token = f"<lora:{name}:{new_w:.4g}>"
|
| 1102 |
+
else:
|
| 1103 |
+
new_token = f"<lora:{name}:{factor:.4g}>"
|
| 1104 |
+
out.append(new_token)
|
| 1105 |
+
else:
|
| 1106 |
+
out.append(token)
|
| 1107 |
+
i = end + 1
|
| 1108 |
+
return "".join(out)
|
| 1109 |
+
|
| 1110 |
+
def _prepare_conditioning(self, width, height, steps_for_cond: int, prompt_override: str = None):
|
| 1111 |
+
"""Build (cond, uncond) with optional LRU caching and LoRA scaling."""
|
| 1112 |
+
base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip()
|
| 1113 |
+
negative_base = self.config.get("negative_prompt", "").strip() or (getattr(self.p, "negative_prompt", "") or "").strip()
|
| 1114 |
+
|
| 1115 |
+
if prompt_override:
|
| 1116 |
+
base_prompt = prompt_override.strip()
|
| 1117 |
+
|
| 1118 |
+
# Apply LoRA scaling for this pass
|
| 1119 |
+
scaled_prompt = self._scale_lora_in_prompt(base_prompt, self._current_lora_factor)
|
| 1120 |
+
|
| 1121 |
+
clip_skip = int(self.config.get("clip_skip", 0))
|
| 1122 |
+
|
| 1123 |
+
# Cache lookup
|
| 1124 |
+
cache_key = self._cond_key(width, height, steps_for_cond, scaled_prompt, negative_base, clip_skip)
|
| 1125 |
+
cached = self._cond_cache_get(cache_key)
|
| 1126 |
+
if cached is not None:
|
| 1127 |
+
self.cond, self.uncond = cached
|
| 1128 |
+
return
|
| 1129 |
+
|
| 1130 |
+
# Parse extra networks and build cond
|
| 1131 |
+
prompt_text = scaled_prompt
|
| 1132 |
+
if not getattr(self.p, "disable_extra_networks", False):
|
| 1133 |
+
try:
|
| 1134 |
+
prompt_text, extra = extra_networks.parse_prompt(prompt_text)
|
| 1135 |
+
if extra:
|
| 1136 |
+
extra_networks.activate(self.p, extra)
|
| 1137 |
+
try:
|
| 1138 |
+
self._activated_extras.append(extra)
|
| 1139 |
+
except Exception:
|
| 1140 |
+
pass
|
| 1141 |
+
except Exception:
|
| 1142 |
+
pass
|
| 1143 |
+
|
| 1144 |
+
if width and height and hasattr(prompt_parser, "SdConditioning"):
|
| 1145 |
+
c = prompt_parser.SdConditioning([prompt_text], False, width, height)
|
| 1146 |
+
uc = prompt_parser.SdConditioning([negative_base], False, width, height)
|
| 1147 |
+
else:
|
| 1148 |
+
c, uc = [prompt_text], [negative_base]
|
| 1149 |
+
|
| 1150 |
+
cond = prompt_parser.get_multicond_learned_conditioning(shared.sd_model, c, steps_for_cond)
|
| 1151 |
+
uncond = prompt_parser.get_learned_conditioning(shared.sd_model, uc, steps_for_cond)
|
| 1152 |
+
self.cond, self.uncond = cond, uncond
|
| 1153 |
+
|
| 1154 |
+
# Store in cache
|
| 1155 |
+
self._cond_cache_put(cache_key, (cond, uncond))
|
| 1156 |
+
|
| 1157 |
+
def _to_sample(self, x_img: Image.Image):
|
| 1158 |
+
image = np.array(x_img).astype(np.float32) / 255.0
|
| 1159 |
+
image = np.moveaxis(image, 2, 0)
|
| 1160 |
+
decoded = torch.from_numpy(image).to(shared.device).to(devices.dtype_vae)
|
| 1161 |
+
decoded = 2.0 * decoded - 1.0
|
| 1162 |
+
encoded = shared.sd_model.encode_first_stage(decoded.unsqueeze(0).to(devices.dtype_vae))
|
| 1163 |
+
sample = shared.sd_model.get_first_stage_encoding(encoded)
|
| 1164 |
+
return decoded, sample
|
| 1165 |
+
|
| 1166 |
+
def _create_sampler(self, sampler_name: str):
|
| 1167 |
+
if "Restart" in sampler_name:
|
| 1168 |
+
try:
|
| 1169 |
+
return sd_samplers.create_sampler("Restart", shared.sd_model)
|
| 1170 |
+
except Exception:
|
| 1171 |
+
return sd_samplers.create_sampler("DPM++ 2M Karras", shared.sd_model)
|
| 1172 |
+
return sd_samplers.create_sampler(sampler_name, shared.sd_model)
|
| 1173 |
+
|
| 1174 |
+
def _apply_clahe(self, img: Image.Image) -> Image.Image:
|
| 1175 |
+
if not bool(self.config.get("clahe_enabled", False)):
|
| 1176 |
+
return img
|
| 1177 |
+
np_img = np.array(img)
|
| 1178 |
+
if _CV2_OK:
|
| 1179 |
+
lab = cv2.cvtColor(np_img, cv2.COLOR_RGB2LAB)
|
| 1180 |
+
l, a, b = cv2.split(lab)
|
| 1181 |
+
clip = float(self.config.get("clahe_clip", 2.0))
|
| 1182 |
+
tiles = int(self.config.get("clahe_tile_grid", 8))
|
| 1183 |
+
clahe = cv2.createCLAHE(clipLimit=max(0.1, clip), tileGridSize=(tiles, tiles))
|
| 1184 |
+
l2 = clahe.apply(l)
|
| 1185 |
+
lab2 = cv2.merge((l2, a, b))
|
| 1186 |
+
rgb = cv2.cvtColor(lab2, cv2.COLOR_LAB2RGB)
|
| 1187 |
+
return Image.fromarray(rgb)
|
| 1188 |
+
if _SKIMAGE_OK:
|
| 1189 |
+
# skimage fallback on L channel in LAB
|
| 1190 |
+
lab = skcolor.rgb2lab(np_img / 255.0)
|
| 1191 |
+
l = lab[..., 0] / 100.0
|
| 1192 |
+
l2 = equalize_adapthist(l, clip_limit=float(self.config.get("clahe_clip", 2.0)))
|
| 1193 |
+
lab[..., 0] = np.clip(l2 * 100.0, 0, 100.0)
|
| 1194 |
+
rgb = skcolor.lab2rgb(lab)
|
| 1195 |
+
rgb8 = np.clip(rgb * 255.0, 0, 255).astype(np.uint8)
|
| 1196 |
+
return Image.fromarray(rgb8)
|
| 1197 |
+
# No-op fallback
|
| 1198 |
+
return img
|
| 1199 |
+
|
| 1200 |
+
def _apply_unsharp(self, img: Image.Image) -> Image.Image:
|
| 1201 |
+
if not bool(self.config.get("unsharp_enabled", False)):
|
| 1202 |
+
return img
|
| 1203 |
+
radius = float(self.config.get("unsharp_radius", 1.5))
|
| 1204 |
+
amount = float(self.config.get("unsharp_amount", 0.75))
|
| 1205 |
+
threshold = int(self.config.get("unsharp_threshold", 0))
|
| 1206 |
+
return img.filter(ImageFilter.UnsharpMask(radius=radius, percent=int(amount * 100), threshold=threshold))
|
| 1207 |
+
|
| 1208 |
+
def _apply_match_colors(self, img: Image.Image, ref: Image.Image) -> Image.Image:
|
| 1209 |
+
if not bool(self.config.get("match_colors_enabled", False)):
|
| 1210 |
+
return img
|
| 1211 |
+
strength = float(self.config.get("match_colors_strength", 0.5))
|
| 1212 |
+
strength = max(0.0, min(1.0, strength))
|
| 1213 |
+
if strength <= 0.0:
|
| 1214 |
+
return img
|
| 1215 |
+
|
| 1216 |
+
arr = np.array(img).astype(np.float32)
|
| 1217 |
+
ref_arr = np.array(ref).astype(np.float32)
|
| 1218 |
+
|
| 1219 |
+
matched = None
|
| 1220 |
+
if _SKIMAGE_OK:
|
| 1221 |
+
try:
|
| 1222 |
+
matched = match_histograms(arr, ref_arr, channel_axis=-1).astype(np.float32)
|
| 1223 |
+
except TypeError:
|
| 1224 |
+
# older skimage
|
| 1225 |
+
matched = match_histograms(arr, ref_arr, multichannel=True).astype(np.float32)
|
| 1226 |
+
else:
|
| 1227 |
+
# simple mean-std per channel fallback
|
| 1228 |
+
eps = 1e-6
|
| 1229 |
+
for c in range(arr.shape[2]):
|
| 1230 |
+
src = arr[..., c]
|
| 1231 |
+
dst = ref_arr[..., c]
|
| 1232 |
+
src_m, src_s = src.mean(), src.std() + eps
|
| 1233 |
+
dst_m, dst_s = dst.mean(), dst.std() + eps
|
| 1234 |
+
arr[..., c] = np.clip((src - src_m) * (dst_s / src_s) + dst_m, 0, 255)
|
| 1235 |
+
matched = arr
|
| 1236 |
+
|
| 1237 |
+
out = (1.0 - strength) * arr + strength * matched
|
| 1238 |
+
out = np.clip(out, 0, 255).astype(np.uint8)
|
| 1239 |
+
return Image.fromarray(out)
|
| 1240 |
+
|
| 1241 |
+
# ----- Adaptive sigma shaping (override builder) -----
|
| 1242 |
+
def _build_sigma_override(self, which_pass: str):
|
| 1243 |
+
"""
|
| 1244 |
+
Возвращает функцию p.sampler_noise_scheduler_override, которая
|
| 1245 |
+
формирует сигмы polyexponential с лёгкой коррекцией по filter_mode/strength.
|
| 1246 |
+
which_pass: "first" | "second"
|
| 1247 |
+
"""
|
| 1248 |
+
# База по умолчанию (как было раньше)
|
| 1249 |
+
if which_pass == "first":
|
| 1250 |
+
base_min, base_max, base_rho = 0.005, 20.0, 0.6
|
| 1251 |
+
else:
|
| 1252 |
+
base_min, base_max, base_rho = 0.01, 15.0, 0.5
|
| 1253 |
+
|
| 1254 |
+
if not bool(self.config.get("adaptive_sigma_enable", False)):
|
| 1255 |
+
# Без адаптации — возвращаем исходные «фиксированные» параметры
|
| 1256 |
+
def _no_adapt(n):
|
| 1257 |
+
return K.sampling.get_sigmas_polyexponential(n, base_min, base_max, base_rho, devices.device) # type: ignore
|
| 1258 |
+
return _no_adapt
|
| 1259 |
+
|
| 1260 |
+
# Нормируем strength в 0..1 (ползунок 0.5..4.0)
|
| 1261 |
+
raw_strength = float(self.config.get("strength", 2.0))
|
| 1262 |
+
s = (raw_strength - 0.5) / 3.5
|
| 1263 |
+
s = max(0.0, min(1.0, s))
|
| 1264 |
+
|
| 1265 |
+
filt = self.config.get("filter_mode", "Noise sync (sharp)") or "Noise sync (sharp)"
|
| 1266 |
+
f_off = float(self.config.get("filter_offset", 0.0)) # -1..1 мягкий сдвиг
|
| 1267 |
+
|
| 1268 |
+
# Лёгкие корректировки: clamp-им и не даём sigma_min >= sigma_max
|
| 1269 |
+
if "Morphological" in filt:
|
| 1270 |
+
sigma_min = base_min * (1.0 + 0.5 * s + 0.25 * f_off)
|
| 1271 |
+
sigma_max = base_max * (1.0 - 0.2 * s)
|
| 1272 |
+
rho = base_rho - 0.2 * s
|
| 1273 |
+
elif "Combined" in filt:
|
| 1274 |
+
sigma_min = base_min * (1.0 + 0.20 * (0.5 - s) + 0.10 * f_off)
|
| 1275 |
+
sigma_max = base_max
|
| 1276 |
+
rho = base_rho
|
| 1277 |
+
else: # "Noise sync (sharp)"
|
| 1278 |
+
sigma_min = base_min * (1.0 - 0.5 * s - 0.25 * f_off)
|
| 1279 |
+
sigma_max = base_max * (1.0 + 0.10 * s)
|
| 1280 |
+
rho = base_rho + 0.20 * s
|
| 1281 |
+
|
| 1282 |
+
sigma_min = max(1e-4, sigma_min)
|
| 1283 |
+
sigma_max = max(sigma_min * 1.01, sigma_max) # гарантируем > sigma_min
|
| 1284 |
+
rho = max(0.1, min(1.5, rho))
|
| 1285 |
+
|
| 1286 |
+
def _adapt(n):
|
| 1287 |
+
return K.sampling.get_sigmas_polyexponential(n, sigma_min, sigma_max, rho, devices.device) # type: ignore
|
| 1288 |
+
|
| 1289 |
+
return _adapt
|
| 1290 |
+
|
| 1291 |
+
def _first_pass(self, x: Image.Image) -> Image.Image:
|
| 1292 |
+
# Determine target size
|
| 1293 |
+
if bool(self.config.get("mp_target_enabled", False)):
|
| 1294 |
+
w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0)))
|
| 1295 |
+
else:
|
| 1296 |
+
aspect = x.width / x.height if x.height else 1.0
|
| 1297 |
+
|
| 1298 |
+
le = int(self.config.get("long_edge", 0))
|
| 1299 |
+
if le > 0:
|
| 1300 |
+
if x.width >= x.height:
|
| 1301 |
+
w = int(max(8, round(le / 8) * 8))
|
| 1302 |
+
h = int(max(8, round((le / aspect) / 8) * 8))
|
| 1303 |
+
else:
|
| 1304 |
+
h = int(max(8, round(le / 8) * 8))
|
| 1305 |
+
w = int(max(8, round((le * aspect) / 8) * 8))
|
| 1306 |
+
elif int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and float(self.config.get("ratio", 0)) > 0:
|
| 1307 |
+
w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
|
| 1308 |
+
h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
|
| 1309 |
+
else:
|
| 1310 |
+
if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
|
| 1311 |
+
w, h = int(self.config["width"]), int(self.config["height"])
|
| 1312 |
+
else:
|
| 1313 |
+
# Fallback (не должен сработать при строгой валидации; оставлен для совместимости)
|
| 1314 |
+
w, h = x.width, x.height
|
| 1315 |
+
|
| 1316 |
+
self.width, self.height = w, h
|
| 1317 |
+
|
| 1318 |
+
self._apply_token_merging(for_hr=True, halve=True)
|
| 1319 |
+
|
| 1320 |
+
# Per-pass scheduler
|
| 1321 |
+
sched_first = self.config.get("scheduler_first", self.config.get("scheduler", "Use same scheduler"))
|
| 1322 |
+
self._set_scheduler_by_label(sched_first)
|
| 1323 |
+
|
| 1324 |
+
# Optional ControlNet
|
| 1325 |
+
if self._use_cn:
|
| 1326 |
+
try:
|
| 1327 |
+
cn_np = np.array(x.resize((self.width, self.height)))
|
| 1328 |
+
self._enable_controlnet(cn_np)
|
| 1329 |
+
except Exception:
|
| 1330 |
+
pass
|
| 1331 |
+
|
| 1332 |
+
# Build override prompt for first pass (none; base prompt)
|
| 1333 |
+
self._current_lora_factor = float(self.config.get("lora_weight_first_factor", 1.0))
|
| 1334 |
+
with devices.autocast(), torch.inference_mode():
|
| 1335 |
+
self._prepare_conditioning(self.width, self.height, int(self.config.get("steps_first", 20)))
|
| 1336 |
+
|
| 1337 |
+
# Upscale (image domain) then (optionally) blend latent
|
| 1338 |
+
x_img = images.resize_image(0, x, self.width, self.height, upscaler_name=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
|
| 1339 |
+
decoded, sample = self._to_sample(x_img)
|
| 1340 |
+
# Латент из исходника (до апскейла) для осмысленного смешивания
|
| 1341 |
+
_, sample_orig = self._to_sample(x)
|
| 1342 |
+
x_latent = torch.nn.functional.interpolate(sample_orig, (self.height // 8, self.width // 8), mode="nearest")
|
| 1343 |
+
|
| 1344 |
+
first_latent = float(self.config.get("first_latent", 0.3))
|
| 1345 |
+
if 0.0 <= first_latent <= 1.0:
|
| 1346 |
+
sample = sample * first_latent + x_latent * (1.0 - first_latent)
|
| 1347 |
+
|
| 1348 |
+
image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
|
| 1349 |
+
|
| 1350 |
+
# RNG setup
|
| 1351 |
+
self._saved_seeds = list(getattr(self.p, "seeds", [])) or None
|
| 1352 |
+
self._saved_subseeds = list(getattr(self.p, "subseeds", [])) or None
|
| 1353 |
+
self._saved_subseed_strength = getattr(self.p, "subseed_strength", None)
|
| 1354 |
+
self._saved_seed_resize_from_h = getattr(self.p, "seed_resize_from_h", None)
|
| 1355 |
+
self._saved_seed_resize_from_w = getattr(self.p, "seed_resize_from_w", None)
|
| 1356 |
+
|
| 1357 |
+
self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
|
| 1358 |
+
subseed_strength=self.p.subseed_strength,
|
| 1359 |
+
seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
|
| 1360 |
+
|
| 1361 |
+
# Denoise config for first pass
|
| 1362 |
+
steps = int(self.config.get("steps_first", int(self.config.get("steps", 20))))
|
| 1363 |
+
noise = torch.randn_like(sample)
|
| 1364 |
+
if bool(self.config.get("reuse_seed_noise", False)):
|
| 1365 |
+
self._first_noise = noise.detach().clone()
|
| 1366 |
+
self._first_noise_shape = tuple(sample.shape)
|
| 1367 |
+
|
| 1368 |
+
self.p.denoising_strength = self._compute_denoise("denoise_first")
|
| 1369 |
+
self.p.cfg_scale = float(self.cfg)
|
| 1370 |
+
|
| 1371 |
+
# --- sigma schedule override (с адаптацией или без) ---
|
| 1372 |
+
self.p.sampler_noise_scheduler_override = self._build_sigma_override("first")
|
| 1373 |
+
self.p.batch_size = 1
|
| 1374 |
+
|
| 1375 |
+
# Per-pass sampler
|
| 1376 |
+
sampler_first = self.config.get("sampler_first", self.config.get("sampler", "DPM++ 2M Karras"))
|
| 1377 |
+
sampler = self._create_sampler(sampler_first)
|
| 1378 |
+
|
| 1379 |
+
samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
|
| 1380 |
+
steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
|
| 1381 |
+
|
| 1382 |
+
devices.torch_gc()
|
| 1383 |
+
decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
|
| 1384 |
+
if torch.isnan(decoded_sample).any().item():
|
| 1385 |
+
devices.torch_gc()
|
| 1386 |
+
samples = torch.clamp(samples, -3, 3)
|
| 1387 |
+
decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
|
| 1388 |
+
|
| 1389 |
+
decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
|
| 1390 |
+
x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
|
| 1391 |
+
return Image.fromarray(x_np.astype(np.uint8))
|
| 1392 |
+
|
| 1393 |
+
def _second_pass(self, x: Image.Image) -> Image.Image:
|
| 1394 |
+
# Determine target size for second pass
|
| 1395 |
+
if bool(self.config.get("mp_target_enabled", False)):
|
| 1396 |
+
w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0)))
|
| 1397 |
+
else:
|
| 1398 |
+
le = int(self.config.get("long_edge", 0))
|
| 1399 |
+
if le > 0:
|
| 1400 |
+
aspect = x.width / x.height if x.height else 1.0
|
| 1401 |
+
if x.width >= x.height:
|
| 1402 |
+
w = int(max(8, round(le / 8) * 8))
|
| 1403 |
+
h = int(max(8, round((le / aspect) / 8) * 8))
|
| 1404 |
+
else:
|
| 1405 |
+
h = int(max(8, round(le / 8) * 8))
|
| 1406 |
+
w = int(max(8, round((le * aspect) / 8) * 8))
|
| 1407 |
+
elif (int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and
|
| 1408 |
+
float(self.config.get("ratio", 0)) > 0):
|
| 1409 |
+
w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
|
| 1410 |
+
h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
|
| 1411 |
+
else:
|
| 1412 |
+
if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
|
| 1413 |
+
w, h = int(self.config["width"]), int(self.config["height"])
|
| 1414 |
+
else:
|
| 1415 |
+
# Fallback (не должен сработать при строгой валидации; оставлен для совместимости)
|
| 1416 |
+
w, h = x.width, x.height
|
| 1417 |
+
|
| 1418 |
+
self._apply_token_merging(for_hr=True)
|
| 1419 |
+
|
| 1420 |
+
# Per-pass scheduler
|
| 1421 |
+
sched_second = self.config.get("scheduler_second", self.config.get("scheduler", "Use same scheduler"))
|
| 1422 |
+
self._set_scheduler_by_label(sched_second)
|
| 1423 |
+
|
| 1424 |
+
if self._use_cn:
|
| 1425 |
+
cn_img = x if bool(self.config.get("cn_ref", False)) else self.pp.image
|
| 1426 |
+
try:
|
| 1427 |
+
self._enable_controlnet(np.array(cn_img.resize((w, h))))
|
| 1428 |
+
except Exception:
|
| 1429 |
+
pass
|
| 1430 |
+
|
| 1431 |
+
# Build override prompt for second pass
|
| 1432 |
+
base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip()
|
| 1433 |
+
p2 = (self.config.get("second_pass_prompt", "") or "").strip()
|
| 1434 |
+
if p2:
|
| 1435 |
+
if bool(self.config.get("second_pass_prompt_append", True)):
|
| 1436 |
+
prompt_override = (base_prompt + ", " + p2) if base_prompt else p2
|
| 1437 |
+
else:
|
| 1438 |
+
prompt_override = p2
|
| 1439 |
+
else:
|
| 1440 |
+
prompt_override = None
|
| 1441 |
+
|
| 1442 |
+
# Apply LoRA scaling for second pass
|
| 1443 |
+
self._current_lora_factor = float(self.config.get("lora_weight_second_factor", 1.0))
|
| 1444 |
+
with devices.autocast(), torch.inference_mode():
|
| 1445 |
+
self._prepare_conditioning(w, h, int(self.config.get("steps_second", 20)), prompt_override=prompt_override)
|
| 1446 |
+
|
| 1447 |
+
# Optional latent mix
|
| 1448 |
+
x_latent = None
|
| 1449 |
+
second_latent = float(self.config.get("second_latent", 0.1))
|
| 1450 |
+
if second_latent > 0:
|
| 1451 |
+
_, sample_from_img = self._to_sample(x)
|
| 1452 |
+
x_latent = torch.nn.functional.interpolate(sample_from_img, (h // 8, w // 8), mode="nearest")
|
| 1453 |
+
|
| 1454 |
+
# Upscale to target and encode
|
| 1455 |
+
if second_latent < 1.0:
|
| 1456 |
+
x_up = images.resize_image(0, x, w, h, upscaler_name=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
|
| 1457 |
+
decoded, sample = self._to_sample(x_up)
|
| 1458 |
+
else:
|
| 1459 |
+
decoded, sample = self._to_sample(x)
|
| 1460 |
+
|
| 1461 |
+
if x_latent is not None and 0.0 <= second_latent <= 1.0:
|
| 1462 |
+
sample = (sample * (1.0 - second_latent)) + (x_latent * second_latent)
|
| 1463 |
+
|
| 1464 |
+
image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
|
| 1465 |
+
|
| 1466 |
+
# RNG: optionally reuse seed/noise
|
| 1467 |
+
if bool(self.config.get("reuse_seed_noise", False)) and self._saved_seeds is not None:
|
| 1468 |
+
try:
|
| 1469 |
+
self.p.seeds = list(self._saved_seeds)
|
| 1470 |
+
self.p.subseeds = list(self._saved_subseeds) if self._saved_subseeds is not None else self.p.subseeds
|
| 1471 |
+
self.p.subseed_strength = self._saved_subseed_strength if self._saved_subseed_strength is not None else self.p.subseed_strength
|
| 1472 |
+
self.p.seed_resize_from_h = self._saved_seed_resize_from_h if self._saved_seed_resize_from_h is not None else self.p.seed_resize_from_h
|
| 1473 |
+
self.p.seed_resize_from_w = self._saved_seed_resize_from_w if self._saved_seed_resize_from_w is not None else self.p.seed_resize_from_w
|
| 1474 |
+
except Exception:
|
| 1475 |
+
pass
|
| 1476 |
+
|
| 1477 |
+
self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
|
| 1478 |
+
subseed_strength=self.p.subseed_strength,
|
| 1479 |
+
seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
|
| 1480 |
+
|
| 1481 |
+
# Denoise config for second pass
|
| 1482 |
+
steps = int(self.config.get("steps_second", int(self.config.get("steps", 20))))
|
| 1483 |
+
if bool(self.config.get("cfg_second_pass_boost", True)):
|
| 1484 |
+
self.p.cfg_scale = float(self.cfg) + float(self.config.get("cfg_second_pass_delta", 3.0))
|
| 1485 |
+
else:
|
| 1486 |
+
self.p.cfg_scale = float(self.cfg)
|
| 1487 |
+
self.p.denoising_strength = self._compute_denoise("denoise_second")
|
| 1488 |
+
|
| 1489 |
+
# Noise: reuse tensor if shapes match, else fresh noise
|
| 1490 |
+
if bool(self.config.get("reuse_seed_noise", False)) and self._first_noise is not None:
|
| 1491 |
+
if tuple(sample.shape) == tuple(self._first_noise_shape or ()):
|
| 1492 |
+
noise = self._first_noise.to(sample.device, dtype=sample.dtype)
|
| 1493 |
+
else:
|
| 1494 |
+
noise = torch.randn_like(sample)
|
| 1495 |
+
else:
|
| 1496 |
+
noise = torch.randn_like(sample)
|
| 1497 |
+
|
| 1498 |
+
# --- sigma schedule override (с адаптацией или без) ---
|
| 1499 |
+
self.p.sampler_noise_scheduler_override = self._build_sigma_override("second")
|
| 1500 |
+
self.p.batch_size = 1
|
| 1501 |
+
|
| 1502 |
+
# Per-pass sampler
|
| 1503 |
+
sampler_second = self.config.get("sampler_second", self.config.get("sampler", "DPM++ 2M Karras"))
|
| 1504 |
+
sampler = self._create_sampler(sampler_second)
|
| 1505 |
+
|
| 1506 |
+
samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
|
| 1507 |
+
steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
|
| 1508 |
+
|
| 1509 |
+
devices.torch_gc()
|
| 1510 |
+
decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
|
| 1511 |
+
if torch.isnan(decoded_sample).any().item():
|
| 1512 |
+
devices.torch_gc()
|
| 1513 |
+
samples = torch.clamp(samples, -3, 3)
|
| 1514 |
+
decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
|
| 1515 |
+
|
| 1516 |
+
decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
|
| 1517 |
+
x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
|
| 1518 |
+
out_img = Image.fromarray(x_np.astype(np.uint8))
|
| 1519 |
+
|
| 1520 |
+
# Post-FX: CLAHE -> Unsharp -> Color match
|
| 1521 |
+
out_img = self._apply_clahe(out_img)
|
| 1522 |
+
out_img = self._apply_unsharp(out_img)
|
| 1523 |
+
if bool(self.config.get("match_colors_enabled", False)):
|
| 1524 |
+
# match to original input
|
| 1525 |
+
out_img = self._apply_match_colors(out_img, self.pp.image)
|
| 1526 |
+
|
| 1527 |
+
return out_img
|
| 1528 |
+
|
| 1529 |
+
def _final_upscale_4x(self, img: Image.Image) -> Image.Image:
|
| 1530 |
+
"""
|
| 1531 |
+
Final ×4 upscale with tiling + linear feathering over overlaps.
|
| 1532 |
+
- tile: pre-scale size on the *input* image
|
| 1533 |
+
- overlap: pre-scale overlap on the *input* image
|
| 1534 |
+
Note: each tile is upscaled via images.resize_image using chosen upscaler.
|
| 1535 |
+
"""
|
| 1536 |
+
upscaler_name = self.config.get("final_upscaler", "R-ESRGAN 4x+")
|
| 1537 |
+
tile = int(self.config.get("final_tile", 512))
|
| 1538 |
+
overlap = int(self.config.get("final_tile_overlap", 16))
|
| 1539 |
+
tile = max(64, tile)
|
| 1540 |
+
overlap = max(0, min(overlap, tile // 2))
|
| 1541 |
+
|
| 1542 |
+
scale = 4
|
| 1543 |
+
W, H = img.width, img.height
|
| 1544 |
+
TW, TH = W * scale, H * scale
|
| 1545 |
+
|
| 1546 |
+
# Accumulator buffers for blending
|
| 1547 |
+
accum = np.zeros((TH, TW, 3), dtype=np.float32)
|
| 1548 |
+
weight = np.zeros((TH, TW), dtype=np.float32)
|
| 1549 |
+
|
| 1550 |
+
def ramp(length, left_ovl, right_ovl):
|
| 1551 |
+
# 1D feathering: 0..1 over left overlap, 1 in center, 1..0 over right overlap
|
| 1552 |
+
arr = np.ones(length, dtype=np.float32)
|
| 1553 |
+
if left_ovl > 0:
|
| 1554 |
+
arr[:left_ovl] = np.linspace(0.0, 1.0, left_ovl, endpoint=False, dtype=np.float32)
|
| 1555 |
+
if right_ovl > 0:
|
| 1556 |
+
arr[-right_ovl:] = np.minimum(arr[-right_ovl:], np.linspace(1.0, 0.0, right_ovl, endpoint=False, dtype=np.float32))
|
| 1557 |
+
return arr
|
| 1558 |
+
|
| 1559 |
+
step = tile - overlap
|
| 1560 |
+
if step <= 0:
|
| 1561 |
+
step = tile # degenerate: no overlap
|
| 1562 |
+
|
| 1563 |
+
for y in range(0, H, step):
|
| 1564 |
+
for x in range(0, W, step):
|
| 1565 |
+
x0 = max(0, x - overlap)
|
| 1566 |
+
y0 = max(0, y - overlap)
|
| 1567 |
+
x1 = min(W, x + tile)
|
| 1568 |
+
y1 = min(H, y + tile)
|
| 1569 |
+
|
| 1570 |
+
crop = img.crop((x0, y0, x1, y1))
|
| 1571 |
+
up = images.resize_image(0, crop, (x1 - x0) * scale, (y1 - y0) * scale, upscaler_name=upscaler_name)
|
| 1572 |
+
up_np = np.array(up).astype(np.float32)
|
| 1573 |
+
|
| 1574 |
+
ox = x0 * scale
|
| 1575 |
+
oy = y0 * scale
|
| 1576 |
+
uh, uw = up_np.shape[0], up_np.shape[1]
|
| 1577 |
+
|
| 1578 |
+
# Feather mask
|
| 1579 |
+
left_ovl = (x - x0) * scale
|
| 1580 |
+
top_ovl = (y - y0) * scale
|
| 1581 |
+
right_ovl = (x1 - x) * scale - (tile - overlap) * scale if (x + step) < W else 0
|
| 1582 |
+
bottom_ovl = (y1 - y) * scale - (tile - overlap) * scale if (y + step) < H else 0
|
| 1583 |
+
left_ovl = max(0, min(left_ovl, uw // 2))
|
| 1584 |
+
right_ovl = max(0, min(int(right_ovl), uw // 2))
|
| 1585 |
+
top_ovl = max(0, min(top_ovl, uh // 2))
|
| 1586 |
+
bottom_ovl = max(0, min(int(bottom_ovl), uh // 2))
|
| 1587 |
+
|
| 1588 |
+
wx = ramp(uw, left_ovl, right_ovl)
|
| 1589 |
+
wy = ramp(uh, top_ovl, bottom_ovl)
|
| 1590 |
+
w2d = (wy[:, None] * wx[None, :]).astype(np.float32)
|
| 1591 |
+
# Accumulate
|
| 1592 |
+
accum[oy:oy+uh, ox:ox+uw, :] += up_np * w2d[..., None]
|
| 1593 |
+
weight[oy:oy+uh, ox:ox+uw] += w2d
|
| 1594 |
+
|
| 1595 |
+
weight = np.clip(weight, 1e-6, None)
|
| 1596 |
+
out = (accum / weight[..., None]).astype(np.uint8)
|
| 1597 |
+
return Image.fromarray(out, mode="RGB")
|
| 1598 |
+
|
| 1599 |
+
def _enable_controlnet(self, image_np: np.ndarray):
|
| 1600 |
+
if not getattr(self, "_cn_ext", None):
|
| 1601 |
+
return
|
| 1602 |
+
for unit in self._cn_units:
|
| 1603 |
+
try:
|
| 1604 |
+
if getattr(unit, "model", "None") != "None":
|
| 1605 |
+
if getattr(unit, "enabled", True):
|
| 1606 |
+
unit.guidance_start = float(self.config.get("start_control_at", 0.0))
|
| 1607 |
+
unit.processor_res = min(image_np.shape[0], image_np.shape[1])
|
| 1608 |
+
if getattr(unit, "image", None) is None:
|
| 1609 |
+
unit.image = image_np
|
| 1610 |
+
self.p.width = image_np.shape[1]
|
| 1611 |
+
self.p.height = image_np.shape[0]
|
| 1612 |
+
except Exception:
|
| 1613 |
+
continue
|
| 1614 |
+
try:
|
| 1615 |
+
self._cn_ext.update_cn_script_in_processing(self.p, self._cn_units)
|
| 1616 |
+
for script in self.p.scripts.alwayson_scripts:
|
| 1617 |
+
if script.title().lower() == "controlnet":
|
| 1618 |
+
script.controlnet_hack(self.p)
|
| 1619 |
+
except Exception:
|
| 1620 |
+
pass
|
| 1621 |
+
|
| 1622 |
+
|
| 1623 |
+
def parse_infotext(infotext, params):
|
| 1624 |
+
try:
|
| 1625 |
+
block = params.get("Custom Hires Fix")
|
| 1626 |
+
if not block:
|
| 1627 |
+
return
|
| 1628 |
+
data = json.loads(block.translate(quote_swap)) if isinstance(block, str) else block
|
| 1629 |
+
params["Custom Hires Fix"] = data
|
| 1630 |
+
scale = data.get("scale", 0)
|
| 1631 |
+
if isinstance(scale, str) and "x" in scale:
|
| 1632 |
+
w, _, h = scale.partition("x")
|
| 1633 |
+
data["ratio"] = 0.0
|
| 1634 |
+
data["width"] = int(w)
|
| 1635 |
+
data["height"] = int(h)
|
| 1636 |
+
else:
|
| 1637 |
+
try:
|
| 1638 |
+
r = float(scale)
|
| 1639 |
+
except Exception:
|
| 1640 |
+
r = 0.0
|
| 1641 |
+
data["ratio"] = r
|
| 1642 |
+
data["width"] = int(data.get("width", 0) or 0)
|
| 1643 |
+
data["height"] = int(data.get("height", 0) or 0)
|
| 1644 |
+
|
| 1645 |
+
# Defaults for new/legacy fields
|
| 1646 |
+
if "steps_first" not in data:
|
| 1647 |
+
data["steps_first"] = int(data.get("steps", 20))
|
| 1648 |
+
if "steps_second" not in data:
|
| 1649 |
+
data["steps_second"] = int(data.get("steps", 20))
|
| 1650 |
+
|
| 1651 |
+
# per-pass sampler/scheduler defaults from legacy single values
|
| 1652 |
+
data.setdefault("sampler_first", data.get("sampler", ""))
|
| 1653 |
+
data.setdefault("sampler_second", data.get("sampler", ""))
|
| 1654 |
+
data.setdefault("scheduler_first", data.get("scheduler", "Use same scheduler"))
|
| 1655 |
+
data.setdefault("scheduler_second", data.get("scheduler", "Use same scheduler"))
|
| 1656 |
+
|
| 1657 |
+
# CFG delta defaults
|
| 1658 |
+
data.setdefault("cfg_second_pass_boost", True)
|
| 1659 |
+
data.setdefault("cfg_second_pass_delta", 3.0)
|
| 1660 |
+
|
| 1661 |
+
# Flags defaults
|
| 1662 |
+
data.setdefault("reuse_seed_noise", False)
|
| 1663 |
+
data.setdefault("mp_target_enabled", False)
|
| 1664 |
+
data.setdefault("mp_target", 2.0)
|
| 1665 |
+
data.setdefault("cond_cache_enabled", True)
|
| 1666 |
+
data.setdefault("cond_cache_max", 64)
|
| 1667 |
+
data.setdefault("vae_tiling_enabled", False)
|
| 1668 |
+
data.setdefault("seamless_tiling_enabled", False)
|
| 1669 |
+
data.setdefault("tile_overlap", 12)
|
| 1670 |
+
data.setdefault("lora_weight_first_factor", 1.0)
|
| 1671 |
+
data.setdefault("lora_weight_second_factor", 1.0)
|
| 1672 |
+
data.setdefault("match_colors_preset", "Off")
|
| 1673 |
+
data.setdefault("match_colors_enabled", False)
|
| 1674 |
+
data.setdefault("match_colors_strength", 0.5)
|
| 1675 |
+
data.setdefault("postfx_preset", "Off")
|
| 1676 |
+
data.setdefault("clahe_enabled", False)
|
| 1677 |
+
data.setdefault("clahe_clip", 2.0)
|
| 1678 |
+
data.setdefault("clahe_tile_grid", 8)
|
| 1679 |
+
data.setdefault("unsharp_enabled", False)
|
| 1680 |
+
data.setdefault("unsharp_radius", 1.5)
|
| 1681 |
+
data.setdefault("unsharp_amount", 0.75)
|
| 1682 |
+
data.setdefault("unsharp_threshold", 0)
|
| 1683 |
+
data.setdefault("second_pass_prompt", "")
|
| 1684 |
+
data.setdefault("second_pass_prompt_append", True)
|
| 1685 |
+
|
| 1686 |
+
# final upscale defaults
|
| 1687 |
+
data.setdefault("final_upscale_enable", False)
|
| 1688 |
+
data.setdefault("final_upscaler", "R-ESRGAN 4x+")
|
| 1689 |
+
data.setdefault("final_tile", 512)
|
| 1690 |
+
data.setdefault("final_tile_overlap", 16)
|
| 1691 |
+
|
| 1692 |
+
# Новое поле по умолчанию — добавляем внутри try
|
| 1693 |
+
data.setdefault("adaptive_sigma_enable", False)
|
| 1694 |
+
data.setdefault("restore_scheduler_after", True)
|
| 1695 |
+
|
| 1696 |
+
except Exception:
|
| 1697 |
+
return
|
| 1698 |
+
|
| 1699 |
+
# Register paste-params hook
|
| 1700 |
+
script_callbacks.on_infotext_pasted(parse_infotext)
|