diff --git "a/custom-hires-fix-mod-for-automatic1111-2.7/scripts/custom_hires_fix.py" "b/custom-hires-fix-mod-for-automatic1111-2.7/scripts/custom_hires_fix.py" new file mode 100644--- /dev/null +++ "b/custom-hires-fix-mod-for-automatic1111-2.7/scripts/custom_hires_fix.py" @@ -0,0 +1,2191 @@ +import math +import torch +import torch.nn.functional as F +import base64 +import json +import hashlib +from pathlib import Path +from collections import OrderedDict + +import gradio as gr +import numpy as np +from PIL import Image, ImageFilter + +from modules import scripts, shared, processing, sd_schedulers, sd_samplers, script_callbacks, rng +from modules import images, devices, prompt_parser, sd_models, extra_networks +from typing import Optional + +# Ensure we call the upscaler path when resizing (A1111 compat) +RESIZE_WITH_UPSCALER = getattr(images, "RESIZE_WITH_UPSCALER", 2) + +# Optional deps (best-effort) +def _safe_import(modname, pipname=None): + try: + __import__(modname) + return True + except Exception: + try: + import pip + if hasattr(pip, "main"): + pip.main(["install", pipname or modname]) + else: + pip._internal.main(["install", pipname or modname]) + __import__(modname) + return True + except Exception: + return False + +_safe_import("omegaconf") +_safe_import("kornia") +_safe_import("k_diffusion", "k-diffusion") +_safe_import("skimage") +_safe_import("cv2") + +try: + from omegaconf import OmegaConf, DictConfig # type: ignore +except Exception: # graceful fallback if OmegaConf not available + class DictConfig(dict): # minimal stub + pass + class OmegaConf: # minimal stub + @staticmethod + def load(path): + return DictConfig() + @staticmethod + def create(obj): + return DictConfig(obj) + +import kornia # type: ignore +import k_diffusion as K # type: ignore + +# skimage helpers (optional) + +# ---- Latent resampling helpers (with anti-alias) ---- +def _parse_upscale_method(upscale_method: str): + """ + Поддерживает 'bilinear-antialiased' / 'bicubic-antialiased' (флаг сглаживания) + + обычные 'nearest'/'bilinear'/'bicubic'. + """ + if upscale_method in ("bilinear-antialiased", "bicubic-antialiased"): + base = upscale_method.split("-")[0] + return base, True + return upscale_method, False + +def _interpolate_latent(tensor: torch.Tensor, size_hw: tuple[int, int], mode_name: str) -> torch.Tensor: + """ + Унифицированная интерполяция латента с безопасным использованием antialias и align_corners. + """ + mode, aa = _parse_upscale_method(mode_name) + kwargs = {"mode": mode} + if mode in ("bilinear", "bicubic"): + kwargs["align_corners"] = False + try: + # PyTorch >= 2.0: есть antialias + return F.interpolate(tensor, size=size_hw, antialias=aa, **kwargs) + except TypeError: + # Старые версии — без параметра antialias + return F.interpolate(tensor, size=size_hw, **kwargs) + +try: + from skimage.exposure import match_histograms, equalize_adapthist # type: ignore + from skimage import color as skcolor # type: ignore + _SKIMAGE_OK = True +except Exception: + _SKIMAGE_OK = False + +# OpenCV (optional) +try: + import cv2 # type: ignore + _CV2_OK = True +except Exception: + _CV2_OK = False + +quote_swap = str.maketrans("\'\"", "\"\'") +config_path = (Path(__file__).parent.resolve() / "../config.yaml").resolve() + + +class CustomHiresFix(scripts.Script): + """Two-stage img2img upscaling with optional latent mixing and prompt overrides. + + Features: + - Ratio/width/height or Megapixels target (+ quick MP buttons) + - Compact preset panel (global presets) + - Separate steps for 1st/2nd pass + - Per-pass sampler + scheduler + - CFG base + optional delta on 2nd pass + - Reuse seed/noise on 2nd pass + - Conditioning cache (LRU) with capacity + - Second-pass prompt (append/replace) + - Per-pass LoRA weight scaling + - Seamless tiling (+ overlap) + - VAE tiling toggle (low VRAM) + - Color match to original (strength) with presets + - Post-FX presets: CLAHE (local contrast), Unsharp Mask + - PNG-info serialization + paste support + - Final ×4 upscale (optional), with its own upscaler and tiling overlap for seam-safe stitching. + """ + def __init__(self): + super().__init__() + # Load or init config + if config_path.exists(): + try: + self.config: DictConfig = OmegaConf.load(str(config_path)) or OmegaConf.create({}) # type: ignore + except Exception: + self.config = OmegaConf.create({}) # type: ignore + else: + self.config = OmegaConf.create({}) # type: ignore + + # Runtime state + self.p = None + self.pp = None + self.cfg = 0.0 + self.cond = None + self.uncond = None + self.width = None + self.height = None + self._orig_clip_skip = None + self._cn_units = [] + self._use_cn = False + + # Reuse state + self._saved_seeds = None + # subseeds may not exist in all pipelines; keep best-effort + self._saved_subseeds = None + self._saved_subseed_strength = None + self._saved_seed_resize_from_h = None + self._saved_seed_resize_from_w = None + self._first_noise = None + self._first_noise_shape = None + + # Conditioning cache (LRU) + self._cond_cache: OrderedDict[str, tuple] = OrderedDict() + + # VAE tiling state restore + self._orig_opt_vae_tiling = None + + # Seamless tiling restore + self._orig_tiling = None + self._orig_tile_overlap = None + + # Prompt override for second pass + self._override_prompt_second = None + + # LoRA scaling factor per pass (used during _prepare_conditioning by pass context) + self._current_lora_factor = 1.0 + # Scheduler restore state + self._orig_scheduler = None + self._orig_size = (None, None) + + + # NEW: флаг семейства модели (SDXL/SD3) и LRU-кэш шума + self.is_sdxl = False + # Небольшой LRU на тензоры шума: ключ (pass|shape|seed) -> Tensor + # Важно держать размер маленьким, чтобы не проедать VRAM + self._noise_cache: OrderedDict[str, torch.Tensor] = OrderedDict() + + + + def _apply_token_merging(self, *, for_hr: bool = False, halve: bool = False): + """Safely apply token merging ratio across webui versions.""" + ratio_fn = getattr(self.p, "get_token_merging_ratio", None) + r: float = 0.0 + if callable(ratio_fn): + try: + r = float(ratio_fn(for_hr=for_hr)) + except TypeError: + r = float(ratio_fn()) + except Exception: + r = 0.0 + if halve: + r = r / 2.0 + try: + # используем p.sd_model если есть, иначе shared.sd_model + model = getattr(self.p, "sd_model", None) or shared.sd_model + sd_models.apply_token_merging(model, r) + except Exception: + pass + + def _set_scheduler_by_label(self, label_or_obj): + """ + Безопасно устанавливает планировщик по его видимому label. + На новых версиях — объект из sd_schedulers.schedulers, + на старых — откат к строке (как было). + """ + if not label_or_obj or label_or_obj == "Use same scheduler": + return + # Нормализуем к СТРОКЕ (ключу в schedulers_map) + if isinstance(label_or_obj, str): + label = label_or_obj + else: + label = getattr(label_or_obj, "label", getattr(label_or_obj, "name", str(label_or_obj))) + # Попытка выдать объект-планировщик; если не найден — строка (бэкап) + obj = None + try: + sched_map = getattr(sd_schedulers, "schedulers_map", {}) + if getattr(sched_map, "get", None): + obj = sched_map.get(label) + if obj is None: + for s in getattr(sd_schedulers, "schedulers", []): + if getattr(s, "label", None) == label or getattr(s, "name", None) == label: + obj = s + break + except Exception: + obj = None + self.p.scheduler = obj if obj is not None else label + + # ---- A1111 Script API ---- + def title(self): + return "Custom Hires Fix" + + def show(self, is_img2img): + return scripts.AlwaysVisible + + def ui(self, is_img2img): + visible_names = [x.name for x in sd_samplers.visible_samplers()] + sampler_names = ["Restart + DPM++ 3M SDE"] + visible_names + _scheds = getattr(sd_schedulers, "schedulers", []) + scheduler_names = ["Use same scheduler"] + [ + getattr(x, "label", getattr(x, "name", str(x))) for x in _scheds] + + with gr.Accordion(label="Custom Hires Fix", open=False) as enable_box: + enable = gr.Checkbox(label="Enable extension", value=bool(self.config.get("enable", False))) + + # ---------- Compact preset panel ---------- + with gr.Row(): + quick_preset = gr.Dropdown( + ["None", "Hi-Res Portrait", "Hi-Res Texture", "Hi-Res Illustration", "Hi-Res Product Shot"], + label="Quick preset", + value="None" + ) + btn_apply_preset = gr.Button(value="Apply preset", variant="primary") + + btn_mp_1 = gr.Button(value="MP 1.0") + btn_mp_2 = gr.Button(value="MP 2.0") + btn_mp_4 = gr.Button(value="MP 4.0") + btn_mp_8 = gr.Button(value="MP 8.0") + + with gr.Row(): + ratio = gr.Slider(minimum=0.0, maximum=4.0, step=0.05, label="Upscale by (ratio)", + value=float(self.config.get("ratio", 0.0))) + width = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize width to", + value=int(self.config.get("width", 0))) + height = gr.Slider(minimum=0, maximum=4096, step=8, label="Resize height to", + value=int(self.config.get("height", 0))) + # --------- Size helpers --------- + with gr.Row(): + long_edge = gr.Slider(minimum=0, maximum=8192, step=8, + label="Resize by long edge (0 = off)", + value=int(self.config.get("long_edge", 0))) + btn_swap_wh = gr.Button(value="Swap W↔H") + + # NEW: кастомный размер для 2-го прохода + with gr.Row(): + second_custom_size_enable = gr.Checkbox( + label="Second pass: override size", + value=bool(self.config.get("second_custom_size_enable", False)) + ) + second_width = gr.Slider(minimum=0, maximum=4096, step=8, + label="Second pass width", + value=int(self.config.get("second_width", 0))) + second_height = gr.Slider(minimum=0, maximum=4096, step=8, + label="Second pass height", + value=int(self.config.get("second_height", 0))) + + with gr.Row(): + steps_first = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 1st pass", + value=int(self.config.get("steps_first", max(1, int(self.config.get("steps", 20)))))) + steps_second = gr.Slider(minimum=1, maximum=100, step=1, label="Hires steps — 2nd pass", + value=int(self.config.get("steps_second", int(self.config.get("steps", 20))))) + + # --------- Per-pass denoising --------- + with gr.Row(): + denoise_first = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, + label="Denoising strength — 1st pass", + value=float(self.config.get("denoise_first", 0.33))) + denoise_second = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, + label="Denoising strength — 2nd pass", + value=float(self.config.get("denoise_second", 0.45))) + with gr.Row(): + first_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers], + label="First upscaler", value=self.config.get("first_upscaler", "R-ESRGAN 4x+")) + second_upscaler = gr.Dropdown([x.name for x in shared.sd_upscalers], + label="Second upscaler", value=self.config.get("second_upscaler", "R-ESRGAN 4x+")) + + with gr.Row(): + first_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (first stage)", + value=float(self.config.get("first_latent", 0.3))) + second_latent = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label="Latent mix (second stage)", + value=float(self.config.get("second_latent", 0.1))) + # рядом с латент-слайдами + with gr.Row(): + first_latent_invert = gr.Checkbox( + label="Invert 1st-pass latent mix (slider = original image latent weight)", + value=bool(self.config.get("first_latent_invert", False)) + ) # NEW + + # Режим ресемпла латента + with gr.Row(): + latent_resample_mode = gr.Dropdown( + ["nearest", "bilinear", "bicubic", "bilinear-antialiased", "bicubic-antialiased"], + label="Latent resample mode", + value=self.config.get("latent_resample_mode", "nearest") + ) + + with gr.Row(): + filter_mode = gr.Dropdown(["Noise sync (sharp)", "Morphological (smooth)", "Combined (balanced)"], + label="Filter mode", value=self.config.get("filter_mode", "Noise sync (sharp)")) + strength = gr.Slider(minimum=0.5, maximum=4.0, step=0.1, label="Generation strength", + value=float(self.config.get("strength", 2.0))) + denoise_offset = gr.Slider(minimum=-0.1, maximum=0.2, step=0.01, label="Denoise offset", + value=float(self.config.get("denoise_offset", 0.05))) + # NEW: чекбокс включения адаптивной формы сигм + adaptive_sigma_enable = gr.Checkbox(label="Adaptive denoiser shaping (uses Filter/Strength)", + value=bool(self.config.get("adaptive_sigma_enable", False))) + + with gr.Row(): + prompt = gr.Textbox(label="Prompt override (1st pass)", placeholder="Leave empty to use main UI prompt", + value=self.config.get("prompt", "")) + negative_prompt = gr.Textbox(label="Negative prompt override", placeholder="Leave empty to use main UI negative prompt", + value=self.config.get("negative_prompt", "")) + + with gr.Row(): + second_pass_prompt = gr.Textbox(label="Second-pass prompt", placeholder="Append or replace on 2nd pass", + value=self.config.get("second_pass_prompt", "")) + second_pass_prompt_append = gr.Checkbox(label="Append instead of replace", + value=bool(self.config.get("second_pass_prompt_append", True))) + + with gr.Accordion(label="Extra", open=False): + with gr.Row(): + filter_offset = gr.Slider(minimum=-1.0, maximum=1.0, step=0.1, label="Filter offset", + value=float(self.config.get("filter_offset", 0.0))) + clip_skip = gr.Slider(minimum=0, maximum=12, step=1, label="CLIP skip (0 = keep)", + value=int(self.config.get("clip_skip", 0))) + + # Режим расписания сигм + with gr.Row(): + noise_schedule_mode = gr.Dropdown( + [ + "Use sampler default", "Adaptive (filter/strength)", "Karras", "Exponential", + "Polyexponential", "DDIM uniform", "Normal", "Simple" + ], + label="Noise schedule override", + value=self.config.get("noise_schedule_mode", "Use sampler default")) + + # Per-pass sampler/scheduler + with gr.Row(): + sampler_first = gr.Dropdown(sampler_names, label="Sampler — 1st pass", + value=self.config.get("sampler_first", sampler_names[0])) + sampler_second = gr.Dropdown(sampler_names, label="Sampler — 2nd pass", + value=self.config.get("sampler_second", self.config.get("sampler", sampler_names[0]))) + with gr.Row(): + scheduler_first = gr.Dropdown( + choices=scheduler_names, label="Schedule type — 1st pass", + value=self.config.get("scheduler_first", self.config.get("scheduler", scheduler_names[0])) + ) + scheduler_second = gr.Dropdown( + choices=scheduler_names, label="Schedule type — 2nd pass", + value=self.config.get("scheduler_second", self.config.get("scheduler", scheduler_names[0])) + ) + + # Restore scheduler toggle + restore_scheduler_after = gr.Checkbox( + label="Restore scheduler after run", + value=bool(self.config.get("restore_scheduler_after", True)) + ) + + with gr.Row(): + cfg = gr.Slider(minimum=0, maximum=30, step=0.5, label="CFG Scale (base)", + value=float(self.config.get("cfg", 7.0))) + cfg_second_pass_boost = gr.Checkbox(label="Enable CFG delta on 2nd pass", + value=bool(self.config.get("cfg_second_pass_boost", True))) + cfg_second_pass_delta = gr.Slider(minimum=-5.0, maximum=5.0, step=0.5, label="CFG delta (2nd pass)", + value=float(self.config.get("cfg_second_pass_delta", 3.0))) + + # Reuse seed/noise + Megapixels target + with gr.Row(): + reuse_seed_noise = gr.Checkbox(label="Reuse seed/noise on 2nd pass", + value=bool(self.config.get("reuse_seed_noise", False))) + mp_target_enabled = gr.Checkbox(label="Enable Megapixels target", + value=bool(self.config.get("mp_target_enabled", False))) + mp_target = gr.Slider(minimum=0.3, maximum=16.0, step=0.1, label="Megapixels", + value=float(self.config.get("mp_target", 2.0))) + + # Conditioning cache controls + with gr.Row(): + cond_cache_enabled = gr.Checkbox(label="Enable conditioning cache (LRU)", + value=bool(self.config.get("cond_cache_enabled", True))) + cond_cache_max = gr.Slider(minimum=8, maximum=256, step=8, label="Conditioning cache size", + value=int(self.config.get("cond_cache_max", 64))) + + # VAE tiling + with gr.Row(): + vae_tiling_enabled = gr.Checkbox(label="Enable VAE tiling (low VRAM)", + value=bool(self.config.get("vae_tiling_enabled", False))) + + # Seamless tiling + with gr.Row(): + seamless_tiling_enabled = gr.Checkbox(label="Seamless tiling (texture)", + value=bool(self.config.get("seamless_tiling_enabled", False))) + tile_overlap = gr.Slider(minimum=0, maximum=64, step=1, label="Tile overlap (px)", + value=int(self.config.get("tile_overlap", 12))) + + # LoRA scaling + with gr.Row(): + lora_weight_first_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (1st pass)", + value=float(self.config.get("lora_weight_first_factor", 1.0))) + lora_weight_second_factor = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="LoRA weight × (2nd pass)", + value=float(self.config.get("lora_weight_second_factor", 1.0))) + + # Match colors presets & controls + with gr.Row(): + match_colors_preset = gr.Dropdown( + ["Off", "Subtle (0.3)", "Natural (0.5)", "Strong (0.8)"], + label="Match colors preset", + value=self.config.get("match_colors_preset", "Off") + ) + match_colors_enabled = gr.Checkbox(label="Match colors to original", + value=bool(self.config.get("match_colors_enabled", False))) + match_colors_strength = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, label="Match strength", + value=float(self.config.get("match_colors_strength", 0.5))) + + # Post-processing presets & controls + with gr.Row(): + postfx_preset = gr.Dropdown( + ["Off", "Soft clarity", "Portrait safe", "Texture boost", "Crisp detail"], + label="Post-FX preset", + value=self.config.get("postfx_preset", "Off") + ) + clahe_enabled = gr.Checkbox(label="CLAHE (local contrast)", + value=bool(self.config.get("clahe_enabled", False))) + clahe_clip = gr.Slider(minimum=1.0, maximum=5.0, step=0.1, label="CLAHE clip limit", + value=float(self.config.get("clahe_clip", 2.0))) + clahe_tile_grid = gr.Slider(minimum=4, maximum=16, step=2, label="CLAHE tile grid", + value=int(self.config.get("clahe_tile_grid", 8))) + + with gr.Row(): + unsharp_enabled = gr.Checkbox(label="Unsharp Mask (sharpen)", + value=bool(self.config.get("unsharp_enabled", False))) + unsharp_radius = gr.Slider(minimum=0.5, maximum=5.0, step=0.1, label="Unsharp radius", + value=float(self.config.get("unsharp_radius", 1.5))) + unsharp_amount = gr.Slider(minimum=0.0, maximum=2.0, step=0.05, label="Unsharp amount", + value=float(self.config.get("unsharp_amount", 0.75))) + unsharp_threshold = gr.Slider(minimum=0, maximum=10, step=1, label="Unsharp threshold", + value=int(self.config.get("unsharp_threshold", 0))) + + with gr.Row(): + cn_ref = gr.Checkbox(label="Use last image as ControlNet reference", value=bool(self.config.get("cn_ref", False))) + start_control_at = gr.Slider(minimum=0.0, maximum=0.7, step=0.01, label="CN start (enabled units)", + value=float(self.config.get("start_control_at", 0.0))) + cn_proc_res_cap = gr.Slider(minimum=256, maximum=2048, step=64, + label="ControlNet processor_res cap", + value=int(self.config.get("cn_proc_res_cap", 1024))) + + # --- Final upscale controls (configurable scale) --- + with gr.Row(): + final_upscale_enable = gr.Checkbox( + label="Final upscale (after 2nd pass)", + value=bool(self.config.get("final_upscale_enable", False)) + ) + final_upscaler = gr.Dropdown( + [x.name for x in shared.sd_upscalers], + label="Final upscaler", + value=self.config.get("final_upscaler", "R-ESRGAN 4x+") + ) + # NEW: настраиваемый масштаб + final_scale = gr.Slider( + minimum=1.0, maximum=8.0, step=0.5, + label="Final upscale scale (×)", + value=float(self.config.get("final_scale", 4.0)) + ) + + with gr.Row(): + final_tile = gr.Slider( + minimum=128, maximum=1024, step=32, + label="Final tile size (px, pre-scale)", + value=int(self.config.get("final_tile", 512)) + ) + final_tile_overlap = gr.Slider( + minimum=0, maximum=64, step=2, + label="Final tile overlap (px, pre-scale)", + value=int(self.config.get("final_tile_overlap", 16)) + ) + + # NEW: Anti-twinning (latent deep shrink) и SDXL-подсказки + with gr.Row(): + deep_shrink_enable = gr.Checkbox( + label="Enable Deep Shrink (anti-twinning)", + value=bool(self.config.get("deep_shrink_enable", False)) + ) + deep_shrink_strength = gr.Slider( + minimum=0.1, maximum=0.9, step=0.05, + label="Shrink strength", + value=float(self.config.get("deep_shrink_strength", 0.5)), + visible=bool(self.config.get("deep_shrink_enable", False)) + ) + # toggle visibility + deep_shrink_enable.change( + fn=lambda v: gr.update(visible=bool(v)), + inputs=deep_shrink_enable, + outputs=deep_shrink_strength + ) + + with gr.Row(): + sdxl_mode = gr.Checkbox( + label="SDXL/SD3 mode (gentle denoise boost)", + value=bool(self.config.get("sdxl_mode", False)) + ) + sdxl_denoise_boost = gr.Slider( + minimum=0.0, maximum=0.3, step=0.01, + label="SDXL denoise boost (+)", + value=float(self.config.get("sdxl_denoise_boost", 0.1)) + ) + + # ---------- Preset logic (UI events) ---------- + def _apply_match_preset(preset_name): + if preset_name == "Off": + return (gr.update(value=False), gr.update(value=0.5)) + if preset_name == "Subtle (0.3)": + return (gr.update(value=True), gr.update(value=0.3)) + if preset_name == "Natural (0.5)": + return (gr.update(value=True), gr.update(value=0.5)) + if preset_name == "Strong (0.8)": + return (gr.update(value=True), gr.update(value=0.8)) + return (gr.update(), gr.update()) + + def _apply_postfx_preset(preset_name): + # Returns: clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold + if preset_name == "Off": + return (gr.update(value=False), gr.update(value=2.0), gr.update(value=8), + gr.update(value=False), gr.update(value=1.5), gr.update(value=0.75), gr.update(value=0)) + if preset_name == "Soft clarity": + return (gr.update(value=True), gr.update(value=1.8), gr.update(value=8), + gr.update(value=True), gr.update(value=1.2), gr.update(value=0.6), gr.update(value=0)) + if preset_name == "Portrait safe": + return (gr.update(value=True), gr.update(value=1.6), gr.update(value=8), + gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2)) + if preset_name == "Texture boost": + return (gr.update(value=True), gr.update(value=2.4), gr.update(value=8), + gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0)) + if preset_name == "Crisp detail": + return (gr.update(value=True), gr.update(value=2.1), gr.update(value=8), + gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0)) + return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()) + + def _apply_quick_preset(name): + # Returns a large tuple of updates for several controls + out = [ + gr.update(), # steps_first + gr.update(), # steps_second + gr.update(), # cfg_second_pass_boost + gr.update(), # cfg_second_pass_delta + gr.update(), # sampler_first + gr.update(), # sampler_second + gr.update(), # scheduler_first + gr.update(), # scheduler_second + gr.update(), # vae_tiling_enabled + gr.update(), # seamless_tiling_enabled + gr.update(), # tile_overlap + gr.update(), # match_colors_preset + gr.update(), # match_colors_enabled + gr.update(), # match_colors_strength + gr.update(), # postfx_preset + gr.update(), # clahe_enabled + gr.update(), # clahe_clip + gr.update(), # clahe_tile_grid + gr.update(), # unsharp_enabled + gr.update(), # unsharp_radius + gr.update(), # unsharp_amount + gr.update(), # unsharp_threshold + gr.update(), # reuse_seed_noise + gr.update(), # cond_cache_max + gr.update(), # lora_weight_first_factor + gr.update(), # lora_weight_second_factor + gr.update(), # mp_target_enabled + gr.update(), # mp_target + ] + if name == "Hi-Res Portrait": + out = [ + gr.update(value=18), gr.update(value=28), + gr.update(value=True), gr.update(value=2.5), + gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"), + gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"), + gr.update(value=True), gr.update(value=False), gr.update(value=12), + gr.update(value="Subtle (0.3)"), gr.update(value=True), gr.update(value=0.3), + gr.update(value="Portrait safe"), gr.update(value=True), gr.update(value=1.6), gr.update(value=8), + gr.update(value=True), gr.update(value=1.4), gr.update(value=0.8), gr.update(value=2), + gr.update(value=True), gr.update(value=64), + gr.update(value=1.0), gr.update(value=1.1), + gr.update(value=True), gr.update(value=2.0), + ] + elif name == "Hi-Res Texture": + out = [ + gr.update(value=14), gr.update(value=22), + gr.update(value=True), gr.update(value=3.0), + gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"), + gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"), + gr.update(value=True), gr.update(value=True), gr.update(value=12), + gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5), + gr.update(value="Texture boost"), gr.update(value=True), gr.update(value=2.2), gr.update(value=8), + gr.update(value=True), gr.update(value=1.6), gr.update(value=1.0), gr.update(value=0), + gr.update(value=True), gr.update(value=128), + gr.update(value=0.9), gr.update(value=1.25), + gr.update(value=True), gr.update(value=4.0), + ] + elif name == "Hi-Res Illustration": + out = [ + gr.update(value=16), gr.update(value=24), + gr.update(value=True), gr.update(value=2.0), + gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ 3M SDE"), + gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"), + gr.update(value=True), gr.update(value=False), gr.update(value=8), + gr.update(value="Off"), gr.update(value=False), gr.update(value=0.5), + gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.0), gr.update(value=8), + gr.update(value=True), gr.update(value=1.2), gr.update(value=0.9), gr.update(value=0), + gr.update(value=True), gr.update(value=64), + gr.update(value=0.85), gr.update(value=1.2), + gr.update(value=True), gr.update(value=2.0), + ] + elif name == "Hi-Res Product Shot": + out = [ + gr.update(value=18), gr.update(value=26), + gr.update(value=True), gr.update(value=2.0), + gr.update(value="DPM++ 2M Karras"), gr.update(value="DPM++ SDE Karras"), + gr.update(value="Use same scheduler"), gr.update(value="Use same scheduler"), + gr.update(value=True), gr.update(value=False), gr.update(value=8), + gr.update(value="Natural (0.5)"), gr.update(value=True), gr.update(value=0.5), + gr.update(value="Crisp detail"), gr.update(value=True), gr.update(value=2.1), gr.update(value=8), + gr.update(value=True), gr.update(value=1.3), gr.update(value=0.9), gr.update(value=0), + gr.update(value=True), gr.update(value=96), + gr.update(value=1.0), gr.update(value=1.15), + gr.update(value=True), gr.update(value=2.0), + ] + return tuple(out) + + # --- Привязка событий пресетов --- + match_colors_preset.change( + fn=_apply_match_preset, + inputs=[match_colors_preset], + outputs=[match_colors_enabled, match_colors_strength] + ) + postfx_preset.change( + fn=_apply_postfx_preset, + inputs=[postfx_preset], + outputs=[clahe_enabled, clahe_clip, clahe_tile_grid, unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold] + ) + btn_apply_preset.click( + fn=_apply_quick_preset, + inputs=[quick_preset], + outputs=[ + steps_first, steps_second, + cfg_second_pass_boost, cfg_second_pass_delta, + sampler_first, sampler_second, + scheduler_first, scheduler_second, + vae_tiling_enabled, seamless_tiling_enabled, tile_overlap, + match_colors_preset, match_colors_enabled, match_colors_strength, + postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid, + unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold, + reuse_seed_noise, cond_cache_max, + lora_weight_first_factor, lora_weight_second_factor, + mp_target_enabled, mp_target + ] + ) + # MP buttons + btn_mp_1.click(fn=lambda: (gr.update(value=True), gr.update(value=1.0)), inputs=[], outputs=[mp_target_enabled, mp_target]) + btn_mp_2.click(fn=lambda: (gr.update(value=True), gr.update(value=2.0)), inputs=[], outputs=[mp_target_enabled, mp_target]) + btn_mp_4.click(fn=lambda: (gr.update(value=True), gr.update(value=4.0)), inputs=[], outputs=[mp_target_enabled, mp_target]) + btn_mp_8.click(fn=lambda: (gr.update(value=True), gr.update(value=8.0)), inputs=[], outputs=[mp_target_enabled, mp_target]) + + # Exclusivity helpers (STRICT two-mode: exact W×H OR pure ratio) + # Изменение любой стороны отключает ratio, НО не обнуляет вторую сторону — можно задать обе. + width.change(fn=lambda _: (gr.update(value=0.0), gr.update(value=False)), + inputs=width, outputs=[ratio, mp_target_enabled]) + height.change(fn=lambda _: (gr.update(value=0.0), gr.update(value=False)), + inputs=height, outputs=[ratio, mp_target_enabled]) + ratio.change(fn=lambda _: (gr.update(value=0), gr.update(value=0), gr.update(value=False)), + inputs=ratio, outputs=[width, height, mp_target_enabled]) + # Long edge excludes ratio and explicit W/H + long_edge.change(fn=lambda _: (gr.update(value=0), gr.update(value=0), gr.update(value=0.0), gr.update(value=False)), + inputs=long_edge, outputs=[width, height, ratio, mp_target_enabled]) + # Swap button + btn_swap_wh.click(fn=lambda w, h: (h, w), inputs=[width, height], outputs=[width, height]) + + # infotext paste support + def read_params(d, key, default=None): + try: + return d["Custom Hires Fix"].get(key, default) + except Exception: + return default + + self.infotext_fields = [ + (enable, lambda d: "Custom Hires Fix" in d), + (ratio, lambda d: read_params(d, "ratio", 0.0)), + (width, lambda d: read_params(d, "width", 0)), + (height, lambda d: read_params(d, "height", 0)), + (long_edge, lambda d: read_params(d, "long_edge", 0)), + (steps_first, lambda d: read_params(d, "steps_first", read_params(d, "steps", 20))), + (steps_second, lambda d: read_params(d, "steps_second", read_params(d, "steps", 20))), + (denoise_first, lambda d: read_params(d, "denoise_first", 0.33)), + (denoise_second, lambda d: read_params(d, "denoise_second", 0.45)), + (first_upscaler, lambda d: read_params(d, "first_upscaler")), + (second_upscaler, lambda d: read_params(d, "second_upscaler")), + (first_latent, lambda d: read_params(d, "first_latent", 0.0)), + (second_latent, lambda d: read_params(d, "second_latent", 0.0)), + (latent_resample_mode, lambda d: read_params(d, "latent_resample_mode", "nearest")), + (prompt, lambda d: read_params(d, "prompt", "")), + (negative_prompt, lambda d: read_params(d, "negative_prompt", "")), + (second_pass_prompt, lambda d: read_params(d, "second_pass_prompt", "")), + (second_pass_prompt_append, lambda d: read_params(d, "second_pass_prompt_append", True)), + (strength, lambda d: read_params(d, "strength", 0.0)), + (filter_mode, lambda d: read_params(d, "filter_mode")), + (denoise_offset, lambda d: read_params(d, "denoise_offset", 0.0)), + (filter_offset, lambda d: read_params(d, "filter_offset", 0.0)), + (adaptive_sigma_enable, lambda d: read_params(d, "adaptive_sigma_enable", False)), + (noise_schedule_mode, lambda d: read_params(d, "noise_schedule_mode", "Use sampler default")), + (clip_skip, lambda d: read_params(d, "clip_skip", 0)), + # per-pass samplers/schedulers + legacy fallbacks + (sampler_first, lambda d: read_params(d, "sampler_first", read_params(d, "sampler", sampler_names[0]))), + (sampler_second, lambda d: read_params(d, "sampler_second", read_params(d, "sampler", sampler_names[0]))), + (scheduler_first, lambda d: read_params(d, "scheduler_first", read_params(d, "scheduler", scheduler_names[0]))), + (scheduler_second, lambda d: read_params(d, "scheduler_second", read_params(d, "scheduler", scheduler_names[0]))), + (restore_scheduler_after, lambda d: read_params(d, "restore_scheduler_after", True)), + # cfg/delta + (cfg, lambda d: read_params(d, "cfg", 7.0)), + (cfg_second_pass_boost, lambda d: read_params(d, "cfg_second_pass_boost", True)), + (cfg_second_pass_delta, lambda d: read_params(d, "cfg_second_pass_delta", 3.0)), + # flags + (reuse_seed_noise, lambda d: read_params(d, "reuse_seed_noise", False)), + (mp_target_enabled, lambda d: read_params(d, "mp_target_enabled", False)), + (mp_target, lambda d: read_params(d, "mp_target", 2.0)), + (cond_cache_enabled, lambda d: read_params(d, "cond_cache_enabled", True)), + (cond_cache_max, lambda d: read_params(d, "cond_cache_max", 64)), + (vae_tiling_enabled, lambda d: read_params(d, "vae_tiling_enabled", False)), + (seamless_tiling_enabled, lambda d: read_params(d, "seamless_tiling_enabled", False)), + (tile_overlap, lambda d: read_params(d, "tile_overlap", 12)), + (lora_weight_first_factor, lambda d: read_params(d, "lora_weight_first_factor", 1.0)), + (lora_weight_second_factor, lambda d: read_params(d, "lora_weight_second_factor", 1.0)), + (match_colors_preset, lambda d: read_params(d, "match_colors_preset", "Off")), + (match_colors_enabled, lambda d: read_params(d, "match_colors_enabled", False)), + (match_colors_strength, lambda d: read_params(d, "match_colors_strength", 0.5)), + (postfx_preset, lambda d: read_params(d, "postfx_preset", "Off")), + (clahe_enabled, lambda d: read_params(d, "clahe_enabled", False)), + (clahe_clip, lambda d: read_params(d, "clahe_clip", 2.0)), + (clahe_tile_grid, lambda d: read_params(d, "clahe_tile_grid", 8)), + (unsharp_enabled, lambda d: read_params(d, "unsharp_enabled", False)), + (unsharp_radius, lambda d: read_params(d, "unsharp_radius", 1.5)), + (unsharp_amount, lambda d: read_params(d, "unsharp_amount", 0.75)), + (unsharp_threshold, lambda d: read_params(d, "unsharp_threshold", 0)), + (cn_ref, lambda d: read_params(d, "cn_ref", False)), + (start_control_at, lambda d: read_params(d, "start_control_at", 0.0)), + (cn_proc_res_cap, lambda d: read_params(d, "cn_proc_res_cap", 1024)), + # final upscale + (final_upscale_enable, lambda d: read_params(d, "final_upscale_enable", False)), + (final_upscaler, lambda d: read_params(d, "final_upscaler", "R-ESRGAN 4x+")), + (final_scale, lambda d: read_params(d, "final_scale", 4.0)), # NEW + (final_tile, lambda d: read_params(d, "final_tile", 512)), + (final_tile_overlap, lambda d: read_params(d, "final_tile_overlap", 16)), + # NEW + (deep_shrink_enable, lambda d: read_params(d, "deep_shrink_enable", False)), + (deep_shrink_strength, lambda d: read_params(d, "deep_shrink_strength", 0.5)), + (sdxl_mode, lambda d: read_params(d, "sdxl_mode", False)), + (sdxl_denoise_boost, lambda d: read_params(d, "sdxl_denoise_boost", 0.1)), + (first_latent_invert, lambda d: read_params(d, "first_latent_invert", False)), + (second_custom_size_enable, lambda d: read_params(d, "second_custom_size_enable", False)), + (second_width, lambda d: read_params(d, "second_width", 0)), + (second_height, lambda d: read_params(d, "second_height", 0)), + + ] + + return [ + enable, quick_preset, + ratio, width, height, long_edge, + steps_first, steps_second, denoise_first, denoise_second, + first_upscaler, second_upscaler, first_latent, second_latent, + latent_resample_mode, + prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append, + strength, filter_mode, filter_offset, denoise_offset, adaptive_sigma_enable, + noise_schedule_mode, + sampler_first, sampler_second, scheduler_first, scheduler_second, + restore_scheduler_after, + cfg, cfg_second_pass_boost, cfg_second_pass_delta, + reuse_seed_noise, mp_target_enabled, mp_target, + cond_cache_enabled, cond_cache_max, + vae_tiling_enabled, + seamless_tiling_enabled, tile_overlap, + lora_weight_first_factor, lora_weight_second_factor, + match_colors_preset, match_colors_enabled, match_colors_strength, + postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid, + unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold, + cn_ref, start_control_at, cn_proc_res_cap, + # final upscale + final_upscale_enable, final_upscaler, final_scale, final_tile, final_tile_overlap, + # NEW (UI → сигнатура, добавлены элементы ниже) + deep_shrink_enable, deep_shrink_strength, + sdxl_mode, sdxl_denoise_boost, + # существующие NEW + first_latent_invert, + second_custom_size_enable, second_width, second_height + ] + + # Capture base processing object and optional ControlNet state + def process(self, p, *args, **kwargs): + self.p = p + # Запомним исходные W/H, если есть + self._orig_size = (getattr(p, "width", None), getattr(p, "height", None)) + self._cn_units = [] + self._use_cn = False + self._first_noise = None + self._first_noise_shape = None + self._saved_seeds = None + self._saved_subseeds = None + self._saved_subseed_strength = None + self._saved_seed_resize_from_h = None + self._saved_seed_resize_from_w = None + self._override_prompt_second = None + + # Try detect ControlNet (best-effort; path may vary across installs) + ext_candidates = [ + "extensions.sd_webui_controlnet.scripts.external_code", + "extensions.sd-webui-controlnet.scripts.external_code", + "extensions-builtin.sd-webui-controlnet.scripts.external_code", + ] + self._cn_ext = None + for mod in ext_candidates: + try: + self._cn_ext = __import__(mod, fromlist=["external_code"]) + break + except Exception: + continue + if self._cn_ext: + try: + units = self._cn_ext.get_all_units_in_processing(p) + self._cn_units = list(units) if units else [] + self._use_cn = len(self._cn_units) > 0 + except Exception: + self._use_cn = False + + # Log settings into PNG-info (single JSON block) + def before_process_batch(self, p, *args, **kwargs): + if not bool(self.config.get("enable", False)): + return + # сохраняем информ-блок (json64, чтобы не ломать кавычки) + p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p) + + def create_infotext(self, p, *args, **kwargs): + scale_val = 0 + if int(self.config.get("width", 0)) and int(self.config.get("height", 0)): + scale_val = f"{int(self.config.get('width'))}x{int(self.config.get('height'))}" + elif float(self.config.get("ratio", 0)): + scale_val = float(self.config.get("ratio")) + + payload = { + "scale": scale_val, + "ratio": float(self.config.get("ratio", 0.0)), + "width": int(self.config.get("width", 0) or 0), + "height": int(self.config.get("height", 0) or 0), + "long_edge": int(self.config.get("long_edge", 0)), + # NEW + "second_custom_size_enable": bool(self.config.get("second_custom_size_enable", False)), + "second_width": int(self.config.get("second_width", 0)), + "second_height": int(self.config.get("second_height", 0)), + "denoise_first": float(self.config.get("denoise_first", 0.33)), + "denoise_second": float(self.config.get("denoise_second", 0.45)), + + "steps_first": int(self.config.get("steps_first", int(self.config.get("steps", 20)))), + "steps_second": int(self.config.get("steps_second", int(self.config.get("steps", 20)))), + "steps": int(self.config.get("steps", int(self.config.get("steps_first", 20)))), + "first_upscaler": self.config.get("first_upscaler", ""), + "second_upscaler": self.config.get("second_upscaler", ""), + "first_latent": float(self.config.get("first_latent", 0.3)), + "first_latent_invert": bool(self.config.get("first_latent_invert", False)), # NEW + "second_latent": float(self.config.get("second_latent", 0.1)), + "prompt": self.config.get("prompt", ""), + "negative_prompt": self.config.get("negative_prompt", ""), + "second_pass_prompt": self.config.get("second_pass_prompt", ""), + "second_pass_prompt_append": bool(self.config.get("second_pass_prompt_append", True)), + "strength": float(self.config.get("strength", 2.0)), + "filter_mode": self.config.get("filter_mode", ""), + "filter_offset": float(self.config.get("filter_offset", 0.0)), + "denoise_offset": float(self.config.get("denoise_offset", 0.05)), + "latent_resample_mode": self.config.get("latent_resample_mode", "nearest"), + "noise_schedule_mode": self.config.get("noise_schedule_mode", "Use sampler default"), + "adaptive_sigma_enable": bool(self.config.get("adaptive_sigma_enable", False)), + "clip_skip": int(self.config.get("clip_skip", 0)), + # per-pass sampler/scheduler (include legacy for context) + "sampler_first": self.config.get("sampler_first", ""), + "sampler_second": self.config.get("sampler_second", self.config.get("sampler", "")), + "scheduler_first": self.config.get("scheduler_first", self.config.get("scheduler", "")), + "scheduler_second": self.config.get("scheduler_second", self.config.get("scheduler", "")), + "restore_scheduler_after": bool(self.config.get("restore_scheduler_after", True)), + # cfg + "cfg": float(getattr(p, "cfg_scale", self.cfg)), + "cfg_second_pass_boost": bool(self.config.get("cfg_second_pass_boost", True)), + "cfg_second_pass_delta": float(self.config.get("cfg_second_pass_delta", 3.0)), + # flags + "reuse_seed_noise": bool(self.config.get("reuse_seed_noise", False)), + "mp_target_enabled": bool(self.config.get("mp_target_enabled", False)), + "mp_target": float(self.config.get("mp_target", 2.0)), + "cond_cache_enabled": bool(self.config.get("cond_cache_enabled", True)), + "cond_cache_max": int(self.config.get("cond_cache_max", 64)), + "vae_tiling_enabled": bool(self.config.get("vae_tiling_enabled", False)), + "seamless_tiling_enabled": bool(self.config.get("seamless_tiling_enabled", False)), + "tile_overlap": int(self.config.get("tile_overlap", 12)), + "lora_weight_first_factor": float(self.config.get("lora_weight_first_factor", 1.0)), + "lora_weight_second_factor": float(self.config.get("lora_weight_second_factor", 1.0)), + "match_colors_preset": self.config.get("match_colors_preset", "Off"), + "match_colors_enabled": bool(self.config.get("match_colors_enabled", False)), + "match_colors_strength": float(self.config.get("match_colors_strength", 0.5)), + "postfx_preset": self.config.get("postfx_preset", "Off"), + "clahe_enabled": bool(self.config.get("clahe_enabled", False)), + "clahe_clip": float(self.config.get("clahe_clip", 2.0)), + "clahe_tile_grid": int(self.config.get("clahe_tile_grid", 8)), + "unsharp_enabled": bool(self.config.get("unsharp_enabled", False)), + "unsharp_radius": float(self.config.get("unsharp_radius", 1.5)), + "unsharp_amount": float(self.config.get("unsharp_amount", 0.75)), + "unsharp_threshold": int(self.config.get("unsharp_threshold", 0)), + "cn_ref": bool(self.config.get("cn_ref", False)), + "start_control_at": float(self.config.get("start_control_at", 0.0)), + "cn_proc_res_cap": int(self.config.get("cn_proc_res_cap", 1024)), + # final upscale + "final_upscale_enable": bool(self.config.get("final_upscale_enable", False)), + "final_upscaler": self.config.get("final_upscaler", "R-ESRGAN 4x+"), + "final_scale": float(self.config.get("final_scale", 4.0)), + "final_tile": int(self.config.get("final_tile", 512)), + "final_tile_overlap": int(self.config.get("final_tile_overlap", 16)), + # NEW: anti-twinning & SDXL + "deep_shrink_enable": bool(self.config.get("deep_shrink_enable", False)), + "deep_shrink_strength": float(self.config.get("deep_shrink_strength", 0.5)), + "sdxl_mode": bool(self.config.get("sdxl_mode", False)), + "sdxl_denoise_boost": float(self.config.get("sdxl_denoise_boost", 0.1)), + } + # кодируем надёжно — в base64 + payload_str = json.dumps(payload, ensure_ascii=False) + return "json64:" + base64.b64encode(payload_str.encode("utf-8")).decode("ascii") + + # --- Main postprocess hook --- + def postprocess_image(self, p, pp, + enable, quick_preset, + ratio, width, height, long_edge, + steps_first, steps_second, denoise_first, denoise_second, + first_upscaler, second_upscaler, first_latent, second_latent, + latent_resample_mode, + prompt, negative_prompt, second_pass_prompt, second_pass_prompt_append, + strength, filter_mode, filter_offset, denoise_offset, adaptive_sigma_enable, + noise_schedule_mode, + sampler_first, sampler_second, scheduler_first, scheduler_second, + restore_scheduler_after, + cfg, cfg_second_pass_boost, cfg_second_pass_delta, + reuse_seed_noise, mp_target_enabled, mp_target, + cond_cache_enabled, cond_cache_max, + vae_tiling_enabled, + seamless_tiling_enabled, tile_overlap, + lora_weight_first_factor, lora_weight_second_factor, + match_colors_preset, match_colors_enabled, match_colors_strength, + postfx_preset, clahe_enabled, clahe_clip, clahe_tile_grid, + unsharp_enabled, unsharp_radius, unsharp_amount, unsharp_threshold, + cn_ref, start_control_at, cn_proc_res_cap, + final_upscale_enable, final_upscaler, final_scale, final_tile, final_tile_overlap, + # NEW из UI + deep_shrink_enable, deep_shrink_strength, + sdxl_mode, sdxl_denoise_boost, + # NEW ↓ + first_latent_invert, + second_custom_size_enable, second_width, second_height): + if not enable: + return + + # Save config chosen in UI + self.pp = pp + self.config["enable"] = bool(enable) + self.config["ratio"] = float(ratio) + self.config["width"] = int(width) + self.config["height"] = int(height) + self.config["long_edge"] = int(long_edge) + self.config["steps_first"] = int(steps_first) + self.config["steps_second"] = int(steps_second) + self.config["denoise_first"] = float(denoise_first) + self.config["denoise_second"] = float(denoise_second) + self.config["steps"] = int(steps_second) # legacy aggregate + self.config["first_upscaler"] = first_upscaler + self.config["second_upscaler"] = second_upscaler + self.config["first_latent"] = float(first_latent) + self.config["second_latent"] = float(second_latent) + self.config["prompt"] = prompt.strip() + self.config["negative_prompt"] = negative_prompt.strip() + self.config["second_pass_prompt"] = second_pass_prompt.strip() + self.config["second_pass_prompt_append"] = bool(second_pass_prompt_append) + self.config["strength"] = float(strength) + self.config["filter_mode"] = filter_mode + self.config["filter_offset"] = float(filter_offset) + self.config["denoise_offset"] = float(denoise_offset) + self.config["latent_resample_mode"] = str(latent_resample_mode) + self.config["noise_schedule_mode"] = str(noise_schedule_mode) + self.config["adaptive_sigma_enable"] = bool(adaptive_sigma_enable) + # per-pass sampler/scheduler + self.config["sampler_first"] = sampler_first + self.config["sampler_second"] = sampler_second + self.config["scheduler_first"] = scheduler_first + self.config["scheduler_second"] = scheduler_second + self.config["restore_scheduler_after"] = bool(restore_scheduler_after) + # cfg/delta + self.config["cfg"] = float(cfg) + self.config["cfg_second_pass_boost"] = bool(cfg_second_pass_boost) + self.config["cfg_second_pass_delta"] = float(cfg_second_pass_delta) + # flags & extras + self.config["reuse_seed_noise"] = bool(reuse_seed_noise) + self.config["mp_target_enabled"] = bool(mp_target_enabled) + self.config["mp_target"] = float(mp_target) + self.config["cond_cache_enabled"] = bool(cond_cache_enabled) + self.config["cond_cache_max"] = int(cond_cache_max) + self.config["vae_tiling_enabled"] = bool(vae_tiling_enabled) + self.config["seamless_tiling_enabled"] = bool(seamless_tiling_enabled) + self.config["tile_overlap"] = int(tile_overlap) + self.config["lora_weight_first_factor"] = float(lora_weight_first_factor) + self.config["lora_weight_second_factor"] = float(lora_weight_second_factor) + self.config["match_colors_preset"] = match_colors_preset + self.config["match_colors_enabled"] = bool(match_colors_enabled) + self.config["match_colors_strength"] = float(match_colors_strength) + self.config["postfx_preset"] = postfx_preset + self.config["clahe_enabled"] = bool(clahe_enabled) + self.config["clahe_clip"] = float(clahe_clip) + self.config["clahe_tile_grid"] = int(clahe_tile_grid) + self.config["unsharp_enabled"] = bool(unsharp_enabled) + self.config["unsharp_radius"] = float(unsharp_radius) + self.config["unsharp_amount"] = float(unsharp_amount) + self.config["unsharp_threshold"] = int(unsharp_threshold) + self.config["cn_ref"] = bool(cn_ref) + self.config["start_control_at"] = float(start_control_at) + self.config["cn_proc_res_cap"] = int(cn_proc_res_cap) + # final upscale + self.config["final_upscale_enable"] = bool(final_upscale_enable) + self.config["final_upscaler"] = final_upscaler + self.config["final_scale"] = float(final_scale) + self.config["final_tile"] = int(final_tile) + self.config["final_tile_overlap"] = int(final_tile_overlap) + # NEW + self.config["first_latent_invert"] = bool(first_latent_invert) + # NEW (anti-twinning & SDXL) + self.config["deep_shrink_enable"] = bool(deep_shrink_enable) + self.config["deep_shrink_strength"] = float(deep_shrink_strength) + self.config["sdxl_mode"] = bool(sdxl_mode) + self.config["sdxl_denoise_boost"] = float(sdxl_denoise_boost) + + self.config["second_custom_size_enable"] = bool(second_custom_size_enable) + self.config["second_width"] = int(second_width) + self.config["second_height"] = int(second_height) + self.cfg = float(cfg) if cfg else float(p.cfg_scale) + # Обновить PNG-info уже с актуальным self.config + p.extra_generation_params["Custom Hires Fix"] = self.create_infotext(p) + + # Validate sizing: + # Если MP target выключен и long_edge=0 — строго ДВА режима: + # 1) точные W×H (ratio обязан быть 0), + # 2) или чистый ratio>0 при width=height=0. + if (not self.config["mp_target_enabled"] + and int(self.config.get("long_edge", 0)) == 0 + and not bool(self.config.get("second_custom_size_enable", False))): + ok = ( + (int(width) > 0 and int(height) > 0 and float(ratio) == 0.0) + or + (int(width) == 0 and int(height) == 0 and float(ratio) > 0.0) + ) + if not ok: + Err = getattr(gr, "Error", RuntimeError) + raise Err("Strict sizing: set both width & height (ratio must be 0) OR set ratio>0 with width=height=0.") + + # Track extras activated during conditioning + self._activated_extras = [] + + # Preserve original batch size + self._orig_batch_size = getattr(self.p, 'batch_size', None) + + # Apply CLIP skip for the run + self._orig_clip_skip = shared.opts.CLIP_stop_at_last_layers + if int(self.config.get("clip_skip", 0)) > 0: + shared.opts.CLIP_stop_at_last_layers = int(self.config.get("clip_skip", 0)) + + # Toggle VAE tiling for the run + self._set_vae_tiling(self.config["vae_tiling_enabled"]) + + # Toggle seamless tiling + # Save original scheduler (before first/second pass may change it) + self._orig_scheduler = getattr(self.p, "scheduler", None) + + self._orig_tiling = getattr(self.p, "tiling", None) + self._orig_tile_overlap = getattr(self.p, "tile_overlap", None) + if bool(self.config.get("seamless_tiling_enabled", False)): + try: + self.p.tiling = True + if hasattr(self.p, "tile_overlap"): + self.p.tile_overlap = int(self.config.get("tile_overlap", 12)) + except Exception: + pass + try: + with devices.autocast(): + shared.state.nextjob() + x = self._first_pass(pp.image) + shared.state.nextjob() + x = self._second_pass(x) + # Final ×4 upscale (optional) + if bool(self.config.get("final_upscale_enable", False)): + scale_val = float(self.config.get("final_scale", 4.0)) + if scale_val > 1.0: + x = self._final_upscale_tiled(x, scale_val) # NEW + # ВАЖНО: пробрасываем результат обратно в WebUI + pp.image = x + + self._apply_token_merging(for_hr=False) + # Post-FX chain is inside _second_pass; final upscale is pure upscaler + # сохранить актуальный конфиг на диск + self._save_config() + finally: + # Restore options + shared.opts.CLIP_stop_at_last_layers = self._orig_clip_skip + self._restore_vae_tiling() + # Restore scheduler if requested (independent of tiling) + try: + if bool(self.config.get("restore_scheduler_after", True)) and getattr(self, "_orig_scheduler", None) is not None: + self.p.scheduler = self._orig_scheduler + except Exception: + pass + finally: + self._orig_scheduler = None + + # Restore tiling if it existed + if self._orig_tiling is not None: + try: + self.p.tiling = self._orig_tiling + if hasattr(self.p, "tile_overlap") and self._orig_tile_overlap is not None: + self.p.tile_overlap = self._orig_tile_overlap + except Exception: + pass + try: + if getattr(self, "_orig_batch_size", None) is not None: + self.p.batch_size = self._orig_batch_size + except Exception: + pass + try: + # Deactivate any extras we activated during conditioning + for _extra in getattr(self, "_activated_extras", []) or []: + try: + extra_networks.deactivate(self.p, _extra) + except Exception: + pass + finally: + self._activated_extras = [] + try: + # Сбросить override сигм для последующих шагов + if hasattr(self.p, "sampler_noise_scheduler_override"): + self.p.sampler_noise_scheduler_override = None + except Exception: + pass + # Восстановить исходные размеры после возможного _enable_controlnet + try: + ow, oh = getattr(self, "_orig_size", (None, None)) + if ow is not None: + self.p.width = ow + if oh is not None: + self.p.height = oh + except Exception: + pass + self._orig_size = (None, None) + + # ---- Helpers ---- + def _save_config(self): + """Сохранение конфигурации в config.yaml (best-effort).""" + try: + from omegaconf import OmegaConf + OmegaConf.save(self.config, str(config_path)) + except Exception: + pass + + def _vae_down_factor(self) -> int: + """Возвращает фактический коэффициент даунсемпла VAE (по умолчанию 8).""" + try: + f = getattr(getattr(shared.sd_model, "first_stage_model", None), "downsample_factor", None) + return int(f) if f else 8 + except Exception: + return 8 + + # ---- Helpers ---- + def _maybe_mp_resize(self, base_w, base_h, target_mp: float): + """Compute size from megapixels while keeping aspect ratio; quantize to multiple of 8.""" + aspect = base_w / base_h if base_h else 1.0 + total_px = max(0.01, target_mp) * 1_000_000.0 + w_float = math.sqrt(total_px * aspect) + h_float = w_float / aspect + w = max(8, int(round(w_float / 8) * 8)) + h = max(8, int(round(h_float / 8) * 8)) + return w, h + + def _compute_denoise(self, base_key: str) -> float: + """ + Returns clamped denoising strength in [0,1] as (config[base_key] + denoise_offset). + Keeps backwards-compatibility with the previous "denoise_offset" knob. + """ + try: + base = float(self.config.get(base_key, 0.5)) + except Exception: + base = 0.5 + try: + off = float(self.config.get("denoise_offset", 0.0)) + except Exception: + off = 0.0 + val = max(0.0, min(1.0, base + off)) + return val + + # ───────────── NEW: SDXL/SD3 detection & denoise boost ───────────── + def _detect_is_sdxl(self) -> bool: + """Best-effort детект SDXL/SD3 для разных форков WebUI.""" + try: + m = shared.sd_model + if bool(getattr(m, "is_sdxl", False)) or bool(getattr(m, "is_sd3", False)): + return True + t = str(getattr(m, "model_type", "") or getattr(m, "modelname", "") or "").lower() + if any(k in t for k in ("sdxl", "sd3", "xl-")): + return True + # У SDXL часто dual-CLIP (clip_l/clip_g) в cond_stage_model + c = getattr(m, "cond_stage_model", None) + if c is not None and (hasattr(c, "clip_l") and hasattr(c, "clip_g")): + return True + except Exception: + pass + return False + + def _maybe_apply_sdxl_denoise_boost(self): + """ + Мягко увеличивает denoising_strength для SDXL/SD3 (по желанию). + Контролируется чекбоксом в UI; безопасно для SD1.5/SD2.x. + """ + try: + if bool(self.config.get("sdxl_mode", False)): + boost = float(self.config.get("sdxl_denoise_boost", 0.1)) + self.p.denoising_strength = float(min(1.0, max(0.0, self.p.denoising_strength + boost))) + except Exception: + pass + + + def _model_hash_for_cache(self): + # best-effort model hash + однократный детект SDXL/SD3 + try: + h = getattr(shared.sd_model, "sd_model_hash", None) or getattr(shared.sd_model, "hash", None) + self.is_sdxl = self._detect_is_sdxl() + return h or str(id(shared.sd_model)) + except Exception: + return str(id(shared.sd_model)) + + def _cond_key(self, width, height, steps_for_cond, prompt: str, negative: str, clip_skip: int): + h = hashlib.sha256() + h.update((prompt or "").encode("utf-8")) + h.update(b"::") + h.update((negative or "").encode("utf-8")) + # NEW: учитываем семейство модели (SDXL/SD3) в ключе кэша + xl_flag = int(bool(self.config.get("sdxl_mode", False)) or self.is_sdxl) + key = f"{self._model_hash_for_cache()}|xl={xl_flag}|{width}x{height}|{steps_for_cond}|cs={clip_skip}|{h.hexdigest()}" + return key + + def _cond_cache_get(self, key: str): + if not bool(self.config.get("cond_cache_enabled", True)): + return None + item = self._cond_cache.get(key) + if item is not None: + self._cond_cache.move_to_end(key) + return item + + def _cond_cache_put(self, key: str, value: tuple): + if not bool(self.config.get("cond_cache_enabled", True)): + return + self._cond_cache[key] = value + self._cond_cache.move_to_end(key) + max_items = int(self.config.get("cond_cache_max", 64)) + while len(self._cond_cache) > max_items: + self._cond_cache.popitem(last=False) + + def _set_vae_tiling(self, enabled: bool): + # Save original state if we have not yet + if self._orig_opt_vae_tiling is None and hasattr(shared.opts, "sd_vae_tiling"): + self._orig_opt_vae_tiling = bool(shared.opts.sd_vae_tiling) + # Toggle option + if hasattr(shared.opts, "sd_vae_tiling"): + shared.opts.sd_vae_tiling = bool(enabled) + # Try model-level toggle + vae = getattr(shared.sd_model, "first_stage_model", None) + if vae is not None: + try: + if enabled and hasattr(vae, "enable_tiling"): + vae.enable_tiling() + if not enabled and hasattr(vae, "disable_tiling"): + vae.disable_tiling() + except Exception: + pass + + def _restore_vae_tiling(self): + if self._orig_opt_vae_tiling is not None and hasattr(shared.opts, "sd_vae_tiling"): + shared.opts.sd_vae_tiling = self._orig_opt_vae_tiling + vae = getattr(shared.sd_model, "first_stage_model", None) + if vae is not None: + try: + if self._orig_opt_vae_tiling and hasattr(vae, "enable_tiling"): + vae.enable_tiling() + elif not self._orig_opt_vae_tiling and hasattr(vae, "disable_tiling"): + vae.disable_tiling() + except Exception: + pass + self._orig_opt_vae_tiling = None + + def _scale_lora_in_prompt(self, text: str, factor: float) -> str: + # Multiply existing , or append weight if missing + # Very lightweight parser to keep prompt untouched otherwise + if factor is None or abs(factor - 1.0) < 1e-6: + return text + out = [] + i = 0 + while i < len(text): + start = text.find("", start) + if end == -1: + out.append(text[start:]) + break + token = text[start:end+1] + parts = token[1:-1].split(":") # lora,name,weight? + if len(parts) >= 2 and parts[0] == "lora": + name = parts[1] + if len(parts) >= 3: + try: + w = float(parts[2]) + except Exception: + w = 1.0 + new_w = max(0.0, w * factor) + new_token = f"" + else: + new_token = f"" + out.append(new_token) + else: + out.append(token) + i = end + 1 + return "".join(out) + + def _prepare_conditioning(self, width, height, steps_for_cond: int, prompt_override: str = None): + """Build (cond, uncond) with optional LRU caching and LoRA scaling.""" + base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip() + negative_base = self.config.get("negative_prompt", "").strip() or (getattr(self.p, "negative_prompt", "") or "").strip() + + if prompt_override: + base_prompt = prompt_override.strip() + + # Apply LoRA scaling for this pass + scaled_prompt = self._scale_lora_in_prompt(base_prompt, self._current_lora_factor) + + clip_skip = int(self.config.get("clip_skip", 0)) + + # Cache lookup + cache_key = self._cond_key(width, height, steps_for_cond, scaled_prompt, negative_base, clip_skip) + cached = self._cond_cache_get(cache_key) + if cached is not None: + self.cond, self.uncond = cached + return + + # Parse extra networks and build cond + prompt_text = scaled_prompt + if not getattr(self.p, "disable_extra_networks", False): + try: + prompt_text, extra = extra_networks.parse_prompt(prompt_text) + if extra: + extra_networks.activate(self.p, extra) + try: + self._activated_extras.append(extra) + except Exception: + pass + except Exception: + pass + + if width and height and hasattr(prompt_parser, "SdConditioning"): + c = prompt_parser.SdConditioning([prompt_text], False, width, height) + uc = prompt_parser.SdConditioning([negative_base], False, width, height) + else: + c, uc = [prompt_text], [negative_base] + + cond = prompt_parser.get_multicond_learned_conditioning(shared.sd_model, c, steps_for_cond) + uncond = prompt_parser.get_learned_conditioning(shared.sd_model, uc, steps_for_cond) + self.cond, self.uncond = cond, uncond + + # Store in cache + self._cond_cache_put(cache_key, (cond, uncond)) + + def _to_sample(self, x_img: Image.Image): + image = np.array(x_img).astype(np.float32) / 255.0 + image = np.moveaxis(image, 2, 0) + decoded = torch.from_numpy(image).to(shared.device).to(devices.dtype_vae) + decoded = 2.0 * decoded - 1.0 + encoded = shared.sd_model.encode_first_stage(decoded.unsqueeze(0).to(devices.dtype_vae)) + sample = shared.sd_model.get_first_stage_encoding(encoded) + return decoded, sample + + def _create_sampler(self, sampler_name: str): + # Поддержка синтаксиса "Restart + " + if isinstance(sampler_name, str) and sampler_name.startswith("Restart + "): + inner = sampler_name.replace("Restart + ", "", 1).strip() + try: + s = sd_samplers.create_sampler("Restart", shared.sd_model) + # если у Restart есть поле для внутреннего сэмплера — зададим + try: + setattr(s, "inner_sampler_name", inner) + except Exception: + pass + return s + except Exception: + # fallback: напрямую создать указанный «внутренний» сэмплер + try: + return sd_samplers.create_sampler(inner, shared.sd_model) + except Exception: + return sd_samplers.create_sampler("DPM++ 2M Karras", shared.sd_model) + return sd_samplers.create_sampler(sampler_name, shared.sd_model) + + def _apply_clahe(self, img: Image.Image) -> Image.Image: + if not bool(self.config.get("clahe_enabled", False)): + return img + np_img = np.array(img) + if _CV2_OK: + lab = cv2.cvtColor(np_img, cv2.COLOR_RGB2LAB) + l, a, b = cv2.split(lab) + clip = float(self.config.get("clahe_clip", 2.0)) + tiles = int(self.config.get("clahe_tile_grid", 8)) + clahe = cv2.createCLAHE(clipLimit=max(0.1, clip), tileGridSize=(tiles, tiles)) + l2 = clahe.apply(l) + lab2 = cv2.merge((l2, a, b)) + rgb = cv2.cvtColor(lab2, cv2.COLOR_LAB2RGB) + return Image.fromarray(rgb) + if _SKIMAGE_OK: + # skimage fallback on L channel in LAB + lab = skcolor.rgb2lab(np_img / 255.0) + l = lab[..., 0] / 100.0 + # skimage expects clip_limit ~= [0..1]; map UI [1..5] -> [0.005..0.1] + clip = float(self.config.get("clahe_clip", 2.0)) + tiles = int(self.config.get("clahe_tile_grid", 8)) + clip_ski = max(0.005, min(0.1, clip / 20.0)) + l2 = equalize_adapthist(l, clip_limit=clip_ski, kernel_size=(tiles, tiles)) + lab[..., 0] = np.clip(l2 * 100.0, 0, 100.0) + rgb = skcolor.lab2rgb(lab) + rgb8 = np.clip(rgb * 255.0, 0, 255).astype(np.uint8) + return Image.fromarray(rgb8) + # No-op fallback + return img + + def _apply_unsharp(self, img: Image.Image) -> Image.Image: + if not bool(self.config.get("unsharp_enabled", False)): + return img + radius = float(self.config.get("unsharp_radius", 1.5)) + amount = float(self.config.get("unsharp_amount", 0.75)) + threshold = int(self.config.get("unsharp_threshold", 0)) + return img.filter(ImageFilter.UnsharpMask(radius=radius, percent=int(amount * 100), threshold=threshold)) + + def _apply_match_colors(self, img: Image.Image, ref: Image.Image) -> Image.Image: + if not bool(self.config.get("match_colors_enabled", False)): + return img + strength = float(self.config.get("match_colors_strength", 0.5)) + strength = max(0.0, min(1.0, strength)) + if strength <= 0.0: + return img + + arr = np.array(img).astype(np.float32) + ref_arr = np.array(ref).astype(np.float32) + + matched = None + if _SKIMAGE_OK: + try: + matched = match_histograms(arr, ref_arr, channel_axis=-1).astype(np.float32) + except TypeError: + # older skimage + matched = match_histograms(arr, ref_arr, multichannel=True).astype(np.float32) + else: + # simple mean-std per channel fallback + eps = 1e-6 + for c in range(arr.shape[2]): + src = arr[..., c] + dst = ref_arr[..., c] + src_m, src_s = src.mean(), src.std() + eps + dst_m, dst_s = dst.mean(), dst.std() + eps + arr[..., c] = np.clip((src - src_m) * (dst_s / src_s) + dst_m, 0, 255) + matched = arr + + out = (1.0 - strength) * arr + strength * matched + out = np.clip(out, 0, 255).astype(np.uint8) + return Image.fromarray(out) + + # ----- Sigma schedule builder (multiple modes) ----- + def _build_sigma_override(self, which_pass: str, schedule_mode: str): + """ + Возвращает callable: f(n_steps) -> Tensor(sigmas) ИЛИ None (оставить дефолт сэмплера). + which_pass: "first" | "second" + schedule_mode: "Use sampler default" | "Adaptive (filter/strength)" | fixed family. + """ + # 0) Ничего не переопределяем + if not schedule_mode or schedule_mode == "Use sampler default": + return None + + # 1) Адаптивная форма polyexponential (с учётом filter_mode/strength) + def _adaptive_builder(): + if which_pass == "first": + base_min, base_max, base_rho = 0.005, 20.0, 0.6 + else: + base_min, base_max, base_rho = 0.01, 15.0, 0.5 + + # Если adaptive выключен — используем фикс. базу, как раньше + if not bool(self.config.get("adaptive_sigma_enable", False)): + def _no_adapt(n): + return K.sampling.get_sigmas_polyexponential(n, base_min, base_max, base_rho, devices.device) + return _no_adapt + + raw_strength = float(self.config.get("strength", 2.0)) + s = (raw_strength - 0.5) / 3.5 + s = max(0.0, min(1.0, s)) + + filt = self.config.get("filter_mode", "Noise sync (sharp)") or "Noise sync (sharp)" + f_off = float(self.config.get("filter_offset", 0.0)) # -1..1 мягкий сдвиг + + if "Morphological" in filt: + sigma_min = base_min * (1.0 + 0.5 * s + 0.25 * f_off) + sigma_max = base_max * (1.0 - 0.2 * s) + rho = base_rho - 0.2 * s + elif "Combined" in filt: + sigma_min = base_min * (1.0 + 0.20 * (0.5 - s) + 0.10 * f_off) + sigma_max = base_max + rho = base_rho + else: + sigma_min = base_min * (1.0 - 0.5 * s - 0.25 * f_off) + sigma_max = base_max * (1.0 + 0.10 * s) + rho = base_rho + 0.20 * s + + sigma_min = max(1e-4, sigma_min) + sigma_max = max(sigma_min * 1.01, sigma_max) + rho = max(0.1, min(1.5, rho)) + + def _f(n): + return K.sampling.get_sigmas_polyexponential(n, sigma_min, sigma_max, rho, devices.device) + return _f + + if schedule_mode == "Adaptive (filter/strength)": + return _adaptive_builder() + + # 2) Фиксированные режимы (через k-diffusion external wrapper) + try: + quantize = bool(getattr(shared.opts, "enable_quantization", False)) + if getattr(shared.sd_model, "parameterization", "eps") == "v": + denoiser = K.external.CompVisVDenoiser + else: + denoiser = K.external.CompVisDenoiser + model_wrap = denoiser(shared.sd_model, quantize=quantize) + + def _simple(n): + # простая выборка из доступных сигм модели + sigmas_all = model_wrap.sigmas + step = max(1, int(len(sigmas_all) / max(1, n))) + picked = [float(sigmas_all[-(1 + i * step)].item()) for i in range(n)] + return torch.tensor(picked + [0.0], dtype=torch.float32, device=devices.device) + + def _ddim_uniform(n): + # Униформные DDIM-тиктаймы -> сигмы + try: + num_ddpm = model_wrap.inner_model.inner_model.num_timesteps + except Exception: + num_ddpm = 1000 + c = max(1, num_ddpm // max(1, n)) + ddim_ts = np.asarray(list(range(0, num_ddpm, c))) + steps_out = (ddim_ts + 1)[::-1] + sigs = [] + for ts in steps_out: + ts = min(int(ts), 999) + sigs.append(model_wrap.t_to_sigma(torch.tensor(ts, device=devices.device))) + sigs.append(torch.tensor(0.0, device=devices.device)) + return torch.stack([s if torch.is_tensor(s) else torch.tensor(float(s), device=devices.device) for s in sigs]).float() + + use_old = bool(getattr(shared.opts, "use_old_karras_scheduler_sigmas", False)) + if use_old: + sigma_min, sigma_max = (0.1, 10.0) + else: + sigma_min = float(model_wrap.sigmas[0].item()) + sigma_max = float(model_wrap.sigmas[-1].item()) + + def _karras(n): + return K.sampling.get_sigmas_karras(n=n, sigma_min=sigma_min, sigma_max=sigma_max, device=devices.device) + + def _exp(n): + return K.sampling.get_sigmas_exponential(n=n, sigma_min=sigma_min, sigma_max=sigma_max, device=devices.device) + + def _poly(n): + rho = 0.5 + return K.sampling.get_sigmas_polyexponential(n=n, sigma_min=sigma_min, sigma_max=sigma_max, rho=rho, device=devices.device) + + def _normal(n): + return model_wrap.get_sigmas(n).to(devices.device) + + table = { + "Karras": _karras, + "Exponential": _exp, + "Polyexponential": _poly, + "Normal": _normal, + "Simple": _simple, + "DDIM uniform": _ddim_uniform, + } + fn = table.get(schedule_mode, None) + return fn or _adaptive_builder() + except Exception: + # мягкий откат к адаптивной логике + return _adaptive_builder() + + def _first_pass(self, x: Image.Image) -> Image.Image: + # Determine target size + if bool(self.config.get("mp_target_enabled", False)): + w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0))) + else: + aspect = x.width / x.height if x.height else 1.0 + + le = int(self.config.get("long_edge", 0)) + if le > 0: + if x.width >= x.height: + w = int(max(8, round(le / 8) * 8)) + h = int(max(8, round((le / aspect) / 8) * 8)) + else: + h = int(max(8, round(le / 8) * 8)) + w = int(max(8, round((le * aspect) / 8) * 8)) + elif int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and float(self.config.get("ratio", 0)) > 0: + w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8)) + h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8)) + else: + if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0: + w, h = int(self.config["width"]), int(self.config["height"]) + else: + # Fallback (не должен сработать при строгой валидации; оставлен для совместимости) + w, h = x.width, x.height + + self.width, self.height = w, h + + self._apply_token_merging(for_hr=True, halve=True) + + # Per-pass scheduler + sched_first = self.config.get("scheduler_first", self.config.get("scheduler", "Use same scheduler")) + self._set_scheduler_by_label(sched_first) + + # Optional ControlNet + if self._use_cn: + try: + cn_np = np.array(x.resize((self.width, self.height))) + self._enable_controlnet(cn_np) + except Exception: + pass + + # Build override prompt for first pass (none; base prompt) + self._current_lora_factor = float(self.config.get("lora_weight_first_factor", 1.0)) + with devices.autocast(), torch.inference_mode(): + self._prepare_conditioning(self.width, self.height, int(self.config.get("steps_first", 20))) + + # Upscale (image domain) then (optionally) blend latent + x_img = images.resize_image(RESIZE_WITH_UPSCALER, x, self.width, self.height, upscaler_name=self.config.get("first_upscaler", "R-ESRGAN 4x+")) + decoded, sample = self._to_sample(x_img) + # Латент из исходника (до апскейла) для осмысленного смешивания + _, sample_orig = self._to_sample(x) + factor = self._vae_down_factor() + x_latent = _interpolate_latent(sample_orig, + (self.height // factor, self.width // factor), + self.config.get("latent_resample_mode", "nearest")) + + # NEW: Anti-twinning latent shrink/expand (опционально) + if bool(self.config.get("deep_shrink_enable", False)): + shrink = float(self.config.get("deep_shrink_strength", 0.5)) + shrink = max(0.1, min(0.9, shrink)) + b, c, h, w = sample.shape + sh, sw = max(1, int(h * shrink)), max(1, int(w * shrink)) + with torch.no_grad(): + try: + sample = F.interpolate(sample, size=(sh, sw), mode="bicubic", align_corners=False, antialias=True) + sample = F.interpolate(sample, size=(h, w), mode="bicubic", align_corners=False, antialias=True) + except TypeError: + sample = F.interpolate(sample, size=(sh, sw), mode="bicubic", align_corners=False) + sample = F.interpolate(sample, size=(h, w), mode="bicubic", align_corners=False) + + first_latent = float(self.config.get("first_latent", 0.3)) + if 0.0 <= first_latent <= 1.0: + invert = bool(self.config.get("first_latent_invert", False)) # NEW + if invert: + # Новая семантика: слайдер = вес исходного латента, как на 2-м проходе + sample = sample * (1.0 - first_latent) + x_latent * first_latent + else: + # Старая семантика + sample = sample * first_latent + x_latent * (1.0 - first_latent) + + image_conditioning = self.p.img2img_image_conditioning(decoded, sample) + + # RNG setup + self._saved_seeds = list(getattr(self.p, "seeds", [])) or None + self._saved_subseeds = list(getattr(self.p, "subseeds", [])) or None + self._saved_subseed_strength = getattr(self.p, "subseed_strength", None) + self._saved_seed_resize_from_h = getattr(self.p, "seed_resize_from_h", None) + self._saved_seed_resize_from_w = getattr(self.p, "seed_resize_from_w", None) + + self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds, + subseed_strength=self.p.subseed_strength, + seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w) + + # Denoise config for first pass + steps = int(self.config.get("steps_first", int(self.config.get("steps", 20)))) + # NEW: LRU-кэш шума для первой стадии + try: + key_seed = (self.p.seeds[0] if getattr(self.p, "seeds", None) else getattr(self.p, "seed", None)) + except Exception: + key_seed = None + noise_key = f"first|{tuple(sample.shape)}|{key_seed}" + cached_noise = self._noise_cache.get(noise_key) + if cached_noise is not None: + noise = cached_noise.to(sample.device, dtype=sample.dtype) + else: + noise = torch.randn_like(sample) + self._noise_cache[noise_key] = noise.detach().clone() + if len(self._noise_cache) > 32: + self._noise_cache.popitem(last=False) + if bool(self.config.get("reuse_seed_noise", False)): + self._first_noise = noise.detach().clone() + self._first_noise_shape = tuple(sample.shape) + + self.p.denoising_strength = self._compute_denoise("denoise_first") + # NEW: мягкий буст денойза для SDXL/SD3 (если включено) + self._maybe_apply_sdxl_denoise_boost() + self.p.cfg_scale = float(self.cfg) + + # --- sigma schedule override (с адаптацией или без) --- + schedule_mode = self.config.get("noise_schedule_mode", "Use sampler default") + if hasattr(self.p, "sampler_noise_scheduler_override"): + self.p.sampler_noise_scheduler_override = self._build_sigma_override("first", schedule_mode) + + self.p.batch_size = 1 + + # Per-pass sampler + sampler_first = self.config.get("sampler_first", self.config.get("sampler", "DPM++ 2M Karras")) + sampler = self._create_sampler(sampler_first) + + samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond, + steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae) + + devices.torch_gc() + decoded_sample = processing.decode_first_stage(shared.sd_model, samples) + if torch.isnan(decoded_sample).any().item(): + devices.torch_gc() + samples = torch.clamp(samples, -3, 3) + decoded_sample = processing.decode_first_stage(shared.sd_model, samples) + + decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze() + x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2) + return Image.fromarray(x_np.astype(np.uint8)) + + def _second_pass(self, x: Image.Image) -> Image.Image: + # Determine target size for second pass + # NEW: приоритетный кастомный размер для 2-го прохода + w = h = 0 + if bool(self.config.get("second_custom_size_enable", False)): + sw = int(self.config.get("second_width", 0)) + sh = int(self.config.get("second_height", 0)) + if sw > 0 and sh > 0: + w = max(8, int(round(sw / 8) * 8)) + h = max(8, int(round(sh / 8) * 8)) + + if not (w and h): + # старая логика выбора размера + if bool(self.config.get("mp_target_enabled", False)): + w, h = self._maybe_mp_resize(x.width, x.height, float(self.config.get("mp_target", 2.0))) + else: + le = int(self.config.get("long_edge", 0)) + if le > 0: + aspect = x.width / x.height if x.height else 1.0 + if x.width >= x.height: + w = int(max(8, round(le / 8) * 8)) + h = int(max(8, round((le / aspect) / 8) * 8)) + else: + h = int(max(8, round(le / 8) * 8)) + w = int(max(8, round((le * aspect) / 8) * 8)) + elif (int(self.config.get("width", 0)) == 0 and int(self.config.get("height", 0)) == 0 and + float(self.config.get("ratio", 0)) > 0): + w = int(max(8, round(x.width * float(self.config["ratio"]) / 8) * 8)) + h = int(max(8, round(x.height * float(self.config["ratio"]) / 8) * 8)) + else: + if int(self.config.get("width", 0)) > 0 and int(self.config.get("height", 0)) > 0: + w, h = int(self.config["width"]), int(self.config["height"]) + else: + # Fallback (не должен сработать при строгой валидации; оставлен для совместимости) + w, h = x.width, x.height + + self._apply_token_merging(for_hr=True) + + # Per-pass scheduler + sched_second = self.config.get("scheduler_second", self.config.get("scheduler", "Use same scheduler")) + self._set_scheduler_by_label(sched_second) + + if self._use_cn: + cn_img = x if bool(self.config.get("cn_ref", False)) else self.pp.image + try: + self._enable_controlnet(np.array(cn_img.resize((w, h)))) + except Exception: + pass + + # Build override prompt for second pass + base_prompt = self.config.get("prompt", "").strip() or self.p.prompt.strip() + p2 = (self.config.get("second_pass_prompt", "") or "").strip() + if p2: + if bool(self.config.get("second_pass_prompt_append", True)): + prompt_override = (base_prompt + ", " + p2) if base_prompt else p2 + else: + prompt_override = p2 + else: + prompt_override = None + + # Apply LoRA scaling for second pass + self._current_lora_factor = float(self.config.get("lora_weight_second_factor", 1.0)) + with devices.autocast(), torch.inference_mode(): + self._prepare_conditioning(w, h, int(self.config.get("steps_second", 20)), prompt_override=prompt_override) + + # Optional latent mix + x_latent = None + second_latent = float(self.config.get("second_latent", 0.1)) + if second_latent > 0: + _, sample_from_img = self._to_sample(x) + factor = self._vae_down_factor() + x_latent = _interpolate_latent(sample_from_img, + (h // factor, w // factor), + self.config.get("latent_resample_mode", "nearest")) + + # Upscale to target and encode + if second_latent < 1.0: + x_up = images.resize_image(RESIZE_WITH_UPSCALER, x, w, h, upscaler_name=self.config.get("second_upscaler", "R-ESRGAN 4x+")) + decoded, sample = self._to_sample(x_up) + else: + decoded, sample = self._to_sample(x) + + if x_latent is not None and 0.0 <= second_latent <= 1.0: + sample = (sample * (1.0 - second_latent)) + (x_latent * second_latent) + + # Гарантируем, что decoded и sample согласованы по spatial-размеру (латент*8 пикселей) + factor = self._vae_down_factor() + tH, tW = sample.shape[-2] * factor, sample.shape[-1] * factor + if decoded.shape[-2:] != (tH, tW): + decoded = F.interpolate( + decoded.unsqueeze(0), size=(tH, tW), + mode="bilinear", align_corners=False + ).squeeze(0) + + image_conditioning = self.p.img2img_image_conditioning(decoded, sample) + + # RNG: optionally reuse seed/noise + if bool(self.config.get("reuse_seed_noise", False)) and self._saved_seeds is not None: + try: + self.p.seeds = list(self._saved_seeds) + self.p.subseeds = list(self._saved_subseeds) if self._saved_subseeds is not None else self.p.subseeds + self.p.subseed_strength = self._saved_subseed_strength if self._saved_subseed_strength is not None else self.p.subseed_strength + self.p.seed_resize_from_h = self._saved_seed_resize_from_h if self._saved_seed_resize_from_h is not None else self.p.seed_resize_from_h + self.p.seed_resize_from_w = self._saved_seed_resize_from_w if self._saved_seed_resize_from_w is not None else self.p.seed_resize_from_w + except Exception: + pass + + self.p.rng = rng.ImageRNG(sample.shape[1:], self.p.seeds, subseeds=self.p.subseeds, + subseed_strength=self.p.subseed_strength, + seed_resize_from_h=self.p.seed_resize_from_h, seed_resize_from_w=self.p.seed_resize_from_w) + + # Denoise config for second pass + steps = int(self.config.get("steps_second", int(self.config.get("steps", 20)))) + if bool(self.config.get("cfg_second_pass_boost", True)): + self.p.cfg_scale = float(self.cfg) + float(self.config.get("cfg_second_pass_delta", 3.0)) + else: + self.p.cfg_scale = float(self.cfg) + self.p.denoising_strength = self._compute_denoise("denoise_second") + # NEW: мягкий буст денойза для SDXL/SD3 (если включено) + self._maybe_apply_sdxl_denoise_boost() + + # Noise: reuse tensor if shapes match, else fresh noise + if bool(self.config.get("reuse_seed_noise", False)) and self._first_noise is not None: + if tuple(sample.shape) == tuple(self._first_noise_shape or ()): + noise = self._first_noise.to(sample.device, dtype=sample.dtype) + else: + # NEW: если размеры не совпали — попробуем кэш + try: + key_seed = (self.p.seeds[0] if getattr(self.p, "seeds", None) else getattr(self.p, "seed", None)) + except Exception: + key_seed = None + noise_key = f"second|{tuple(sample.shape)}|{key_seed}" + cached_noise = self._noise_cache.get(noise_key) + noise = cached_noise.to(sample.device, dtype=sample.dtype) if cached_noise is not None else torch.randn_like(sample) + else: + # NEW: обычный путь — используем LRU-кэш + try: + key_seed = (self.p.seeds[0] if getattr(self.p, "seeds", None) else getattr(self.p, "seed", None)) + except Exception: + key_seed = None + noise_key = f"second|{tuple(sample.shape)}|{key_seed}" + cached_noise = self._noise_cache.get(noise_key) + if cached_noise is not None: + noise = cached_noise.to(sample.device, dtype=sample.dtype) + else: + noise = torch.randn_like(sample) + # хранить в CPU-памяти + self._noise_cache[noise_key] = noise.detach().to("cpu", copy=True) + if len(self._noise_cache) > 16: + self._noise_cache.popitem(last=False) + + # --- sigma schedule override (с адаптацией или без) --- + schedule_mode = self.config.get("noise_schedule_mode", "Use sampler default") + if hasattr(self.p, "sampler_noise_scheduler_override"): + self.p.sampler_noise_scheduler_override = self._build_sigma_override("second", schedule_mode) + + self.p.batch_size = 1 + + # Per-pass sampler + sampler_second = self.config.get("sampler_second", self.config.get("sampler", "DPM++ 2M Karras")) + sampler = self._create_sampler(sampler_second) + + samples = sampler.sample_img2img(self.p, sample.to(devices.dtype), noise, self.cond, self.uncond, + steps=steps, image_conditioning=image_conditioning).to(devices.dtype_vae) + + devices.torch_gc() + decoded_sample = processing.decode_first_stage(shared.sd_model, samples) + if torch.isnan(decoded_sample).any().item(): + devices.torch_gc() + samples = torch.clamp(samples, -3, 3) + decoded_sample = processing.decode_first_stage(shared.sd_model, samples) + + decoded_sample = torch.clamp((decoded_sample + 1.0) / 2.0, min=0.0, max=1.0).squeeze() + x_np = 255.0 * np.moveaxis(decoded_sample.to(torch.float32).cpu().numpy(), 0, 2) + out_img = Image.fromarray(x_np.astype(np.uint8)) + + # Post-FX (более предсказуемый порядок): + # Match colors → CLAHE → Unsharp + if bool(self.config.get("match_colors_enabled", False)): + out_img = self._apply_match_colors(out_img, self.pp.image) + out_img = self._apply_clahe(out_img) + out_img = self._apply_unsharp(out_img) + + return out_img + + def _final_upscale_tiled(self, img: Image.Image, scale: float) -> Image.Image: + """ + Final upscale with tiling + linear feathering over overlaps. + - `scale`: произвольный масштаб (например, 2.0, 3.0, 4.0, 6.0, 8.0). + - `tile` и `overlap` заданы в ПРЕДмасштабных пикселях входного изображения. + Примечание: каждый тайл апскейлится через images.resize_image выбранным апскейлером. + """ + upscaler_name = self.config.get("final_upscaler", "R-ESRGAN 4x+") + tile = int(self.config.get("final_tile", 512)) + overlap = int(self.config.get("final_tile_overlap", 16)) + + tile = max(64, tile) + overlap = max(0, min(overlap, tile // 2)) + scale = max(1.0, float(scale)) + + # Если масштаб 1× — нет смысла гонять тайлы + if scale <= 1.0: + return img + + W, H = img.width, img.height + TW, TH = int(round(W * scale)), int(round(H * scale)) + + # Аккумуляторы для взвешенного смешивания + accum = np.zeros((TH, TW, 3), dtype=np.float32) + weight = np.zeros((TH, TW), dtype=np.float32) + + def ramp(length: int, left_ovl: int, right_ovl: int) -> np.ndarray: + """ + 1D-перышко: 0..1 на левой зоне, 1 в центре, 1..0 на правой зоне. + """ + arr = np.ones(int(length), dtype=np.float32) + if left_ovl > 0: + arr[:left_ovl] = np.linspace(0.0, 1.0, left_ovl, endpoint=False, dtype=np.float32) + if right_ovl > 0: + arr[-right_ovl:] = np.minimum(arr[-right_ovl:], np.linspace(1.0, 0.0, right_ovl, endpoint=False, dtype=np.float32)) + return arr + + step_pre = tile - overlap + if step_pre <= 0: + step_pre = tile # защита от вырождения + + for y in range(0, H, step_pre): + for x in range(0, W, step_pre): + x0 = max(0, x - overlap) + y0 = max(0, y - overlap) + x1 = min(W, x + tile) + y1 = min(H, y + tile) + + crop = img.crop((x0, y0, x1, y1)) + + # Апскейлим тайл до целевого масштаба + pre_w = (x1 - x0) + pre_h = (y1 - y0) + up_w = int(round(pre_w * scale)) + up_h = int(round(pre_h * scale)) + + up = images.resize_image( + RESIZE_WITH_UPSCALER, crop, up_w, up_h, upscaler_name=upscaler_name + ) + up_np = np.array(up).astype(np.float32) + + # Координаты вставки в апскейленное полотно + ox = int(round(x0 * scale)) + oy = int(round(y0 * scale)) + uh, uw = up_np.shape[0], up_np.shape[1] + + # Перекрытия в апскейленном пространстве, согласованные с сеткой тайлов + left_ovl = int(round((x - x0) * scale)) + top_ovl = int(round((y - y0) * scale)) + + # Правое/нижнее перекрытие — только если будет следующий шаг + right_ovl = 0 + bottom_ovl = 0 + + if (x + step_pre) < W: + # Сколько колонок справа в этом тайле уйдёт в перо + right_ovl = int(round(((x1 - x) - step_pre) * scale)) + if (y + step_pre) < H: + bottom_ovl = int(round(((y1 - y) - step_pre) * scale)) + + # Жёсткие границы, чтобы не выходить за половину размера тайла + left_ovl = max(0, min(left_ovl, uw // 2)) + right_ovl = max(0, min(right_ovl, uw // 2)) + top_ovl = max(0, min(top_ovl, uh // 2)) + bottom_ovl = max(0, min(bottom_ovl, uh // 2)) + + wx = ramp(uw, left_ovl, right_ovl) + wy = ramp(uh, top_ovl, bottom_ovl) + w2d = (wy[:, None] * wx[None, :]).astype(np.float32) + + # Аккумулируем с весами + y_to = oy + uh + x_to = ox + uw + + accum[oy:y_to, ox:x_to, :] += up_np * w2d[..., None] + weight[oy:y_to, ox:x_to] += w2d + + # Нормализация и сборка результата + weight = np.clip(weight, 1e-6, None) + out = (accum / weight[..., None]).astype(np.uint8) + return Image.fromarray(out, mode="RGB") + + def _enable_controlnet(self, image_np: np.ndarray): + if not getattr(self, "_cn_ext", None): + return + for unit in self._cn_units: + try: + if getattr(unit, "model", "None") != "None": + if getattr(unit, "enabled", True): + unit.guidance_start = float(self.config.get("start_control_at", 0.0)) + # безопасный предел для VRAM (теперь настраиваемый) + min_side = min(image_np.shape[0], image_np.shape[1]) + cap = int(self.config.get("cn_proc_res_cap", 1024)) + cap = max(256, min(4096, cap)) + unit.processor_res = max(256, min(cap, min_side)) + if getattr(unit, "image", None) is None: + unit.image = image_np + self.p.width = image_np.shape[1] + self.p.height = image_np.shape[0] + except Exception: + continue + try: + self._cn_ext.update_cn_script_in_processing(self.p, self._cn_units) + for script in self.p.scripts.alwayson_scripts: + if script.title().lower() == "controlnet": + script.controlnet_hack(self.p) + except Exception: + pass + + +def parse_infotext(infotext, params): + try: + block = params.get("Custom Hires Fix") + if not block: + return + # поддержка нового формата "json64:", а также обратная совместимость + if isinstance(block, str) and block.startswith("json64:"): + data = json.loads(base64.b64decode(block[7:]).decode("utf-8")) + else: + data = json.loads(block.translate(quote_swap)) if isinstance(block, str) else block + params["Custom Hires Fix"] = data + scale = data.get("scale", 0) + if isinstance(scale, str) and "x" in scale: + w, _, h = scale.partition("x") + data["ratio"] = 0.0 + data["width"] = int(w) + data["height"] = int(h) + else: + try: + r = float(scale) + except Exception: + r = 0.0 + data["ratio"] = r + data["width"] = int(data.get("width", 0) or 0) + data["height"] = int(data.get("height", 0) or 0) + + # Defaults for new/legacy fields + if "steps_first" not in data: + data["steps_first"] = int(data.get("steps", 20)) + if "steps_second" not in data: + data["steps_second"] = int(data.get("steps", 20)) + + # per-pass sampler/scheduler defaults from legacy single values + data.setdefault("sampler_first", data.get("sampler", "")) + data.setdefault("sampler_second", data.get("sampler", "")) + data.setdefault("scheduler_first", data.get("scheduler", "Use same scheduler")) + data.setdefault("scheduler_second", data.get("scheduler", "Use same scheduler")) + + # CFG delta defaults + data.setdefault("cfg_second_pass_boost", True) + data.setdefault("cfg_second_pass_delta", 3.0) + + # Flags defaults + data.setdefault("reuse_seed_noise", False) + data.setdefault("mp_target_enabled", False) + data.setdefault("mp_target", 2.0) + data.setdefault("cond_cache_enabled", True) + data.setdefault("cond_cache_max", 64) + data.setdefault("vae_tiling_enabled", False) + data.setdefault("seamless_tiling_enabled", False) + data.setdefault("tile_overlap", 12) + data.setdefault("lora_weight_first_factor", 1.0) + data.setdefault("lora_weight_second_factor", 1.0) + data.setdefault("match_colors_preset", "Off") + data.setdefault("match_colors_enabled", False) + data.setdefault("match_colors_strength", 0.5) + data.setdefault("postfx_preset", "Off") + data.setdefault("clahe_enabled", False) + data.setdefault("clahe_clip", 2.0) + data.setdefault("clahe_tile_grid", 8) + data.setdefault("unsharp_enabled", False) + data.setdefault("unsharp_radius", 1.5) + data.setdefault("unsharp_amount", 0.75) + data.setdefault("unsharp_threshold", 0) + data.setdefault("second_pass_prompt", "") + data.setdefault("second_pass_prompt_append", True) + data.setdefault("cn_proc_res_cap", 1024) + + # final upscale defaults + data.setdefault("final_upscale_enable", False) + data.setdefault("final_upscaler", "R-ESRGAN 4x+") + data.setdefault("final_scale", 4.0) # NEW + data.setdefault("final_tile", 512) + data.setdefault("final_tile_overlap", 16) + # NEW: anti-twinning & SDXL defaults + data.setdefault("deep_shrink_enable", False) + data.setdefault("deep_shrink_strength", 0.5) + data.setdefault("sdxl_mode", False) + data.setdefault("sdxl_denoise_boost", 0.1) + # NEW (инверсия латента и размер 2-го прохода) + data.setdefault("first_latent_invert", False) + data.setdefault("second_custom_size_enable", False) + data.setdefault("second_width", 0) + data.setdefault("second_height", 0) + + # Новое поле по умолчанию — добавляем внутри try + data.setdefault("adaptive_sigma_enable", False) + data.setdefault("restore_scheduler_after", True) + data.setdefault("latent_resample_mode", "nearest") + data.setdefault("noise_schedule_mode", "Use sampler default") + + except Exception: + return + +# Register paste-params hook +script_callbacks.on_infotext_pasted(parse_infotext) \ No newline at end of file