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

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

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