dikdimon commited on
Commit
9fd099c
·
verified ·
1 Parent(s): 6d53c19

Upload sd-webui-kohya-hiresfix-saveable using SD-Hub

Browse files
sd-webui-kohya-hiresfix-saveable/.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
sd-webui-kohya-hiresfix-saveable/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+
2
+ *.pyc
3
+ config.yaml
sd-webui-kohya-hiresfix-saveable/README.md ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Implementation Kohya Hires.fix for Auto1111 webui
2
+
3
+ #### Stop step - at which sampling step disable fix, increase at higher resolution
4
+ #### Depth - on which layer fix will be applied
5
+ #### Downsampling scale - decrease at higher resolution, helps preserve composition at very high resolutions
6
+ #### Upsampling scale - increasing can slightly improve quality at cost of VRAM
7
+
8
+ ## Source:
9
+ https://gist.github.com/kohya-ss/3f774da220df102548093a7abc8538ed
sd-webui-kohya-hiresfix-saveable/scripts/__pycache__/khrfix.cpython-310.pyc ADDED
Binary file (18.9 kB). View file
 
sd-webui-kohya-hiresfix-saveable/scripts/khrfix.py ADDED
@@ -0,0 +1,671 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # kohya_hires_fix_ru.py
2
+ # Версия: 1.5 (RU)
3
+ # Совместимость: A1111 / modules.scripts API, PyTorch >= 1.12, OmegaConf >= 2.2
4
+ # Новое в 1.5:
5
+ # - Переключатели align_corners (Авто/True/False) и recompute_scale_factor (Авто/True/False).
6
+ # - Сохранение/загрузка этих параметров в пресетах и конфиге.
7
+ # - По умолчанию False/False (как в 1.4) для стабильности и отсутствия предупреждений.
8
+
9
+ from __future__ import annotations
10
+
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ import gradio as gr
15
+ import torch
16
+ import torch.nn.functional as F
17
+ from omegaconf import DictConfig, OmegaConf
18
+ from modules import scripts, script_callbacks
19
+
20
+ CONFIG_PATH = Path(__file__).with_suffix(".yaml")
21
+ PRESETS_PATH = Path(__file__).with_name(Path(__file__).stem + ".presets.yaml")
22
+
23
+ # ---- Предустановленные разрешения ----
24
+
25
+ RESOLUTION_GROUPS = {
26
+ "Квадрат": [(1024, 1024)],
27
+ "Портрет": [(640, 1536), (768, 1344), (832, 1216), (896, 1152)],
28
+ "Альбом": [(1536, 640), (1344, 768), (1216, 832), (1152, 896)],
29
+ }
30
+ RESOLUTION_CHOICES: List[str] = ["— не применять —"]
31
+ for group, dims in RESOLUTION_GROUPS.items():
32
+ for w, h in dims:
33
+ RESOLUTION_CHOICES.append(f"{group}: {w}x{h}")
34
+
35
+
36
+ def parse_resolution_label(label: str) -> Optional[Tuple[int, int]]:
37
+ if not label or label.startswith("—"):
38
+ return None
39
+ try:
40
+ _, wh = label.split(":")
41
+ w, h = wh.strip().lower().split("x")
42
+ return int(w), int(h)
43
+ except Exception:
44
+ return None
45
+
46
+
47
+ # ---- Вспомогательные утилиты ----
48
+
49
+ def _safe_mode(mode: str) -> str:
50
+ if mode == "nearest-exact":
51
+ return mode # при ошибке fallback в forward()
52
+ if mode in {"bicubic", "bilinear", "nearest"}:
53
+ return mode
54
+ return "bilinear"
55
+
56
+
57
+ def _load_yaml(path: Path, default: dict) -> dict:
58
+ try:
59
+ return OmegaConf.to_container(OmegaConf.load(path), resolve=True) or default
60
+ except Exception:
61
+ return default
62
+
63
+
64
+ def _atomic_save_yaml(path: Path, data: dict) -> None:
65
+ try:
66
+ tmp = path.with_suffix(path.suffix + ".tmp")
67
+ OmegaConf.save(DictConfig(data), tmp)
68
+ tmp.replace(path)
69
+ except Exception:
70
+ pass
71
+
72
+
73
+ def _load_presets() -> Dict[str, dict]:
74
+ data = _load_yaml(PRESETS_PATH, {})
75
+ return {str(k): dict(v) for k, v in data.items()}
76
+
77
+
78
+ def _save_presets(presets: Dict[str, dict]) -> None:
79
+ _atomic_save_yaml(PRESETS_PATH, presets)
80
+
81
+
82
+ def _clamp(x: float, lo: float, hi: float) -> float:
83
+ return float(max(lo, min(hi, x)))
84
+
85
+
86
+ def _norm_mode_choice(value: str, default_: str = "false") -> str:
87
+ """Привести выбор из UI к {'true','false','auto'}."""
88
+ s = str(value or "").strip().lower()
89
+ if s in ("true",):
90
+ return "true"
91
+ if s in ("false",):
92
+ return "false"
93
+ if s in ("авто", "auto"):
94
+ return "auto"
95
+ return default_
96
+
97
+
98
+ def _compute_adaptive_params(
99
+ width: int,
100
+ height: int,
101
+ profile: str,
102
+ base_s1: float,
103
+ base_s2: float,
104
+ base_d1: int,
105
+ base_d2: int,
106
+ base_down: float,
107
+ base_up: float,
108
+ keep_unitary_product: bool,
109
+ ) -> Tuple[float, float, int, int, float, float]:
110
+ """Адаптировать (s1, s2, d1, d2, downscale, upscale) под MPix и аспект."""
111
+ rel_mpx = (max(1, int(width)) * max(1, int(height))) / float(1024 * 1024)
112
+ aspect = max(width, height) / float(max(1, min(width, height)))
113
+
114
+ s_add = 0.0
115
+ d_add = 0
116
+ down = float(base_down)
117
+
118
+ # MPix
119
+ if rel_mpx >= 1.5:
120
+ s_add += 0.08
121
+ down -= 0.10
122
+ elif rel_mpx >= 1.1:
123
+ s_add += 0.05
124
+ down -= 0.05
125
+ elif rel_mpx <= 0.8:
126
+ s_add -= 0.02
127
+ down += 0.05
128
+
129
+ # Аспект
130
+ if aspect >= 1.6:
131
+ d_add += 1
132
+ down -= 0.05
133
+ if aspect >= 2.0:
134
+ d_add += 1
135
+ s_add += 0.02
136
+
137
+ # Профиль
138
+ prof = (profile or "Сбалансированный").strip().lower()
139
+ if "консер" in prof:
140
+ s_add *= 0.6
141
+ down = 0.5 + 0.5 * (down - 0.5)
142
+ elif "агресс" in prof:
143
+ s_add *= 1.3
144
+ down -= 0.05
145
+
146
+ s1 = _clamp(base_s1 + s_add, 0.0, 0.5)
147
+ s2 = _clamp(base_s2 + s_add, 0.0, 0.5)
148
+ d1 = max(1, min(10, int(base_d1 + d_add)))
149
+ d2 = max(1, min(10, int(base_d2 + d_add)))
150
+ down = _clamp(down, 0.3, 0.9)
151
+
152
+ if keep_unitary_product:
153
+ up = 1.0 / max(1e-6, down)
154
+ else:
155
+ up = float(base_up)
156
+
157
+ return s1, s2, d1, d2, down, up
158
+
159
+
160
+ # ---- Основные классы ----
161
+
162
+ class Scaler(torch.nn.Module):
163
+ """Обёртка блока U-Net: масштабировать вход, вызвать исходный модуль."""
164
+
165
+ def __init__(
166
+ self,
167
+ scale: float,
168
+ block: torch.nn.Module,
169
+ scaler: str,
170
+ align_mode: str = "false", # 'true' | 'false' | 'auto'
171
+ recompute_mode: str = "false", # 'true' | 'false' | 'auto'
172
+ ) -> None:
173
+ super().__init__()
174
+ self.scale: float = float(scale)
175
+ self.block: torch.nn.Module = block
176
+ self.scaler: str = _safe_mode(scaler)
177
+ self.align_mode: str = _norm_mode_choice(align_mode, "false")
178
+ self.recompute_mode: str = _norm_mode_choice(recompute_mode, "false")
179
+
180
+ def forward(self, x: torch.Tensor, *args: Any, **kwargs: Any) -> torch.Tensor:
181
+ mode = self.scaler
182
+ try:
183
+ kw = dict(scale_factor=self.scale, mode=mode)
184
+ # align_corners только для линейных режимов
185
+ if mode in ("bilinear", "bicubic"):
186
+ if self.align_mode == "true":
187
+ kw["align_corners"] = True
188
+ elif self.align_mode == "false":
189
+ kw["align_corners"] = False
190
+ # 'auto' -> не передаём параметр
191
+
192
+ # recompute_scale_factor для любых режимов
193
+ if self.recompute_mode == "true":
194
+ kw["recompute_scale_factor"] = True
195
+ elif self.recompute_mode == "false":
196
+ kw["recompute_scale_factor"] = False
197
+ # 'auto' -> не передаём параметр
198
+
199
+ x = F.interpolate(x, **kw)
200
+
201
+ except Exception:
202
+ # Фоллбек при несовместимом режиме
203
+ safe = "nearest" if mode == "nearest-exact" else "bilinear"
204
+ kw = dict(scale_factor=self.scale, mode=safe)
205
+ if safe in ("bilinear", "bicubic"):
206
+ if self.align_mode == "true":
207
+ kw["align_corners"] = True
208
+ elif self.align_mode == "false":
209
+ kw["align_corners"] = False
210
+ if self.recompute_mode == "true":
211
+ kw["recompute_scale_factor"] = True
212
+ elif self.recompute_mode == "false":
213
+ kw["recompute_scale_factor"] = False
214
+ x = F.interpolate(x, **kw)
215
+
216
+ return self.block(x, *args, **kwargs)
217
+
218
+
219
+ class KohyaHiresFix(scripts.Script):
220
+ """Динамический hires.fix через временную смену масштаба внутренних фич U-Net."""
221
+
222
+ def __init__(self) -> None:
223
+ super().__init__()
224
+ self.config: DictConfig = DictConfig(_load_yaml(CONFIG_PATH, {}))
225
+ self.disable: bool = False
226
+ self.step_limit: int = 0
227
+ self.infotext_fields = []
228
+ self._cb_registered: bool = False
229
+
230
+ self.p1: Tuple[float, int] = (0.15, 2)
231
+ self.p2: Tuple[float, int] = (0.30, 3)
232
+
233
+ def title(self) -> str:
234
+ return "Kohya Hires.fix · Русская версия"
235
+
236
+ def show(self, is_img2img: bool):
237
+ return scripts.AlwaysVisible
238
+
239
+ def ui(self, is_img2img: bool):
240
+ # Сброс infotext при горячей перезагрузке
241
+ self.infotext_fields = []
242
+ presets = _load_presets()
243
+
244
+ with gr.Accordion(label="Kohya Hires.fix", open=False):
245
+ enable = gr.Checkbox(label="Включить расширение", value=False)
246
+
247
+ # Разрешения
248
+ with gr.Group():
249
+ gr.Markdown("**Предустановленные разрешения**")
250
+ with gr.Row():
251
+ resolution_choice = gr.Dropdown(
252
+ choices=RESOLUTION_CHOICES,
253
+ value=self.config.get("resolution_choice", RESOLUTION_CHOICES[0]),
254
+ label="Выбрать разрешение",
255
+ )
256
+ apply_resolution = gr.Checkbox(
257
+ label="Применять выбранное разрешение к ширине/высоте",
258
+ value=self.config.get("apply_resolution", False),
259
+ )
260
+
261
+ # Параметры масштабирования
262
+ with gr.Group():
263
+ gr.Markdown("**Параметры масштабирования**")
264
+ with gr.Row():
265
+ s1 = gr.Slider(0.0, 0.5, step=0.01, label="Остановить на (доля шага) — Пара 1",
266
+ value=self.config.get("s1", 0.15))
267
+ d1 = gr.Slider(1, 10, step=1, label="Глубина блока — Пара 1",
268
+ value=self.config.get("d1", 3))
269
+ with gr.Row():
270
+ s2 = gr.Slider(0.0, 0.5, step=0.01, label="Остановить на (доля шага) — Пара 2",
271
+ value=self.config.get("s2", 0.30))
272
+ d2 = gr.Slider(1, 10, step=1, label="Глубина блока — Пара 2",
273
+ value=self.config.get("d2", 4))
274
+
275
+ with gr.Row():
276
+ scaler = gr.Dropdown(
277
+ choices=["bicubic", "bilinear", "nearest", "nearest-exact"],
278
+ label="Режим интерполяции слоя",
279
+ value=self.config.get("scaler", "bicubic"),
280
+ )
281
+ downscale = gr.Slider(0.1, 1.0, step=0.05, label="Коэффициент даунскейла (вход)",
282
+ value=self.config.get("downscale", 0.5))
283
+ upscale = gr.Slider(1.0, 4.0, step=0.1, label="Коэффициент апскейла (выход)",
284
+ value=self.config.get("upscale", 2.0))
285
+
286
+ with gr.Row():
287
+ smooth_scaling = gr.Checkbox(label="Плавное изменение масштаба",
288
+ value=self.config.get("smooth_scaling", True))
289
+ keep_unitary_product = gr.Checkbox(
290
+ label="Сохранять суммарный масштаб = 1 при сглаживании",
291
+ value=self.config.get("keep_unitary_product", False),
292
+ )
293
+ early_out = gr.Checkbox(label="Ранний апскейл на прямом индексе выхода",
294
+ value=self.config.get("early_out", False))
295
+ only_one_pass = gr.Checkbox(label="Только один проход (отключить на следующих шагах)",
296
+ value=self.config.get("only_one_pass", True))
297
+
298
+ # Интерполяция: новые переключатели
299
+ with gr.Group():
300
+ gr.Markdown("**Интерполяция (продвинутое)**")
301
+ with gr.Row():
302
+ align_corners_mode = gr.Dropdown(
303
+ choices=["False", "True", "Авто"],
304
+ value=self.config.get("align_corners_mode", "False"),
305
+ label="align_corners режим",
306
+ )
307
+ recompute_scale_factor_mode = gr.Dropdown(
308
+ choices=["False", "True", "Авто"],
309
+ value=self.config.get("recompute_scale_factor_mode", "False"),
310
+ label="recompute_scale_factor режим",
311
+ )
312
+
313
+ # Адаптация
314
+ with gr.Group():
315
+ gr.Markdown("**Адаптация под разрешение**")
316
+ with gr.Row():
317
+ adaptive_by_resolution = gr.Checkbox(
318
+ label="Адаптировать параметры под текущее разрешение",
319
+ value=self.config.get("adaptive_by_resolution", True),
320
+ )
321
+ adaptive_profile = gr.Dropdown(
322
+ choices=["Консервативный", "Сбалансированный", "Агрессивный"],
323
+ value=self.config.get("adaptive_profile", "Сбалансированный"),
324
+ label="Профиль адаптации",
325
+ )
326
+
327
+ # Пресеты
328
+ with gr.Group():
329
+ gr.Markdown("**Именуемые пресеты**")
330
+ with gr.Row():
331
+ preset_select = gr.Dropdown(
332
+ choices=sorted(list(presets.keys())),
333
+ value=None,
334
+ label="Выбрать пресет",
335
+ )
336
+ preset_name = gr.Textbox(
337
+ label="Имя пресета для сохранения/переопределения",
338
+ placeholder="например: xl-portrait-hires",
339
+ value="",
340
+ )
341
+ with gr.Row():
342
+ btn_save = gr.Button("Сохранить как пресет", variant="primary")
343
+ btn_load = gr.Button("Загрузить пресет")
344
+ btn_delete = gr.Button("Удалить пресет", variant="stop")
345
+ preset_status = gr.Markdown("")
346
+
347
+ # Коллбеки пресетов
348
+
349
+ def _save_preset_cb(
350
+ name: str,
351
+ d1_v: int, d2_v: int, s1_v: float, s2_v: float,
352
+ scaler_v: str, down_v: float, up_v: float,
353
+ smooth_v: bool, early_v: bool, one_v: bool, keep1_v: bool,
354
+ align_v: str, recompute_v: str,
355
+ res_choice_v: str, apply_res_v: bool,
356
+ adapt_v: bool, adapt_prof_v: str,
357
+ ):
358
+ name = (name or "").strip()
359
+ if not name:
360
+ return gr.update(), "⚠️ Укажите имя пресета."
361
+ current = _load_presets()
362
+ current[name] = {
363
+ "d1": int(d1_v), "d2": int(d2_v),
364
+ "s1": float(s1_v), "s2": float(s2_v),
365
+ "scaler": str(scaler_v),
366
+ "downscale": float(down_v),
367
+ "upscale": float(up_v),
368
+ "smooth_scaling": bool(smooth_v),
369
+ "early_out": bool(early_v),
370
+ "only_one_pass": bool(one_v),
371
+ "keep_unitary_product": bool(keep1_v),
372
+ "align_corners_mode": str(align_v),
373
+ "recompute_scale_factor_mode": str(recompute_v),
374
+ "resolution_choice": str(res_choice_v),
375
+ "apply_resolution": bool(apply_res_v),
376
+ "adaptive_by_resolution": bool(adapt_v),
377
+ "adaptive_profile": str(adapt_prof_v),
378
+ }
379
+ _save_presets(current)
380
+ return gr.update(choices=sorted(list(current.keys())), value=name), f"✅ Сохранено пресет «{name}»."
381
+
382
+ btn_save.click(
383
+ _save_preset_cb,
384
+ inputs=[
385
+ preset_name,
386
+ d1, d2, s1, s2,
387
+ scaler, downscale, upscale,
388
+ smooth_scaling, early_out, only_one_pass, keep_unitary_product,
389
+ align_corners_mode, recompute_scale_factor_mode,
390
+ resolution_choice, apply_resolution,
391
+ adaptive_by_resolution, adaptive_profile,
392
+ ],
393
+ outputs=[preset_select, preset_status],
394
+ )
395
+
396
+ def _load_preset_cb(selected: Optional[str]):
397
+ name = (selected or "").strip()
398
+ allp = _load_presets()
399
+ if not name or name not in allp:
400
+ return (
401
+ gr.update(), gr.update(), gr.update(), gr.update(),
402
+ gr.update(), gr.update(), gr.update(),
403
+ gr.update(), gr.update(), gr.update(), gr.update(),
404
+ gr.update(), gr.update(),
405
+ gr.update(), gr.update(),
406
+ gr.update(), gr.update(),
407
+ gr.update(value=name),
408
+ "⚠️ Пресет не выбран или не найден."
409
+ )
410
+ p = allp[name]
411
+ return (
412
+ int(p.get("d1", 3)),
413
+ int(p.get("d2", 4)),
414
+ float(p.get("s1", 0.15)),
415
+ float(p.get("s2", 0.30)),
416
+ str(p.get("scaler", "bicubic")),
417
+ float(p.get("downscale", 0.5)),
418
+ float(p.get("upscale", 2.0)),
419
+ bool(p.get("smooth_scaling", True)),
420
+ bool(p.get("early_out", False)),
421
+ bool(p.get("only_one_pass", True)),
422
+ bool(p.get("keep_unitary_product", False)),
423
+ str(p.get("align_corners_mode", "False")),
424
+ str(p.get("recompute_scale_factor_mode", "False")),
425
+ str(p.get("resolution_choice", RESOLUTION_CHOICES[0])),
426
+ bool(p.get("apply_resolution", False)),
427
+ bool(p.get("adaptive_by_resolution", True)),
428
+ str(p.get("adaptive_profile", "Сбалансированный")),
429
+ gr.update(value=name),
430
+ f"✅ Загружен пресет «{name}».",
431
+ )
432
+
433
+ btn_load.click(
434
+ _load_preset_cb,
435
+ inputs=[preset_select],
436
+ outputs=[
437
+ d1, d2, s1, s2,
438
+ scaler, downscale, upscale,
439
+ smooth_scaling, early_out, only_one_pass, keep_unitary_product,
440
+ align_corners_mode, recompute_scale_factor_mode,
441
+ resolution_choice, apply_resolution,
442
+ adaptive_by_resolution, adaptive_profile,
443
+ preset_name, preset_status,
444
+ ],
445
+ )
446
+
447
+ def _delete_preset_cb(selected: Optional[str]):
448
+ name = (selected or "").strip()
449
+ current = _load_presets()
450
+ if not name or name not in current:
451
+ return gr.update(), "⚠️ Пресет не выбран или не найден."
452
+ current.pop(name, None)
453
+ _save_presets(current)
454
+ return gr.update(choices=sorted(list(current.keys())), value=None), f"🗑️ Удалён пресет «{name}»."
455
+
456
+ btn_delete.click(
457
+ _delete_preset_cb,
458
+ inputs=[preset_select],
459
+ outputs=[preset_select, preset_status],
460
+ )
461
+
462
+ # Поля для infotext
463
+ self.infotext_fields.append((enable, lambda d: d.get("DSHF_s1", False)))
464
+ for k, element in {
465
+ "DSHF_res": resolution_choice, "DSHF_apply_res": apply_resolution,
466
+ "DSHF_s1": s1, "DSHF_d1": d1, "DSHF_s2": s2, "DSHF_d2": d2,
467
+ "DSHF_scaler": scaler, "DSHF_down": downscale, "DSHF_up": upscale,
468
+ "DSHF_smooth": smooth_scaling, "DSHF_early": early_out,
469
+ "DSHF_one": only_one_pass, "DSHF_keep1": keep_unitary_product,
470
+ "DSHF_align": align_corners_mode, "DSHF_recompute": recompute_scale_factor_mode,
471
+ "DSHF_adapt": adaptive_by_resolution, "DSHF_adapt_profile": adaptive_profile,
472
+ }.items():
473
+ self.infotext_fields.append((element, k))
474
+
475
+ # Порядок должен соответствовать process(...)
476
+ return [
477
+ enable,
478
+ only_one_pass, d1, d2, s1, s2, scaler, downscale, upscale,
479
+ smooth_scaling, early_out, keep_unitary_product,
480
+ align_corners_mode, recompute_scale_factor_mode,
481
+ resolution_choice, apply_resolution,
482
+ adaptive_by_resolution, adaptive_profile,
483
+ # пресеты (в process не участвуют)
484
+ preset_select, preset_name,
485
+ ]
486
+
487
+ @staticmethod
488
+ def _unwrap_all(model) -> None:
489
+ if not model:
490
+ return
491
+ for i, b in enumerate(getattr(model, "input_blocks", [])):
492
+ if isinstance(b, Scaler):
493
+ model.input_blocks[i] = b.block
494
+ for i, b in enumerate(getattr(model, "output_blocks", [])):
495
+ if isinstance(b, Scaler):
496
+ model.output_blocks[i] = b.block
497
+
498
+ def process(
499
+ self,
500
+ p,
501
+ enable: bool,
502
+ only_one_pass: bool,
503
+ d1: int,
504
+ d2: int,
505
+ s1: float,
506
+ s2: float,
507
+ scaler: str,
508
+ downscale: float,
509
+ upscale: float,
510
+ smooth_scaling: bool,
511
+ early_out: bool,
512
+ keep_unitary_product: bool,
513
+ align_corners_mode_ui: str,
514
+ recompute_scale_factor_mode_ui: str,
515
+ resolution_choice: str,
516
+ apply_resolution: bool,
517
+ adaptive_by_resolution: bool,
518
+ adaptive_profile: str,
519
+ selected_preset: Optional[str],
520
+ new_preset_name: str,
521
+ ):
522
+ # Нормализовать режимы интерполяции из UI
523
+ align_mode = _norm_mode_choice(align_corners_mode_ui, "false")
524
+ recompute_mode = _norm_mode_choice(recompute_scale_factor_mode_ui, "false")
525
+
526
+ # Сохранить конфиг последних значений
527
+ self.config = DictConfig({
528
+ "s1": s1, "s2": s2, "d1": d1, "d2": d2,
529
+ "scaler": scaler, "downscale": downscale, "upscale": upscale,
530
+ "smooth_scaling": smooth_scaling, "early_out": early_out, "only_one_pass": only_one_pass,
531
+ "keep_unitary_product": keep_unitary_product,
532
+ "align_corners_mode": align_corners_mode_ui,
533
+ "recompute_scale_factor_mode": recompute_scale_factor_mode_ui,
534
+ "resolution_choice": resolution_choice, "apply_resolution": apply_resolution,
535
+ "adaptive_by_resolution": adaptive_by_resolution, "adaptive_profile": adaptive_profile,
536
+ })
537
+ self.step_limit = 0
538
+
539
+ # Применить выбранное разрешение
540
+ if apply_resolution:
541
+ wh = parse_resolution_label(resolution_choice)
542
+ if wh:
543
+ p.width, p.height = wh
544
+
545
+ # Выключено — снять коллбеки и обёртки
546
+ if not enable or self.disable:
547
+ try:
548
+ script_callbacks.remove_current_script_callbacks()
549
+ except Exception:
550
+ pass
551
+ self._cb_registered = False
552
+ try:
553
+ KohyaHiresFix._unwrap_all(p.sd_model.model.diffusion_model)
554
+ except Exception:
555
+ pass
556
+ return
557
+
558
+ # Адаптация значений под фактическое разрешение
559
+ use_s1, use_s2 = s1, s2
560
+ use_d1, use_d2 = d1, d2
561
+ use_down, use_up = downscale, upscale
562
+
563
+ if adaptive_by_resolution:
564
+ try:
565
+ use_s1, use_s2, use_d1, use_d2, use_down, use_up = _compute_adaptive_params(
566
+ int(p.width), int(p.height),
567
+ adaptive_profile,
568
+ s1, s2, d1, d2,
569
+ downscale, upscale,
570
+ keep_unitary_product,
571
+ )
572
+ except Exception:
573
+ pass
574
+
575
+ if use_s1 > use_s2:
576
+ use_s2 = use_s1
577
+
578
+ model = p.sd_model.model.diffusion_model
579
+ max_inp = len(getattr(model, "input_blocks", [])) - 1
580
+ if max_inp < 0:
581
+ return
582
+
583
+ d1_idx = max(0, min(int(use_d1) - 1, max_inp))
584
+ d2_idx = max(0, min(int(use_d2) - 1, max_inp))
585
+ scaler_mode = _safe_mode(scaler)
586
+
587
+ # Объединить пары по глубине
588
+ combined: Dict[int, float] = {}
589
+ for s_stop, d_idx in ((float(use_s1), d1_idx), (float(use_s2), d2_idx)):
590
+ combined[d_idx] = max(combined.get(d_idx, 0.0), s_stop)
591
+
592
+ def denoiser_callback(params: script_callbacks.CFGDenoiserParams):
593
+ if params.sampling_step < self.step_limit:
594
+ return
595
+
596
+ total = max(1, int(params.total_sampling_steps))
597
+
598
+ for d_idx, s_stop in combined.items():
599
+ out_idx = d_idx if early_out else -(d_idx + 1)
600
+ try:
601
+ if params.sampling_step < total * s_stop:
602
+ if not isinstance(model.input_blocks[d_idx], Scaler):
603
+ model.input_blocks[d_idx] = Scaler(
604
+ use_down, model.input_blocks[d_idx], scaler_mode,
605
+ align_mode, recompute_mode
606
+ )
607
+ model.output_blocks[out_idx] = Scaler(
608
+ use_up, model.output_blocks[out_idx], scaler_mode,
609
+ align_mode, recompute_mode
610
+ )
611
+
612
+ if smooth_scaling:
613
+ ratio = params.sampling_step / (total * s_stop)
614
+ ratio = float(max(0.0, min(1.0, ratio)))
615
+ cur_down = min((1.0 - use_down) * ratio + use_down, 1.0)
616
+ model.input_blocks[d_idx].scale = cur_down
617
+
618
+ if keep_unitary_product:
619
+ cur_up = 1.0 / max(1e-6, cur_down)
620
+ else:
621
+ cur_up = use_up * (use_down / max(1e-6, cur_down))
622
+ model.output_blocks[out_idx].scale = cur_up
623
+ else:
624
+ if isinstance(model.input_blocks[d_idx], Scaler):
625
+ model.input_blocks[d_idx] = model.input_blocks[d_idx].block
626
+ model.output_blocks[out_idx] = model.output_blocks[out_idx].block
627
+
628
+ except Exception:
629
+ try:
630
+ KohyaHiresFix._unwrap_all(model)
631
+ except Exception:
632
+ pass
633
+
634
+ self.step_limit = int(params.sampling_step) if only_one_pass else 0
635
+
636
+ # Обновить коллбек
637
+ if self._cb_registered:
638
+ try:
639
+ script_callbacks.remove_current_script_callbacks()
640
+ except Exception:
641
+ pass
642
+ self._cb_registered = False
643
+
644
+ script_callbacks.on_cfg_denoiser(denoiser_callback)
645
+ self._cb_registered = True
646
+
647
+ # Инфотекст: фактические значения + выбранные режимы
648
+ parameters = {
649
+ "DSHF_res": resolution_choice, "DSHF_apply_res": apply_resolution,
650
+ "DSHF_s1": use_s1, "DSHF_d1": use_d1, "DSHF_s2": use_s2, "DSHF_d2": use_d2,
651
+ "DSHF_scaler": scaler_mode, "DSHF_down": use_down, "DSHF_up": use_up,
652
+ "DSHF_smooth": smooth_scaling, "DSHF_early": early_out,
653
+ "DSHF_one": only_one_pass, "DSHF_keep1": keep_unitary_product,
654
+ "DSHF_align": align_corners_mode_ui, "DSHF_recompute": recompute_scale_factor_mode_ui,
655
+ "DSHF_adapt": adaptive_by_resolution, "DSHF_adapt_profile": adaptive_profile,
656
+ }
657
+ for k, v in parameters.items():
658
+ p.extra_generation_params[k] = v
659
+
660
+ def postprocess(self, p, processed, *args):
661
+ try:
662
+ KohyaHiresFix._unwrap_all(p.sd_model.model.diffusion_model)
663
+ finally:
664
+ try:
665
+ _atomic_save_yaml(CONFIG_PATH, OmegaConf.to_container(self.config, resolve=True) or {})
666
+ except Exception:
667
+ pass
668
+ self._cb_registered = False
669
+
670
+ def process_batch(self, p, *args, **kwargs):
671
+ self.step_limit = 0
sd-webui-kohya-hiresfix-saveable/scripts/khrfix.yaml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ s1: 0.15
2
+ s2: 0.3
3
+ d1: 3
4
+ d2: 4
5
+ scaler: nearest-exact
6
+ downscale: 0.5
7
+ upscale: 2
8
+ smooth_scaling: true
9
+ early_out: true
10
+ only_one_pass: true
11
+ keep_unitary_product: true
12
+ align_corners_mode: 'False'
13
+ recompute_scale_factor_mode: 'False'
14
+ resolution_choice: 'Портрет: 832x1216'
15
+ apply_resolution: false
16
+ adaptive_by_resolution: false
17
+ adaptive_profile: Агрессивный