dikdimon commited on
Commit
46420f8
·
verified ·
1 Parent(s): fcf5483

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

Browse files
custom-hires-fix-mod-for-automatic1111-2.0/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.0/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.0/scripts/__pycache__/custom_hires_fix.cpython-310.pyc ADDED
Binary file (36.9 kB). View file
 
custom-hires-fix-mod-for-automatic1111-2.0/scripts/custom_hires_fix.py ADDED
@@ -0,0 +1,1367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ """
92
+ def __init__(self):
93
+ super().__init__()
94
+ # Load or init config
95
+ if config_path.exists():
96
+ try:
97
+ self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore
98
+ except Exception:
99
+ self.config = OmegaConf.create({}) # type: ignore
100
+ else:
101
+ self.config = OmegaConf.create({}) # type: ignore
102
+
103
+ # Runtime state
104
+ self.p = None
105
+ self.pp = None
106
+ self.cfg = 0.0
107
+ self.cond = None
108
+ self.uncond = None
109
+ self.width = None
110
+ self.height = None
111
+ self._orig_clip_skip = None
112
+ self._cn_units = []
113
+ self._use_cn = False
114
+
115
+ # Reuse state
116
+ self._saved_seeds = None
117
+ self._saved_subseeds = None
118
+ self._saved_subseed_strength = None
119
+ self._saved_seed_resize_from_h = None
120
+ self._saved_seed_resize_from_w = None
121
+ self._first_noise = None
122
+ self._first_noise_shape = None
123
+
124
+ # Conditioning cache (LRU)
125
+ self._cond_cache: OrderedDict[str, tuple] = OrderedDict()
126
+
127
+ # VAE tiling state restore
128
+ self._orig_opt_vae_tiling = None
129
+
130
+ # Seamless tiling restore
131
+ self._orig_tiling = None
132
+ self._orig_tile_overlap = None
133
+
134
+ # Prompt override for second pass
135
+ self._override_prompt_second = None
136
+
137
+ # LoRA scaling factor per pass (used during _prepare_conditioning by pass context)
138
+ self._current_lora_factor = 1.0
139
+
140
+ # ---- A1111 Script API ----
141
+ def title(self):
142
+ return "Custom Hires Fix"
143
+
144
+ def show(self, is_img2img):
145
+ return scripts.AlwaysVisible
146
+
147
+ def ui(self, is_img2img):
148
+ visible_names = [x.name for x in sd_samplers.visible_samplers()]
149
+ sampler_names = ["Restart + DPM++ 3M SDE"] + visible_names
150
+ scheduler_names = ["Use same scheduler"] + [x.label for x in sd_schedulers.schedulers]
151
+
152
+ with gr.Accordion(label="Custom Hires Fix", open=False) as enable_box:
153
+ enable = gr.Checkbox(label="Enable extension", value=bool(self.config.get("enable", False)))
154
+
155
+ # ---------- Compact preset panel ----------
156
+ with gr.Row():
157
+ quick_preset = gr.Dropdown(
158
+ ["None", "Hi-Res Portrait", "Hi-Res Texture", "Hi-Res Illustration", "Hi-Res Product Shot"],
159
+ label="Quick preset",
160
+ value="None"
161
+ )
162
+ btn_apply_preset = gr.Button(value="Apply preset", variant="primary")
163
+
164
+ btn_mp_1 = gr.Button(value="MP 1.0")
165
+ btn_mp_2 = gr.Button(value="MP 2.0")
166
+ btn_mp_4 = gr.Button(value="MP 4.0")
167
+ btn_mp_8 = gr.Button(value="MP 8.0")
168
+
169
+ with gr.Row():
170
+ ratio = gr.Slider(minimum=0.0, maximum=4.0, step=0.05, label="Upscale by (ratio)",
171
+ value=float(self.config.get("ratio", 0.0)))
172
+ width = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize width to",
173
+ value=int(self.config.get("width", 0)))
174
+ height = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize height to",
175
+ value=int(self.config.get("height", 0)))
176
+
177
+ with gr.Row():
178
+ steps_first = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 1st pass",
179
+ value=int(self.config.get("steps_first", max(1, int(self.config.get("steps", 20))))))
180
+ steps_second = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 2nd pass",
181
+ value=int(self.config.get("steps_second", int(self.config.get("steps", 20)))))
182
+
183
+ with gr.Row():
184
+ first_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
185
+ label="First upscaler", value=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
186
+ second_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
187
+ label="Second upscaler", value=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
188
+
189
+ with gr.Row():
190
+ first_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (first stage)",
191
+ value=float(self.config.get("first_latent", 0.3)))
192
+ second_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (second stage)",
193
+ value=float(self.config.get("second_latent", 0.1)))
194
+
195
+ with gr.Row():
196
+ filter_mode = gr.Dropdown(["Noise sync (sharp)", "Morphological (smooth)", "Combined (balanced)"],
197
+ label="Filter mode", value=self.config.get("filter_mode", "Noise sync (sharp)"))
198
+ strength = gr.Slider(minimum=0.5, maximum=4.0, step=0.1, label="Generation strength",
199
+ value=float(self.config.get("strength", 2.0)))
200
+ denoise_offset = gr.Slider(minimum=-0.1, maximum=0.2, step=0.01, label="Denoise offset",
201
+ value=float(self.config.get("denoise_offset", 0.05)))
202
+
203
+ with gr.Row():
204
+ prompt = gr.Textbox(label="Prompt override (1st pass)", placeholder="Leave empty to use main UI prompt",
205
+ value=self.config.get("prompt", ""))
206
+ negative_prompt = gr.Textbox(label="Negative prompt override", placeholder="Leave empty to use main UI negative prompt",
207
+ value=self.config.get("negative_prompt", ""))
208
+
209
+ with gr.Row():
210
+ second_pass_prompt = gr.Textbox(label="Second-pass prompt", placeholder="Append or replace on 2nd pass",
211
+ value=self.config.get("second_pass_prompt", ""))
212
+ second_pass_prompt_append = gr.Checkbox(label="Append instead of replace",
213
+ value=bool(self.config.get("second_pass_prompt_append", True)))
214
+
215
+ with gr.Accordion(label="Extra", open=False):
216
+ with gr.Row():
217
+ filter_offset = gr.Slider(minimum=-1.0, maximum=1.0, step=0.1, label="Filter offset",
218
+ value=float(self.config.get("filter_offset", 0.0)))
219
+ clip_skip = gr.Slider(minimum=0, maximum=12, step=1, label="CLIP skip (0 = keep)",
220
+ value=int(self.config.get("clip_skip", 0)))
221
+
222
+ # Per-pass sampler/scheduler
223
+ with gr.Row():
224
+ sampler_first = gr.Dropdown(sampler_names, label="Sampler — 1st pass",
225
+ value=self.config.get("sampler_first", sampler_names[0]))
226
+ sampler_second = gr.Dropdown(sampler_names, label="Sampler — 2nd pass",
227
+ value=self.config.get("sampler_second", self.config.get("sampler", sampler_names[0])))
228
+ with gr.Row():
229
+ scheduler_first = gr.Dropdown(choices=scheduler_names, label="Schedule type — 1st pass",
230
+ value=self.config.get("scheduler_first", self.config.get("scheduler", scheduler_names[0])))
231
+ scheduler_second = gr.Dropdown(choices=scheduler_names, label="Schedule type — 2nd pass",
232
+ value=self.config.get("scheduler_second", self.config.get("scheduler", scheduler_names[0])))
233
+
234
+ with gr.Row():
235
+ cfg = gr.Slider(minimum=0, maximum=30, step=0.5, label="CFG Scale (base)",
236
+ value=float(self.config.get("cfg", 7.0)))
237
+ cfg_second_pass_boost = gr.Checkbox(label="Enable CFG delta on 2nd pass",
238
+ value=bool(self.config.get("cfg_second_pass_boost", True)))
239
+ cfg_second_pass_delta = gr.Slider(minimum=-5.0, maximum=5.0, step=0.5, label="CFG delta (2nd pass)",
240
+ value=float(self.config.get("cfg_second_pass_delta", 3.0)))
241
+
242
+ # Reuse seed/noise + Megapixels target
243
+ with gr.Row():
244
+ reuse_seed_noise = gr.Checkbox(label="Reuse seed/noise on 2nd pass",
245
+ value=bool(self.config.get("reuse_seed_noise", False)))
246
+ mp_target_enabled = gr.Checkbox(label="Enable Megapixels target",
247
+ value=bool(self.config.get("mp_target_enabled", False)))
248
+ mp_target = gr.Slider(minimum=0.3, maximum=16.0, step=0.1, label="Megapixels",
249
+ value=float(self.config.get("mp_target", 2.0)))
250
+
251
+ # Conditioning cache controls
252
+ with gr.Row():
253
+ cond_cache_enabled = gr.Checkbox(label="Enable conditioning cache (LRU)",
254
+ value=bool(self.config.get("cond_cache_enabled", True)))
255
+ cond_cache_max = gr.Slider(minimum=8, maximum=256, step=8, label="Conditioning cache size",
256
+ value=int(self.config.get("cond_cache_max", 64)))
257
+
258
+ # VAE tiling
259
+ with gr.Row():
260
+ vae_tiling_enabled = gr.Checkbox(label="Enable VAE tiling (low VRAM)",
261
+ value=bool(self.config.get("vae_tiling_enabled", False)))
262
+
263
+ # Seamless tiling
264
+ with gr.Row():
265
+ seamless_tiling_enabled = gr.Checkbox(label="Seamless tiling (texture)",
266
+ value=bool(self.config.get("seamless_tiling_enabled", False)))
267
+ tile_overlap = gr.Slider(minimum=0, maximum=64, step=1, label="Tile overlap (px)",
268
+ value=int(self.config.get("tile_overlap", 12)))
269
+
270
+ # LoRA scaling
271
+ with gr.Row():
272
+ lora_weight_first_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (1st pass)",
273
+ value=float(self.config.get("lora_weight_first_factor", 1.0)))
274
+ lora_weight_second_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (2nd pass)",
275
+ value=float(self.config.get("lora_weight_second_factor", 1.0)))
276
+
277
+ # Match colors presets & controls
278
+ with gr.Row():
279
+ match_colors_preset = gr.Dropdown(
280
+ ["Off", "Subtle (0.3)", "Natural (0.5)", "Strong (0.8)"],
281
+ label="Match colors preset",
282
+ value=self.config.get("match_colors_preset", "Off")
283
+ )
284
+ match_colors_enabled = gr.Checkbox(label="Match colors to original",
285
+ value=bool(self.config.get("match_colors_enabled", False)))
286
+ match_colors_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label="Match strength",
287
+ value=float(self.config.get("match_colors_strength", 0.5)))
288
+
289
+ # Post-processing presets & controls
290
+ with gr.Row():
291
+ postfx_preset = gr.Dropdown(
292
+ ["Off", "Soft clarity", "Portrait safe", "Texture boost", "Crisp detail"],
293
+ label="Post-FX preset",
294
+ value=self.config.get("postfx_preset", "Off")
295
+ )
296
+ clahe_enabled = gr.Checkbox(label="CLAHE (local contrast)",
297
+ value=bool(self.config.get("clahe_enabled", False)))
298
+ clahe_clip = gr.Slider(minimum=1.0, maximum=5.0, step=0.1, label="CLAHE clip limit",
299
+ value=float(self.config.get("clahe_clip", 2.0)))
300
+ clahe_tile_grid = gr.Slider(minimum=4, maximum=16, step=2, label="CLAHE tile grid",
301
+ value=int(self.config.get("clahe_tile_grid", 8)))
302
+
303
+ with gr.Row():
304
+ unsharp_enabled = gr.Checkbox(label="Unsharp Mask (sharpen)",
305
+ value=bool(self.config.get("unsharp_enabled", False)))
306
+ unsharp_radius = gr.Slider(minimum=0.5, maximum=5.0, step=0.1, label="Unsharp radius",
307
+ value=float(self.config.get("unsharp_radius", 1.5)))
308
+ unsharp_amount = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="Unsharp amount",
309
+ value=float(self.config.get("unsharp_amount", 0.75)))
310
+ unsharp_threshold = gr.Slider(minimum=0, maximum=10, step=1, label="Unsharp threshold",
311
+ value=int(self.config.get("unsharp_threshold", 0)))
312
+
313
+ with gr.Row():
314
+ cn_ref = gr.Checkbox(label="Use last image as ControlNet reference", value=bool(self.config.get("cn_ref", False)))
315
+ start_control_at = gr.Slider(minimum=0.0, maximum=0.7, step=0.01, label="CN start (enabled units)",
316
+ value=float(self.config.get("start_control_at", 0.0)))
317
+
318
+ # ---------- Preset logic (UI events) ----------
319
+
320
+ def _apply_match_preset(preset_name):
321
+ if preset_name == "Off":
322
+ return (gr.update(value=False), gr.update(value=0.5))
323
+ if preset_name == "Subtle (0.3)":
324
+ return (gr.update(value=True), gr.update(value=0.3))
325
+ if preset_name == "Natural (0.5)":
326
+ return (gr.update(value=True), gr.update(value=0.5))
327
+ if preset_name == "Strong (0.8)":
328
+ return (gr.update(value=True), gr.update(value=0.8))
329
+ return (gr.update(), gr.update())
330
+
331
+ def _apply_postfx_preset(preset_name):
332
+ # Returns: clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold
333
+ if preset_name == "Off":
334
+ return (gr.update(value=False), gr.update(value=2.0), gr.update(value=8),
335
+ gr.update(value=False), gr.update(value=1.5), gr.update(value=0.75), gr.update(value=0))
336
+ if preset_name == "Soft clarity":
337
+ return (gr.update(value=True), gr.update(value=1.8), gr.update(value=8),
338
+ gr.update(value=True), gr.update(value=1.2), gr.update(value=0.6), gr.update(value=0))
339
+ if preset_name == "Portrait safe":
340
+ return (gr.update(value=True), gr.update(value=1.6), gr.update(value=8),
341
+ gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2))
342
+ if preset_name == "Texture boost":
343
+ return (gr.update(value=True), gr.update(value=2.4), gr.update(value=8),
344
+ gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0))
345
+ if preset_name == "Crisp detail":
346
+ return (gr.update(value=True), gr.update(value=2.1), gr.update(value=8),
347
+ gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0))
348
+ return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update())
349
+
350
+ def _apply_quick_preset(name):
351
+ # Returns a large tuple of updates for several controls
352
+ # Order must match the outputs list in .click below
353
+ # Defaults (no change)
354
+ out = [
355
+ gr.update(), # steps_first
356
+ gr.update(), # steps_second
357
+ gr.update(), # cfg_second_pass_boost
358
+ gr.update(), # cfg_second_pass_delta
359
+ gr.update(), # sampler_first
360
+ gr.update(), # sampler_second
361
+ gr.update(), # scheduler_first
362
+ gr.update(), # scheduler_second
363
+ gr.update(), # vae_tiling_enabled
364
+ gr.update(), # seamless_tiling_enabled
365
+ gr.update(), # tile_overlap
366
+ gr.update(), # match_colors_preset
367
+ gr.update(), # match_colors_enabled
368
+ gr.update(), # match_colors_strength
369
+ gr.update(), # postfx_preset
370
+ gr.update(), # clahe_enabled
371
+ gr.update(), # clahe_clip
372
+ gr.update(), # clahe_tile_grid
373
+ gr.update(), # unsharp_enabled
374
+ gr.update(), # unsharp_radius
375
+ gr.update(), # unsharp_amount
376
+ gr.update(), # unsharp_threshold
377
+ gr.update(), # reuse_seed_noise
378
+ gr.update(), # cond_cache_max
379
+ gr.update(), # lora_weight_first_factor
380
+ gr.update(), # lora_weight_second_factor
381
+ gr.update(), # mp_target_enabled
382
+ gr.update(), # mp_target
383
+ ]
384
+ if name == "Hi-Res Portrait":
385
+ out = [
386
+ gr.update(value=18), gr.update(value=28),
387
+ gr.update(value=True), gr.update(value=2.5),
388
+ gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"),
389
+ gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
390
+ gr.update(value=True), gr.update(value=False), gr.update(value=12),
391
+ gr.update(value="Subtle (0.3)"), gr.update(value=True), gr.update(value=0.3),
392
+ gr.update(value="Portrait safe"), gr.update(value=True), gr.update(value=1.6), gr.update(value=8),
393
+ gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2),
394
+ gr.update(value=True), gr.update(value=64),
395
+ gr.update(value=1.0), gr.update(value=1.1),
396
+ gr.update(value=True), gr.update(value=2.0),
397
+ ]
398
+ elif name == "Hi-Res Texture":
399
+ out = [
400
+ gr.update(value=14), gr.update(value=22),
401
+ gr.update(value=True), gr.update(value=3.0),
402
+ gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"),
403
+ gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
404
+ gr.update(value=True), gr.update(value=True), gr.update(value=12),
405
+ gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5),
406
+ gr.update(value="Texture boost"), gr.update(value=True), gr.update(value=2.2), gr.update(value=8),
407
+ gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0),
408
+ gr.update(value=True), gr.update(value=128),
409
+ gr.update(value=0.9), gr.update(value=1.25),
410
+ gr.update(value=True), gr.update(value=4.0),
411
+ ]
412
+ elif name == "Hi-Res Illustration":
413
+ out = [
414
+ gr.update(value=16), gr.update(value=24),
415
+ gr.update(value=True), gr.update(value=2.0),
416
+ gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"),
417
+ gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
418
+ gr.update(value=True), gr.update(value=False), gr.update(value=8),
419
+ gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5),
420
+ gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.0), gr.update(value=8),
421
+ gr.update(value=True), gr.update(value=1.2), gr.update(value=0.9), gr.update(value=0),
422
+ gr.update(value=True), gr.update(value=64),
423
+ gr.update(value=0.85), gr.update(value=1.2),
424
+ gr.update(value=True), gr.update(value=2.0),
425
+ ]
426
+ elif name == "Hi-Res Product Shot":
427
+ out = [
428
+ gr.update(value=18), gr.update(value=26),
429
+ gr.update(value=True), gr.update(value=2.0),
430
+ gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"),
431
+ gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"),
432
+ gr.update(value=True), gr.update(value=False), gr.update(value=8),
433
+ gr.update(value="Natural (0.5)"), gr.update(value=True), gr.update(value=0.5),
434
+ gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.1), gr.update(value=8),
435
+ gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0),
436
+ gr.update(value=True), gr.update(value=96),
437
+ gr.update(value=1.0), gr.update(value=1.15),
438
+ gr.update(value=True), gr.update(value=2.0),
439
+ ]
440
+ return tuple(out)
441
+
442
+ def _set_mp():
443
+ # Will be replaced by lambda with closure per button via _kwargs in A1111? Not guaranteed.
444
+ # Provide a neutral default; we wire explicit functions below in WebUI won't support _kwargs.
445
+ return (gr.update(value=True), gr.update(value=2.0))
446
+
447
+ # Wire events
448
+ match_colors_preset.change(
449
+ fn=_apply_match_preset,
450
+ inputs=[match_colors_preset],
451
+ outputs=[match_colors_enabled, match_colors_strength]
452
+ )
453
+ postfx_preset.change(
454
+ fn=_apply_postfx_preset,
455
+ inputs=[postfx_preset],
456
+ outputs=[clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold]
457
+ )
458
+ btn_apply_preset.click(
459
+ fn=_apply_quick_preset,
460
+ inputs=[quick_preset],
461
+ outputs=[
462
+ steps_first, steps_second,
463
+ cfg_second_pass_boost, cfg_second_pass_delta,
464
+ sampler_first, sampler_second,
465
+ scheduler_first, scheduler_second,
466
+ vae_tiling_enabled, seamless_tiling_enabled, tile_overlap,
467
+ match_colors_preset, match_colors_enabled, match_colors_strength,
468
+ postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
469
+ unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
470
+ reuse_seed_noise, cond_cache_max,
471
+ lora_weight_first_factor, lora_weight_second_factor,
472
+ mp_target_enabled, mp_target
473
+ ]
474
+ )
475
+ # MP buttons (Gradio inside A1111 doesn't allow passing kwargs; define per-target lambdas)
476
+ btn_mp_1.click(fn=lambda: (gr.update(value=True), gr.update(value=1.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
477
+ btn_mp_2.click(fn=lambda: (gr.update(value=True), gr.update(value=2.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
478
+ btn_mp_4.click(fn=lambda: (gr.update(value=True), gr.update(value=4.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
479
+ btn_mp_8.click(fn=lambda: (gr.update(value=True), gr.update(value=8.0)), inputs=[], outputs=[mp_target_enabled, mp_target])
480
+
481
+ # Exclusivity helpers
482
+ width.change(fn=lambda x: gr.update(value=0), inputs=width, outputs=height)
483
+ height.change(fn=lambda x: gr.update(value=0), inputs=height, outputs=width)
484
+ ratio.change(fn=lambda x: (gr.update(value=0), gr.update(value=0)), inputs=ratio, outputs=[width, height])
485
+
486
+ # infotext paste support
487
+ def read_params(d, key, default=None):
488
+ try:
489
+ return d["Custom Hires Fix"].get(key, default)
490
+ except Exception:
491
+ return default
492
+
493
+ self.infotext_fields = [
494
+ (enable, lambda d: "Custom Hires Fix" in d),
495
+ (ratio, lambda d: read_params(d, "ratio", 0.0)),
496
+ (width, lambda d: read_params(d, "width", 0)),
497
+ (height, lambda d: read_params(d, "height", 0)),
498
+ (steps_first, lambda d: read_params(d, "steps_first", read_params(d, "steps", 20))),
499
+ (steps_second, lambda d: read_params(d, "steps_second", read_params(d, "steps", 20))),
500
+ (first_upscaler, lambda d: read_params(d, "first_upscaler")),
501
+ (second_upscaler, lambda d: read_params(d, "second_upscaler")),
502
+ (first_latent, lambda d: read_params(d, "first_latent", 0.0)),
503
+ (second_latent, lambda d: read_params(d, "second_latent", 0.0)),
504
+ (prompt, lambda d: read_params(d, "prompt", "")),
505
+ (negative_prompt, lambda d: read_params(d, "negative_prompt", "")),
506
+ (second_pass_prompt, lambda d: read_params(d, "second_pass_prompt", "")),
507
+ (second_pass_prompt_append, lambda d: read_params(d, "second_pass_prompt_append", True)),
508
+ (strength, lambda d: read_params(d, "strength", 0.0)),
509
+ (filter_mode, lambda d: read_params(d, "filter_mode")),
510
+ (filter_offset, lambda d: read_params(d, "filter_offset", 0.0)),
511
+ (denoise_offset, lambda d: read_params(d, "denoise_offset", 0.0)),
512
+ (clip_skip, lambda d: read_params(d, "clip_skip", 0)),
513
+ # per-pass samplers/schedulers + legacy fallbacks
514
+ (sampler_first, lambda d: read_params(d, "sampler_first", read_params(d, "sampler", sampler_names[0]))),
515
+ (sampler_second, lambda d: read_params(d, "sampler_second", read_params(d, "sampler", sampler_names[0]))),
516
+ (scheduler_first, lambda d: read_params(d, "scheduler_first", read_params(d, "scheduler", scheduler_names[0]))),
517
+ (scheduler_second, lambda d: read_params(d, "scheduler_second", read_params(d, "scheduler", scheduler_names[0]))),
518
+ # cfg/delta
519
+ (cfg, lambda d: read_params(d, "cfg", 7.0)),
520
+ (cfg_second_pass_boost, lambda d: read_params(d, "cfg_second_pass_boost", True)),
521
+ (cfg_second_pass_delta, lambda d: read_params(d, "cfg_second_pass_delta", 3.0)),
522
+ # flags
523
+ (reuse_seed_noise, lambda d: read_params(d, "reuse_seed_noise", False)),
524
+ (mp_target_enabled, lambda d: read_params(d, "mp_target_enabled", False)),
525
+ (mp_target, lambda d: read_params(d, "mp_target", 2.0)),
526
+ (cond_cache_enabled, lambda d: read_params(d, "cond_cache_enabled", True)),
527
+ (cond_cache_max, lambda d: read_params(d, "cond_cache_max", 64)),
528
+ (vae_tiling_enabled, lambda d: read_params(d, "vae_tiling_enabled", False)),
529
+ (seamless_tiling_enabled, lambda d: read_params(d, "seamless_tiling_enabled", False)),
530
+ (tile_overlap, lambda d: read_params(d, "tile_overlap", 12)),
531
+ (lora_weight_first_factor, lambda d: read_params(d, "lora_weight_first_factor", 1.0)),
532
+ (lora_weight_second_factor, lambda d: read_params(d, "lora_weight_second_factor", 1.0)),
533
+ (match_colors_preset, lambda d: read_params(d, "match_colors_preset", "Off")),
534
+ (match_colors_enabled, lambda d: read_params(d, "match_colors_enabled", False)),
535
+ (match_colors_strength, lambda d: read_params(d, "match_colors_strength", 0.5)),
536
+ (postfx_preset, lambda d: read_params(d, "postfx_preset", "Off")),
537
+ (clahe_enabled, lambda d: read_params(d, "clahe_enabled", False)),
538
+ (clahe_clip, lambda d: read_params(d, "clahe_clip", 2.0)),
539
+ (clahe_tile_grid, lambda d: read_params(d, "clahe_tile_grid", 8)),
540
+ (unsharp_enabled, lambda d: read_params(d, "unsharp_enabled", False)),
541
+ (unsharp_radius, lambda d: read_params(d, "unsharp_radius", 1.5)),
542
+ (unsharp_amount, lambda d: read_params(d, "unsharp_amount", 0.75)),
543
+ (unsharp_threshold, lambda d: read_params(d, "unsharp_threshold", 0)),
544
+ (cn_ref, lambda d: read_params(d, "cn_ref", False)),
545
+ (start_control_at, lambda d: read_params(d, "start_control_at", 0.0)),
546
+ ]
547
+
548
+ return [
549
+ enable, quick_preset,
550
+ ratio, width, height,
551
+ steps_first, steps_second,
552
+ first_upscaler, second_upscaler, first_latent, second_latent,
553
+ prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append,
554
+ strength, filter_mode, filter_offset, denoise_offset,
555
+ sampler_first, sampler_second, scheduler_first, scheduler_second,
556
+ cfg, cfg_second_pass_boost, cfg_second_pass_delta,
557
+ reuse_seed_noise, mp_target_enabled, mp_target,
558
+ cond_cache_enabled, cond_cache_max,
559
+ vae_tiling_enabled,
560
+ seamless_tiling_enabled, tile_overlap,
561
+ lora_weight_first_factor, lora_weight_second_factor,
562
+ match_colors_preset, match_colors_enabled, match_colors_strength,
563
+ postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
564
+ unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
565
+ cn_ref, start_control_at
566
+ ]
567
+
568
+ # Capture base processing object and optional ControlNet state
569
+ def process(self, p, *args, **kwargs):
570
+ self.p = p
571
+ self._cn_units = []
572
+ self._use_cn = False
573
+ self._first_noise = None
574
+ self._first_noise_shape = None
575
+ self._saved_seeds = None
576
+ self._saved_subseeds = None
577
+ self._saved_subseed_strength = None
578
+ self._saved_seed_resize_from_h = None
579
+ self._saved_seed_resize_from_w = None
580
+ self._override_prompt_second = None
581
+
582
+ # Try detect ControlNet (best-effort; path may vary across installs)
583
+ ext_candidates = [
584
+ "extensions.sd_webui_controlnet.scripts.external_code",
585
+ "extensions.sd-webui-controlnet.scripts.external_code",
586
+ "extensions-builtin.sd-webui-controlnet.scripts.external_code",
587
+ ]
588
+ self._cn_ext = None
589
+ for mod in ext_candidates:
590
+ try:
591
+ self._cn_ext = __import__(mod, fromlist=["external_code"])
592
+ break
593
+ except Exception:
594
+ continue
595
+ if self._cn_ext:
596
+ try:
597
+ units = self._cn_ext.get_all_units_in_processing(p)
598
+ self._cn_units = list(units) if units else []
599
+ self._use_cn = len(self._cn_units) > 0
600
+ except Exception:
601
+ self._use_cn = False
602
+
603
+ # Log settings into PNG-info (single JSON block)
604
+ def before_process_batch(self, p, *args, **kwargs):
605
+ if not bool(self.config.get("enable", False)):
606
+ return
607
+ p.extra_generation_params["Custom Hires Fix"] = self.create_infotext
608
+
609
+ def create_infotext(self, p, *args, **kwargs):
610
+ scale_val = 0
611
+ if int(self.config.get("width", 0)) and int(self.config.get("height", 0)):
612
+ scale_val = f"{int(self.config.get('width'))}x{int(self.config.get('height'))}"
613
+ elif float(self.config.get("ratio", 0)):
614
+ scale_val = float(self.config.get("ratio"))
615
+
616
+ payload = {
617
+ "scale": scale_val,
618
+ "ratio": float(self.config.get("ratio", 0.0)),
619
+ "width": int(self.config.get("width", 0) or 0),
620
+ "height": int(self.config.get("height", 0) or 0),
621
+ "steps_first": int(self.config.get("steps_first", int(self.config.get("steps", 20)))),
622
+ "steps_second": int(self.config.get("steps_second", int(self.config.get("steps", 20)))),
623
+ "steps": int(self.config.get("steps", int(self.config.get("steps_first", 20)))),
624
+ "first_upscaler": self.config.get("first_upscaler", ""),
625
+ "second_upscaler": self.config.get("second_upscaler", ""),
626
+ "first_latent": float(self.config.get("first_latent", 0.3)),
627
+ "second_latent": float(self.config.get("second_latent", 0.1)),
628
+ "prompt": self.config.get("prompt", ""),
629
+ "negative_prompt": self.config.get("negative_prompt", ""),
630
+ "second_pass_prompt": self.config.get("second_pass_prompt", ""),
631
+ "second_pass_prompt_append": bool(self.config.get("second_pass_prompt_append", True)),
632
+ "strength": float(self.config.get("strength", 2.0)),
633
+ "filter_mode": self.config.get("filter_mode", ""),
634
+ "filter_offset": float(self.config.get("filter_offset", 0.0)),
635
+ "denoise_offset": float(self.config.get("denoise_offset", 0.05)),
636
+ "clip_skip": int(self.config.get("clip_skip", 0)),
637
+ # per-pass sampler/scheduler (include legacy for context)
638
+ "sampler_first": self.config.get("sampler_first", ""),
639
+ "sampler_second": self.config.get("sampler_second", self.config.get("sampler", "")),
640
+ "scheduler_first": self.config.get("scheduler_first", self.config.get("scheduler", "")),
641
+ "scheduler_second": self.config.get("scheduler_second", self.config.get("scheduler", "")),
642
+ # cfg
643
+ "cfg": float(self.cfg),
644
+ "cfg_second_pass_boost": bool(self.config.get("cfg_second_pass_boost", True)),
645
+ "cfg_second_pass_delta": float(self.config.get("cfg_second_pass_delta", 3.0)),
646
+ # flags
647
+ "reuse_seed_noise": bool(self.config.get("reuse_seed_noise", False)),
648
+ "mp_target_enabled": bool(self.config.get("mp_target_enabled", False)),
649
+ "mp_target": float(self.config.get("mp_target", 2.0)),
650
+ "cond_cache_enabled": bool(self.config.get("cond_cache_enabled", True)),
651
+ "cond_cache_max": int(self.config.get("cond_cache_max", 64)),
652
+ "vae_tiling_enabled": bool(self.config.get("vae_tiling_enabled", False)),
653
+ "seamless_tiling_enabled": bool(self.config.get("seamless_tiling_enabled", False)),
654
+ "tile_overlap": int(self.config.get("tile_overlap", 12)),
655
+ "lora_weight_first_factor": float(self.config.get("lora_weight_first_factor", 1.0)),
656
+ "lora_weight_second_factor": float(self.config.get("lora_weight_second_factor", 1.0)),
657
+ "match_colors_preset": self.config.get("match_colors_preset", "Off"),
658
+ "match_colors_enabled": bool(self.config.get("match_colors_enabled", False)),
659
+ "match_colors_strength": float(self.config.get("match_colors_strength", 0.5)),
660
+ "postfx_preset": self.config.get("postfx_preset", "Off"),
661
+ "clahe_enabled": bool(self.config.get("clahe_enabled", False)),
662
+ "clahe_clip": float(self.config.get("clahe_clip", 2.0)),
663
+ "clahe_tile_grid": int(self.config.get("clahe_tile_grid", 8)),
664
+ "unsharp_enabled": bool(self.config.get("unsharp_enabled", False)),
665
+ "unsharp_radius": float(self.config.get("unsharp_radius", 1.5)),
666
+ "unsharp_amount": float(self.config.get("unsharp_amount", 0.75)),
667
+ "unsharp_threshold": int(self.config.get("unsharp_threshold", 0)),
668
+ "cn_ref": bool(self.config.get("cn_ref", False)),
669
+ "start_control_at": float(self.config.get("start_control_at", 0.0)),
670
+ }
671
+ return json.dumps(payload, ensure_ascii=False).translate(quote_swap)
672
+
673
+ # --- Main postprocess hook ---
674
+ def postprocess_image(self, p, pp,
675
+ enable, quick_preset,
676
+ ratio, width, height,
677
+ steps_first, steps_second,
678
+ first_upscaler, second_upscaler, first_latent, second_latent,
679
+ prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append,
680
+ strength, filter_mode, filter_offset, denoise_offset,
681
+ sampler_first, sampler_second, scheduler_first, scheduler_second,
682
+ cfg, cfg_second_pass_boost, cfg_second_pass_delta,
683
+ reuse_seed_noise, mp_target_enabled, mp_target,
684
+ cond_cache_enabled, cond_cache_max,
685
+ vae_tiling_enabled,
686
+ seamless_tiling_enabled, tile_overlap,
687
+ lora_weight_first_factor, lora_weight_second_factor,
688
+ match_colors_preset, match_colors_enabled, match_colors_strength,
689
+ postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid,
690
+ unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold,
691
+ cn_ref, start_control_at):
692
+ if not enable:
693
+ return
694
+
695
+ # Save config chosen in UI
696
+ self.pp = pp
697
+ self.config["enable"] = bool(enable)
698
+ self.config["ratio"] = float(ratio)
699
+ self.config["width"] = int(width)
700
+ self.config["height"] = int(height)
701
+ self.config["steps_first"] = int(steps_first)
702
+ self.config["steps_second"] = int(steps_second)
703
+ self.config["steps"] = int(steps_second) # legacy aggregate
704
+ self.config["first_upscaler"] = first_upscaler
705
+ self.config["second_upscaler"] = second_upscaler
706
+ self.config["first_latent"] = float(first_latent)
707
+ self.config["second_latent"] = float(second_latent)
708
+ self.config["prompt"] = prompt.strip()
709
+ self.config["negative_prompt"] = negative_prompt.strip()
710
+ self.config["second_pass_prompt"] = second_pass_prompt.strip()
711
+ self.config["second_pass_prompt_append"] = bool(second_pass_prompt_append)
712
+ self.config["strength"] = float(strength)
713
+ self.config["filter_mode"] = filter_mode
714
+ self.config["filter_offset"] = float(filter_offset)
715
+ self.config["denoise_offset"] = float(denoise_offset)
716
+ # per-pass sampler/scheduler
717
+ self.config["sampler_first"] = sampler_first
718
+ self.config["sampler_second"] = sampler_second
719
+ self.config["scheduler_first"] = scheduler_first
720
+ self.config["scheduler_second"] = scheduler_second
721
+ # cfg/delta
722
+ self.config["cfg"] = float(cfg)
723
+ self.config["cfg_second_pass_boost"] = bool(cfg_second_pass_boost)
724
+ self.config["cfg_second_pass_delta"] = float(cfg_second_pass_delta)
725
+ # flags & extras
726
+ self.config["reuse_seed_noise"] = bool(reuse_seed_noise)
727
+ self.config["mp_target_enabled"] = bool(mp_target_enabled)
728
+ self.config["mp_target"] = float(mp_target)
729
+ self.config["cond_cache_enabled"] = bool(cond_cache_enabled)
730
+ self.config["cond_cache_max"] = int(cond_cache_max)
731
+ self.config["vae_tiling_enabled"] = bool(vae_tiling_enabled)
732
+ self.config["seamless_tiling_enabled"] = bool(seamless_tiling_enabled)
733
+ self.config["tile_overlap"] = int(tile_overlap)
734
+ self.config["lora_weight_first_factor"] = float(lora_weight_first_factor)
735
+ self.config["lora_weight_second_factor"] = float(lora_weight_second_factor)
736
+ self.config["match_colors_preset"] = match_colors_preset
737
+ self.config["match_colors_enabled"] = bool(match_colors_enabled)
738
+ self.config["match_colors_strength"] = float(match_colors_strength)
739
+ self.config["postfx_preset"] = postfx_preset
740
+ self.config["clahe_enabled"] = bool(clahe_enabled)
741
+ self.config["clahe_clip"] = float(clahe_clip)
742
+ self.config["clahe_tile_grid"] = int(clahe_tile_grid)
743
+ self.config["unsharp_enabled"] = bool(unsharp_enabled)
744
+ self.config["unsharp_radius"] = float(unsharp_radius)
745
+ self.config["unsharp_amount"] = float(unsharp_amount)
746
+ self.config["unsharp_threshold"] = int(unsharp_threshold)
747
+ self.config["cn_ref"] = bool(cn_ref)
748
+ self.config["start_control_at"] = float(start_control_at)
749
+ self.cfg = float(cfg) if cfg else float(p.cfg_scale)
750
+
751
+ # Validate sizing rules if MP target is off; if MP is on, sizing will be computed
752
+ if not self.config["mp_target_enabled"]:
753
+ assert ((width and height) or
754
+ ((width == 0 and height == 0) and (ratio and ratio > 0)) or
755
+ (width > 0) or (height > 0)), "Set width+height, or set both to 0 and ratio>0, or provide at least one dimension."
756
+
757
+ # Apply CLIP skip for the run
758
+ self._orig_clip_skip = shared.opts.CLIP_stop_at_last_layers
759
+ if int(self.config.get("clip_skip", 0)) > 0:
760
+ shared.opts.CLIP_stop_at_last_layers = int(self.config.get("clip_skip", 0))
761
+
762
+ # Toggle VAE tiling for the run
763
+ self._set_vae_tiling(self.config["vae_tiling_enabled"])
764
+
765
+ # Toggle seamless tiling
766
+ self._orig_tiling = getattr(self.p, "tiling", None)
767
+ self._orig_tile_overlap = getattr(self.p, "tile_overlap", None)
768
+ if bool(self.config.get("seamless_tiling_enabled", False)):
769
+ try:
770
+ self.p.tiling = True
771
+ if hasattr(self.p, "tile_overlap"):
772
+ self.p.tile_overlap = int(self.config.get("tile_overlap", 12))
773
+ except Exception:
774
+ pass
775
+
776
+ # Activate extra networks from prompts
777
+ _, loras_act = extra_networks.parse_prompt(self.config["prompt"])
778
+ extra_networks.activate(p, loras_act)
779
+ _, loras_deact = extra_networks.parse_prompt(self.config["negative_prompt"])
780
+ extra_networks.deactivate(p, loras_deact)
781
+
782
+ try:
783
+ with devices.autocast():
784
+ shared.state.nextjob()
785
+ x = self._first_pass(pp.image)
786
+ shared.state.nextjob()
787
+ x = self._second_pass(x)
788
+ sd_models.apply_token_merging(p.sd_model, p.get_token_merging_ratio())
789
+ # Post-FX chain is inside _second_pass
790
+ pp.image = x
791
+ finally:
792
+ # Restore options
793
+ shared.opts.CLIP_stop_at_last_layers = self._orig_clip_skip
794
+ self._restore_vae_tiling()
795
+ if self._orig_tiling is not None:
796
+ try:
797
+ self.p.tiling = self._orig_tiling
798
+ if hasattr(self.p, "tile_overlap") and self._orig_tile_overlap is not None:
799
+ self.p.tile_overlap = self._orig_tile_overlap
800
+ except Exception:
801
+ pass
802
+ extra_networks.deactivate(p, loras_act)
803
+
804
+ # ---- Helpers ----
805
+ def _maybe_mp_resize(self, base_w, base_h, target_mp: float):
806
+ """Compute size from megapixels while keeping aspect ratio; quantize to multiple of 8."""
807
+ aspect = base_w / base_h if base_h else 1.0
808
+ total_px = max(0.01, target_mp) * 1_000_000.0
809
+ w_float = math.sqrt(total_px * aspect)
810
+ h_float = w_float / aspect
811
+ w = max(8, int(round(w_float / 8) * 8))
812
+ h = max(8, int(round(h_float / 8) * 8))
813
+ return w, h
814
+
815
+ def _model_hash_for_cache(self):
816
+ # best-effort model hash for cache key
817
+ try:
818
+ return getattr(shared.sd_model, "sd_model_hash", None) or getattr(shared.sd_model, "hash", None) or str(id(shared.sd_model))
819
+ except Exception:
820
+ return str(id(shared.sd_model))
821
+
822
+ def _cond_key(self, width, height, steps_for_cond, prompt: str, negative: str, clip_skip: int):
823
+ h = hashlib.sha256()
824
+ h.update((prompt or "").encode("utf-8"))
825
+ h.update(b"::")
826
+ h.update((negative or "").encode("utf-8"))
827
+ key = f"{self._model_hash_for_cache()}|{width}x{height}|{steps_for_cond}|cs={clip_skip}|{h.hexdigest()}"
828
+ return key
829
+
830
+ def _cond_cache_get(self, key: str):
831
+ if not bool(self.config.get("cond_cache_enabled", True)):
832
+ return None
833
+ item = self._cond_cache.get(key)
834
+ if item is not None:
835
+ self._cond_cache.move_to_end(key)
836
+ return item
837
+
838
+ def _cond_cache_put(self, key: str, value: tuple):
839
+ if not bool(self.config.get("cond_cache_enabled", True)):
840
+ return
841
+ self._cond_cache[key] = value
842
+ self._cond_cache.move_to_end(key)
843
+ max_items = int(self.config.get("cond_cache_max", 64))
844
+ while len(self._cond_cache) > max_items:
845
+ self._cond_cache.popitem(last=False)
846
+
847
+ def _set_vae_tiling(self, enabled: bool):
848
+ # Save original state if we have not yet
849
+ if self._orig_opt_vae_tiling is None and hasattr(shared.opts, "sd_vae_tiling"):
850
+ self._orig_opt_vae_tiling = bool(shared.opts.sd_vae_tiling)
851
+ # Toggle option
852
+ if hasattr(shared.opts, "sd_vae_tiling"):
853
+ shared.opts.sd_vae_tiling = bool(enabled)
854
+ # Try model-level toggle
855
+ vae = getattr(shared.sd_model, "first_stage_model", None)
856
+ if vae is not None:
857
+ try:
858
+ if enabled and hasattr(vae, "enable_tiling"):
859
+ vae.enable_tiling()
860
+ if not enabled and hasattr(vae, "disable_tiling"):
861
+ vae.disable_tiling()
862
+ except Exception:
863
+ pass
864
+
865
+ def _restore_vae_tiling(self):
866
+ if self._orig_opt_vae_tiling is not None and hasattr(shared.opts, "sd_vae_tiling"):
867
+ shared.opts.sd_vae_tiling = self._orig_opt_vae_tiling
868
+ vae = getattr(shared.sd_model, "first_stage_model", None)
869
+ if vae is not None:
870
+ try:
871
+ if self._orig_opt_vae_tiling and hasattr(vae, "enable_tiling"):
872
+ vae.enable_tiling()
873
+ elif not self._orig_opt_vae_tiling and hasattr(vae, "disable_tiling"):
874
+ vae.disable_tiling()
875
+ except Exception:
876
+ pass
877
+ self._orig_opt_vae_tiling = None
878
+
879
+ def _scale_lora_in_prompt(self, text: str, factor: float) -> str:
880
+ # Multiply existing <lora:name:weight>, or append weight if missing
881
+ # Very lightweight parser to keep prompt untouched otherwise
882
+ if factor is None or abs(factor - 1.0) < 1e-6:
883
+ return text
884
+ out = []
885
+ i = 0
886
+ while i < len(text):
887
+ start = text.find("<lora:", i)
888
+ if start == -1:
889
+ out.append(text[i:])
890
+ break
891
+ out.append(text[i:start])
892
+ end = text.find(">", start)
893
+ if end == -1:
894
+ out.append(text[start:])
895
+ break
896
+ token = text[start:end+1]
897
+ parts = token[1:-1].split(":") # lora,name,weight?
898
+ if len(parts) >= 2 and parts[0] == "lora":
899
+ name = parts[1]
900
+ if len(parts) >= 3:
901
+ try:
902
+ w = float(parts[2])
903
+ except Exception:
904
+ w = 1.0
905
+ new_w = max(0.0, w * factor)
906
+ new_token = f"<lora:{name}:{new_w:.4g}>"
907
+ else:
908
+ new_token = f"<lora:{name}:{factor:.4g}>"
909
+ out.append(new_token)
910
+ else:
911
+ out.append(token)
912
+ i = end + 1
913
+ return "".join(out)
914
+
915
+ def _prepare_conditioning(self, width, height, steps_for_cond: int, prompt_override: str = None):
916
+ """Build (cond, uncond) with optional LRU caching and LoRA scaling."""
917
+ base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip()
918
+ negative_base = self.config.get("negative_prompt", "").strip() or self.p.negative_prompt.strip()
919
+
920
+ if prompt_override:
921
+ base_prompt = prompt_override.strip()
922
+
923
+ # Apply LoRA scaling for this pass
924
+ scaled_prompt = self._scale_lora_in_prompt(base_prompt, self._current_lora_factor)
925
+
926
+ clip_skip = int(self.config.get("clip_skip", 0))
927
+
928
+ # Cache lookup
929
+ cache_key = self._cond_key(width, height, steps_for_cond, scaled_prompt, negative_base, clip_skip)
930
+ cached = self._cond_cache_get(cache_key)
931
+ if cached is not None:
932
+ self.cond, self.uncond = cached
933
+ return
934
+
935
+ # Parse extra networks and build cond
936
+ prompt_text = scaled_prompt
937
+ if not getattr(self.p, "disable_extra_networks", False):
938
+ try:
939
+ prompt_text, extra = extra_networks.parse_prompt(prompt_text)
940
+ if extra:
941
+ extra_networks.activate(self.p, extra)
942
+ except Exception:
943
+ pass
944
+
945
+ if width and height and hasattr(prompt_parser, "SdConditioning"):
946
+ c = prompt_parser.SdConditioning([prompt_text], False, width, height)
947
+ uc = prompt_parser.SdConditioning([negative_base], False, width, height)
948
+ else:
949
+ c, uc = [prompt_text], [negative_base]
950
+
951
+ cond = prompt_parser.get_multicond_learned_conditioning(shared.sd_model, c, steps_for_cond)
952
+ uncond = prompt_parser.get_learned_conditioning(shared.sd_model, uc, steps_for_cond)
953
+ self.cond, self.uncond = cond, uncond
954
+
955
+ # Store in cache
956
+ self._cond_cache_put(cache_key, (cond, uncond))
957
+
958
+ def _to_sample(self, x_img: Image.Image):
959
+ image = np.array(x_img).astype(np.float32) / 255.0
960
+ image = np.moveaxis(image, 2, 0)
961
+ decoded = torch.from_numpy(image).to(shared.device).to(devices.dtype_vae)
962
+ decoded = 2.0 * decoded - 1.0
963
+ encoded = shared.sd_model.encode_first_stage(decoded.unsqueeze(0).to(devices.dtype_vae))
964
+ sample = shared.sd_model.get_first_stage_encoding(encoded)
965
+ return decoded, sample
966
+
967
+ def _create_sampler(self, sampler_name: str):
968
+ if "Restart" in sampler_name:
969
+ try:
970
+ return sd_samplers.create_sampler("Restart", shared.sd_model)
971
+ except Exception:
972
+ return sd_samplers.create_sampler("DPM++ 2M Karras", shared.sd_model)
973
+ return sd_samplers.create_sampler(sampler_name, shared.sd_model)
974
+
975
+ def _apply_clahe(self, img: Image.Image) -> Image.Image:
976
+ if not bool(self.config.get("clahe_enabled", False)):
977
+ return img
978
+ np_img = np.array(img)
979
+ if _CV2_OK:
980
+ lab = cv2.cvtColor(np_img, cv2.COLOR_RGB2LAB)
981
+ l, a, b = cv2.split(lab)
982
+ clip = float(self.config.get("clahe_clip", 2.0))
983
+ tiles = int(self.config.get("clahe_tile_grid", 8))
984
+ clahe = cv2.createCLAHE(clipLimit=max(0.1, clip), tileGridSize=(tiles, tiles))
985
+ l2 = clahe.apply(l)
986
+ lab2 = cv2.merge((l2, a, b))
987
+ rgb = cv2.cvtColor(lab2, cv2.COLOR_LAB2RGB)
988
+ return Image.fromarray(rgb)
989
+ if _SKIMAGE_OK:
990
+ # skimage fallback on L channel in LAB
991
+ lab = skcolor.rgb2lab(np_img / 255.0)
992
+ l = lab[..., 0] / 100.0
993
+ l2 = equalize_adapthist(l, clip_limit=float(self.config.get("clahe_clip", 2.0)))
994
+ lab[..., 0] = np.clip(l2 * 100.0, 0, 100.0)
995
+ rgb = skcolor.lab2rgb(lab)
996
+ rgb8 = np.clip(rgb * 255.0, 0, 255).astype(np.uint8)
997
+ return Image.fromarray(rgb8)
998
+ # No-op fallback
999
+ return img
1000
+
1001
+ def _apply_unsharp(self, img: Image.Image) -> Image.Image:
1002
+ if not bool(self.config.get("unsharp_enabled", False)):
1003
+ return img
1004
+ radius = float(self.config.get("unsharp_radius", 1.5))
1005
+ amount = float(self.config.get("unsharp_amount", 0.75))
1006
+ threshold = int(self.config.get("unsharp_threshold", 0))
1007
+ return img.filter(ImageFilter.UnsharpMask(radius=radius, percent=int(amount * 100), threshold=threshold))
1008
+
1009
+ def _apply_match_colors(self, img: Image.Image, ref: Image.Image) -> Image.Image:
1010
+ if not bool(self.config.get("match_colors_enabled", False)):
1011
+ return img
1012
+ strength = float(self.config.get("match_colors_strength", 0.5))
1013
+ strength = max(0.0, min(1.0, strength))
1014
+ if strength <= 0.0:
1015
+ return img
1016
+
1017
+ arr = np.array(img).astype(np.float32)
1018
+ ref_arr = np.array(ref).astype(np.float32)
1019
+
1020
+ matched = None
1021
+ if _SKIMAGE_OK:
1022
+ try:
1023
+ matched = match_histograms(arr, ref_arr, channel_axis=-1).astype(np.float32)
1024
+ except TypeError:
1025
+ # older skimage
1026
+ matched = match_histograms(arr, ref_arr, multichannel=True).astype(np.float32)
1027
+ else:
1028
+ # simple mean-std per channel fallback
1029
+ eps = 1e-6
1030
+ for c in range(arr.shape[2]):
1031
+ src = arr[..., c]
1032
+ dst = ref_arr[..., c]
1033
+ src_m, src_s = src.mean(), src.std() + eps
1034
+ dst_m, dst_s = dst.mean(), dst.std() + eps
1035
+ arr[..., c] = np.clip((src - src_m) * (dst_s / src_s) + dst_m, 0, 255)
1036
+ matched = arr
1037
+
1038
+ out = (1.0 - strength) * arr + strength * matched
1039
+ out = np.clip(out, 0, 255).astype(np.uint8)
1040
+ return Image.fromarray(out)
1041
+
1042
+ def _first_pass(self, x: Image.Image) -> Image.Image:
1043
+ # Determine target size
1044
+ if bool(self.config.get("mp_target_enabled", False)):
1045
+ w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0)))
1046
+ else:
1047
+ ratio = x.width / x.height if x.height else 1.0
1048
+ if int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and float(self.config.get("ratio", 0)) > 0:
1049
+ w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
1050
+ h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
1051
+ else:
1052
+ if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
1053
+ w, h = int(self.config["width"]), int(self.config["height"])
1054
+ elif int(self.config.get("width", 0)) > 0:
1055
+ w = int(self.config["width"])
1056
+ h = int(round(w / ratio / 8) * 8)
1057
+ elif int(self.config.get("height", 0)) > 0:
1058
+ h = int(self.config["height"])
1059
+ w = int(round(h * ratio / 8) * 8)
1060
+ else:
1061
+ w, h = x.width, x.height
1062
+
1063
+ self.width, self.height = w, h
1064
+
1065
+ sd_models.apply_token_merging(self.p.sd_model, self.p.get_token_merging_ratio(for_hr=True) / 2)
1066
+
1067
+ # Per-pass scheduler
1068
+ sched_first = self.config.get("scheduler_first", self.config.get("scheduler", "Use same scheduler"))
1069
+ self.p.scheduler = self.p.scheduler if sched_first == "Use same scheduler" else sched_first
1070
+
1071
+ # Optional ControlNet
1072
+ if self._use_cn:
1073
+ try:
1074
+ cn_np = np.array(x.resize((self.width, self.height)))
1075
+ self._enable_controlnet(cn_np)
1076
+ except Exception:
1077
+ pass
1078
+
1079
+ # Build override prompt for first pass (none; base prompt)
1080
+ self._current_lora_factor = float(self.config.get("lora_weight_first_factor", 1.0))
1081
+ with devices.autocast(), torch.inference_mode():
1082
+ self._prepare_conditioning(self.width, self.height, int(self.config.get("steps_first", 20)))
1083
+
1084
+ # Upscale (image domain) then (optionally) blend latent
1085
+ x_img = images.resize_image(0, x, self.width, self.height, upscaler_name=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
1086
+ decoded, sample = self._to_sample(x_img)
1087
+ x_latent = torch.nn.functional.interpolate(sample, (self.height // 8, self.width // 8), mode="nearest")
1088
+
1089
+ first_latent = float(self.config.get("first_latent", 0.3))
1090
+ if 0.0 <= first_latent <= 1.0:
1091
+ sample = (x_latent * (1.0 - first_latent)) + (sample * first_latent)
1092
+
1093
+ image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
1094
+
1095
+ # RNG setup
1096
+ self._saved_seeds = list(getattr(self.p, "seeds", [])) or None
1097
+ self._saved_subseeds = list(getattr(self.p, "subseeds", [])) or None
1098
+ self._saved_subseed_strength = getattr(self.p, "subseed_strength", None)
1099
+ self._saved_seed_resize_from_h = getattr(self.p, "seed_resize_from_h", None)
1100
+ self._saved_seed_resize_from_w = getattr(self.p, "seed_resize_from_w", None)
1101
+
1102
+ self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
1103
+ subseed_strength=self.p.subseed_strength,
1104
+ seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
1105
+
1106
+ # Denoise config for first pass
1107
+ steps = int(self.config.get("steps_first", int(self.config.get("steps", 20))))
1108
+ noise = torch.randn_like(sample)
1109
+ if bool(self.config.get("reuse_seed_noise", False)):
1110
+ self._first_noise = noise.detach().clone()
1111
+ self._first_noise_shape = tuple(sample.shape)
1112
+
1113
+ self.p.denoising_strength = 0.33 + float(self.config.get("denoise_offset", 0.05)) * 0.2
1114
+ self.p.cfg_scale = float(self.cfg)
1115
+
1116
+ def denoiser_override(n):
1117
+ return K.sampling.get_sigmas_polyexponential(n, 0.005, 20, 0.6, devices.device) # type: ignore
1118
+
1119
+ self.p.sampler_noise_scheduler_override = denoiser_override
1120
+ self.p.batch_size = 1
1121
+
1122
+ # Per-pass sampler
1123
+ sampler_first = self.config.get("sampler_first", self.config.get("sampler", "DPM++ 2M Karras"))
1124
+ sampler = self._create_sampler(sampler_first)
1125
+
1126
+ samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
1127
+ steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
1128
+
1129
+ devices.torch_gc()
1130
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
1131
+ if math.isnan(decoded_sample.min() if hasattr(decoded_sample, "min") else 0):
1132
+ devices.torch_gc()
1133
+ samples = torch.clamp(samples, -3, 3)
1134
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
1135
+
1136
+ decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
1137
+ x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
1138
+ return Image.fromarray(x_np.astype(np.uint8))
1139
+
1140
+ def _second_pass(self, x: Image.Image) -> Image.Image:
1141
+ # Determine target size for second pass
1142
+ if bool(self.config.get("mp_target_enabled", False)):
1143
+ w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0)))
1144
+ else:
1145
+ if (int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and
1146
+ float(self.config.get("ratio", 0)) > 0):
1147
+ w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
1148
+ h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
1149
+ else:
1150
+ aspect = x.width / x.height if x.height else 1.0
1151
+ if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
1152
+ w, h = int(self.config["width"]), int(self.config["height"])
1153
+ elif int(self.config.get("width", 0)) > 0:
1154
+ w = int(self.config["width"])
1155
+ h = int(round(w / aspect / 8) * 8)
1156
+ elif int(self.config.get("height", 0)) > 0:
1157
+ h = int(self.config["height"])
1158
+ w = int(round(h * aspect / 8) * 8)
1159
+ else:
1160
+ w, h = x.width, x.height
1161
+
1162
+ sd_models.apply_token_merging(self.p.sd_model, self.p.get_token_merging_ratio(for_hr=True))
1163
+
1164
+ # Per-pass scheduler
1165
+ sched_second = self.config.get("scheduler_second", self.config.get("scheduler", "Use same scheduler"))
1166
+ self.p.scheduler = self.p.scheduler if sched_second == "Use same scheduler" else sched_second
1167
+
1168
+ if self._use_cn:
1169
+ cn_img = x if bool(self.config.get("cn_ref", False)) else self.pp.image
1170
+ try:
1171
+ self._enable_controlnet(np.array(cn_img.resize((w, h))))
1172
+ except Exception:
1173
+ pass
1174
+
1175
+ # Build override prompt for second pass
1176
+ base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip()
1177
+ p2 = (self.config.get("second_pass_prompt", "") or "").strip()
1178
+ if p2:
1179
+ if bool(self.config.get("second_pass_prompt_append", True)):
1180
+ prompt_override = (base_prompt + ", " + p2) if base_prompt else p2
1181
+ else:
1182
+ prompt_override = p2
1183
+ else:
1184
+ prompt_override = None
1185
+
1186
+ # Apply LoRA scaling for second pass
1187
+ self._current_lora_factor = float(self.config.get("lora_weight_second_factor", 1.0))
1188
+ with devices.autocast(), torch.inference_mode():
1189
+ self._prepare_conditioning(w, h, int(self.config.get("steps_second", 20)), prompt_override=prompt_override)
1190
+
1191
+ # Optional latent mix
1192
+ x_latent = None
1193
+ second_latent = float(self.config.get("second_latent", 0.1))
1194
+ if second_latent > 0:
1195
+ _, sample_from_img = self._to_sample(x)
1196
+ x_latent = torch.nn.functional.interpolate(sample_from_img, (h // 8, w // 8), mode="nearest")
1197
+
1198
+ # Upscale to target and encode
1199
+ if second_latent < 1.0:
1200
+ x_up = images.resize_image(0, x, w, h, upscaler_name=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
1201
+ decoded, sample = self._to_sample(x_up)
1202
+ else:
1203
+ decoded, sample = self._to_sample(x)
1204
+
1205
+ if x_latent is not None and 0.0 <= second_latent <= 1.0:
1206
+ sample = (sample * (1.0 - second_latent)) + (x_latent * second_latent)
1207
+
1208
+ image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
1209
+
1210
+ # RNG: optionally reuse seed/noise
1211
+ if bool(self.config.get("reuse_seed_noise", False)) and self._saved_seeds is not None:
1212
+ try:
1213
+ self.p.seeds = list(self._saved_seeds)
1214
+ self.p.subseeds = list(self._saved_subseeds) if self._saved_subseeds is not None else self.p.subseeds
1215
+ self.p.subseed_strength = self._saved_subseed_strength if self._saved_subseed_strength is not None else self.p.subseed_strength
1216
+ 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
1217
+ 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
1218
+ except Exception:
1219
+ pass
1220
+
1221
+ self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
1222
+ subseed_strength=self.p.subseed_strength,
1223
+ seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
1224
+
1225
+ # Denoise config for second pass
1226
+ steps = int(self.config.get("steps_second", int(self.config.get("steps", 20))))
1227
+ if bool(self.config.get("cfg_second_pass_boost", True)):
1228
+ self.p.cfg_scale = float(self.cfg) + float(self.config.get("cfg_second_pass_delta", 3.0))
1229
+ else:
1230
+ self.p.cfg_scale = float(self.cfg)
1231
+ self.p.denoising_strength = 0.45 + float(self.config.get("denoise_offset", 0.05)) * 0.2
1232
+
1233
+ # Noise: reuse tensor if shapes match, else fresh noise
1234
+ if bool(self.config.get("reuse_seed_noise", False)) and self._first_noise is not None:
1235
+ if tuple(sample.shape) == tuple(self._first_noise_shape or ()):
1236
+ noise = self._first_noise.to(sample.device, dtype=sample.dtype)
1237
+ else:
1238
+ noise = torch.randn_like(sample)
1239
+ else:
1240
+ noise = torch.randn_like(sample)
1241
+
1242
+ def denoiser_override(n):
1243
+ return K.sampling.get_sigmas_polyexponential(n, 0.01, 15, 0.5, devices.device) # type: ignore
1244
+
1245
+ self.p.sampler_noise_scheduler_override = denoiser_override
1246
+ self.p.batch_size = 1
1247
+
1248
+ # Per-pass sampler
1249
+ sampler_second = self.config.get("sampler_second", self.config.get("sampler", "DPM++ 2M Karras"))
1250
+ sampler = self._create_sampler(sampler_second)
1251
+
1252
+ samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
1253
+ steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
1254
+
1255
+ devices.torch_gc()
1256
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
1257
+ if math.isnan(decoded_sample.min() if hasattr(decoded_sample, "min") else 0):
1258
+ devices.torch_gc()
1259
+ samples = torch.clamp(samples, -3, 3)
1260
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
1261
+
1262
+ decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
1263
+ x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
1264
+ out_img = Image.fromarray(x_np.astype(np.uint8))
1265
+
1266
+ # Post-FX: CLAHE -> Unsharp -> Color match
1267
+ out_img = self._apply_clahe(out_img)
1268
+ out_img = self._apply_unsharp(out_img)
1269
+ if bool(self.config.get("match_colors_enabled", False)):
1270
+ # match to original input
1271
+ out_img = self._apply_match_colors(out_img, self.pp.image)
1272
+
1273
+ return out_img
1274
+
1275
+ def _enable_controlnet(self, image_np: np.ndarray):
1276
+ if not getattr(self, "_cn_ext", None):
1277
+ return
1278
+ for unit in self._cn_units:
1279
+ try:
1280
+ if getattr(unit, "model", "None") != "None":
1281
+ if getattr(unit, "enabled", True):
1282
+ unit.guidance_start = float(self.config.get("start_control_at", 0.0))
1283
+ unit.processor_res = min(image_np.shape[0], image_np.shape[1])
1284
+ unit.enabled = True
1285
+ if getattr(unit, "image", None) is None:
1286
+ unit.image = image_np
1287
+ self.p.width = image_np.shape[1]
1288
+ self.p.height = image_np.shape[0]
1289
+ except Exception:
1290
+ continue
1291
+ try:
1292
+ self._cn_ext.update_cn_script_in_processing(self.p, self._cn_units)
1293
+ for script in self.p.scripts.alwayson_scripts:
1294
+ if script.title().lower() == "controlnet":
1295
+ script.controlnet_hack(self.p)
1296
+ except Exception:
1297
+ pass
1298
+
1299
+
1300
+ def parse_infotext(infotext, params):
1301
+ try:
1302
+ block = params.get("Custom Hires Fix")
1303
+ if not block:
1304
+ return
1305
+ data = json.loads(block.translate(quote_swap)) if isinstance(block, str) else block
1306
+ params["Custom Hires Fix"] = data
1307
+ scale = data.get("scale", 0)
1308
+ if isinstance(scale, str) and "x" in scale:
1309
+ w, _, h = scale.partition("x")
1310
+ data["ratio"] = 0.0
1311
+ data["width"] = int(w)
1312
+ data["height"] = int(h)
1313
+ else:
1314
+ try:
1315
+ r = float(scale)
1316
+ except Exception:
1317
+ r = 0.0
1318
+ data["ratio"] = r
1319
+ data["width"] = int(data.get("width", 0) or 0)
1320
+ data["height"] = int(data.get("height", 0) or 0)
1321
+
1322
+ # Defaults for new/legacy fields
1323
+ if "steps_first" not in data:
1324
+ data["steps_first"] = int(data.get("steps", 20))
1325
+ if "steps_second" not in data:
1326
+ data["steps_second"] = int(data.get("steps", 20))
1327
+
1328
+ # per-pass sampler/scheduler defaults from legacy single values
1329
+ data.setdefault("sampler_first", data.get("sampler", ""))
1330
+ data.setdefault("sampler_second", data.get("sampler", ""))
1331
+ data.setdefault("scheduler_first", data.get("scheduler", "Use same scheduler"))
1332
+ data.setdefault("scheduler_second", data.get("scheduler", "Use same scheduler"))
1333
+
1334
+ # CFG delta defaults
1335
+ data.setdefault("cfg_second_pass_boost", True)
1336
+ data.setdefault("cfg_second_pass_delta", 3.0)
1337
+
1338
+ # Flags defaults
1339
+ data.setdefault("reuse_seed_noise", False)
1340
+ data.setdefault("mp_target_enabled", False)
1341
+ data.setdefault("mp_target", 2.0)
1342
+ data.setdefault("cond_cache_enabled", True)
1343
+ data.setdefault("cond_cache_max", 64)
1344
+ data.setdefault("vae_tiling_enabled", False)
1345
+ data.setdefault("seamless_tiling_enabled", False)
1346
+ data.setdefault("tile_overlap", 12)
1347
+ data.setdefault("lora_weight_first_factor", 1.0)
1348
+ data.setdefault("lora_weight_second_factor", 1.0)
1349
+ data.setdefault("match_colors_preset", "Off")
1350
+ data.setdefault("match_colors_enabled", False)
1351
+ data.setdefault("match_colors_strength", 0.5)
1352
+ data.setdefault("postfx_preset", "Off")
1353
+ data.setdefault("clahe_enabled", False)
1354
+ data.setdefault("clahe_clip", 2.0)
1355
+ data.setdefault("clahe_tile_grid", 8)
1356
+ data.setdefault("unsharp_enabled", False)
1357
+ data.setdefault("unsharp_radius", 1.5)
1358
+ data.setdefault("unsharp_amount", 0.75)
1359
+ data.setdefault("unsharp_threshold", 0)
1360
+ data.setdefault("second_pass_prompt", "")
1361
+ data.setdefault("second_pass_prompt_append", True)
1362
+
1363
+ except Exception:
1364
+ pass
1365
+
1366
+ # Register paste-params hook
1367
+ script_callbacks.on_infotext_pasted(parse_infotext)