dikdimon commited on
Commit
13e1a6e
Β·
verified Β·
1 Parent(s): 5bdd43b

Update sd-webui-progressive-growing/scripts/progressive_growing_always.py

Browse files
sd-webui-progressive-growing/scripts/progressive_growing_always.py CHANGED
@@ -1,139 +1,1024 @@
1
- """sd-webui-progressive-growing
 
2
 
3
- Always-visible UI + runtime patch for AUTOMATIC1111.
 
 
 
 
4
 
5
- This extension ports the user's provided implementation of `sample_progressive()` 1:1.
6
- It does NOT modify core files on disk; instead it monkey-patches
7
- `modules.processing.StableDiffusionProcessingTxt2Img.sample` at runtime.
 
8
 
9
- UI is AlwaysVisible (not in the Scripts dropdown).
 
 
 
 
 
 
10
  """
11
 
12
  from __future__ import annotations
13
 
 
 
14
  import gradio as gr
 
 
15
 
16
  from modules import scripts, sd_samplers, devices
17
  from modules import processing as processing_mod
18
- from modules.processing import create_random_tensors, decode_latent_batch, opt_C, opt_f
 
 
 
 
 
19
 
 
 
 
20
 
21
- # -----------------------------
22
- # Progressive Growing versions
23
- # -----------------------------
24
 
25
- def sample_progressive_v1_exact(self, conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts):
26
- """Exact copy of the user-provided implementation (processing.py::sample_progressive)."""
27
 
28
- import numpy as np
29
- import torch
30
 
31
- is_sdxl = getattr(self.sd_model, 'is_sdxl', False)
 
32
 
33
- # 1) Π‘ΠΎΠ»ΡŒΡˆΠ΅ НЕВ ΠΏΡ€ΠΈΠ½ΡƒΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ min_scale>=0.5 для SDXL:
34
- min_scale = float(self.progressive_growing_min_scale)
35
- max_scale = float(self.progressive_growing_max_scale)
36
 
37
- # На всякий случай: Ссли ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ ΠΏΠ΅Ρ€Π΅ΠΏΡƒΡ‚Π°Π» мСстами β€” Π΄Π΅Π»Π°Π΅ΠΌ чСстный "рост"
38
- # (Ссли Ρ…ΠΎΡ‡Π΅ΡˆΡŒ ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡ‚ΡŒ "shrink", просто ΡƒΠ±Π΅Ρ€ΠΈ этот swap)
39
- # if min_scale > max_scale:
40
- # min_scale, max_scale = max_scale, min_scale
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- resolution_steps = np.linspace(min_scale, max_scale, int(self.progressive_growing_steps))
 
 
 
43
 
44
- def _snap(v):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  v_int = int(v)
46
  v_int = max(opt_f, v_int)
47
  v_int = (v_int // opt_f) * opt_f
48
  return max(opt_f, v_int)
49
 
50
- # 2) Π‘Ρ‚Π°Ρ€Ρ‚ΠΎΠ²ΠΎΠ΅ Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΈΠ΅
51
- initial_width = _snap(self.width * resolution_steps[0])
52
- initial_height = _snap(self.height * resolution_steps[0])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- # 3) ΠΠ°Ρ‡Π°Π»ΡŒΠ½Ρ‹ΠΉ Π»Π°Ρ‚Π΅Π½Ρ‚ (noise)
55
  x = create_random_tensors(
56
  (opt_C, initial_height // opt_f, initial_width // opt_f),
57
- seeds,
58
- subseeds=subseeds,
59
- subseed_strength=subseed_strength,
60
- seed_resize_from_h=self.seed_resize_from_h,
61
- seed_resize_from_w=self.seed_resize_from_w,
62
- p=self
63
  )
64
 
65
- # 4) ΠŸΠ΅Ρ€Π²Ρ‹ΠΉ ΠΏΡ€ΠΎΡ…ΠΎΠ΄ sampler.sample()
66
- samples = self.sampler.sample(
67
- self,
68
- x,
69
- conditioning,
70
- unconditional_conditioning,
71
- image_conditioning=self.txt2img_image_conditioning(x)
72
  )
73
 
74
  total_stages = len(resolution_steps)
75
 
76
- # 5) ΠŸΡ€ΠΎΠ³Ρ€Π΅ΡΡΠΈΠ²Π½Ρ‹ΠΉ рост
77
  for i in range(1, total_stages):
78
- target_width = _snap(self.width * resolution_steps[i])
79
- target_height = _snap(self.height * resolution_steps[i])
80
 
81
- # upscale latent
82
  samples = torch.nn.functional.interpolate(
83
  samples,
84
  size=(target_height // opt_f, target_width // opt_f),
85
- mode='bicubic',
86
- align_corners=False
87
  )
88
 
89
- # 6) Refinement Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΌ шагС (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)
90
- if self.progressive_growing_refinement:
91
- steps_for_refinement = max(1, self.steps // total_stages)
92
-
93
- noise = create_random_tensors(
94
- samples.shape[1:],
95
- seeds,
96
- subseeds=subseeds,
97
- subseed_strength=subseed_strength,
98
- seed_resize_from_h=self.seed_resize_from_h,
99
- seed_resize_from_w=self.seed_resize_from_w,
100
- p=self
101
  )
102
 
103
- decoded = decode_latent_batch(self.sd_model, samples, target_device=devices.cpu, check_for_nans=True)
104
- decoded = torch.stack(decoded).float()
105
- decoded = torch.clamp((decoded + 1.0) / 2.0, 0.0, 1.0)
106
-
107
- source_img = decoded * 2.0 - 1.0
108
- self.image_conditioning = self.img2img_image_conditioning(source_img, samples)
109
- samples = self.sampler.sample_img2img(
110
- self,
111
- samples,
112
- noise,
113
- conditioning,
114
- unconditional_conditioning,
115
- steps=steps_for_refinement,
116
- image_conditioning=self.image_conditioning
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  )
 
118
 
119
  return samples
120
 
121
 
122
- _VERSIONS = {
123
- "v1 (exact)": sample_progressive_v1_exact,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
 
 
126
 
127
- # -----------------------------
128
- # Runtime patching
129
- # -----------------------------
130
 
131
- _PATCHED = False
132
  _ORIG_SAMPLE = None
133
 
134
 
135
  def _apply_patch_once() -> None:
136
- """Patch StableDiffusionProcessingTxt2Img.sample to route into sample_progressive_* when enabled."""
137
 
138
  global _PATCHED, _ORIG_SAMPLE
139
  if _PATCHED:
@@ -143,83 +1028,148 @@ def _apply_patch_once() -> None:
143
  if cls is None:
144
  return
145
 
146
- # already patched by us (or another copy)
147
- if getattr(cls, '_progressive_growing_ext_patched', False):
148
  _PATCHED = True
149
  return
150
 
151
  _ORIG_SAMPLE = cls.sample
152
 
153
- def _sample_wrapper(self, conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts):
154
- # Only intercept when user enabled the feature
155
- if getattr(self, 'enable_progressive_growing', False):
156
- # mirror the user code: sampler is created at the start of sample()
157
- self.sampler = sd_samplers.create_sampler(self.sampler_name, self.sd_model)
158
 
159
- # pick version (defaults to exact v1)
160
- ver = getattr(self, 'progressive_growing_version', 'v1 (exact)')
161
- fn = _VERSIONS.get(ver, sample_progressive_v1_exact)
162
- return fn(self, conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts)
 
 
 
 
 
 
163
 
164
- # fallback to original behaviour (including its sampler creation)
165
- return _ORIG_SAMPLE(self, conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts)
 
 
 
 
166
 
167
- cls.sample = _sample_wrapper
168
- cls._progressive_growing_ext_patched = True
 
 
 
 
 
 
 
 
 
 
169
  _PATCHED = True
170
 
171
 
172
- # -----------------------------
173
  # Always-visible UI script
174
- # -----------------------------
175
-
176
 
177
  class ProgressiveGrowingAlwaysVisible(scripts.Script):
 
178
  def title(self):
179
  return "Progressive Growing"
180
 
181
  def show(self, is_img2img):
182
- # Only for txt2img, always visible
183
  return scripts.AlwaysVisible if not is_img2img else False
184
 
185
  def ui(self, is_img2img):
186
  with gr.Accordion("Progressive Growing", open=False):
187
  enabled = gr.Checkbox(value=False, label="Enable")
188
- version = gr.Dropdown(choices=list(_VERSIONS.keys()), value="v1 (exact)", label="Version")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
- min_scale = gr.Slider(minimum=0.1, maximum=1.0, step=0.05, value=0.25, label="Min scale")
191
- max_scale = gr.Slider(minimum=0.1, maximum=1.0, step=0.05, value=1.0, label="Max scale")
192
- steps = gr.Slider(minimum=2, maximum=16, step=1, value=4, label="Stages")
193
- refinement = gr.Checkbox(value=True, label="Refinement between stages")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
  gr.Markdown(
196
- "- Starts at Min scale, then increases latent resolution up to Max scale.\n"
197
- "- Optional short img2img refinement at each stage.\n"
198
- "- This implementation matches your provided code (v1 exact)."
 
 
 
 
 
 
 
 
 
 
 
 
199
  )
200
 
201
- return [enabled, version, min_scale, max_scale, steps, refinement]
 
 
202
 
203
- def process(self, p, enabled, version, min_scale, max_scale, steps, refinement):
204
- _apply_patch_once()
 
 
205
 
206
- # store parameters on p (matching the reference implementation's attribute names)
207
- p.enable_progressive_growing = bool(enabled)
208
- p.progressive_growing_version = str(version)
209
- p.progressive_growing_min_scale = float(min_scale)
210
- p.progressive_growing_max_scale = float(max_scale)
211
- p.progressive_growing_steps = int(steps)
212
- p.progressive_growing_refinement = bool(refinement)
213
 
214
- if p.enable_progressive_growing:
215
- # Keep generation params so they show up in infotext (if UI/processing prints them)
216
- try:
217
- p.extra_generation_params["Progressive Growing"] = "True"
218
- p.extra_generation_params["Min Scale"] = p.progressive_growing_min_scale
219
- p.extra_generation_params["Max Scale"] = p.progressive_growing_max_scale
220
- p.extra_generation_params["Progressive Growing Steps"] = p.progressive_growing_steps
221
- p.extra_generation_params["Refinement"] = "True" if p.progressive_growing_refinement else None
222
- p.extra_generation_params["PG Version"] = p.progressive_growing_version
223
- except Exception:
224
- # extra_generation_params may not exist in some contexts
225
- pass
 
1
+ """sd-webui-progressive-growing Β· extension-only rewrite
2
+ ===========================================================
3
 
4
+ Architecture contract
5
+ ---------------------
6
+ UI β†’ process() β†’ patch wrapper β†’ version fn(p, plan, ...) β†’ samples
7
+ ↓ on any conflict/disable
8
+ _ORIG_SAMPLE(...)
9
 
10
+ Files
11
+ -----
12
+ This single file is self-contained for easy drop-in.
13
+ Split into lib_progressive/ when the project grows beyond ~600 lines.
14
 
15
+ Versions
16
+ --------
17
+ v1 (exact) – original code 1:1, kept as reference / regression baseline
18
+ v2 (safe) – validation, dedup stages, conflict guards, predictable output
19
+ v3 (fast) – minimal refinement, no VAE decode on intermediate stages
20
+ v4 (balanced) – refinement only on stages >= BALANCED_REFINE_THRESHOLD (linear scale, not area)
21
+ v5 (latent) – pure latent upscale, zero refinement
22
  """
23
 
24
  from __future__ import annotations
25
 
26
+ import contextlib
27
+ import math
28
  import gradio as gr
29
+ import numpy as np
30
+ import torch
31
 
32
  from modules import scripts, sd_samplers, devices
33
  from modules import processing as processing_mod
34
+ from modules.processing import (
35
+ create_random_tensors,
36
+ decode_latent_batch,
37
+ opt_C,
38
+ opt_f,
39
+ )
40
 
41
+ # ─────────────────────────────────────────────────────────────────────────────
42
+ # Constants
43
+ # ─────────────────────────────────────────────────────────────────────────────
44
 
45
+ BALANCED_REFINE_THRESHOLD = 0.70 # v4: only refine stages whose linear scale >= this
46
+ ADAPTIVE_REFINE_THRESHOLD = 0.60 # v6: threshold for long-plan (5+ stages) adaptive refinement
 
47
 
48
+ LATENT_INTERP_MODES = ["bicubic", "bilinear", "nearest", "area"]
49
+ LATENT_INTERP_DEFAULT = "bicubic" # best general-purpose default; nearest is fastest
50
 
51
+ MIN_STAGES_AFTER_DEDUP = 1 # if dedup collapses plan to 1 size, skip progressive entirely
 
52
 
53
+ AUTO_STAGE_JUMP_DEFAULT = 1.35 # target linear growth factor per stage
54
+ AUTO_STAGE_MAX_DEFAULT = 6 # hard cap on auto-computed stage count
55
 
56
+ REFINE_STEP_MODES = ["uniform", "late-heavy", "final-heavy"]
57
+ REFINE_STEP_DEFAULT = "uniform" # matches legacy behaviour; switch to late-heavy once comfortable
 
58
 
59
+ # ── Sampler compatibility profiles ───────────────────────────────────────────
60
+ #
61
+ # Progressive growing runs each stage sampler on a *smaller* latent than the
62
+ # final target. Two classes of problem arise:
63
+ #
64
+ # 1. Custom sigma schedulers (e.g. "CosineExponential blend") compute sigmas
65
+ # that are spatially shaped – [steps+1, H, W] – by reading p.width/p.height
66
+ # at sampler.sample() time. With stage dims still at final size the sigmas
67
+ # mismatch the stage latent β†’ RuntimeError on the first sigma operation.
68
+ #
69
+ # 2. img2img contexts: p.init_latent / p.mask / p.nmask are full-size tensors.
70
+ # KDiffusionSampler.sample() copies them into model_wrap_cfg before
71
+ # launching the sampler loop, causing shape mismatches inside the loop.
72
+ #
73
+ # The "smea" profile wraps every sampler.sample() / sample_img2img() call with
74
+ # _stage_sampler_context(), which temporarily sets p.width/p.height to stage
75
+ # dims and rescales init_latent/mask/nmask. This directly mirrors the
76
+ # _Rescaler pattern already used inside sd-webui-smea itself.
77
+ #
78
+ # SMEA_SAMPLER_PATTERNS – families that need _stage_sampler_context.
79
+ # Each entry is a lowercase substring of p.sampler_name.
80
+ SMEA_SAMPLER_PATTERNS: list[str] = [
81
+ "euler dy", # Euler Dy, Euler Dy koishi-star
82
+ "euler smea", # Euler Smea, Euler Smea Dy, Euler Smea Max, all multi-*
83
+ "euler h max", # Euler h max a/b/c/…
84
+ "euler max", # Euler Max, Max1b … Max4f (catches all Max* variants)
85
+ "kohaku_lonyu", # Kohaku_LoNyu_Yog
86
+ "tcd", # TCD / TCD Euler a
87
+ ]
88
 
89
+ # INCOMPATIBLE_SAMPLER_PATTERNS – families with no viable compatibility path yet.
90
+ # Unlike SMEA, these are blocked entirely until a specific adapter is built.
91
+ # Empty for now; add entries here when a truly irreconcilable sampler is found.
92
+ INCOMPATIBLE_SAMPLER_PATTERNS: list[str] = []
93
 
94
+
95
+ def _get_sampler_profile(p) -> tuple[str, str]:
96
+ """
97
+ Classify the active sampler into one of three profiles:
98
+
99
+ 'standard' – normal k-diffusion; no special handling required.
100
+ 'smea' – sd-webui-smea family; needs _stage_sampler_context.
101
+ 'unsupported' – no compatibility path yet; progressive will be skipped.
102
+
103
+ Returns (profile_name, reason). reason is '' unless profile == 'unsupported'.
104
+ """
105
+ name = (getattr(p, 'sampler_name', '') or '').lower()
106
+
107
+ for pattern in INCOMPATIBLE_SAMPLER_PATTERNS:
108
+ if pattern in name:
109
+ return 'unsupported', (
110
+ f'disabled – sampler "{p.sampler_name}" has no compatibility '
111
+ f'profile yet. Use a standard k-diffusion sampler or the SMEA family.'
112
+ )
113
+
114
+ for pattern in SMEA_SAMPLER_PATTERNS:
115
+ if pattern in name:
116
+ return 'smea', ''
117
+
118
+ return 'standard', ''
119
+
120
+
121
+ @contextlib.contextmanager
122
+ def _stage_sampler_context(p, stage_w: int, stage_h: int):
123
+ """
124
+ Temporarily adapt p's size-dependent state to stage dimensions.
125
+
126
+ Three adaptations, all restored in finally:
127
+
128
+ 1. p.width / p.height -> stage dims.
129
+
130
+ 2. p.init_latent / p.mask / p.nmask -> rescaled to stage latent dims.
131
+ KDiffusionSampler.sample() copies these into model_wrap_cfg before
132
+ the sampler loop.
133
+
134
+ 3. p.sampler_noise_scheduler_override -> wrapped to resize spatial sigmas.
135
+ SMEA's process() hook sets this override with full-size dims captured
136
+ in a closure. KDiffusionSampler.sample() calls the override DIRECTLY
137
+ (bypassing get_sigmas entirely) when it is set, so any spatial sigma
138
+ tensor [T,H,W] or [T,C,H,W] it returns will have full-size H/W dims.
139
+ We wrap the override here so its output is resized to stage_hw before
140
+ the sampler loop sees it.
141
+ """
142
+ import torch.nn.functional as _F
143
+
144
+ stage_hw = (stage_h // opt_f, stage_w // opt_f)
145
+
146
+ # save
147
+ orig_w, orig_h = p.width, p.height
148
+ orig_init = getattr(p, 'init_latent', None)
149
+ orig_mask = getattr(p, 'mask', None)
150
+ orig_nmask = getattr(p, 'nmask', None)
151
+ orig_override = getattr(p, 'sampler_noise_scheduler_override', None)
152
+
153
+ # adapt dims
154
+ p.width, p.height = stage_w, stage_h
155
+
156
+ if orig_init is not None:
157
+ p.init_latent = _F.interpolate(orig_init, size=stage_hw, mode='nearest-exact')
158
+ if orig_mask is not None:
159
+ p.mask = _F.interpolate(
160
+ orig_mask.unsqueeze(0), size=stage_hw, mode='nearest-exact'
161
+ ).squeeze(0)
162
+ if orig_nmask is not None:
163
+ p.nmask = _F.interpolate(
164
+ orig_nmask.unsqueeze(0), size=stage_hw, mode='nearest-exact'
165
+ ).squeeze(0)
166
+
167
+ # wrap noise scheduler override to resize spatial sigmas to stage dims
168
+ if orig_override is not None:
169
+ def _stage_override(steps, _orig=orig_override, _hw=stage_hw):
170
+ sigs = _orig(steps)
171
+ if not isinstance(sigs, torch.Tensor) or sigs.ndim < 3:
172
+ return sigs
173
+ try:
174
+ if sigs.ndim == 3: # [T, H, W]
175
+ sigs = _F.interpolate(
176
+ sigs.unsqueeze(1), size=_hw, mode='nearest-exact'
177
+ ).squeeze(1)
178
+ elif sigs.ndim == 4: # [T, C, H, W]
179
+ sigs = _F.interpolate(sigs, size=_hw, mode='nearest-exact')
180
+ except Exception:
181
+ pass
182
+ return sigs
183
+ p.sampler_noise_scheduler_override = _stage_override
184
+
185
+ try:
186
+ yield
187
+ finally:
188
+ p.width, p.height = orig_w, orig_h
189
+ if orig_init is not None: p.init_latent = orig_init
190
+ if orig_mask is not None: p.mask = orig_mask
191
+ if orig_nmask is not None: p.nmask = orig_nmask
192
+ p.sampler_noise_scheduler_override = orig_override
193
+
194
+ # ──────────────────────────────────────────────────────────────────��──────────
195
+ # Stage planner
196
+ # ─────────────────────────────────────────────────────────────────────────────
197
+
198
+ def _snap(v: float, factor: int = opt_f) -> int:
199
+ """Round v down to nearest multiple of factor (min = factor)."""
200
+ v_int = max(factor, int(v))
201
+ return max(factor, (v_int // factor) * factor)
202
+
203
+
204
+ class StagePlan:
205
+ """Validated, deduplicated list of (width, height) latent sizes."""
206
+
207
+ def __init__(self, sizes: list[tuple[int, int]], final_w: int, final_h: int):
208
+ self.sizes = sizes # [(w, h), ...], last entry == (final_w, final_h)
209
+ self.final_w = final_w
210
+ self.final_h = final_h
211
+ self.n_stages = len(sizes)
212
+
213
+ def __repr__(self) -> str:
214
+ parts = [f"{w}Γ—{h}" for w, h in self.sizes]
215
+ return " β†’ ".join(parts)
216
+
217
+
218
+ def _auto_n_steps(min_s: float, max_s: float,
219
+ target_jump: float, max_stages: int) -> int:
220
+ """
221
+ Compute stage count so each step grows the linear scale by ~target_jump.
222
+
223
+ Formula: n = 1 + ceil( log(max_s / min_s) / log(target_jump) )
224
+ Clamped to [2, max_stages].
225
+
226
+ Examples with target_jump=1.35:
227
+ 0.25 β†’ 1.0 : β‰ˆ 6 stages
228
+ 0.50 β†’ 1.0 : β‰ˆ 3 stages
229
+ 0.75 β†’ 1.0 : β‰ˆ 2 stages
230
+ """
231
+ growth = max_s / max(min_s, 1e-6)
232
+ n_steps = 1 + math.ceil(math.log(max(growth, 1.0)) / math.log(max(target_jump, 1.001)))
233
+ return max(2, min(max_stages, n_steps))
234
+
235
+
236
+ def build_plan(p) -> StagePlan:
237
+ """
238
+ Build a deduplicated stage plan from p's progressive-growing attributes.
239
+
240
+ When p.progressive_growing_auto_stages is True the stage count is computed
241
+ automatically from min/max scale and p.progressive_growing_auto_jump
242
+ (target linear growth per step), capped at p.progressive_growing_auto_max.
243
+ The manual Stages slider is ignored in auto mode.
244
+
245
+ Guarantees:
246
+ - min_scale <= max_scale (swapped silently)
247
+ - last stage == (p.width, p.height) snapped to opt_f
248
+ - duplicate sizes removed
249
+ - if only one unique size remains: plan.n_stages == 1
250
+ (caller should fall back to normal sampling)
251
+ """
252
+ min_s = float(getattr(p, 'progressive_growing_min_scale', 0.25))
253
+ max_s = float(getattr(p, 'progressive_growing_max_scale', 1.0))
254
+
255
+ if min_s > max_s:
256
+ min_s, max_s = max_s, min_s
257
+
258
+ auto = bool(getattr(p, 'progressive_growing_auto_stages', False))
259
+ if auto:
260
+ target_jump = float(getattr(p, 'progressive_growing_auto_jump', AUTO_STAGE_JUMP_DEFAULT))
261
+ max_stages = int(getattr(p, 'progressive_growing_auto_max', AUTO_STAGE_MAX_DEFAULT))
262
+ n_steps = _auto_n_steps(min_s, max_s, target_jump, max_stages)
263
+ else:
264
+ n_steps = max(2, int(getattr(p, 'progressive_growing_steps', 4)))
265
+
266
+ scales = np.linspace(min_s, max_s, n_steps)
267
+ final_w = _snap(p.width)
268
+ final_h = _snap(p.height)
269
+
270
+ raw_sizes: list[tuple[int, int]] = []
271
+ for s in scales:
272
+ raw_sizes.append((_snap(p.width * s), _snap(p.height * s)))
273
+
274
+ # deduplicate while preserving order
275
+ seen: set[tuple[int, int]] = set()
276
+ unique: list[tuple[int, int]] = []
277
+ for sz in raw_sizes:
278
+ if sz not in seen:
279
+ seen.add(sz)
280
+ unique.append(sz)
281
+
282
+ # force the last entry to be the true final size
283
+ if not unique or unique[-1] != (final_w, final_h):
284
+ if (final_w, final_h) in seen:
285
+ unique = [sz for sz in unique if sz != (final_w, final_h)]
286
+ unique.append((final_w, final_h))
287
+
288
+ plan = StagePlan(unique, final_w, final_h)
289
+ plan.auto_mode = auto # carry metadata for infotext
290
+ plan.requested = n_steps # stages before dedup
291
+ return plan
292
+
293
+
294
+ # ─────────────────────────────────────────────────────────────────────────────
295
+ # Conflict / capability guards
296
+ # ─────────────────────────────────────────────────────────────────────────────
297
+
298
+ def _should_use_progressive(p):
299
+ """
300
+ Return (True, '', plan) if progressive should run, else (False, reason, None).
301
+ Building the plan here avoids a second build_plan() call inside the version fn.
302
+ Reasons appear in infotext via extra_generation_params.
303
+ """
304
+ if not getattr(p, 'enable_progressive_growing', False):
305
+ return False, '', None
306
+
307
+ if getattr(p, 'enable_hr', False):
308
+ return False, 'disabled – incompatible with Hires. fix', None
309
+
310
+ profile, reason = _get_sampler_profile(p)
311
+ if profile == 'unsupported':
312
+ return False, reason, None
313
+
314
+ plan = build_plan(p)
315
+ if plan.n_stages <= MIN_STAGES_AFTER_DEDUP:
316
+ return False, f'disabled – all stages collapsed to one size ({plan.final_w}Γ—{plan.final_h})', None
317
+
318
+ return True, '', plan
319
+
320
+
321
+ def _write_params(p, plan: StagePlan, refine_policy: str, refine_step_policy: str) -> None:
322
+ """
323
+ Write run parameters into infotext.
324
+
325
+ refine_policy – effective policy for *which* stages refine
326
+ e.g. 'all stages', 'final stage only', 'none'
327
+ refine_step_policy – effective step budget policy actually used
328
+ e.g. 'uniform', 'late-heavy (v1 fixed)', 'none'
329
+ Callers must pass the *actual* behaviour, not the
330
+ UI dropdown value, so v1 / v5 / checkbox-off are honest.
331
+
332
+ If the refinement checkbox is off, both refine fields collapse to 'off (checkbox)'.
333
+ """
334
+ refinement_on = getattr(p, 'progressive_growing_refinement', True)
335
+ try:
336
+ ep = p.extra_generation_params
337
+ ep['PG'] = getattr(p, 'progressive_growing_version', 'v2 (safe)')
338
+ ep['PG plan'] = str(plan)
339
+ ep['PG stages'] = plan.n_stages
340
+ ep['PG refine'] = refine_policy if refinement_on else 'off (checkbox)'
341
+ ep['PG refine steps'] = refine_step_policy if refinement_on else 'off (checkbox)'
342
+ ep['PG interp'] = getattr(p, 'progressive_growing_interp_mode', LATENT_INTERP_DEFAULT)
343
+ # auto-stage metadata
344
+ if getattr(plan, 'auto_mode', False):
345
+ ep['PG stages mode'] = 'auto'
346
+ ep['PG target jump'] = getattr(p, 'progressive_growing_auto_jump', AUTO_STAGE_JUMP_DEFAULT)
347
+ ep['PG requested'] = getattr(plan, 'requested', plan.n_stages)
348
+ ep['PG actual'] = plan.n_stages
349
+ else:
350
+ ep['PG stages mode'] = 'manual'
351
+ except Exception:
352
+ pass
353
+
354
+
355
+ # ─────────────────────────────────────────────────────────────────────────────
356
+ # Shared helpers
357
+ # ─────────────────────────────────────────────────────────────────────────────
358
+
359
+ def _initial_latent(p, w: int, h: int, seeds, subseeds, subseed_strength):
360
+ return create_random_tensors(
361
+ (opt_C, h // opt_f, w // opt_f),
362
+ seeds,
363
+ subseeds=subseeds,
364
+ subseed_strength=subseed_strength,
365
+ seed_resize_from_h=p.seed_resize_from_h,
366
+ seed_resize_from_w=p.seed_resize_from_w,
367
+ p=p,
368
+ )
369
+
370
+
371
+ def _upscale_latent(samples: torch.Tensor, w: int, h: int, interp_mode: str = LATENT_INTERP_DEFAULT) -> torch.Tensor:
372
+ """
373
+ Upscale latent to (h // opt_f, w // opt_f) using the requested interpolation.
374
+
375
+ Supported modes mirror torch.nn.functional.interpolate:
376
+ bicubic – smooth, best for photographic content (default)
377
+ bilinear – slightly faster, softer result
378
+ nearest – fastest, hard edges; useful for pixel art / very structured images
379
+ area – anti-aliased average pooling; suited for large downscales if
380
+ you manually set min_scale > max_scale outside build_plan()
381
+ (build_plan() itself always swaps them, so area rarely fires
382
+ in normal use β€” kept for completeness)
383
+
384
+ align_corners=False for bicubic/bilinear matches PyTorch convention and
385
+ the behaviour of v1 (exact), avoiding edge-pixel drift on upscale.
386
+ """
387
+ align = interp_mode in ("bicubic", "bilinear")
388
+ return torch.nn.functional.interpolate(
389
+ samples,
390
+ size=(h // opt_f, w // opt_f),
391
+ mode=interp_mode,
392
+ align_corners=False if align else None,
393
+ )
394
+
395
+
396
+ def _make_noise(p, shape, seeds, subseeds, subseed_strength):
397
+ return create_random_tensors(
398
+ shape,
399
+ seeds,
400
+ subseeds=subseeds,
401
+ subseed_strength=subseed_strength,
402
+ seed_resize_from_h=p.seed_resize_from_h,
403
+ seed_resize_from_w=p.seed_resize_from_w,
404
+ p=p,
405
+ )
406
+
407
+
408
+ def _call_script_hooks(p, samples) -> None:
409
+ """
410
+ Fire process_before_every_sampling on all registered scripts, if available.
411
+
412
+ In stock A1111 this hook lets ControlNet, ADetailer, and other extensions
413
+ inject per-sampling adjustments (e.g. attention maps, masks). Calling it
414
+ before every sampler.sample / sampler.sample_img2img invocation keeps
415
+ progressive growing compatible with those extensions.
416
+
417
+ Guarded so the extension degrades gracefully on forks that lack the hook.
418
+ """
419
+ scripts_obj = getattr(p, 'scripts', None)
420
+ if scripts_obj is None:
421
+ return
422
+ hook = getattr(scripts_obj, 'process_before_every_sampling', None)
423
+ if hook is None:
424
+ return
425
+ try:
426
+ hook(p, samples)
427
+ except Exception:
428
+ pass
429
+
430
+
431
+ def _get_refine_steps(p, refine_idx: int, n_refine: int, mode: str) -> int:
432
+ """
433
+ Compute the step budget for a single refinement pass.
434
+
435
+ Parameters
436
+ ----------
437
+ p : processing object, provides p.steps (total generation steps)
438
+ refine_idx : 0-based index among refine passes that will actually run
439
+ (0 = earliest/smallest stage being refined)
440
+ n_refine : total number of refine passes that will run this generation
441
+ mode : one of REFINE_STEP_MODES
442
+
443
+ Modes
444
+ -----
445
+ uniform current behaviour: p.steps // total_refine_stages, equal for all
446
+ late-heavy weights grow towards the final pass. Weights for n passes:
447
+ n=1 β†’ [1.0]
448
+ n=2 β†’ [0.4, 0.6]
449
+ n=3 β†’ [0.2, 0.3, 0.5]
450
+ n=4+ β†’ geometric series r=1.5 normalised to 1.0
451
+ final-heavy almost all budget to the last pass:
452
+ non-final passes each get floor(p.steps * 0.08)
453
+ final pass gets whatever remains, minimum 1
454
+ """
455
+ total = max(1, p.steps)
456
+ n = max(1, n_refine)
457
+ idx = max(0, min(refine_idx, n - 1))
458
+
459
+ if mode == 'final-heavy':
460
+ if idx < n - 1:
461
+ return max(1, int(total * 0.08))
462
+ non_final_total = max(1, int(total * 0.08)) * (n - 1)
463
+ return max(1, total - non_final_total)
464
+
465
+ if mode == 'late-heavy':
466
+ if n == 1:
467
+ weights = [1.0]
468
+ elif n == 2:
469
+ weights = [0.4, 0.6]
470
+ elif n == 3:
471
+ weights = [0.2, 0.3, 0.5]
472
+ else:
473
+ # geometric series with ratio 1.5
474
+ r = 1.5
475
+ raw = [r ** i for i in range(n)]
476
+ s = sum(raw)
477
+ weights = [v / s for v in raw]
478
+ steps = max(1, round(total * weights[idx]))
479
+ return steps
480
+
481
+ # uniform (default / legacy)
482
+ return max(1, total // n)
483
+
484
+
485
+ def _resize_model_wrap_cfg(p, stage_w: int, stage_h: int) -> None:
486
+ """
487
+ Resize spatial state inside p.sampler.model_wrap_cfg to match stage dims.
488
+
489
+ KDiffusionSampler.sample() calls get_scalings() and copies model_wrap_cfg.*
490
+ into the sampler loop *before* the first step. Any full-size spatial tensor
491
+ still living there will mismatch the stage-sized latent `x` on the first
492
+ sigma operation (e.g. `x - eps * (sigma_hat**2 - sigmas[i]**2)**0.5`).
493
+
494
+ This must be called *after* create_sampler() and *inside* _stage_sampler_context
495
+ so the resize is coherent with the p.width/p.height override.
496
+
497
+ Only non-None tensors are touched; all errors are silently swallowed so a
498
+ fork with a different model_wrap_cfg structure cannot crash the whole pass.
499
+ """
500
+ import torch.nn.functional as _F
501
+ stage_hw = (stage_h // opt_f, stage_w // opt_f)
502
+ mw = getattr(getattr(p, 'sampler', None), 'model_wrap_cfg', None)
503
+ if mw is None:
504
+ return
505
+ for name in ('init_latent', 'mask', 'nmask'):
506
+ t = getattr(mw, name, None)
507
+ if t is None:
508
+ continue
509
+ try:
510
+ if name == 'init_latent':
511
+ setattr(mw, name, _F.interpolate(t, size=stage_hw, mode='nearest-exact'))
512
+ else:
513
+ setattr(mw, name,
514
+ _F.interpolate(t.unsqueeze(0), size=stage_hw,
515
+ mode='nearest-exact').squeeze(0))
516
+ except Exception:
517
+ pass
518
+
519
+
520
+ @contextlib.contextmanager
521
+ def _force_safe_sigmas_for_smea(p):
522
+ """
523
+ For SMEA-family progressive stage passes, temporarily force plain 1D sigmas.
524
+
525
+ SMEA's custom spatial sigma schedulers (e.g. CosineExponential blend,
526
+ Karras Exponential v3) produce [T, H, W] or [T, C, H, W] tensors keyed
527
+ to full-size H/W. Wrapping p.sampler_noise_scheduler_override (as done in
528
+ _stage_sampler_context) is not always enough because some scheduler paths
529
+ bypass the override entirely and cache sigmas elsewhere (e.g.
530
+ sampler_extra_args['sigmas'] populated during create_sampler).
531
+
532
+ The safest fix for PG + SMEA: drop the override entirely for the duration
533
+ of this stage pass so KDiffusionSampler falls back to its own get_sigmas()
534
+ with the already stage-sized p.width/p.height. The SMEA sampler function
535
+ (sample_euler_max*) receives plain 1D sigmas and works fine.
536
+
537
+ Restores original override unconditionally in finally.
538
+ """
539
+ orig_override = getattr(p, 'sampler_noise_scheduler_override', None)
540
+ orig_scheduler = getattr(p, 'scheduler', None)
541
+ try:
542
+ p.sampler_noise_scheduler_override = None
543
+ try:
544
+ p.scheduler = 'Use sampler default'
545
+ except Exception:
546
+ pass
547
+ yield
548
+ finally:
549
+ p.sampler_noise_scheduler_override = orig_override
550
+ try:
551
+ if orig_scheduler is not None:
552
+ p.scheduler = orig_scheduler
553
+ except Exception:
554
+ pass
555
+
556
+
557
+
558
+ def _patch_sampler_func_sigmas_to_1d(p) -> None:
559
+ """
560
+ Wrap p.sampler.func so that the `sigmas` positional argument passed to
561
+ the SMEA sampler function is always 1D (scalar per step).
562
+
563
+ KDiffusionSampler.sample() computes sigmas (via override OR get_sigmas OR
564
+ sd_schedulers.apply_scheduler) and captures them in a lambda closure:
565
+
566
+ lambda: self.func(model_wrap_cfg, x, sigmas, extra_args=..., ...)
567
+
568
+ Patching get_sigmas or override cannot intercept all paths β€” the sigmas
569
+ variable lives in sample()'s local scope and is passed positionally.
570
+ Wrapping self.func on the instance is the only guaranteed intercept point.
571
+
572
+ Spatial sigma shapes:
573
+ [T, H, W] -> [T] (mean over H, W)
574
+ [T, C, H, W] -> [T] (mean over C, H, W)
575
+
576
+ SMEA / Euler Max* samplers only use sigmas[i] as a per-step scalar, so
577
+ collapsing spatial dims does not affect sampling quality.
578
+ """
579
+ sampler = getattr(p, 'sampler', None)
580
+ if sampler is None:
581
+ return
582
+ orig_func = getattr(sampler, 'func', None)
583
+ if orig_func is None:
584
+ return
585
+
586
+ def _collapse(sigs):
587
+ if not isinstance(sigs, torch.Tensor) or sigs.ndim < 2:
588
+ return sigs
589
+ try:
590
+ dims = tuple(range(1, sigs.ndim))
591
+ return sigs.mean(dim=dims)
592
+ except Exception:
593
+ return sigs
594
+
595
+ def _safe_func(model, x, sigmas, *args, **kwargs):
596
+ return orig_func(model, x, _collapse(sigmas), *args, **kwargs)
597
+
598
+ sampler.func = _safe_func
599
+
600
+
601
+ def _stage_sample_txt2img(p, x, conditioning, unconditional_conditioning,
602
+ image_cond, stage_w: int, stage_h: int,
603
+ sampler_profile: str) -> torch.Tensor:
604
+ """
605
+ Run a single txt2img sampler pass on a stage-sized latent.
606
+
607
+ For 'smea' profile:
608
+ - _stage_sampler_context sets stage-sized p.width/p.height, rescales
609
+ init_latent/mask/nmask, and wraps sampler_noise_scheduler_override.
610
+ - _force_safe_sigmas_for_smea then sets override=None so
611
+ KDiffusionSampler falls back to plain 1D get_sigmas(). This is the
612
+ definitive fix for spatial sigma mismatches in Euler Max* samplers.
613
+ - sampler is created fresh inside the context so all size-dependent
614
+ state (sigma schedules, cached spatial tensors) matches stage dims.
615
+ - sampler_extra_args['sigmas'] is evicted in case create_sampler
616
+ cached a full-size spatial schedule during construction.
617
+
618
+ For 'standard' profile: uses p.sampler as-is (already created in wrapper).
619
+ """
620
+ if sampler_profile == 'smea':
621
+ with _stage_sampler_context(p, stage_w, stage_h):
622
+ with _force_safe_sigmas_for_smea(p):
623
+ p.sampler = sd_samplers.create_sampler(p.sampler_name, p.sd_model)
624
+ _patch_sampler_func_sigmas_to_1d(p)
625
+ _resize_model_wrap_cfg(p, stage_w, stage_h)
626
+ _call_script_hooks(p, x)
627
+ return p.sampler.sample(
628
+ p, x, conditioning, unconditional_conditioning,
629
+ image_conditioning=image_cond,
630
+ )
631
+ else:
632
+ _call_script_hooks(p, x)
633
+ return p.sampler.sample(
634
+ p, x, conditioning, unconditional_conditioning,
635
+ image_conditioning=image_cond,
636
+ )
637
+
638
+
639
+ def _stage_sample_img2img(p, samples, noise, conditioning, unconditional_conditioning,
640
+ stage_w: int, stage_h: int,
641
+ steps: int, sampler_profile: str) -> torch.Tensor:
642
+ """
643
+ Run a single img2img sampler pass on a stage-sized latent.
644
+
645
+ Same contract as _stage_sample_txt2img: for 'smea' profile a fresh sampler
646
+ is created inside _stage_sampler_context and model_wrap_cfg is patched to
647
+ stage dims before the sampler loop starts.
648
+ """
649
+ if sampler_profile == 'smea':
650
+ with _stage_sampler_context(p, stage_w, stage_h):
651
+ with _force_safe_sigmas_for_smea(p):
652
+ p.sampler = sd_samplers.create_sampler(p.sampler_name, p.sd_model)
653
+ _patch_sampler_func_sigmas_to_1d(p)
654
+ _resize_model_wrap_cfg(p, stage_w, stage_h)
655
+ _call_script_hooks(p, samples)
656
+ return p.sampler.sample_img2img(
657
+ p, samples, noise,
658
+ conditioning, unconditional_conditioning,
659
+ steps=steps,
660
+ image_conditioning=p.image_conditioning,
661
+ )
662
+ else:
663
+ _call_script_hooks(p, samples)
664
+ return p.sampler.sample_img2img(
665
+ p, samples, noise,
666
+ conditioning, unconditional_conditioning,
667
+ steps=steps,
668
+ image_conditioning=p.image_conditioning,
669
+ )
670
+
671
+
672
+ def _run_img2img_refinement(p, samples, seeds, subseeds, subseed_strength,
673
+ conditioning, unconditional_conditioning,
674
+ refine_idx: int, n_refine: int,
675
+ refine_step_mode: str,
676
+ sampler_profile: str = 'standard') -> torch.Tensor:
677
+ """
678
+ Full VAE-decode refinement.
679
+ Decodes latents, re-encodes as img2img conditioning, then delegates to
680
+ _stage_sample_img2img which handles sampler lifecycle per profile.
681
+
682
+ refine_idx / n_refine / refine_step_mode feed into _get_refine_steps so
683
+ the step budget reflects the chosen policy rather than always uniform.
684
+ """
685
+ steps_for_refinement = _get_refine_steps(p, refine_idx, n_refine, refine_step_mode)
686
+ noise = _make_noise(p, samples.shape[1:], seeds, subseeds, subseed_strength)
687
+
688
+ decoded = decode_latent_batch(p.sd_model, samples,
689
+ target_device=devices.cpu, check_for_nans=True)
690
+ decoded = torch.stack(decoded).float()
691
+ decoded = torch.clamp((decoded + 1.0) / 2.0, 0.0, 1.0)
692
+ source_img = decoded * 2.0 - 1.0
693
+
694
+ p.image_conditioning = p.img2img_image_conditioning(source_img, samples)
695
+
696
+ # stage dims derived from the current latent (samples is already stage-sized)
697
+ stage_w = samples.shape[3] * opt_f
698
+ stage_h = samples.shape[2] * opt_f
699
+
700
+ return _stage_sample_img2img(
701
+ p, samples, noise, conditioning, unconditional_conditioning,
702
+ stage_w, stage_h, steps_for_refinement, sampler_profile,
703
+ )
704
+
705
+
706
+ # ─────────────────────────────────────────────────────────────────────────────
707
+ # Version implementations
708
+ # ─────────────────────────────────────────────────────────────────────────────
709
+
710
+ def sample_v1_exact(p, conditioning, unconditional_conditioning,
711
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
712
+ """
713
+ v1 (exact) – original implementation, preserved 1:1 as a regression baseline.
714
+ Does NOT use the pre-built (deduped) plan for sampling; rebuilds the raw
715
+ stage list from min_scale/max_scale/steps exactly as the original did.
716
+
717
+ For infotext honesty, a v1-native StagePlan is constructed from the same
718
+ raw stage list so PG plan reflects what v1 actually ran, not the deduped plan
719
+ that the wrapper built for guard purposes.
720
+ """
721
+ min_scale = float(p.progressive_growing_min_scale)
722
+ max_scale = float(p.progressive_growing_max_scale)
723
+ resolution_steps = np.linspace(min_scale, max_scale, int(p.progressive_growing_steps))
724
+
725
+ def _snap_v1(v):
726
  v_int = int(v)
727
  v_int = max(opt_f, v_int)
728
  v_int = (v_int // opt_f) * opt_f
729
  return max(opt_f, v_int)
730
 
731
+ # build the honest v1 stage list (duplicates preserved, no forced final snap)
732
+ v1_sizes = [
733
+ (_snap_v1(p.width * s), _snap_v1(p.height * s))
734
+ for s in resolution_steps
735
+ ]
736
+ v1_plan = StagePlan(
737
+ sizes=v1_sizes,
738
+ final_w=v1_sizes[-1][0],
739
+ final_h=v1_sizes[-1][1],
740
+ )
741
+
742
+ _write_params(p, v1_plan, refine_policy='all stages',
743
+ refine_step_policy='uniform (v1 fixed)')
744
+ sampler_profile, _ = _get_sampler_profile(p)
745
+ # v1 always uses bicubic regardless of the UI dropdown
746
+ try:
747
+ p.extra_generation_params['PG interp'] = 'bicubic (v1 fixed)'
748
+ if sampler_profile != 'standard':
749
+ p.extra_generation_params['PG sampler profile'] = sampler_profile
750
+ # v1 builds its stage list from the manual Stages slider, not from
751
+ # auto stage count – override the fields _write_params may have set
752
+ # so infotext describes what v1 actually ran, not what the user expected
753
+ if getattr(p, 'progressive_growing_auto_stages', False):
754
+ p.extra_generation_params['PG stages mode'] = 'manual (v1 legacy – auto ignored)'\
755
+
756
+ p.extra_generation_params.pop('PG target jump', None)
757
+ p.extra_generation_params.pop('PG requested', None)
758
+ p.extra_generation_params.pop('PG actual', None)
759
+ except Exception:
760
+ pass
761
+
762
+ initial_width, initial_height = v1_sizes[0]
763
 
 
764
  x = create_random_tensors(
765
  (opt_C, initial_height // opt_f, initial_width // opt_f),
766
+ seeds, subseeds=subseeds, subseed_strength=subseed_strength,
767
+ seed_resize_from_h=p.seed_resize_from_h,
768
+ seed_resize_from_w=p.seed_resize_from_w, p=p,
 
 
 
769
  )
770
 
771
+ image_cond = p.txt2img_image_conditioning(x, width=initial_width, height=initial_height)
772
+
773
+ samples = _stage_sample_txt2img(
774
+ p, x, conditioning, unconditional_conditioning,
775
+ image_cond, initial_width, initial_height, sampler_profile,
 
 
776
  )
777
 
778
  total_stages = len(resolution_steps)
779
 
 
780
  for i in range(1, total_stages):
781
+ target_width, target_height = v1_sizes[i]
 
782
 
 
783
  samples = torch.nn.functional.interpolate(
784
  samples,
785
  size=(target_height // opt_f, target_width // opt_f),
786
+ mode='bicubic', align_corners=False,
 
787
  )
788
 
789
+ if p.progressive_growing_refinement:
790
+ samples = _run_img2img_refinement(
791
+ p, samples, seeds, subseeds, subseed_strength,
792
+ conditioning, unconditional_conditioning,
793
+ refine_idx=i - 1,
794
+ n_refine=total_stages - 1,
795
+ refine_step_mode='uniform', # v1 legacy always uniform
796
+ sampler_profile=sampler_profile,
 
 
 
 
797
  )
798
 
799
+ return samples
800
+
801
+
802
+ # ─────────────────────────────────────────────────────────────────────────────
803
+
804
+ def _run_plan_with_policy(p, plan: StagePlan, conditioning, unconditional_conditioning,
805
+ seeds, subseeds, subseed_strength,
806
+ refine_predicate) -> torch.Tensor:
807
+ """
808
+ Shared loop for v2 / v4 / v5 / v6.
809
+
810
+ refine_predicate(stage_idx, w, h, plan) -> bool
811
+ Called for every stage after the first; return True to run refinement.
812
+
813
+ Reads p.progressive_growing_interp_mode for latent upscale (default: bicubic).
814
+ Reads p.progressive_growing_refine_step_mode for per-pass step budget.
815
+
816
+ Pre-computes the number of refine passes so _get_refine_steps can allocate
817
+ a budget that accounts for the total workload (needed by late-heavy / final-heavy).
818
+
819
+ Sampler compatibility
820
+ --------------------
821
+ _get_sampler_profile() is called once to determine the active profile.
822
+ 'smea' profile wraps every sampler.sample() / sample_img2img() call with
823
+ _stage_sampler_context(), which temporarily sets p.width/p.height to stage
824
+ dims and rescales p.init_latent/mask/nmask. This fixes spatial-sigma
825
+ schedulers and model-wrapper state mismatches without touching the rest of
826
+ the pipeline. The profile name is written to extra_generation_params as
827
+ 'PG sampler profile' when non-standard.
828
+ """
829
+ first_w, first_h = plan.sizes[0]
830
+ interp_mode = getattr(p, 'progressive_growing_interp_mode', LATENT_INTERP_DEFAULT)
831
+ step_mode = getattr(p, 'progressive_growing_refine_step_mode', REFINE_STEP_DEFAULT)
832
+ do_refine = getattr(p, 'progressive_growing_refinement', True)
833
+ sampler_profile, _ = _get_sampler_profile(p)
834
+
835
+ # record non-standard profile in infotext
836
+ if sampler_profile != 'standard':
837
+ try:
838
+ p.extra_generation_params['PG sampler profile'] = sampler_profile
839
+ except Exception:
840
+ pass
841
+
842
+ # pre-count how many stages will actually refine so budget can be distributed
843
+ n_refine = sum(
844
+ 1 for i, (w, h) in enumerate(plan.sizes[1:], start=1)
845
+ if do_refine and refine_predicate(i, w, h, plan)
846
+ ) if do_refine else 0
847
+
848
+ x = _initial_latent(p, first_w, first_h, seeds, subseeds, subseed_strength)
849
+ image_cond = p.txt2img_image_conditioning(x, width=first_w, height=first_h)
850
+
851
+ samples = _stage_sample_txt2img(
852
+ p, x, conditioning, unconditional_conditioning,
853
+ image_cond, first_w, first_h, sampler_profile,
854
+ )
855
+
856
+ refine_idx = 0
857
+ for i, (w, h) in enumerate(plan.sizes[1:], start=1):
858
+ samples = _upscale_latent(samples, w, h, interp_mode=interp_mode)
859
+
860
+ if do_refine and refine_predicate(i, w, h, plan):
861
+ samples = _run_img2img_refinement(
862
+ p, samples, seeds, subseeds, subseed_strength,
863
+ conditioning, unconditional_conditioning,
864
+ refine_idx=refine_idx,
865
+ n_refine=max(1, n_refine),
866
+ refine_step_mode=step_mode,
867
+ sampler_profile=sampler_profile,
868
  )
869
+ refine_idx += 1
870
 
871
  return samples
872
 
873
 
874
+ def sample_v2_safe(p, conditioning, unconditional_conditioning,
875
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
876
+ """
877
+ v2 (safe) – validated plan, dedup, correct per-stage conditioning size, script hooks.
878
+ Behaves identically to v1 where plans agree; differs only in edge cases.
879
+ """
880
+ _write_params(p, plan, refine_policy='all stages',
881
+ refine_step_policy=getattr(p, 'progressive_growing_refine_step_mode', REFINE_STEP_DEFAULT))
882
+
883
+ def always(i, w, h, plan):
884
+ return True
885
+
886
+ return _run_plan_with_policy(
887
+ p, plan, conditioning, unconditional_conditioning,
888
+ seeds, subseeds, subseed_strength,
889
+ refine_predicate=always,
890
+ )
891
+
892
+
893
+ def sample_v3_fast(p, conditioning, unconditional_conditioning,
894
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
895
+ """
896
+ v3 (fast) – refinement only on the final stage.
897
+ Avoids expensive VAE decode on every intermediate stage.
898
+ Best for quick iteration or large stage counts.
899
+ """
900
+ _write_params(p, plan, refine_policy='final stage only',
901
+ refine_step_policy=getattr(p, 'progressive_growing_refine_step_mode', REFINE_STEP_DEFAULT))
902
+
903
+ def only_last(i, w, h, plan):
904
+ return i == plan.n_stages - 1
905
+
906
+ return _run_plan_with_policy(
907
+ p, plan, conditioning, unconditional_conditioning,
908
+ seeds, subseeds, subseed_strength,
909
+ refine_predicate=only_last,
910
+ )
911
+
912
+
913
+ def sample_v4_balanced(p, conditioning, unconditional_conditioning,
914
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
915
+ """
916
+ v4 (balanced) – refinement on stages whose linear scale >= BALANCED_REFINE_THRESHOLD.
917
+
918
+ The threshold is a *linear* dimension ratio (sqrt of area ratio), so
919
+ BALANCED_REFINE_THRESHOLD=0.70 means: refine when the stage side is >= 70 %
920
+ of the final side, which corresponds to β‰ˆ 49 % of the final pixel area.
921
+ This avoids refinement on cheap small upscales while still covering the
922
+ detail-sensitive near-final stages.
923
+ """
924
+ _write_params(p, plan, refine_policy=f'linear scale >= {BALANCED_REFINE_THRESHOLD}',
925
+ refine_step_policy=getattr(p, 'progressive_growing_refine_step_mode', REFINE_STEP_DEFAULT))
926
+
927
+ final_area = plan.final_w * plan.final_h
928
+
929
+ def large_stages_only(i, w, h, plan):
930
+ # linear scale = sqrt(stage_area / final_area)
931
+ linear_scale = math.sqrt((w * h) / final_area) if final_area > 0 else 0.0
932
+ return linear_scale >= BALANCED_REFINE_THRESHOLD
933
+
934
+ return _run_plan_with_policy(
935
+ p, plan, conditioning, unconditional_conditioning,
936
+ seeds, subseeds, subseed_strength,
937
+ refine_predicate=large_stages_only,
938
+ )
939
+
940
+
941
+ def sample_v5_latent(p, conditioning, unconditional_conditioning,
942
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
943
+ """
944
+ v5 (latent only) – pure latent upscale, no refinement at any stage.
945
+ Fastest mode; useful to study the raw effect of latent-space growing.
946
+ """
947
+ _write_params(p, plan, refine_policy='none', refine_step_policy='none')
948
+
949
+ def never(i, w, h, plan):
950
+ return False
951
+
952
+ return _run_plan_with_policy(
953
+ p, plan, conditioning, unconditional_conditioning,
954
+ seeds, subseeds, subseed_strength,
955
+ refine_predicate=never,
956
+ )
957
+
958
+
959
+ def sample_v6_adaptive(p, conditioning, unconditional_conditioning,
960
+ seeds, subseeds, subseed_strength, prompts, plan: StagePlan):
961
+ """
962
+ v6 (adaptive) – refinement policy scales with stage count.
963
+
964
+ 2 stages β†’ refine every post-initial stage (progressive would be pointless otherwise)
965
+ 3–4 stages β†’ refine the last 2 stages only
966
+ 5+ stages β†’ refine stages whose linear scale >= ADAPTIVE_REFINE_THRESHOLD,
967
+ plus always force-refine the final stage
968
+
969
+ Sits between v3 (final only) and v2 (all stages): cheaper than v2 on long
970
+ plans, more thorough than v3 on short ones.
971
+ """
972
+ _write_params(p, plan, refine_policy=f'adaptive (threshold >= {ADAPTIVE_REFINE_THRESHOLD:.2f})',
973
+ refine_step_policy=getattr(p, 'progressive_growing_refine_step_mode', REFINE_STEP_DEFAULT))
974
+
975
+ final_area = plan.final_w * plan.final_h
976
+
977
+ def adaptive(i, w, h, plan):
978
+ n = plan.n_stages
979
+ if n <= 2:
980
+ # only 1 post-initial stage – always refine it
981
+ return True
982
+ if n <= 4:
983
+ # short plan: refine the last 2 stages
984
+ return i >= n - 2
985
+ # long plan: threshold + guaranteed final
986
+ linear_scale = math.sqrt((w * h) / final_area) if final_area > 0 else 0.0
987
+ return i == n - 1 or linear_scale >= ADAPTIVE_REFINE_THRESHOLD
988
+
989
+ return _run_plan_with_policy(
990
+ p, plan, conditioning, unconditional_conditioning,
991
+ seeds, subseeds, subseed_strength,
992
+ refine_predicate=adaptive,
993
+ )
994
+
995
+
996
+ # ─────────────────────────────────────────────────────────────────────────────
997
+ # Version registry
998
+ # ─────────────────────────────────────────────────────────────────────────────
999
+
1000
+ _VERSIONS: dict[str, callable] = {
1001
+ "v2 (safe)": sample_v2_safe,
1002
+ "v3 (fast)": sample_v3_fast,
1003
+ "v4 (balanced)": sample_v4_balanced,
1004
+ "v5 (latent)": sample_v5_latent,
1005
+ "v6 (adaptive)": sample_v6_adaptive,
1006
+ "v1 (exact)": sample_v1_exact, # legacy / regression reference
1007
  }
1008
 
1009
+ _DEFAULT_VERSION = "v2 (safe)"
1010
+
1011
 
1012
+ # ─────────────────────────────────────────────────────────────────────────────
1013
+ # Monkey-patch – thin wrapper, applied exactly once
1014
+ # ─────────────────────────────────────────────────────────────────────────────
1015
 
1016
+ _PATCHED = False
1017
  _ORIG_SAMPLE = None
1018
 
1019
 
1020
  def _apply_patch_once() -> None:
1021
+ """Patch StableDiffusionProcessingTxt2Img.sample once at first use."""
1022
 
1023
  global _PATCHED, _ORIG_SAMPLE
1024
  if _PATCHED:
 
1028
  if cls is None:
1029
  return
1030
 
1031
+ if getattr(cls, '_pg_ext_patched', False):
 
1032
  _PATCHED = True
1033
  return
1034
 
1035
  _ORIG_SAMPLE = cls.sample
1036
 
1037
+ def _sample_wrapper(self,
1038
+ conditioning, unconditional_conditioning,
1039
+ seeds, subseeds, subseed_strength, prompts):
1040
+
1041
+ ok, reason, plan = _should_use_progressive(self)
1042
 
1043
+ if ok:
1044
+ # For 'standard' profile: create sampler once here (mirrors what the
1045
+ # original sample() does internally).
1046
+ # For 'smea' profile: sampler is created fresh per stage-pass inside
1047
+ # _stage_sample_txt2img / _stage_sample_img2img so that all
1048
+ # size-dependent state (sigma schedules, spatial caches) is
1049
+ # initialised against stage dims, not the final target size.
1050
+ profile, _ = _get_sampler_profile(self)
1051
+ if profile == 'standard':
1052
+ self.sampler = sd_samplers.create_sampler(self.sampler_name, self.sd_model)
1053
 
1054
+ ver = getattr(self, 'progressive_growing_version', _DEFAULT_VERSION)
1055
+ fn = _VERSIONS.get(ver, sample_v2_safe)
1056
+ # plan was already built by _should_use_progressive – pass it through
1057
+ # so version fns don't call build_plan() a second time
1058
+ return fn(self, conditioning, unconditional_conditioning,
1059
+ seeds, subseeds, subseed_strength, prompts, plan)
1060
 
1061
+ # record skip reason if there was one
1062
+ if reason:
1063
+ try:
1064
+ self.extra_generation_params['PG skip'] = reason
1065
+ except Exception:
1066
+ pass
1067
+
1068
+ return _ORIG_SAMPLE(self, conditioning, unconditional_conditioning,
1069
+ seeds, subseeds, subseed_strength, prompts)
1070
+
1071
+ cls.sample = _sample_wrapper
1072
+ cls._pg_ext_patched = True
1073
  _PATCHED = True
1074
 
1075
 
1076
+ # ─────────────────────────────────────────────────────────────────────────────
1077
  # Always-visible UI script
1078
+ # ─────────────────────────────────────────────────────────────────────────────
 
1079
 
1080
  class ProgressiveGrowingAlwaysVisible(scripts.Script):
1081
+
1082
  def title(self):
1083
  return "Progressive Growing"
1084
 
1085
  def show(self, is_img2img):
 
1086
  return scripts.AlwaysVisible if not is_img2img else False
1087
 
1088
  def ui(self, is_img2img):
1089
  with gr.Accordion("Progressive Growing", open=False):
1090
  enabled = gr.Checkbox(value=False, label="Enable")
1091
+ version = gr.Dropdown(
1092
+ choices=list(_VERSIONS.keys()),
1093
+ value=_DEFAULT_VERSION,
1094
+ label="Mode",
1095
+ )
1096
+
1097
+ with gr.Row():
1098
+ min_scale = gr.Slider(
1099
+ minimum=0.1, maximum=1.0, step=0.05,
1100
+ value=0.25, label="Min scale",
1101
+ )
1102
+ max_scale = gr.Slider(
1103
+ minimum=0.1, maximum=1.0, step=0.05,
1104
+ value=1.0, label="Max scale",
1105
+ )
1106
 
1107
+ stages = gr.Slider(
1108
+ minimum=2, maximum=16, step=1,
1109
+ value=4, label="Stages (ignored when Auto is on)",
1110
+ )
1111
+ with gr.Row():
1112
+ auto_stages = gr.Checkbox(value=False, label="Auto stage count")
1113
+ auto_jump = gr.Slider(
1114
+ minimum=1.1, maximum=2.0, step=0.05,
1115
+ value=AUTO_STAGE_JUMP_DEFAULT,
1116
+ label="Target jump per stage",
1117
+ )
1118
+ auto_max = gr.Slider(
1119
+ minimum=2, maximum=12, step=1,
1120
+ value=AUTO_STAGE_MAX_DEFAULT,
1121
+ label="Max auto stages",
1122
+ )
1123
+ with gr.Row():
1124
+ refinement = gr.Checkbox(value=True, label="Refinement between stages")
1125
+ refine_step_mode = gr.Dropdown(
1126
+ choices=REFINE_STEP_MODES,
1127
+ value=REFINE_STEP_DEFAULT,
1128
+ label="Refinement step budget",
1129
+ )
1130
+ interp_mode = gr.Dropdown(
1131
+ choices=LATENT_INTERP_MODES,
1132
+ value=LATENT_INTERP_DEFAULT,
1133
+ label="Latent upscale interpolation",
1134
+ )
1135
 
1136
  gr.Markdown(
1137
+ "**Modes**\n"
1138
+ "- **v2 (safe)** – validated, deduped stages, refinement at every stage *(default)*\n"
1139
+ "- **v3 (fast)** – refinement only on the final stage; fastest, no VAE mid-pass\n"
1140
+ "- **v4 (balanced)** – refinement when stage side β‰₯ 70 % of final side (β‰ˆ 49 % of area)\n"
1141
+ "- **v5 (latent)** – pure latent upscale, zero refinement\n"
1142
+ "- **v6 (adaptive)** – 2 stages: all; 3–4 stages: last 2; 5+ stages: threshold + final\n"
1143
+ "- **v1 (exact)** – original implementation, kept for regression comparison; "
1144
+ "always uses manual Stages, ignores Auto stage count\n\n"
1145
+ "**Auto stage count** – ignores Stages slider; computes count so each upscale grows "
1146
+ "the latent side by ~Target jump (1.35 β‰ˆ 35 %). 0.25β†’1.0 β‰ˆ 6 stages, 0.5β†’1.0 β‰ˆ 3 stages.\n\n"
1147
+ "**Interpolation** – bicubic: smooth/photo; bilinear: softer; nearest: fastest/pixel-art; area: anti-aliased\n\n"
1148
+ "**Refinement budget** – uniform: equal steps per pass (legacy); "
1149
+ "late-heavy: growing budget towards final pass; "
1150
+ "final-heavy: minimal steps on all but the last refinement pass\n\n"
1151
+ "⚠ Incompatible with **Hires. fix** β€” enabling both disables Progressive Growing."
1152
  )
1153
 
1154
+ return [enabled, version, min_scale, max_scale,
1155
+ stages, auto_stages, auto_jump, auto_max,
1156
+ refinement, refine_step_mode, interp_mode]
1157
 
1158
+ def process(self, p,
1159
+ enabled, version, min_scale, max_scale,
1160
+ stages, auto_stages, auto_jump, auto_max,
1161
+ refinement, refine_step_mode, interp_mode):
1162
 
1163
+ _apply_patch_once()
 
 
 
 
 
 
1164
 
1165
+ p.enable_progressive_growing = bool(enabled)
1166
+ p.progressive_growing_version = str(version)
1167
+ p.progressive_growing_min_scale = float(min_scale)
1168
+ p.progressive_growing_max_scale = float(max_scale)
1169
+ p.progressive_growing_steps = int(stages)
1170
+ p.progressive_growing_auto_stages = bool(auto_stages)
1171
+ p.progressive_growing_auto_jump = float(auto_jump)
1172
+ p.progressive_growing_auto_max = int(auto_max)
1173
+ p.progressive_growing_refinement = bool(refinement)
1174
+ p.progressive_growing_refine_step_mode = str(refine_step_mode) if refine_step_mode in REFINE_STEP_MODES else REFINE_STEP_DEFAULT
1175
+ p.progressive_growing_interp_mode = str(interp_mode) if interp_mode in LATENT_INTERP_MODES else LATENT_INTERP_DEFAULT