dikdimon commited on
Commit
43647ee
·
verified ·
1 Parent(s): c9b6a8a

Update !1-custom-hires-fix-mod-for-automatic1111-2.9.5/scripts/!!custom_hires_fix.py

Browse files
!1-custom-hires-fix-mod-for-automatic1111-2.9.5/scripts/!!custom_hires_fix.py CHANGED
@@ -45,10 +45,10 @@ def _safe_import(modname: str, pipname: str | None = None) -> bool:
45
  return False
46
 
47
  _safe_import("omegaconf", "omegaconf")
48
- _safe_import("kornia", "kornia")
49
- _safe_import("k_diffusion", "k-diffusion")
50
- _safe_import("skimage", "scikit-image")
51
- _safe_import("cv2", "opencv-python")
52
 
53
  try:
54
  from omegaconf import OmegaConf, DictConfig # type: ignore
@@ -62,21 +62,23 @@ except Exception: # graceful fallback if OmegaConf not available
62
  @staticmethod
63
  def create(obj):
64
  return DictConfig(obj)
65
- try:
66
- import kornia # type: ignore
67
- _HAS_KORNIA = True
68
- except Exception as e:
69
- logger.warning(e)
 
 
70
  kornia = None
71
- _HAS_KORNIA = False
72
 
73
- try:
74
- import k_diffusion as K # type: ignore
75
- _HAS_KDIFF = True
76
- except Exception as e:
77
- logger.warning(e)
78
- _HAS_KDIFF = False
79
 
 
80
  class _KStub:
81
  class sampling:
82
  @staticmethod
@@ -85,8 +87,6 @@ except Exception as e:
85
  Простейшая замена: логарифмическое пространство от sigma_max до sigma_min
86
  + финальный нулевой шаг, как ожидает WebUI.
87
  """
88
- import math
89
- import torch
90
  n = int(n)
91
  sigma_min = float(sigma_min)
92
  sigma_max = float(sigma_max)
@@ -108,7 +108,6 @@ except Exception as e:
108
  """
109
  Экспоненциальный (геометрический) ряд между sigma_max и sigma_min + 0.0.
110
  """
111
- import torch
112
  n = int(n)
113
  sigma_min = float(sigma_min)
114
  sigma_max = float(sigma_max)
@@ -158,27 +157,49 @@ def _interpolate_latent(tensor: torch.Tensor, size_hw: tuple[int, int], mode_nam
158
  # Старые версии — без параметра antialias
159
  return F.interpolate(tensor, size=size_hw, **kwargs)
160
 
161
- try:
162
- from skimage.exposure import match_histograms, equalize_adapthist # type: ignore
163
- from skimage import color as skcolor # type: ignore
164
- _SKIMAGE_OK = True
165
- except Exception as e:
166
- logger.warning(e)
167
- _SKIMAGE_OK = False
 
168
 
169
  # OpenCV (optional)
170
- try:
171
- import cv2 # type: ignore
172
- _CV2_OK = True
173
- except Exception as e:
174
- logger.warning(e)
175
- _CV2_OK = False
 
176
 
177
  QUOTE_SINGLE_TO_DOUBLE = {ord("'"): ord('"')}
178
  config_path = (Path(__file__).parent.resolve() / "../config.yaml").resolve()
179
 
180
 
181
  class CustomHiresFix(scripts.Script):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def _get_free_vram_bytes(self):
183
  """Return free VRAM bytes if available; else None."""
184
  try:
@@ -213,11 +234,11 @@ class CustomHiresFix(scripts.Script):
213
  except Exception as e:
214
  # 1) PyTorch<2.0: попробуем без antialias
215
  if "antialias" in kwargs:
216
- aa = kwargs.pop("antialias")
217
  try:
218
  return F.interpolate(*args, **kwargs)
219
  except Exception:
220
- kwargs["antialias"] = aa # восстановим для следующих веток
221
 
222
  # 2) Старые torch: 'nearest-exact' не поддерживается
223
  if kwargs.get("mode") == "nearest-exact":
@@ -237,38 +258,22 @@ class CustomHiresFix(scripts.Script):
237
  except Exception:
238
  pass
239
 
240
- logger.warning(f"_interp fallback: {e}")
 
241
  try:
242
- return args[0] # мягкий фолбэк без ресайза
243
  except Exception:
244
  return None
245
 
246
 
247
- """Two-stage img2img upscaling with optional latent mixing and prompt overrides.
248
-
249
- Features:
250
- - Ratio/width/height or Megapixels target (+ quick MP buttons)
251
- - Compact preset panel (global presets)
252
- - Separate steps for 1st/2nd pass
253
- - Per-pass sampler + scheduler
254
- - CFG base + optional delta on 2nd pass
255
- - Reuse seed/noise on 2nd pass
256
- - Conditioning cache (LRU) with capacity
257
- - Second-pass prompt (append/replace)
258
- - Per-pass LoRA weight scaling
259
- - Seamless tiling (+ overlap)
260
- - VAE tiling toggle (low VRAM)
261
- - Color match to original (strength) with presets
262
- - Post-FX presets: CLAHE (local contrast), Unsharp Mask
263
- - PNG-info serialization + paste support
264
- - Final ×4 upscale (optional), with its own upscaler and tiling overlap for seam-safe stitching.
265
- """
266
  def __init__(self):
267
  super().__init__()
268
  # Load or init config
 
269
  if config_path.exists():
270
  try:
271
  self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore
 
272
  except Exception as e:
273
  logger.warning(e)
274
  self.config = OmegaConf.create({}) # type: ignore
@@ -326,6 +331,13 @@ class CustomHiresFix(scripts.Script):
326
  # Cache fingerprint
327
  self._last_cache_fp = None
328
 
 
 
 
 
 
 
 
329
  def _get_model_fingerprint(self) -> str | None:
330
  """Build a fingerprint of current model/checkpoint + clip_skip to detect changes."""
331
  parts = []
@@ -1156,7 +1168,8 @@ class CustomHiresFix(scripts.Script):
1156
  """
1157
  Преобразует подпись из UI в аргументы torch.nn.functional.interpolate:
1158
  (mode, antialias). Поддерживает: "nearest", "nearest-exact", "bilinear",
1159
- "bicubic", "lanczos", "area" и варианты с суффиксом "-antialiased".
 
1160
  Для старых torch, где "nearest-exact" недоступен, _interp уже содержит фолбэк.
1161
  """
1162
  try:
@@ -1164,7 +1177,11 @@ class CustomHiresFix(scripts.Script):
1164
  aa = lab.endswith("-antialiased")
1165
  if aa:
1166
  lab = lab.replace("-antialiased", "")
1167
- if lab in ("nearest", "nearest-exact", "bilinear", "bicubic", "lanczos", "area"):
 
 
 
 
1168
  return lab, aa
1169
  return "bicubic", aa
1170
  except Exception:
@@ -1387,10 +1404,23 @@ class CustomHiresFix(scripts.Script):
1387
  # NEW
1388
  noise_cache_max):
1389
  if not enable:
 
 
 
 
1390
  return
1391
 
1392
  # Save config chosen in UI
1393
  self.pp = pp
 
 
 
 
 
 
 
 
 
1394
  self.config["enable"] = bool(enable)
1395
  self.config["ratio"] = float(ratio)
1396
  self.config["width"] = int(width)
@@ -1513,6 +1543,19 @@ class CustomHiresFix(scripts.Script):
1513
  logger.warning(_e)
1514
  # Обновить PNG-info уже с актуальным self.config
1515
  p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p)
 
 
 
 
 
 
 
 
 
 
 
 
 
1516
 
1517
  # Короткий сводный лог запуска — удобно для багрепортов
1518
  try:
@@ -1678,7 +1721,11 @@ class CustomHiresFix(scripts.Script):
1678
 
1679
  # ---- Helpers ----
1680
  def _save_config(self):
1681
- """Сохранение конфигурации: YAML (если доступно) или JSON с явными предупреждениями."""
 
 
 
 
1682
  try:
1683
  config_path.parent.mkdir(parents=True, exist_ok=True)
1684
  except Exception as e:
@@ -1708,8 +1755,11 @@ class CustomHiresFix(scripts.Script):
1708
  with open(json_path, "w", encoding="utf-8") as f:
1709
  _json.dump(data, f, ensure_ascii=False, indent=2)
1710
  logger.info(f"Config saved to JSON: {json_path}")
 
1711
  except Exception as e:
1712
  logger.warning(f"Config save failed: {e}")
 
 
1713
 
1714
 
1715
  def _vae_down_factor(self) -> int:
@@ -1948,15 +1998,10 @@ class CustomHiresFix(scripts.Script):
1948
  scaled_prompt = self._scale_lora_in_prompt(base_prompt, self._current_lora_factor)
1949
 
1950
  clip_skip = int(self.config.get("clip_skip", 0))
1951
-
1952
- # Cache lookup
1953
- cache_key = self._cond_key(width, height, steps_for_cond, scaled_prompt, negative_base, clip_skip)
1954
- cached = self._cond_cache_get(cache_key)
1955
- if cached is not None:
1956
- self.cond, self.uncond = cached
1957
- return
1958
-
1959
- # Parse extra networks and build cond
1960
  prompt_text = scaled_prompt
1961
  if not getattr(self.p, "disable_extra_networks", False):
1962
  try:
@@ -1967,10 +2012,15 @@ class CustomHiresFix(scripts.Script):
1967
  self._activated_extras.append(extra)
1968
  except Exception as e:
1969
  logger.warning(e)
1970
- pass
1971
  except Exception as e:
1972
  logger.warning(e)
1973
- pass
 
 
 
 
 
 
1974
 
1975
  if width and height and hasattr(prompt_parser, "SdConditioning"):
1976
  c = prompt_parser.SdConditioning([prompt_text], False, width, height)
@@ -2273,6 +2323,13 @@ class CustomHiresFix(scripts.Script):
2273
  """
2274
  cb_ref = None
2275
  limit_steps = int(max(1, limit_steps))
 
 
 
 
 
 
 
2276
  try:
2277
  def _cb(params):
2278
  try:
@@ -2304,12 +2361,26 @@ class CustomHiresFix(scripts.Script):
2304
  finally:
2305
  if cb_ref is not None:
2306
  try:
2307
- # В новых WebUI есть явная отписка; если нет — игнор
2308
  remove = getattr(script_callbacks, "remove_callbacks_for_function", None)
2309
  if callable(remove):
2310
  remove(cb_ref)
 
 
 
 
 
 
 
 
 
2311
  except Exception as e:
2312
  logger.warning(f"stop-hook remove warning: {e}")
 
 
 
 
 
2313
 
2314
  def _first_pass(self, x: Image.Image) -> Image.Image:
2315
  # Determine target size
@@ -2416,15 +2487,10 @@ class CustomHiresFix(scripts.Script):
2416
  steps = min(steps, sfirst)
2417
  # NEW: LRU-кэш шума для первой стадии
2418
  try:
2419
- try:
2420
- seeds = getattr(self.p, "seeds", None)
2421
- key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2422
- except (IndexError, TypeError, AttributeError) as e:
2423
- logger.warning(f"Seed extraction warning: {e}")
2424
- key_seed = None
2425
-
2426
- except Exception as e:
2427
- logger.warning(e)
2428
  key_seed = None
2429
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2430
  sam_name = str(self.config.get("sampler_first", self.config.get("sampler", "")))
@@ -2636,14 +2702,10 @@ class CustomHiresFix(scripts.Script):
2636
  else:
2637
  # NEW: если размеры не совпали — попробуем кэш
2638
  try:
2639
- try:
2640
- seeds = getattr(self.p, "seeds", None)
2641
- key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2642
- except (IndexError, TypeError, AttributeError) as e:
2643
- logger.warning(f"Seed extraction warning: {e}")
2644
- key_seed = None
2645
- except Exception as e:
2646
- logger.warning(e)
2647
  key_seed = None
2648
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2649
  sam_name = str(self.config.get("sampler_second", self.config.get("sampler", "")))
@@ -2665,14 +2727,10 @@ class CustomHiresFix(scripts.Script):
2665
  else:
2666
  # NEW: обычный путь — используем LRU-кэш
2667
  try:
2668
- try:
2669
- seeds = getattr(self.p, "seeds", None)
2670
- key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2671
- except (IndexError, TypeError, AttributeError) as e:
2672
- logger.warning(f"Seed extraction warning: {e}")
2673
- key_seed = None
2674
- except Exception as e:
2675
- logger.warning(e)
2676
  key_seed = None
2677
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2678
  sam_name = str(self.config.get("sampler_second", self.config.get("sampler", "")))
@@ -2905,6 +2963,7 @@ class CustomHiresFix(scripts.Script):
2905
  crop = img.crop((x0, y0, x1, y1))
2906
 
2907
  # Reflect-padding по краям кадра, если запрошено
 
2908
  if use_reflect and (x0 == 0 or y0 == 0 or x1 == W or y1 == H):
2909
  arr = np.array(crop)
2910
  # считаем недостающий «выход» за границы слева/сверху
@@ -2918,6 +2977,19 @@ class CustomHiresFix(scripts.Script):
2918
  # Апскейлим тайл
2919
  up = _upscale(crop, final_scale)
2920
  up_np = np.array(up).astype(np.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
2921
  uh, uw = up_np.shape[0], up_np.shape[1]
2922
 
2923
  # Координаты вставки на апскейленном полотне
@@ -2980,8 +3052,9 @@ class CustomHiresFix(scripts.Script):
2980
  cap = int(self.config.get("cn_proc_res_cap", 1024))
2981
  cap = max(256, min(4096, cap))
2982
  unit.processor_res = max(256, min(cap, min_side))
2983
- if getattr(unit, "image", None) is None:
2984
- unit.image = image_np
 
2985
  self.p.width = image_np.shape[1]
2986
  self.p.height = image_np.shape[0]
2987
  except Exception as e:
@@ -3013,8 +3086,17 @@ def parse_infotext(infotext, params):
3013
  try:
3014
  data = json.loads(block)
3015
  except json.JSONDecodeError:
3016
- # Fallback для старых строк с одинарными кавычками
3017
- data = json.loads(block.translate(QUOTE_SINGLE_TO_DOUBLE))
 
 
 
 
 
 
 
 
 
3018
  else:
3019
  logger.warning(f"Unexpected infotext type: {type(block)}")
3020
  return
@@ -3146,10 +3228,6 @@ def parse_infotext(infotext, params):
3146
  logger.warning(e)
3147
  return
3148
  # Register paste-params hook (guard against double registration)
3149
- try:
3150
- _INFOTEXT_HOOK_REGISTERED
3151
- except NameError:
3152
- _INFOTEXT_HOOK_REGISTERED = False
3153
  if not _INFOTEXT_HOOK_REGISTERED:
3154
  script_callbacks.on_infotext_pasted(parse_infotext)
3155
- _INFOTEXT_HOOK_REGISTERED = True
 
45
  return False
46
 
47
  _safe_import("omegaconf", "omegaconf")
48
+ _HAS_KORNIA = _safe_import("kornia", "kornia")
49
+ _HAS_KDIFF = _safe_import("k_diffusion", "k-diffusion")
50
+ _HAS_SKIMAGE = _safe_import("skimage", "scikit-image")
51
+ _HAS_CV2 = _safe_import("cv2", "opencv-python")
52
 
53
  try:
54
  from omegaconf import OmegaConf, DictConfig # type: ignore
 
62
  @staticmethod
63
  def create(obj):
64
  return DictConfig(obj)
65
+ if _HAS_KORNIA:
66
+ try:
67
+ import kornia # type: ignore
68
+ except Exception:
69
+ kornia = None
70
+ _HAS_KORNIA = False
71
+ else:
72
  kornia = None
 
73
 
74
+ if _HAS_KDIFF:
75
+ try:
76
+ import k_diffusion as K # type: ignore
77
+ except Exception as e:
78
+ logger.warning(e)
79
+ _HAS_KDIFF = False
80
 
81
+ if not _HAS_KDIFF:
82
  class _KStub:
83
  class sampling:
84
  @staticmethod
 
87
  Простейшая замена: логарифмическое пространство от sigma_max до sigma_min
88
  + финальный нулевой шаг, как ожидает WebUI.
89
  """
 
 
90
  n = int(n)
91
  sigma_min = float(sigma_min)
92
  sigma_max = float(sigma_max)
 
108
  """
109
  Экспоненциальный (геометрический) ряд между sigma_max и sigma_min + 0.0.
110
  """
 
111
  n = int(n)
112
  sigma_min = float(sigma_min)
113
  sigma_max = float(sigma_max)
 
157
  # Старые версии — без параметра antialias
158
  return F.interpolate(tensor, size=size_hw, **kwargs)
159
 
160
+ _SKIMAGE_OK = False
161
+ if _HAS_SKIMAGE:
162
+ try:
163
+ from skimage.exposure import match_histograms, equalize_adapthist # type: ignore
164
+ from skimage import color as skcolor # type: ignore
165
+ _SKIMAGE_OK = True
166
+ except Exception as e:
167
+ logger.warning(f"skimage import failed: {e}")
168
 
169
  # OpenCV (optional)
170
+ _CV2_OK = False
171
+ if _HAS_CV2:
172
+ try:
173
+ import cv2 # type: ignore
174
+ _CV2_OK = True
175
+ except Exception as e:
176
+ logger.warning(f"cv2 import failed: {e}")
177
 
178
  QUOTE_SINGLE_TO_DOUBLE = {ord("'"): ord('"')}
179
  config_path = (Path(__file__).parent.resolve() / "../config.yaml").resolve()
180
 
181
 
182
  class CustomHiresFix(scripts.Script):
183
+ """Two-stage img2img upscaling with optional latent mixing and prompt overrides.
184
+
185
+ Features:
186
+ - Ratio/width/height or Megapixels target (+ quick MP buttons)
187
+ - Compact preset panel (global presets)
188
+ - Separate steps for 1st/2nd pass
189
+ - Per-pass sampler + scheduler
190
+ - CFG base + optional delta on 2nd pass
191
+ - Reuse seed/noise on 2nd pass
192
+ - Conditioning cache (LRU) with capacity
193
+ - Second-pass prompt (append/replace)
194
+ - Per-pass LoRA weight scaling
195
+ - Seamless tiling (+ overlap)
196
+ - VAE tiling toggle (low VRAM)
197
+ - Color match to original (strength) with presets
198
+ - Post-FX presets: CLAHE (local contrast), Unsharp Mask
199
+ - PNG-info serialization + paste support
200
+ - Final ×4 upscale (optional), with its own upscaler and tiling overlap for seam-safe stitching.
201
+ """
202
+
203
  def _get_free_vram_bytes(self):
204
  """Return free VRAM bytes if available; else None."""
205
  try:
 
234
  except Exception as e:
235
  # 1) PyTorch<2.0: попробуем без antialias
236
  if "antialias" in kwargs:
237
+ kwargs.pop("antialias")
238
  try:
239
  return F.interpolate(*args, **kwargs)
240
  except Exception:
241
+ pass
242
 
243
  # 2) Старые torch: 'nearest-exact' не поддерживается
244
  if kwargs.get("mode") == "nearest-exact":
 
258
  except Exception:
259
  pass
260
 
261
+ # Все фолбэки исчерпаны — возвращаем исходный тензор без ресайза
262
+ logger.warning(f"_interp: all fallbacks failed ({e}), returning tensor unchanged (shape may mismatch)")
263
  try:
264
+ return args[0]
265
  except Exception:
266
  return None
267
 
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  def __init__(self):
270
  super().__init__()
271
  # Load or init config
272
+ _config_loaded_from_disk = False
273
  if config_path.exists():
274
  try:
275
  self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore
276
+ _config_loaded_from_disk = True
277
  except Exception as e:
278
  logger.warning(e)
279
  self.config = OmegaConf.create({}) # type: ignore
 
331
  # Cache fingerprint
332
  self._last_cache_fp = None
333
 
334
+ # Dirty flag: True means config needs to be written to disk.
335
+ # False when config was just loaded from disk (already in sync).
336
+ # True on fresh install (no config file yet) so defaults get persisted on first run.
337
+ self._config_dirty = not _config_loaded_from_disk
338
+ # JSON snapshot of config taken at start of postprocess_image for dirty detection.
339
+ self._config_snapshot = None
340
+
341
  def _get_model_fingerprint(self) -> str | None:
342
  """Build a fingerprint of current model/checkpoint + clip_skip to detect changes."""
343
  parts = []
 
1168
  """
1169
  Преобразует подпись из UI в аргументы torch.nn.functional.interpolate:
1170
  (mode, antialias). Поддерживает: "nearest", "nearest-exact", "bilinear",
1171
+ "bicubic", "area" и варианты с суффиксом "-antialiased".
1172
+ Примечание: "lanczos" не поддерживается F.interpolate — маппится на bicubic+antialias.
1173
  Для старых torch, где "nearest-exact" недоступен, _interp уже содержит фолбэк.
1174
  """
1175
  try:
 
1177
  aa = lab.endswith("-antialiased")
1178
  if aa:
1179
  lab = lab.replace("-antialiased", "")
1180
+ # lanczos не поддерживается torch.nn.functional.interpolate
1181
+ # маппим на bicubic с antialias для схожего качества
1182
+ if lab == "lanczos":
1183
+ return "bicubic", True
1184
+ if lab in ("nearest", "nearest-exact", "bilinear", "bicubic", "area"):
1185
  return lab, aa
1186
  return "bicubic", aa
1187
  except Exception:
 
1404
  # NEW
1405
  noise_cache_max):
1406
  if not enable:
1407
+ # Persist the disabled state so next startup respects it
1408
+ self.config["enable"] = False
1409
+ self._config_dirty = True
1410
+ self._save_config()
1411
  return
1412
 
1413
  # Save config chosen in UI
1414
  self.pp = pp
1415
+ # Snapshot current config BEFORE writing UI values — used below to detect real changes.
1416
+ try:
1417
+ import json as _j
1418
+ self._config_snapshot = _j.dumps(
1419
+ {k: (v if not hasattr(v, '__iter__') or isinstance(v, str)
1420
+ else list(v)) for k, v in self.config.items()},
1421
+ sort_keys=True, default=str)
1422
+ except Exception:
1423
+ self._config_snapshot = None
1424
  self.config["enable"] = bool(enable)
1425
  self.config["ratio"] = float(ratio)
1426
  self.config["width"] = int(width)
 
1543
  logger.warning(_e)
1544
  # Обновить PNG-info уже с актуальным self.config
1545
  p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p)
1546
+ # Mark config dirty only if something actually changed vs the snapshot taken
1547
+ # at the start of this function (set earlier via _config_snapshot).
1548
+ try:
1549
+ import json as _j
1550
+ new_snap = _j.dumps(
1551
+ {k: (v if not hasattr(v, '__iter__') or isinstance(v, str)
1552
+ else list(v)) for k, v in self.config.items()},
1553
+ sort_keys=True, default=str)
1554
+ if new_snap != getattr(self, "_config_snapshot", None):
1555
+ self._config_dirty = True
1556
+ self._config_snapshot = new_snap
1557
+ except Exception:
1558
+ self._config_dirty = True # safe fallback
1559
 
1560
  # Короткий сводный лог запуска — удобно для багрепортов
1561
  try:
 
1721
 
1722
  # ---- Helpers ----
1723
  def _save_config(self):
1724
+ """Сохранение конфигурации: YAML (если доступно) или JSON с явными предупреждениями.
1725
+ Запись на диск происходит только если конфиг был изменён из UI (_config_dirty=True).
1726
+ """
1727
+ if not getattr(self, "_config_dirty", True):
1728
+ return # nothing changed since last save
1729
  try:
1730
  config_path.parent.mkdir(parents=True, exist_ok=True)
1731
  except Exception as e:
 
1755
  with open(json_path, "w", encoding="utf-8") as f:
1756
  _json.dump(data, f, ensure_ascii=False, indent=2)
1757
  logger.info(f"Config saved to JSON: {json_path}")
1758
+ saved = True
1759
  except Exception as e:
1760
  logger.warning(f"Config save failed: {e}")
1761
+ if saved:
1762
+ self._config_dirty = False
1763
 
1764
 
1765
  def _vae_down_factor(self) -> int:
 
1998
  scaled_prompt = self._scale_lora_in_prompt(base_prompt, self._current_lora_factor)
1999
 
2000
  clip_skip = int(self.config.get("clip_skip", 0))
2001
+
2002
+ # Parse and activate extra networks (LoRA etc.) BEFORE cache lookup.
2003
+ # This must happen unconditionally even on cache hit the model weights
2004
+ # need to be patched, because extras are deactivated at the end of each run.
 
 
 
 
 
2005
  prompt_text = scaled_prompt
2006
  if not getattr(self.p, "disable_extra_networks", False):
2007
  try:
 
2012
  self._activated_extras.append(extra)
2013
  except Exception as e:
2014
  logger.warning(e)
 
2015
  except Exception as e:
2016
  logger.warning(e)
2017
+
2018
+ # Cache lookup — cond/uncond tensors can be reused, but extras must be active (done above)
2019
+ cache_key = self._cond_key(width, height, steps_for_cond, scaled_prompt, negative_base, clip_skip)
2020
+ cached = self._cond_cache_get(cache_key)
2021
+ if cached is not None:
2022
+ self.cond, self.uncond = cached
2023
+ return
2024
 
2025
  if width and height and hasattr(prompt_parser, "SdConditioning"):
2026
  c = prompt_parser.SdConditioning([prompt_text], False, width, height)
 
2323
  """
2324
  cb_ref = None
2325
  limit_steps = int(max(1, limit_steps))
2326
+
2327
+ # Save interrupt state so we can reliably restore it even if we set it ourselves.
2328
+ try:
2329
+ prev_interrupt = bool(getattr(shared.state, "interrupt", False))
2330
+ except Exception:
2331
+ prev_interrupt = False
2332
+
2333
  try:
2334
  def _cb(params):
2335
  try:
 
2361
  finally:
2362
  if cb_ref is not None:
2363
  try:
2364
+ # В новых WebUI есть явная отписка; если нет — пробуем ручное удаление
2365
  remove = getattr(script_callbacks, "remove_callbacks_for_function", None)
2366
  if callable(remove):
2367
  remove(cb_ref)
2368
+ else:
2369
+ # Ручной fallback: ищем колбэк в карте и удаляем
2370
+ try:
2371
+ cbs = getattr(script_callbacks, "callback_map", {}).get("callbacks_cfg_denoiser", [])
2372
+ for cb in list(cbs):
2373
+ if getattr(cb, "callback", None) is cb_ref:
2374
+ cbs.remove(cb)
2375
+ except Exception:
2376
+ pass
2377
  except Exception as e:
2378
  logger.warning(f"stop-hook remove warning: {e}")
2379
+ # Restore interrupt flag to avoid leaking into subsequent generations
2380
+ try:
2381
+ shared.state.interrupt = prev_interrupt
2382
+ except Exception:
2383
+ pass
2384
 
2385
  def _first_pass(self, x: Image.Image) -> Image.Image:
2386
  # Determine target size
 
2487
  steps = min(steps, sfirst)
2488
  # NEW: LRU-кэш шума для первой стадии
2489
  try:
2490
+ seeds = getattr(self.p, "seeds", None)
2491
+ key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2492
+ except (IndexError, TypeError, AttributeError) as e:
2493
+ logger.warning(f"Seed extraction warning: {e}")
 
 
 
 
 
2494
  key_seed = None
2495
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2496
  sam_name = str(self.config.get("sampler_first", self.config.get("sampler", "")))
 
2702
  else:
2703
  # NEW: если размеры не совпали — попробуем кэш
2704
  try:
2705
+ seeds = getattr(self.p, "seeds", None)
2706
+ key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2707
+ except (IndexError, TypeError, AttributeError) as e:
2708
+ logger.warning(f"Seed extraction warning: {e}")
 
 
 
 
2709
  key_seed = None
2710
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2711
  sam_name = str(self.config.get("sampler_second", self.config.get("sampler", "")))
 
2727
  else:
2728
  # NEW: обычный путь — используем LRU-кэш
2729
  try:
2730
+ seeds = getattr(self.p, "seeds", None)
2731
+ key_seed = seeds[0] if (seeds and len(seeds) > 0) else getattr(self.p, "seed", None)
2732
+ except (IndexError, TypeError, AttributeError) as e:
2733
+ logger.warning(f"Seed extraction warning: {e}")
 
 
 
 
2734
  key_seed = None
2735
  use_cache = bool(self.config.get("reuse_noise_cache", True))
2736
  sam_name = str(self.config.get("sampler_second", self.config.get("sampler", "")))
 
2963
  crop = img.crop((x0, y0, x1, y1))
2964
 
2965
  # Reflect-padding по краям кадра, если запрошено
2966
+ pad_left = pad_top = pad_right = pad_bottom = 0
2967
  if use_reflect and (x0 == 0 or y0 == 0 or x1 == W or y1 == H):
2968
  arr = np.array(crop)
2969
  # считаем недостающий «выход» за границы слева/сверху
 
2977
  # Апскейлим тайл
2978
  up = _upscale(crop, final_scale)
2979
  up_np = np.array(up).astype(np.float32)
2980
+
2981
+ # Обрезаем reflect-padding после апскейла, чтобы размер и позиция вставки
2982
+ # соответствовали реальным координатам холста (без отражённых краёв).
2983
+ if pad_left or pad_top or pad_right or pad_bottom:
2984
+ pt = int(round(pad_top * final_scale))
2985
+ pb = int(round(pad_bottom * final_scale))
2986
+ pl = int(round(pad_left * final_scale))
2987
+ pr = int(round(pad_right * final_scale))
2988
+ h_up, w_up = up_np.shape[:2]
2989
+ # Оставляем только центральную (реальную) часть
2990
+ up_np = up_np[pt : h_up - pb if pb else h_up,
2991
+ pl : w_up - pr if pr else w_up]
2992
+
2993
  uh, uw = up_np.shape[0], up_np.shape[1]
2994
 
2995
  # Координаты вставки на апскейленном полотне
 
3052
  cap = int(self.config.get("cn_proc_res_cap", 1024))
3053
  cap = max(256, min(4096, cap))
3054
  unit.processor_res = max(256, min(cap, min_side))
3055
+ # Always update unit.image so the 2nd pass uses the current reference,
3056
+ # not the stale one set during the 1st pass.
3057
+ unit.image = image_np
3058
  self.p.width = image_np.shape[1]
3059
  self.p.height = image_np.shape[0]
3060
  except Exception as e:
 
3086
  try:
3087
  data = json.loads(block)
3088
  except json.JSONDecodeError:
3089
+ # Fallback для старых строк с одинарными кавычками.
3090
+ # Используем ast.literal_eval вместо translate(), чтобы не ломать
3091
+ # промпты с апострофами (напр. "it's a dog" → невалидный JSON после translate).
3092
+ try:
3093
+ import ast
3094
+ data = ast.literal_eval(block)
3095
+ if not isinstance(data, dict):
3096
+ raise ValueError("not a dict")
3097
+ except Exception:
3098
+ # Последний резерв — старый translate-хак, только если ast тоже упал
3099
+ data = json.loads(block.translate(QUOTE_SINGLE_TO_DOUBLE))
3100
  else:
3101
  logger.warning(f"Unexpected infotext type: {type(block)}")
3102
  return
 
3228
  logger.warning(e)
3229
  return
3230
  # Register paste-params hook (guard against double registration)
 
 
 
 
3231
  if not _INFOTEXT_HOOK_REGISTERED:
3232
  script_callbacks.on_infotext_pasted(parse_infotext)
3233
+ _INFOTEXT_HOOK_REGISTERED = True