dikdimon commited on
Commit
d9cec7d
·
verified ·
1 Parent(s): 3ac5356

Upload custom-hires-fix-mod-for-automatic1111-2.2 using SD-Hub

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