dikdimon commited on
Commit
89b966c
·
verified ·
1 Parent(s): 968462d

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

Browse files
custom-hires-fix-for-automatic1111/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-for-automatic1111/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-for-automatic1111/scripts/__pycache__/custom_hires_fix.cpython-310.pyc ADDED
Binary file (20.4 kB). View file
 
custom-hires-fix-for-automatic1111/scripts/custom_hires_fix.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import math
3
+ import json
4
+ from copy import copy
5
+ from pathlib import Path
6
+
7
+ import gradio as gr
8
+ import numpy as np
9
+ import torch
10
+ from PIL import Image
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
+
36
+ try:
37
+ from omegaconf import OmegaConf, DictConfig # type: ignore
38
+ except Exception: # graceful fallback if OmegaConf not available
39
+ class DictConfig(dict): # minimal stub
40
+ pass
41
+ class OmegaConf: # minimal stub
42
+ @staticmethod
43
+ def load(path):
44
+ return DictConfig()
45
+ @staticmethod
46
+ def create(obj):
47
+ return DictConfig(obj)
48
+
49
+ import kornia # type: ignore
50
+ import k_diffusion as K # type: ignore
51
+
52
+ quote_swap = str.maketrans("\'\"", "\"\'")
53
+ config_path = (Path(__file__).parent.resolve() / "../config.yaml").resolve()
54
+
55
+
56
+ class CustomHiresFix(scripts.Script):
57
+ """Two-stage img2img upscaling with optional latent mixing and prompt overrides.
58
+ Adds: ratio-scaling, sampler/scheduler wiring, PNG-info logging, and a UI flag
59
+ to add +3 CFG only on the second pass.
60
+ """
61
+ def __init__(self):
62
+ super().__init__()
63
+ # Load or init config
64
+ if config_path.exists():
65
+ try:
66
+ self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore
67
+ except Exception:
68
+ self.config = OmegaConf.create({}) # type: ignore
69
+ else:
70
+ self.config = OmegaConf.create({}) # type: ignore
71
+
72
+ # Runtime state
73
+ self.p = None
74
+ self.pp = None
75
+ self.cfg = 0.0
76
+ self.sampler = None
77
+ self.cond = None
78
+ self.uncond = None
79
+ self.width = None
80
+ self.height = None
81
+ self._callback_set = False
82
+ self._orig_clip_skip = None
83
+ self._cn_units = []
84
+ self._use_cn = False
85
+
86
+ # ---- A1111 Script API ----
87
+ def title(self):
88
+ return "Custom Hires Fix"
89
+
90
+ def show(self, is_img2img):
91
+ return scripts.AlwaysVisible
92
+
93
+ def ui(self, is_img2img):
94
+ sampler_names = ["Restart + DPM++ 3M SDE"] + [x.name for x in sd_samplers.visible_samplers()]
95
+ scheduler_names = ["Use same scheduler"] + [x.label for x in sd_schedulers.schedulers]
96
+
97
+ with gr.Accordion(label="Custom Hires Fix", open=False) as enable_box:
98
+ enable = gr.Checkbox(label="Enable extension", value=bool(self.config.get("enable", False)))
99
+
100
+ with gr.Row():
101
+ ratio = gr.Slider(minimum=0.0, maximum=4.0, step=0.05, label="Upscale by (ratio)",
102
+ value=float(self.config.get("ratio", 0.0)))
103
+ width = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize width to",
104
+ value=int(self.config.get("width", 0)))
105
+ height = gr.Slider(minimum=0, maximum=2048, step=8, label="Resize height to",
106
+ value=int(self.config.get("height", 0)))
107
+ steps = gr.Slider(minimum=1, maximum=50, step=1, label="Hires steps",
108
+ value=int(self.config.get("steps", 20)))
109
+
110
+ with gr.Row():
111
+ first_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
112
+ label="First upscaler", value=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
113
+ second_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers],
114
+ label="Second upscaler", value=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
115
+
116
+ with gr.Row():
117
+ first_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (first stage)",
118
+ value=float(self.config.get("first_latent", 0.3)))
119
+ second_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (second stage)",
120
+ value=float(self.config.get("second_latent", 0.1)))
121
+
122
+ with gr.Row():
123
+ filter_mode = gr.Dropdown(["Noise sync (sharp)", "Morphological (smooth)", "Combined (balanced)"],
124
+ label="Filter mode", value=self.config.get("filter_mode", "Noise sync (sharp)"))
125
+ strength = gr.Slider(minimum=0.5, maximum=4.0, step=0.1, label="Generation strength",
126
+ value=float(self.config.get("strength", 2.0)))
127
+ denoise_offset = gr.Slider(minimum=-0.1, maximum=0.2, step=0.01, label="Denoise offset",
128
+ value=float(self.config.get("denoise_offset", 0.05)))
129
+
130
+ with gr.Row():
131
+ prompt = gr.Textbox(label="Prompt override", placeholder="Leave empty to use main UI prompt",
132
+ value=self.config.get("prompt", ""))
133
+ negative_prompt = gr.Textbox(label="Negative prompt override", placeholder="Leave empty to use main UI negative prompt",
134
+ value=self.config.get("negative_prompt", ""))
135
+
136
+ with gr.Accordion(label="Extra", open=False):
137
+ with gr.Row():
138
+ filter_offset = gr.Slider(minimum=-1.0, maximum=1.0, step=0.1, label="Filter offset",
139
+ value=float(self.config.get("filter_offset", 0.0)))
140
+ clip_skip = gr.Slider(minimum=0, maximum=12, step=1, label="CLIP skip (0 = keep)",
141
+ value=int(self.config.get("clip_skip", 0)))
142
+ with gr.Row():
143
+ sampler = gr.Dropdown(sampler_names, label="Sampler", value=self.config.get("sampler", sampler_names[0]))
144
+ cfg = gr.Slider(minimum=0, maximum=30, step=0.5, label="CFG Scale",
145
+ value=float(self.config.get("cfg", 7.0)))
146
+ # NEW: checkbox to add +3 CFG on second pass
147
+ cfg_second_pass_boost = gr.Checkbox(label="+3 CFG на втором проходе",
148
+ value=bool(self.config.get("cfg_second_pass_boost", True)))
149
+ scheduler = gr.Dropdown(choices=scheduler_names, label="Schedule type",
150
+ value=self.config.get("scheduler", scheduler_names[0]))
151
+ with gr.Row():
152
+ cn_ref = gr.Checkbox(label="Use last image as ControlNet reference", value=bool(self.config.get("cn_ref", False)))
153
+ start_control_at = gr.Slider(minimum=0.0, maximum=0.7, step=0.01, label="CN start (enabled units)",
154
+ value=float(self.config.get("start_control_at", 0.0)))
155
+
156
+ # Mutual exclusivity helpers
157
+ width.change(fn=lambda x: gr.update(value=0), inputs=width, outputs=height)
158
+ height.change(fn=lambda x: gr.update(value=0), inputs=height, outputs=width)
159
+ ratio.change(fn=lambda x: (gr.update(value=0), gr.update(value=0)), inputs=ratio, outputs=[width, height])
160
+
161
+ # infotext paste support
162
+ def read_params(d, key, default=None):
163
+ try:
164
+ return d["Custom Hires Fix"].get(key, default)
165
+ except Exception:
166
+ return default
167
+
168
+ self.infotext_fields = [
169
+ (enable, lambda d: "Custom Hires Fix" in d),
170
+ (ratio, lambda d: read_params(d, "ratio", 0.0)),
171
+ (width, lambda d: read_params(d, "width", 0)),
172
+ (height, lambda d: read_params(d, "height", 0)),
173
+ (steps, lambda d: read_params(d, "steps", 0)),
174
+ (first_upscaler, lambda d: read_params(d, "first_upscaler")),
175
+ (second_upscaler, lambda d: read_params(d, "second_upscaler")),
176
+ (first_latent, lambda d: read_params(d, "first_latent", 0.0)),
177
+ (second_latent, lambda d: read_params(d, "second_latent", 0.0)),
178
+ (prompt, lambda d: read_params(d, "prompt", "")),
179
+ (negative_prompt, lambda d: read_params(d, "negative_prompt", "")),
180
+ (strength, lambda d: read_params(d, "strength", 0.0)),
181
+ (filter_mode, lambda d: read_params(d, "filter_mode")),
182
+ (filter_offset, lambda d: read_params(d, "filter_offset", 0.0)),
183
+ (denoise_offset, lambda d: read_params(d, "denoise_offset", 0.0)),
184
+ (clip_skip, lambda d: read_params(d, "clip_skip", 0)),
185
+ (sampler, lambda d: read_params(d, "sampler", sampler_names[0])),
186
+ (cfg, lambda d: read_params(d, "cfg", 7.0)),
187
+ (cfg_second_pass_boost, lambda d: read_params(d, "cfg_second_pass_boost", True)),
188
+ (scheduler, lambda d: read_params(d, "scheduler", scheduler_names[0])),
189
+ (cn_ref, lambda d: read_params(d, "cn_ref", False)),
190
+ (start_control_at, lambda d: read_params(d, "start_control_at", 0.0)),
191
+ ]
192
+
193
+ return [
194
+ enable, ratio, width, height, steps,
195
+ first_upscaler, second_upscaler, first_latent, second_latent,
196
+ prompt, negative_prompt,
197
+ strength, filter_mode, filter_offset, denoise_offset,
198
+ clip_skip, sampler, cfg, cfg_second_pass_boost, scheduler,
199
+ cn_ref, start_control_at
200
+ ]
201
+
202
+ # Capture base processing object and optional ControlNet state
203
+ def process(self, p, *args, **kwargs):
204
+ self.p = p
205
+ self._cn_units = []
206
+ self._use_cn = False
207
+ # Try detect ControlNet (best-effort; path may vary across installs)
208
+ ext_candidates = [
209
+ "extensions.sd_webui_controlnet.scripts.external_code",
210
+ "extensions.sd-webui-controlnet.scripts.external_code",
211
+ "extensions-builtin.sd-webui-controlnet.scripts.external_code",
212
+ ]
213
+ self._cn_ext = None
214
+ for mod in ext_candidates:
215
+ try:
216
+ self._cn_ext = __import__(mod, fromlist=["external_code"])
217
+ break
218
+ except Exception:
219
+ continue
220
+ if self._cn_ext:
221
+ try:
222
+ units = self._cn_ext.get_all_units_in_processing(p)
223
+ self._cn_units = list(units) if units else []
224
+ self._use_cn = len(self._cn_units) > 0
225
+ except Exception:
226
+ self._use_cn = False
227
+
228
+ # Log settings into PNG-info (single JSON block)
229
+ def before_process_batch(self, p, *args, **kwargs):
230
+ if not getattr(self.config, "enable", False):
231
+ return
232
+ p.extra_generation_params["Custom Hires Fix"] = self.create_infotext
233
+
234
+ def create_infotext(self, p, *args, **kwargs):
235
+ scale_val = 0
236
+ if int(self.config.get("width", 0)) and int(self.config.get("height", 0)):
237
+ scale_val = f"{int(self.config.get('width'))}x{int(self.config.get('height'))}"
238
+ elif float(self.config.get("ratio", 0)):
239
+ scale_val = float(self.config.get("ratio"))
240
+
241
+ payload = {
242
+ "scale": scale_val,
243
+ "ratio": float(self.config.get("ratio", 0.0)),
244
+ "width": int(self.config.get("width", 0) or 0),
245
+ "height": int(self.config.get("height", 0) or 0),
246
+ "steps": int(self.config.get("steps", 20)),
247
+ "first_upscaler": self.config.get("first_upscaler", ""),
248
+ "second_upscaler": self.config.get("second_upscaler", ""),
249
+ "first_latent": float(self.config.get("first_latent", 0.3)),
250
+ "second_latent": float(self.config.get("second_latent", 0.1)),
251
+ "prompt": self.config.get("prompt", ""),
252
+ "negative_prompt": self.config.get("negative_prompt", ""),
253
+ "strength": float(self.config.get("strength", 2.0)),
254
+ "filter_mode": self.config.get("filter_mode", ""),
255
+ "filter_offset": float(self.config.get("filter_offset", 0.0)),
256
+ "denoise_offset": float(self.config.get("denoise_offset", 0.05)),
257
+ "clip_skip": int(self.config.get("clip_skip", 0)),
258
+ "sampler": self.config.get("sampler", ""),
259
+ "cfg": float(self.cfg),
260
+ "cfg_second_pass_boost": bool(self.config.get("cfg_second_pass_boost", True)),
261
+ "scheduler": self.config.get("scheduler", ""),
262
+ "cn_ref": bool(self.config.get("cn_ref", False)),
263
+ "start_control_at": float(self.config.get("start_control_at", 0.0)),
264
+ }
265
+ return json.dumps(payload, ensure_ascii=False).translate(quote_swap)
266
+
267
+ # --- Main postprocess hook ---
268
+ def postprocess_image(self, p, pp,
269
+ enable, ratio, width, height, steps,
270
+ first_upscaler, second_upscaler, first_latent, second_latent,
271
+ prompt, negative_prompt,
272
+ strength, filter_mode, filter_offset, denoise_offset,
273
+ clip_skip, sampler, cfg, cfg_second_pass_boost, scheduler,
274
+ cn_ref, start_control_at):
275
+ if not enable:
276
+ return
277
+
278
+ # Validate sizing: either both width&height, or both 0 + ratio>0, or single side for aspect fit
279
+ assert ((width and height) or
280
+ ((width == 0 and height == 0) and (ratio and ratio > 0)) or
281
+ (width > 0) or (height > 0)), "Set width+height, or set both to 0 and ratio>0, or provide at least one dimension."
282
+
283
+ # Save config chosen in UI
284
+ self.pp = pp
285
+ self.config["enable"] = bool(enable)
286
+ self.config["ratio"] = float(ratio)
287
+ self.config["width"] = int(width)
288
+ self.config["height"] = int(height)
289
+ self.config["steps"] = int(steps)
290
+ self.config["first_upscaler"] = first_upscaler
291
+ self.config["second_upscaler"] = second_upscaler
292
+ self.config["first_latent"] = float(first_latent)
293
+ self.config["second_latent"] = float(second_latent)
294
+ self.config["prompt"] = prompt.strip()
295
+ self.config["negative_prompt"] = negative_prompt.strip()
296
+ self.config["strength"] = float(strength)
297
+ self.config["filter_mode"] = filter_mode
298
+ self.config["filter_offset"] = float(filter_offset)
299
+ self.config["denoise_offset"] = float(denoise_offset)
300
+ self.config["clip_skip"] = int(clip_skip)
301
+ self.config["sampler"] = sampler
302
+ self.config["cfg"] = float(cfg)
303
+ self.config["cfg_second_pass_boost"] = bool(cfg_second_pass_boost)
304
+ self.config["scheduler"] = scheduler
305
+ self.config["cn_ref"] = bool(cn_ref)
306
+ self.config["start_control_at"] = float(start_control_at)
307
+ self.cfg = float(cfg) if cfg else float(p.cfg_scale)
308
+
309
+ # Apply CLIP-skip temporarily for the two-pass run
310
+ self._orig_clip_skip = shared.opts.CLIP_stop_at_last_layers
311
+ if int(clip_skip) > 0:
312
+ shared.opts.CLIP_stop_at_last_layers = int(clip_skip)
313
+
314
+ # Make the sampler from selection
315
+ if "Restart" in sampler:
316
+ self.sampler = sd_samplers.create_sampler("Restart", p.sd_model)
317
+ else:
318
+ self.sampler = sd_samplers.create_sampler(sampler, p.sd_model)
319
+
320
+ # Respect scheduler selection
321
+ p.scheduler = p.scheduler if scheduler == "Use same scheduler" else scheduler
322
+
323
+ # Activate extra networks from prompts
324
+ _, loras_act = extra_networks.parse_prompt(self.config["prompt"])
325
+ extra_networks.activate(p, loras_act)
326
+ _, loras_deact = extra_networks.parse_prompt(self.config["negative_prompt"])
327
+ extra_networks.deactivate(p, loras_deact)
328
+
329
+ try:
330
+ with devices.autocast():
331
+ shared.state.nextjob()
332
+ x = self._first_pass(pp.image)
333
+ shared.state.nextjob()
334
+ x = self._second_pass(x)
335
+ sd_models.apply_token_merging(p.sd_model, p.get_token_merging_ratio())
336
+ pp.image = x
337
+ finally:
338
+ shared.opts.CLIP_stop_at_last_layers = self._orig_clip_skip
339
+ extra_networks.deactivate(p, loras_act)
340
+
341
+ # ---- Helpers ----
342
+ def _enable_controlnet(self, image_np: np.ndarray):
343
+ if not self._cn_ext:
344
+ return
345
+ for unit in self._cn_units:
346
+ try:
347
+ if getattr(unit, "model", "None") != "None":
348
+ if getattr(unit, "enabled", True):
349
+ unit.guidance_start = float(self.config.get("start_control_at", 0.0))
350
+ unit.processor_res = min(image_np.shape[0], image_np.shape[1])
351
+ unit.enabled = True
352
+ if getattr(unit, "image", None) is None:
353
+ unit.image = image_np
354
+ self.p.width = image_np.shape[1]
355
+ self.p.height = image_np.shape[0]
356
+ except Exception:
357
+ continue
358
+ try:
359
+ self._cn_ext.update_cn_script_in_processing(self.p, self._cn_units)
360
+ for script in self.p.scripts.alwayson_scripts:
361
+ if script.title().lower() == "controlnet":
362
+ script.controlnet_hack(self.p)
363
+ except Exception:
364
+ pass
365
+
366
+ def _prepare_conditioning(self, width, height):
367
+ prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip()
368
+ negative = self.config.get("negative_prompt", "").strip() or self.p.negative_prompt.strip()
369
+
370
+ # Parse extra networks and build cond
371
+ if not getattr(self.p, "disable_extra_networks", False):
372
+ try:
373
+ prompt, extra = extra_networks.parse_prompt(prompt)
374
+ if extra:
375
+ extra_networks.activate(self.p, extra)
376
+ except Exception:
377
+ pass
378
+
379
+ if width and height and hasattr(prompt_parser, "SdConditioning"):
380
+ c = prompt_parser.SdConditioning([prompt], False, width, height)
381
+ uc = prompt_parser.SdConditioning([negative], False, width, height)
382
+ else:
383
+ c, uc = [prompt], [negative]
384
+ self.cond = prompt_parser.get_multicond_learned_conditioning(shared.sd_model, c, int(self.config.get("steps", 20)))
385
+ self.uncond = prompt_parser.get_learned_conditioning(shared.sd_model, uc, int(self.config.get("steps", 20)))
386
+
387
+ def _to_sample(self, x_img: Image.Image):
388
+ image = np.array(x_img).astype(np.float32) / 255.0
389
+ image = np.moveaxis(image, 2, 0)
390
+ decoded = torch.from_numpy(image).to(shared.device).to(devices.dtype_vae)
391
+ decoded = 2.0 * decoded - 1.0
392
+ encoded = shared.sd_model.encode_first_stage(decoded.unsqueeze(0).to(devices.dtype_vae))
393
+ sample = shared.sd_model.get_first_stage_encoding(encoded)
394
+ return decoded, sample
395
+
396
+ def _first_pass(self, x: Image.Image) -> Image.Image:
397
+ # Determine target size for first stage
398
+ ratio = x.width / x.height if x.height else 1.0
399
+ if int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and float(self.config.get("ratio", 0)) > 0:
400
+ self.width = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
401
+ self.height = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
402
+ else:
403
+ if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
404
+ self.width, self.height = int(self.config["width"]), int(self.config["height"])
405
+ elif int(self.config.get("width", 0)) > 0:
406
+ self.width = int(self.config["width"])
407
+ self.height = int(round(self.width / ratio / 8) * 8)
408
+ elif int(self.config.get("height", 0)) > 0:
409
+ self.height = int(self.config["height"])
410
+ self.width = int(round(self.height * ratio / 8) * 8)
411
+ else:
412
+ self.width, self.height = x.width, x.height
413
+
414
+ sd_models.apply_token_merging(self.p.sd_model, self.p.get_token_merging_ratio(for_hr=True) / 2)
415
+
416
+ # Optional ControlNet
417
+ if self._use_cn:
418
+ self._enable_controlnet(np.array(x.resize((self.width, self.height))))
419
+
420
+ with devices.autocast(), torch.inference_mode():
421
+ self._prepare_conditioning(self.width, self.height)
422
+
423
+ # Upscale (image domain) then (optionally) blend latent
424
+ x_img = images.resize_image(0, x, self.width, self.height, upscaler_name=self.config.get("first_upscaler", "R-ESRGAN 4x+"))
425
+ decoded, sample = self._to_sample(x_img)
426
+ x_latent = torch.nn.functional.interpolate(sample, (self.height // 8, self.width // 8), mode="nearest")
427
+
428
+ first_latent = float(self.config.get("first_latent", 0.3))
429
+ if 0.0 <= first_latent <= 1.0:
430
+ sample = (x_latent * (1.0 - first_latent)) + (sample * first_latent)
431
+
432
+ image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
433
+
434
+ # Denoise config for first pass
435
+ noise = torch.randn_like(sample)
436
+ steps = int(max(((self.p.steps - int(self.config.get("steps", 20))) / 2) + int(self.config.get("steps", 20)),
437
+ int(self.config.get("steps", 20))))
438
+ self.p.denoising_strength = 0.33 + float(self.config.get("denoise_offset", 0.05)) * 0.2
439
+ self.p.cfg_scale = float(self.cfg)
440
+ # Simple polyexponential schedule override (optional)
441
+ def denoiser_override(n):
442
+ return K.sampling.get_sigmas_polyexponential(n, 0.005, 20, 0.6, devices.device) # type: ignore
443
+
444
+ self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
445
+ subseed_strength=self.p.subseed_strength,
446
+ seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
447
+ self.p.sampler_noise_scheduler_override = denoiser_override
448
+ self.p.batch_size = 1
449
+
450
+ sampler = sd_samplers.create_sampler("Restart", shared.sd_model) if "Restart" in self.config.get("sampler", "") else sd_samplers.create_sampler(self.config.get("sampler", "DPM++ 2M Karras"), shared.sd_model)
451
+
452
+ samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
453
+ steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
454
+
455
+ devices.torch_gc()
456
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
457
+ if math.isnan(decoded_sample.min() if hasattr(decoded_sample, "min") else 0):
458
+ devices.torch_gc()
459
+ samples = torch.clamp(samples, -3, 3)
460
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
461
+
462
+ decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
463
+ x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
464
+ return Image.fromarray(x_np.astype(np.uint8))
465
+
466
+ def _second_pass(self, x: Image.Image) -> Image.Image:
467
+ # Target size for second stage (respect ratio if both dims are 0)
468
+ if (int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and
469
+ float(self.config.get("ratio", 0)) > 0):
470
+ width = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8))
471
+ height = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8))
472
+ else:
473
+ aspect = x.width / x.height if x.height else 1.0
474
+ if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0:
475
+ width, height = int(self.config["width"]), int(self.config["height"])
476
+ elif int(self.config.get("width", 0)) > 0:
477
+ width = int(self.config["width"])
478
+ height = int(round(width / aspect / 8) * 8)
479
+ elif int(self.config.get("height", 0)) > 0:
480
+ height = int(self.config["height"])
481
+ width = int(round(height * aspect / 8) * 8)
482
+ else:
483
+ width, height = x.width, x.height
484
+
485
+ sd_models.apply_token_merging(self.p.sd_model, self.p.get_token_merging_ratio(for_hr=True))
486
+
487
+ if self._use_cn:
488
+ cn_img = x if bool(self.config.get("cn_ref", False)) else self.pp.image
489
+ self._enable_controlnet(np.array(cn_img.resize((width, height))))
490
+
491
+ with devices.autocast(), torch.inference_mode():
492
+ self._prepare_conditioning(width, height)
493
+
494
+ # Optional latent mix
495
+ x_latent = None
496
+ second_latent = float(self.config.get("second_latent", 0.1))
497
+ if second_latent > 0:
498
+ _, sample_from_img = self._to_sample(x)
499
+ x_latent = torch.nn.functional.interpolate(sample_from_img, (height // 8, width // 8), mode="nearest")
500
+
501
+ # Upscale to target and encode
502
+ if second_latent < 1.0:
503
+ x_up = images.resize_image(0, x, width, height, upscaler_name=self.config.get("second_upscaler", "R-ESRGAN 4x+"))
504
+ decoded, sample = self._to_sample(x_up)
505
+ else:
506
+ # Take from original x
507
+ decoded, sample = self._to_sample(x)
508
+
509
+ if x_latent is not None and 0.0 <= second_latent <= 1.0:
510
+ sample = (sample * (1.0 - second_latent)) + (x_latent * second_latent)
511
+
512
+ image_conditioning = self.p.img2img_image_conditioning(decoded, sample)
513
+
514
+ # Denoise config for second pass
515
+ noise = torch.randn_like(sample)
516
+ steps = int(self.config.get("steps", 20))
517
+ # NEW: checkbox controls +3 CFG on second pass
518
+ self.p.cfg_scale = (float(self.cfg) + 3.0) if bool(self.config.get("cfg_second_pass_boost", True)) else float(self.cfg)
519
+ self.p.denoising_strength = 0.45 + float(self.config.get("denoise_offset", 0.05)) * 0.2
520
+
521
+ def denoiser_override(n):
522
+ return K.sampling.get_sigmas_polyexponential(n, 0.01, 15, 0.5, devices.device) # type: ignore
523
+
524
+ self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds,
525
+ subseed_strength=self.p.subseed_strength,
526
+ seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w)
527
+ self.p.sampler_noise_scheduler_override = denoiser_override
528
+ self.p.batch_size = 1
529
+
530
+ sampler = sd_samplers.create_sampler("Restart", shared.sd_model) if "Restart" in self.config.get("sampler", "") else sd_samplers.create_sampler(self.config.get("sampler", "DPM++ 2M Karras"), shared.sd_model)
531
+
532
+ samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond,
533
+ steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae)
534
+
535
+ devices.torch_gc()
536
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
537
+ if math.isnan(decoded_sample.min() if hasattr(decoded_sample, "min") else 0):
538
+ devices.torch_gc()
539
+ samples = torch.clamp(samples, -3, 3)
540
+ decoded_sample = processing.decode_first_stage(shared.sd_model, samples)
541
+
542
+ decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze()
543
+ x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2)
544
+ return Image.fromarray(x_np.astype(np.uint8))
545
+
546
+
547
+ def parse_infotext(infotext, params):
548
+ try:
549
+ block = params.get("Custom Hires Fix")
550
+ if not block:
551
+ return
552
+ data = json.loads(block.translate(quote_swap)) if isinstance(block, str) else block
553
+ params["Custom Hires Fix"] = data
554
+ scale = data.get("scale", 0)
555
+ if isinstance(scale, str) and "x" in scale:
556
+ w, _, h = scale.partition("x")
557
+ data["ratio"] = 0.0
558
+ data["width"] = int(w)
559
+ data["height"] = int(h)
560
+ else:
561
+ try:
562
+ r = float(scale)
563
+ except Exception:
564
+ r = 0.0
565
+ data["ratio"] = r
566
+ data["width"] = int(data.get("width", 0) or 0)
567
+ data["height"] = int(data.get("height", 0) or 0)
568
+ except Exception:
569
+ pass
570
+
571
+ # Register paste-params hook
572
+ script_callbacks.on_infotext_pasted(parse_infotext)