dikdimon commited on
Commit
21fa59d
·
verified ·
1 Parent(s): d9cec7d

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

Browse files
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)