dikdimon commited on
Commit
30fc68c
·
verified ·
1 Parent(s): 89b966c

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

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