dikdimon commited on
Commit
d2c2513
·
verified ·
1 Parent(s): dddbb4b

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

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