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",
|
| 49 |
-
_safe_import("k_diffusion", "k-diffusion")
|
| 50 |
-
_safe_import("skimage",
|
| 51 |
-
_safe_import("cv2",
|
| 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 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
except Exception
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
kornia = None
|
| 71 |
-
_HAS_KORNIA = False
|
| 72 |
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
except Exception as e:
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
| 168 |
|
| 169 |
# OpenCV (optional)
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
|
|
|
| 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 |
-
|
| 217 |
try:
|
| 218 |
return F.interpolate(*args, **kwargs)
|
| 219 |
except Exception:
|
| 220 |
-
|
| 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 |
-
|
|
|
|
| 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", "
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 1953 |
-
|
| 1954 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 2420 |
-
|
| 2421 |
-
|
| 2422 |
-
|
| 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 |
-
|
| 2640 |
-
|
| 2641 |
-
|
| 2642 |
-
|
| 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 |
-
|
| 2669 |
-
|
| 2670 |
-
|
| 2671 |
-
|
| 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 |
-
|
| 2984 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|