dikdimon commited on
Commit
6690a57
·
verified ·
1 Parent(s): eadb5a4

Update asymmetric-tiling-sd-webui-2.0/scripts/asymmetric_tiling.py

Browse files
asymmetric-tiling-sd-webui-2.0/scripts/asymmetric_tiling.py CHANGED
@@ -1,732 +1,976 @@
1
- import torch
2
- import torch.nn as nn
3
- import torch.nn.functional as F
4
- import gradio as gr
5
- from modules import scripts, shared, sd_samplers, sd_samplers_common, sd_samplers_kdiffusion
6
- import k_diffusion.sampling
7
- from k_diffusion.sampling import to_d, default_noise_sampler, get_ancestral_step
8
- from tqdm.auto import trange
9
- import math
10
- import numpy as np
11
-
12
- # ========================================================================
13
- # КОНСТАНТЫ И КОНФИГУРАЦИЯ
14
- # ========================================================================
15
- MODE_OFF = "Default (Off)"
16
- MODE_CIRCULAR = "Circular"
17
- MODE_MIRROR = "Mirror (Reflect)"
18
- MODE_HEXAGONAL = "Hexagonal (Staggered)"
19
- MODE_PANORAMA = "Panorama 360°"
20
- MODE_CUBEMAP = "Cubemap (3D)"
21
- MODE_BLEND = "Soft Blend Edges"
22
- MODE_ANISOTROPIC = "Anisotropic (Directional)"
23
- MODE_POLAR = "Polar (Sphere Correct)"
24
-
25
- # Глобальное хранилище
26
- _ORIGINAL_METHODS_CACHE = {}
27
- _MASK_CACHE = {}
28
- _SAMPLER_REGISTERED = False
29
-
30
- # ========================================================================
31
- # KOHAKU LONYU YOG SAMPLER IMPLEMENTATION
32
- # ========================================================================
33
-
34
- @torch.no_grad()
35
- def sample_kohaku_lonyu_yog(model, x, sigmas, extra_args=None, callback=None,
36
- disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'),
37
- s_noise=1., noise_sampler=None, eta=1.):
38
- """
39
- Kohaku_LoNyu_Yog Sampler - Geometric Second-Order Method
40
-
41
- Принцип работы:
42
- 1. Использует геометрические эвристики в 3D подпространстве
43
- 2. Находит антипод (-x) для определения направления
44
- 3. Вычисляет градиенты d, d2 в разных точках
45
- 4. Усредняет (d+d2)/2 для нахождения "вектора вниз" к визуальному многообразию
46
- 5. Корректирует позицию и вычисляет d3 для финальной траектории
47
- 6. Применяется только на первой половине шагов (выпуклая область)
48
-
49
- Теория: Поскольку 3D пространство - подпространство многомерного,
50
- все операции в 3D осуществимы в высокоразмерном пространстве латентов.
51
- """
52
- extra_args = {} if extra_args is None else extra_args
53
- s_in = x.new_ones([x.shape[0]])
54
- noise_sampler = default_noise_sampler(x) if noise_sampler is None else noise_sampler
55
-
56
- steps_total = len(sigmas) - 1
57
- halfway_point = steps_total // 2
58
-
59
- for i in trange(steps_total, disable=disable, desc="Kohaku Sampling"):
60
- # Ancestral noise injection (опционально)
61
- gamma = min(s_churn / steps_total, 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
62
- sigma_hat = sigmas[i] * (gamma + 1)
63
-
64
- if gamma > 0:
65
- eps = torch.randn_like(x) * s_noise
66
- x = x + eps * (sigma_hat ** 2 - sigmas[i] ** 2) ** 0.5
67
-
68
- # Первый вызов модели - базовый градиент
69
- denoised = model(x, sigma_hat * s_in, **extra_args)
70
- d = to_d(x, sigma_hat, denoised)
71
-
72
- # Вычисление шага
73
- sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1], eta=eta)
74
- dt = sigma_down - sigmas[i]
75
-
76
- # ====== ГЕОМЕТРИЧЕСКИЙ МЕТОД (Первая половина шагов) ======
77
- if i <= halfway_point:
78
- # Шаг 1: Находим антипод (-x)
79
- # Это ключевая идея: рассматриваем противоположную точку
80
- x_antipode = -x
81
-
82
- # Шаг 2: Вычисляем градиент d2 в антиподе
83
- denoised2 = model(x_antipode, sigma_hat * s_in, **extra_args)
84
- d2 = to_d(x_antipode, sigma_hat, denoised2)
85
-
86
- # Шаг 3: Геометрическая дедукция
87
- # Вектор (d+d2)/2 указывает "вниз" к визуальному многообразию
88
- v_down = (d + d2) / 2
89
-
90
- # Шаг 4: Коррекция позиции (Lookahead)
91
- # Двигаемся к точке, которая ближе к целевой области A
92
- x_closer = x + v_down * dt
93
-
94
- # Шаг 5: Вычисляем скорость d3 в уточненной точке
95
- denoised3 = model(x_closer, sigma_hat * s_in, **extra_args)
96
- d3 = to_d(x_closer, sigma_hat, denoised3)
97
-
98
- # Шаг 6: Финальная траектория
99
- # (d+d3)/2 дае�� более точное направление к истинной цели
100
- real_d = (d + d3) / 2
101
- x = x + real_d * dt
102
-
103
- # Добавляем ancestral шум
104
- if sigma_up > 0:
105
- x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
106
-
107
- # ====== СТАНДАРТНЫЙ EULER (Вторая половина) ======
108
- else:
109
- # На поздних шагах геометрический метод может отклоняться
110
- # Причина: траектория становится невыпуклой из-за деталей
111
- x = x + d * dt
112
-
113
- if sigma_up > 0:
114
- x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
115
-
116
- # Callback для мониторинга
117
- if callback is not None:
118
- callback({
119
- 'x': x,
120
- 'i': i,
121
- 'sigma': sigmas[i],
122
- 'sigma_hat': sigma_hat,
123
- 'denoised': denoised
124
- })
125
-
126
- return x
127
-
128
- # ========================================================================
129
- # РАСШИРЕННЫЕ ФУНКЦИИ ПАДДИНГА
130
- # ========================================================================
131
-
132
- def get_or_create_mask(h, w, device):
133
- """Кэширование масок для оптимизации"""
134
- key = (h, w, str(device))
135
- if key not in _MASK_CACHE:
136
- row_indices = torch.arange(h, device=device).view(1, 1, h, 1)
137
- _MASK_CACHE[key] = (row_indices % 2 == 1)
138
- return _MASK_CACHE[key]
139
-
140
- def compute_anisotropic_padding(input_tensor, pad_h, pad_w, angle_deg=45):
141
- """
142
- Анизотропный паддинг - разное поведение по диагоналям.
143
- Эмулирует направленные материалы (дерево, металл, волокна).
144
-
145
- Args:
146
- angle_deg: Угол направления волокон/текстуры (0-360)
147
- """
148
- b, c, h, w = input_tensor.shape
149
-
150
- # Преобразуем угол в радианы
151
- angle_rad = math.radians(angle_deg)
152
-
153
- # Создаем направленную маску весов
154
- # Вычисляем "силу" паддинга в зависимости от направления
155
- y_coords = torch.linspace(-1, 1, h, device=input_tensor.device).view(1, 1, h, 1)
156
- x_coords = torch.linspace(-1, 1, w, device=input_tensor.device).view(1, 1, 1, w)
157
-
158
- # Проекция на направление волокон
159
- directional_strength = torch.abs(
160
- x_coords * math.cos(angle_rad) + y_coords * math.sin(angle_rad)
161
- )
162
-
163
- # Вдоль волокон - circular, поперек - reflect
164
- # Сначала применяем базовый circular паддинг
165
- padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
166
-
167
- # Создаем альтернативный reflect паддинг
168
- padded_reflect = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='reflect')
169
-
170
- # Смешиваем на основе направления
171
- directional_strength_expanded = directional_strength.expand(b, c, h + 2*pad_h, w + 2*pad_w)
172
-
173
- # Альфа-блендинг: вдоль направления больше circular, поперек больше reflect
174
- alpha = directional_strength_expanded.clamp(0, 1)
175
- result = padded * alpha + padded_reflect * (1 - alpha)
176
-
177
- return result
178
-
179
- def compute_polar_padding(input_tensor, pad_h, pad_w):
180
- """
181
- Полярный паддинг для сферических проекций.
182
- Корректирует переход через полюса с учетом топологии сферы.
183
-
184
- При переходе через Северный полюс:
185
- - Сдвиг на W/2 (противоположная сторона сферы)
186
- - Отражение (инверсия широты)
187
- """
188
- b, c, h, w = input_tensor.shape
189
-
190
- # X-axis: стандартный circular (долгота замыкается)
191
- x = F.pad(input_tensor, (pad_w, pad_w, 0, 0), mode='circular')
192
-
193
- # Y-axis: полярная коррекция (широта через полюса)
194
- shift = w // 2
195
-
196
- # Верхний паддинг (Северный полюс)
197
- top_strip = x[:, :, :pad_h, :] # Берем верхние строки
198
- top_pad = torch.roll(top_strip, shifts=shift, dims=3) # Сдвиг на 180°
199
- top_pad = torch.flip(top_pad, dims=[2]) # Отражение (инверсия)
200
-
201
- # Нижний паддинг (Южный полюс)
202
- bot_strip = x[:, :, -pad_h:, :]
203
- bot_pad = torch.roll(bot_strip, shifts=shift, dims=3)
204
- bot_pad = torch.flip(bot_pad, dims=[2])
205
-
206
- # ��онкатенация
207
- result = torch.cat([top_pad, x, bot_pad], dim=2)
208
-
209
- return result
210
-
211
- def compute_stereoscopic_padding(input_tensor, pad_h, pad_w, eye='left',
212
- convergence=0.05, separation=0.065):
213
- """
214
- Стереоскопический паддинг для 3D изображений.
215
-
216
- Args:
217
- eye: 'left' или 'right' - какой глаз
218
- convergence: Точка схождения (0-1, где 0.5 - центр)
219
- separation: Межзрачковое расстояние в долях от ширины
220
- """
221
- b, c, h, w = input_tensor.shape
222
-
223
- # Вычисляем сдвиг в зависимости от глаза
224
- # Левый глаз видит правую часть объектов, правый - левую
225
- shift_amount = int(w * separation)
226
-
227
- if eye == 'left':
228
- # Левый глаз: сдвиг вправо
229
- shifted = torch.roll(input_tensor, shifts=shift_amount, dims=3)
230
- else:
231
- # Правый глаз: сдвиг влево
232
- shifted = torch.roll(input_tensor, shifts=-shift_amount, dims=3)
233
-
234
- # Создаем карту глубины на основе положения пикселя
235
- # Центр имеет меньший сдвиг (ближе), края больший (дальше)
236
- x_coords = torch.linspace(0, 1, w, device=input_tensor.device).view(1, 1, 1, w)
237
- depth_map = torch.abs(x_coords - convergence).expand(b, c, h, w)
238
-
239
- # Смешиваем оригинал и сдвинутый на основе глубины
240
- # Ближние объекты - больше сдвиг, дальние - меньше
241
- alpha = depth_map.clamp(0, 1)
242
- stereo_adjusted = input_tensor * (1 - alpha) + shifted * alpha
243
-
244
- # Применяем стандартный circular паддинг
245
- padded = F.pad(stereo_adjusted, (pad_w, pad_w, pad_h, pad_h), mode='circular')
246
-
247
- return padded
248
-
249
- def compute_hex_padding_x(input_tensor, pad_l, pad_r):
250
- """Гексагональный паддинг (из v2.0)"""
251
- b, c, h, w = input_tensor.shape
252
- odd_mask = get_or_create_mask(h, w, input_tensor.device).expand(b, c, h, w)
253
-
254
- shift = w // 2
255
- input_shifted = torch.roll(input_tensor, shifts=shift, dims=3)
256
- source = torch.where(odd_mask, input_shifted, input_tensor)
257
-
258
- left_pad = source[:, :, :, -pad_l:]
259
- right_pad = source[:, :, :, :pad_r]
260
-
261
- return torch.cat([left_pad, input_tensor, right_pad], dim=3)
262
-
263
- def compute_blend_padding(input_tensor, pad_h, pad_w, blend_strength=0.1):
264
- """Мягкое смешивание краев (из v2.0)"""
265
- b, c, h, w = input_tensor.shape
266
- padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
267
-
268
- blend_w = int(w * blend_strength)
269
- blend_h = int(h * blend_strength)
270
-
271
- if blend_h > 0:
272
- # Вертикальное затухание
273
- top_mask = torch.linspace(0, 1, blend_h, device=input_tensor.device)
274
- top_mask = top_mask.view(1, 1, blend_h, 1).expand(b, c, blend_h, w + 2*pad_w)
275
- padded[:, :, pad_h:pad_h+blend_h, :] *= top_mask
276
-
277
- bottom_mask = torch.linspace(1, 0, blend_h, device=input_tensor.device)
278
- bottom_mask = bottom_mask.view(1, 1, blend_h, 1).expand(b, c, blend_h, w + 2*pad_w)
279
- padded[:, :, pad_h+h-blend_h:pad_h+h, :] *= bottom_mask
280
-
281
- if blend_w > 0:
282
- # Горизонтальное затухание
283
- left_mask = torch.linspace(0, 1, blend_w, device=input_tensor.device)
284
- left_mask = left_mask.view(1, 1, 1, blend_w).expand(b, c, h + 2*pad_h, blend_w)
285
- padded[:, :, :, pad_w:pad_w+blend_w] *= left_mask
286
-
287
- right_mask = torch.linspace(1, 0, blend_w, device=input_tensor.device)
288
- right_mask = right_mask.view(1, 1, 1, blend_w).expand(b, c, h + 2*pad_h, blend_w)
289
- padded[:, :, :, pad_w+w-blend_w:pad_w+w] *= right_mask
290
-
291
- return padded
292
-
293
- # ========================================================================
294
- # ГЛАВНАЯ ФУНКЦИЯ ПАДДИНГА
295
- # ========================================================================
296
-
297
- def custom_padding_forward(input_tensor, weight, bias, stride, padding, dilation, groups, params):
298
- """
299
- Универсальная функция паддинга с поддержкой всех режимов v3.0
300
- """
301
- try:
302
- # Проверка шагов
303
- current_step = getattr(shared.state, 'sampling_step', 0)
304
- if not (params['start_step'] <= current_step <= params['end_step']):
305
- return F.conv2d(input_tensor, weight, bias, stride, padding, dilation, groups)
306
-
307
- # Вычисление размеров паддинга
308
- k_h, k_w = weight.shape[2], weight.shape[3]
309
- d_h, d_w = (dilation, dilation) if isinstance(dilation, int) else dilation
310
-
311
- if isinstance(padding, str):
312
- if padding == 'same':
313
- req_pad_h = ((k_h - 1) * d_h) // 2
314
- req_pad_w = ((k_w - 1) * d_w) // 2
315
- elif padding == 'valid':
316
- req_pad_h = req_pad_w = 0
317
- else:
318
- req_pad_h = req_pad_w = 1
319
- elif isinstance(padding, int):
320
- req_pad_h = req_pad_w = padding
321
- elif isinstance(padding, (tuple, list)):
322
- req_pad_h, req_pad_w = padding[0], padding[1]
323
- else:
324
- req_pad_h = req_pad_w = 0
325
-
326
- x = input_tensor
327
- mode_x = params['mode_x']
328
- mode_y = params['mode_y']
329
-
330
- # ====== СПЕЦИАЛЬНЫЕ РЕЖИМЫ ======
331
-
332
- # Стереоскопия
333
- if params.get('stereo_enabled', False):
334
- eye = params.get('stereo_eye', 'left')
335
- convergence = params.get('stereo_convergence', 0.5)
336
- separation = params.get('stereo_separation', 0.065)
337
- x = compute_stereoscopic_padding(x, req_pad_h, req_pad_w, eye, convergence, separation)
338
-
339
- # Анизотропный
340
- elif mode_x == MODE_ANISOTROPIC or mode_y == MODE_ANISOTROPIC:
341
- angle = params.get('anisotropic_angle', 45)
342
- x = compute_anisotropic_padding(x, req_pad_h, req_pad_w, angle)
343
-
344
- # Полярный (сферический)
345
- elif mode_x == MODE_POLAR or mode_y == MODE_POLAR:
346
- x = compute_polar_padding(x, req_pad_h, req_pad_w)
347
-
348
- # Панорама
349
- elif mode_x == MODE_PANORAMA or mode_y == MODE_PANORAMA:
350
- x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='circular')
351
- x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
352
-
353
- # Blend
354
- elif mode_x == MODE_BLEND or mode_y == MODE_BLEND:
355
- blend_str = params.get('blend_strength', 0.1)
356
- x = compute_blend_padding(x, req_pad_h, req_pad_w, blend_str)
357
-
358
- # Стандартные режимы (раздельно по осям)
359
- else:
360
- # Ось Y
361
- if mode_y == MODE_CIRCULAR:
362
- x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
363
- elif mode_y == MODE_MIRROR:
364
- x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='reflect')
365
- elif mode_y == MODE_HEXAGONAL:
366
- x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
367
- else:
368
- x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='constant', value=0)
369
-
370
- # Ось X
371
- if mode_x == MODE_CIRCULAR:
372
- x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='circular')
373
- elif mode_x == MODE_MIRROR:
374
- x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='reflect')
375
- elif mode_x == MODE_HEXAGONAL:
376
- x = compute_hex_padding_x(x, req_pad_w, req_pad_w)
377
- else:
378
- x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='constant', value=0)
379
-
380
- # Мультиразрешение (адаптивная интенсивность)
381
- if params.get('multires_enabled', False):
382
- step_ratio = current_step / max(params['end_step'], 1)
383
- if step_ratio < 0.3:
384
- x_default = F.pad(input_tensor, (req_pad_w, req_pad_w, req_pad_h, req_pad_h),
385
- mode='constant', value=0)
386
- alpha = step_ratio * 3
387
- x = x * alpha + x_default * (1 - alpha)
388
-
389
- # Выполнение свертки
390
- return F.conv2d(x, weight, bias, stride, 0, dilation, groups)
391
-
392
- except Exception as e:
393
- print(f"Advanced Tiling v3 Error: {e}")
394
- return F.conv2d(input_tensor, weight, bias, stride, padding, dilation, groups)
395
-
396
- # ========================================================================
397
- # РЕГИСТРАЦИЯ KOHAKU SAMPLER
398
- # ========================================================================
399
-
400
- def register_kohaku_sampler():
401
- """Регистрирует Kohaku_LoNyu_Yog сэмплер в WebUI"""
402
- global _SAMPLER_REGISTERED
403
-
404
- if _SAMPLER_REGISTERED:
405
- return
406
-
407
- # Проверка на дубликаты
408
- if any(s.name == 'Kohaku_LoNyu_Yog' for s in sd_samplers.all_samplers):
409
- _SAMPLER_REGISTERED = True
410
- return
411
-
412
- # Добавляем функцию в k_diffusion.sampling
413
- if not hasattr(k_diffusion.sampling, 'sample_kohaku_lonyu_yog'):
414
- setattr(k_diffusion.sampling, 'sample_kohaku_lonyu_yog', sample_kohaku_lonyu_yog)
415
-
416
- # Создаем SamplerData
417
- sampler_data = sd_samplers_common.SamplerData(
418
- name='Kohaku_LoNyu_Yog',
419
- constructor=lambda model: sd_samplers_kdiffusion.KDiffusionSampler('sample_kohaku_lonyu_yog', model),
420
- aliases=['kohaku', 'lonyu'],
421
- options={'second_order': True}
422
- )
423
-
424
- # Регистрируем
425
- sd_samplers.all_samplers.append(sampler_data)
426
- sd_samplers.all_samplers_map = {x.name: x for x in sd_samplers.all_samplers}
427
-
428
- _SAMPLER_REGISTERED = True
429
- print(" Kohaku_LoNyu_Yog sampler registered successfully!")
430
-
431
- # ========================================================================
432
- # КЛАСС СКРИПТА
433
- # ========================================================================
434
-
435
- class AdvancedTilingScriptV3(scripts.Script):
436
- def title(self):
437
- return "Advanced Tiling v3.0 PRO (Kohaku + Stereo + Aniso)"
438
-
439
- def show(self, is_img2img):
440
- return scripts.AlwaysVisible
441
-
442
- def ui(self, is_img2img):
443
- with gr.Accordion("🚀 Advanced Tiling v3.0 PRO Edition", open=False):
444
- with gr.Row():
445
- enabled = gr.Checkbox(label="Enable Tiling", value=False)
446
- use_kohaku = gr.Checkbox(label="Use Kohaku_LoNyu_Yog Sampler", value=False,
447
- info="Geometric second-order method for better seamless quality")
448
-
449
- with gr.Tabs():
450
- with gr.Tab("🎨 Basic Modes"):
451
- with gr.Row():
452
- mode_x = gr.Dropdown(
453
- label="Mode X",
454
- choices=[MODE_OFF, MODE_CIRCULAR, MODE_MIRROR, MODE_HEXAGONAL],
455
- value=MODE_CIRCULAR
456
- )
457
- mode_y = gr.Dropdown(
458
- label="Mode Y",
459
- choices=[MODE_OFF, MODE_CIRCULAR, MODE_MIRROR, MODE_HEXAGONAL],
460
- value=MODE_CIRCULAR
461
- )
462
-
463
- with gr.Row():
464
- multires = gr.Checkbox(label="Multi-Resolution Mode", value=False)
465
- use_blend = gr.Checkbox(label="Soft Blend Edges", value=False)
466
- blend_strength = gr.Slider(0.0, 0.5, step=0.05, label="Blend Strength", value=0.1)
467
-
468
- with gr.Tab("🌐 Advanced Modes"):
469
- gr.Markdown("**Специальные режимы для сложных топологий**")
470
-
471
- with gr.Row():
472
- use_panorama = gr.Checkbox(label="Panorama 360°", value=False)
473
- use_polar = gr.Checkbox(label="Polar (Sphere Correct)", value=False,
474
- info="Correct pole transitions for equirectangular")
475
-
476
- with gr.Row():
477
- use_anisotropic = gr.Checkbox(label="Anisotropic (Directional)", value=False,
478
- info="Different behavior along diagonals")
479
- aniso_angle = gr.Slider(0, 360, step=15, label="Anisotropic Angle", value=45,
480
- info="Direction of fibers/texture (degrees)")
481
-
482
- with gr.Tab("🎭 Stereoscopic 3D"):
483
- gr.Markdown("**Генерация стереопар для 3D контента**")
484
-
485
- with gr.Row():
486
- stereo_enabled = gr.Checkbox(label="Enable Stereoscopic Mode", value=False)
487
- stereo_eye = gr.Radio(
488
- label="Eye",
489
- choices=["left", "right", "both"],
490
- value="left",
491
- info="Which eye view to generate"
492
- )
493
-
494
- with gr.Row():
495
- stereo_separation = gr.Slider(0.0, 0.15, step=0.005,
496
- label="IPD (Inter-Pupillary Distance)",
497
- value=0.065,
498
- info="Eye separation as fraction of width")
499
- stereo_convergence = gr.Slider(0.0, 1.0, step=0.05,
500
- label="Convergence Point",
501
- value=0.5,
502
- info="Depth at which eyes converge")
503
-
504
- gr.Markdown("""
505
- **💡 Совет для стерео:**
506
- - Генерируйте сначала левый глаз
507
- - Затем правый с теми же параметрами
508
- - Используйте Side-by-Side или Anaglyph компоновку
509
- """)
510
-
511
- with gr.Row():
512
- start_step = gr.Slider(0, 150, step=1, label="Start Step", value=0)
513
- end_step = gr.Slider(0, 150, step=1, label="End Step", value=150)
514
-
515
- with gr.Row():
516
- patch_vae = gr.Checkbox(label="Patch VAE Decoder", value=True,
517
- info="Fix seams in final pixel decode")
518
-
519
- # Preview Tool
520
- with gr.Accordion("🔍 Preview & Info", open=False):
521
- with gr.Tabs():
522
- with gr.Tab("Visual Preview"):
523
- preview_btn = gr.Button("Generate Preview", variant="primary")
524
- preview_img = gr.Image(label="Preview Grid (2x2)")
525
-
526
- preview_btn.click(
527
- fn=self.generate_preview,
528
- inputs=[mode_x, mode_y, use_anisotropic, aniso_angle,
529
- stereo_enabled, stereo_eye],
530
- outputs=preview_img
531
- )
532
-
533
- with gr.Tab("Kohaku Info"):
534
- gr.Markdown("""
535
- ## 🔬 Kohaku_LoNyu_Yog Принцип работы
536
-
537
- **Геометрическая интерпретация:**
538
- 1. Трёхмерное пространство - подпространство многомерного латентного
539
- 2. Находим антипод `-x` (противоположная точка)
540
- 3. Вычисляем градиенты `d` и `d2`
541
- 4. `(d+d2)/2` - вектор к визуальному многообразию
542
- 5. Корректируем позицию: `x_new = x + (d+d2)/2 * dt`
543
- 6. Финальный градиент `d3` в новой точке
544
- 7. Итоговый шаг: `(d+d3)/2`
545
-
546
- **Применение только на первой половине шагов** - на поздних стадиях
547
- траектория становится невыпуклой из-за деталей, метод отклоняется.
548
-
549
- **NFE (Function Evaluations):** ~3-4 на шаг (дороже стандартного Euler)
550
- **Рекомендуемые шаги:** 8-12 (вместо 20-30)
551
- """)
552
-
553
- return [enabled, mode_x, mode_y, start_step, end_step, multires,
554
- use_panorama, use_polar, use_blend, blend_strength,
555
- use_anisotropic, aniso_angle,
556
- stereo_enabled, stereo_eye, stereo_separation, stereo_convergence,
557
- patch_vae, use_kohaku]
558
-
559
- def generate_preview(self, mx, my, use_aniso, aniso_angle, stereo, eye):
560
- """Генерация preview с учетом новых режимов"""
561
- h, w = 256, 256
562
- img = np.zeros((h, w, 3), dtype=np.uint8)
563
-
564
- # Базовый градиент
565
- for i in range(h):
566
- for j in range(w):
567
- col = (i + j) % 255
568
- if i < 5 or i > h-5 or j < 5 or j > w-5:
569
- img[i, j] = [255, 50, 50]
570
- else:
571
- img[i, j] = [col, col//2, 255-col]
572
-
573
- def get_tile(r, c):
574
- tile = img.copy()
575
-
576
- if use_aniso:
577
- # Визуализация анизотропии
578
- angle_rad = np.radians(aniso_angle)
579
- for i in range(h):
580
- for j in range(w):
581
- # Создаем направленный паттерн
582
- dist = abs((j-w/2)*np.cos(angle_rad) + (i-h/2)*np.sin(angle_rad))
583
- tile[i, j] = tile[i, j] * (0.5 + 0.5 * np.sin(dist * 0.1))
584
-
585
- elif stereo:
586
- # Эмуляция сдвига стерео
587
- shift = 10 if eye == 'left' else -10
588
- tile = np.roll(tile, shift, axis=1)
589
-
590
- else:
591
- if mx == MODE_MIRROR and c % 2 != 0:
592
- tile = np.fliplr(tile)
593
- if my == MODE_MIRROR and r % 2 != 0:
594
- tile = np.flipud(tile)
595
- if mx == MODE_HEXAGONAL and r % 2 != 0:
596
- tile = np.roll(tile, w // 2, axis=1)
597
-
598
- return tile.astype(np.uint8)
599
-
600
- # Сборка 2x2
601
- canvas = np.zeros((h*2, w*2, 3), dtype=np.uint8)
602
- for r in range(2):
603
- for c in range(2):
604
- canvas[r*h:(r+1)*h, c*w:(c+1)*w] = get_tile(r, c)
605
-
606
- return canvas
607
-
608
- def process(self, p, enabled, mode_x, mode_y, start_step, end_step, multires,
609
- use_panorama, use_polar, use_blend, blend_strength,
610
- use_anisotropic, aniso_angle,
611
- stereo_enabled, stereo_eye, stereo_separation, stereo_convergence,
612
- patch_vae, use_kohaku):
613
-
614
- if not enabled:
615
- return
616
-
617
- # Валидация
618
- if start_step > end_step:
619
- start_step, end_step = end_step, start_step
620
-
621
- # Регистрация Kohaku сэмплера
622
- if use_kohaku:
623
- register_kohaku_sampler()
624
- # Автоматически переключаем на Kohaku если выбран другой
625
- if p.sampler_name != 'Kohaku_LoNyu_Yog':
626
- print(f"Switching sampler from {p.sampler_name} to Kohaku_LoNyu_Yog")
627
- p.sampler_name = 'Kohaku_LoNyu_Yog'
628
-
629
- # Применение специальных режимов
630
- if use_panorama:
631
- mode_x = mode_y = MODE_PANORAMA
632
- if use_polar:
633
- mode_x = mode_y = MODE_POLAR
634
- if use_blend:
635
- mode_x = mode_y = MODE_BLEND
636
- if use_anisotropic:
637
- mode_x = mode_y = MODE_ANISOTROPIC
638
-
639
- # Параметры
640
- params = {
641
- 'mode_x': mode_x,
642
- 'mode_y': mode_y,
643
- 'start_step': start_step,
644
- 'end_step': end_step,
645
- 'multires_enabled': multires,
646
- 'blend_strength': blend_strength,
647
- 'anisotropic_angle': aniso_angle,
648
- 'stereo_enabled': stereo_enabled,
649
- 'stereo_eye': stereo_eye,
650
- 'stereo_separation': stereo_separation,
651
- 'stereo_convergence': stereo_convergence
652
- }
653
-
654
- # Патчинг UNet
655
- unet = self.get_unet(p)
656
- if unet:
657
- self.restore_original(unet)
658
- count_unet = self.patch_conv_layers(unet, params)
659
- print(f"✓ Patched {count_unet} UNet Conv2d layers")
660
-
661
- # Патчинг VAE
662
- if patch_vae and hasattr(p.sd_model, 'first_stage_model'):
663
- vae = p.sd_model.first_stage_model
664
- count_vae = self.patch_conv_layers(vae, params)
665
- print(f"✓ Patched {count_vae} VAE Conv2d layers")
666
-
667
- mode_str = f"{mode_x}/{mode_y}"
668
- if stereo_enabled:
669
- mode_str += f" + Stereo({stereo_eye})"
670
- if use_kohaku:
671
- mode_str += " + Kohaku"
672
-
673
- print(f"✓ Advanced Tiling v3.0: Mode={mode_str}, Steps={start_step}-{end_step}")
674
-
675
- def patch_conv_layers(self, module, params):
676
- """Патчинг с исправленным замыканием"""
677
- count = 0
678
- for layer in module.modules():
679
- if isinstance(layer, torch.nn.Conv2d):
680
- if layer.kernel_size == (1, 1) or layer.kernel_size == 1:
681
- continue
682
-
683
- if layer not in _ORIGINAL_METHODS_CACHE:
684
- _ORIGINAL_METHODS_CACHE[layer] = layer._conv_forward
685
-
686
- def make_patched(mod):
687
- def patched(input, weight, bias):
688
- return custom_padding_forward(
689
- input, weight, bias,
690
- mod.stride, mod.padding, mod.dilation, mod.groups,
691
- params
692
- )
693
- return patched
694
-
695
- layer._conv_forward = make_patched(layer)
696
- count += 1
697
-
698
- return count
699
-
700
- def get_unet(self, p):
701
- """Универсальная детекция UNet"""
702
- if hasattr(p.sd_model, 'forge_objects') and hasattr(p.sd_model.forge_objects, 'unet'):
703
- return p.sd_model.forge_objects.unet
704
- elif hasattr(p.sd_model, 'model') and hasattr(p.sd_model.model, 'diffusion_model'):
705
- return p.sd_model.model.diffusion_model
706
- return p.sd_model
707
-
708
- def postprocess(self, p, processed, *args):
709
- """Восстановление"""
710
- unet = self.get_unet(p)
711
- if unet:
712
- self.restore_original(unet)
713
-
714
- if hasattr(p.sd_model, 'first_stage_model'):
715
- self.restore_original(p.sd_model.first_stage_model)
716
-
717
- def restore_original(self, module):
718
- """Восстановление оригинальных методов"""
719
- if not _ORIGINAL_METHODS_CACHE:
720
- return
721
-
722
- restored = 0
723
- for layer in list(_ORIGINAL_METHODS_CACHE.keys()):
724
- if hasattr(layer, '_conv_forward'):
725
- layer._conv_forward = _ORIGINAL_METHODS_CACHE[layer]
726
- restored += 1
727
-
728
- _ORIGINAL_METHODS_CACHE.clear()
729
- _MASK_CACHE.clear()
730
-
731
- if restored > 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
  print(f"✓ Restored {restored} layers to original state")
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import gradio as gr
5
+ from modules import scripts, shared, sd_samplers, sd_samplers_common, sd_samplers_kdiffusion
6
+ from modules.script_callbacks import on_cfg_denoiser
7
+ import k_diffusion.sampling
8
+ from k_diffusion.sampling import to_d, default_noise_sampler, get_ancestral_step
9
+ from tqdm.auto import trange
10
+ import math
11
+ import numpy as np
12
+
13
+ # ========================================================================
14
+ # КОНСТАНТЫ И КОНФИГУРАЦИЯ
15
+ # ========================================================================
16
+ MODE_OFF = "Default (Off)"
17
+ MODE_CIRCULAR = "Circular"
18
+ MODE_MIRROR = "Mirror (Reflect)"
19
+ MODE_HEXAGONAL = "Hexagonal (Staggered)"
20
+ MODE_PANORAMA = "Panorama 360°"
21
+ MODE_CUBEMAP = "Cubemap (3D)"
22
+ MODE_BLEND = "Soft Blend Edges"
23
+ MODE_ANISOTROPIC = "Anisotropic (Directional)"
24
+ MODE_POLAR = "Polar (Sphere Correct)"
25
+
26
+ # Глобальное хранилище
27
+ _ORIGINAL_METHODS_CACHE = {}
28
+ _MASK_CACHE = {}
29
+ _SAMPLER_REGISTERED = False
30
+
31
+ # ========================================================================
32
+ # KOHAKU LONYU YOG SAMPLER IMPLEMENTATION
33
+ # ========================================================================
34
+
35
+ @torch.no_grad()
36
+ def sample_kohaku_lonyu_yog(model, x, sigmas, extra_args=None, callback=None,
37
+ disable=None, s_churn=0., s_tmin=0., s_tmax=float('inf'),
38
+ s_noise=1., noise_sampler=None, eta=1.):
39
+ """
40
+ Kohaku_LoNyu_Yog Sampler - Geometric Second-Order Method
41
+ """
42
+ extra_args = {} if extra_args is None else extra_args
43
+ s_in = x.new_ones([x.shape[0]])
44
+ noise_sampler = default_noise_sampler(x) if noise_sampler is None else noise_sampler
45
+
46
+ steps_total = len(sigmas) - 1
47
+ halfway_point = steps_total // 2
48
+
49
+ for i in trange(steps_total, disable=disable, desc="Kohaku Sampling"):
50
+ gamma = min(s_churn / steps_total, 2 ** 0.5 - 1) if s_tmin <= sigmas[i] <= s_tmax else 0.
51
+ sigma_hat = sigmas[i] * (gamma + 1)
52
+
53
+ if gamma > 0:
54
+ eps = torch.randn_like(x) * s_noise
55
+ x = x + eps * (sigma_hat ** 2 - sigmas[i] ** 2) ** 0.5
56
+
57
+ denoised = model(x, sigma_hat * s_in, **extra_args)
58
+ d = to_d(x, sigma_hat, denoised)
59
+
60
+ sigma_down, sigma_up = get_ancestral_step(sigmas[i], sigmas[i + 1], eta=eta)
61
+ dt = sigma_down - sigmas[i]
62
+
63
+ if i <= halfway_point:
64
+ x_antipode = -x
65
+
66
+ denoised2 = model(x_antipode, sigma_hat * s_in, **extra_args)
67
+ d2 = to_d(x_antipode, sigma_hat, denoised2)
68
+
69
+ v_down = (d + d2) / 2
70
+ x_closer = x + v_down * dt
71
+
72
+ denoised3 = model(x_closer, sigma_hat * s_in, **extra_args)
73
+ d3 = to_d(x_closer, sigma_hat, denoised3)
74
+
75
+ real_d = (d + d3) / 2
76
+ x = x + real_d * dt
77
+
78
+ if sigma_up > 0:
79
+ x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
80
+ else:
81
+ x = x + d * dt
82
+ if sigma_up > 0:
83
+ x = x + noise_sampler(sigmas[i], sigmas[i + 1]) * s_noise * sigma_up
84
+
85
+ if callback is not None:
86
+ callback({
87
+ 'x': x,
88
+ 'i': i,
89
+ 'sigma': sigmas[i],
90
+ 'sigma_hat': sigma_hat,
91
+ 'denoised': denoised
92
+ })
93
+
94
+ return x
95
+
96
+ # ========================================================================
97
+ # РАСШИРЕННЫЕ ФУНКЦИИ ПАДДИНГА
98
+ # ========================================================================
99
+
100
+ def get_or_create_mask(h, w, device):
101
+ """Кэширование масок для оптимизации"""
102
+ key = (h, w, str(device))
103
+ if key not in _MASK_CACHE:
104
+ row_indices = torch.arange(h, device=device).view(1, 1, h, 1)
105
+ _MASK_CACHE[key] = (row_indices % 2 == 1)
106
+ return _MASK_CACHE[key]
107
+
108
+ def compute_anisotropic_padding(input_tensor, pad_h, pad_w, angle_deg=45):
109
+ """
110
+ Анизотропный паддинг - разное поведение по диагоналям.
111
+ Эмулирует направленные материалы (дерево, металл, волокна).
112
+ """
113
+ b, c, h, w = input_tensor.shape
114
+
115
+ # Базовые паддинги: circular и reflect
116
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
117
+ padded_reflect = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='reflect')
118
+
119
+ # Размеры уже с учетом паддинга
120
+ _, H, W = padded.shape[1:]
121
+
122
+ # Преобразуем угол в радианы
123
+ angle_rad = math.radians(angle_deg)
124
+
125
+ # Координаты в нормализованной системе для всего padded-тензора
126
+ y_coords = torch.linspace(-1.0, 1.0, steps=H, device=input_tensor.device, dtype=input_tensor.dtype).view(1, 1, H, 1)
127
+ x_coords = torch.linspace(-1.0, 1.0, steps=W, device=input_tensor.device, dtype=input_tensor.dtype).view(1, 1, 1, W)
128
+
129
+ # Проекция на направление волокон
130
+ directional_component = x_coords * math.cos(angle_rad) + y_coords * math.sin(angle_rad)
131
+ directional_strength = directional_component.abs()
132
+
133
+ # Альфа-блендинг: вдоль направления больше circular, поперек больше reflect
134
+ alpha = directional_strength.clamp(0.0, 1.0)
135
+ result = padded * alpha + padded_reflect * (1.0 - alpha)
136
+
137
+ return result
138
+
139
+ def compute_polar_padding(input_tensor, pad_h, pad_w):
140
+ """
141
+ Полярный паддинг для сферических проекций.
142
+ """
143
+ b, c, h, w = input_tensor.shape
144
+
145
+ # X-axis: стандартный circular (долгота замыкается)
146
+ x = F.pad(input_tensor, (pad_w, pad_w, 0, 0), mode='circular')
147
+
148
+ # Y-axis: полярная коррекция (широта через полюса)
149
+ shift = w // 2
150
+
151
+ # Верхний паддинг (Северный полюс)
152
+ top_strip = x[:, :, :pad_h, :]
153
+ top_pad = torch.roll(top_strip, shifts=shift, dims=3)
154
+ top_pad = torch.flip(top_pad, dims=[2])
155
+
156
+ # Нижний паддинг (Южный полюс)
157
+ bot_strip = x[:, :, -pad_h:, :]
158
+ bot_pad = torch.roll(bot_strip, shifts=shift, dims=3)
159
+ bot_pad = torch.flip(bot_pad, dims=[2])
160
+
161
+ result = torch.cat([top_pad, x, bot_pad], dim=2)
162
+ return result
163
+
164
+ def compute_stereoscopic_padding(input_tensor, pad_h, pad_w, eye='left',
165
+ convergence=0.05, separation=0.065):
166
+ """
167
+ Стереоскопический паддинг для 3D изображений.
168
+ eye: 'left', 'right' или 'both'
169
+ """
170
+ b, c, h, w = input_tensor.shape
171
+
172
+ eye = (eye or 'left').lower()
173
+ shift_amount = int(w * separation)
174
+
175
+ x_coords = torch.linspace(
176
+ 0.0, 1.0, w,
177
+ device=input_tensor.device,
178
+ dtype=input_tensor.dtype
179
+ ).view(1, 1, 1, w)
180
+ depth_map = torch.abs(x_coords - convergence).expand(b, c, h, w)
181
+ alpha = depth_map.clamp(0.0, 1.0)
182
+
183
+ if eye == 'left':
184
+ shifted = torch.roll(input_tensor, shifts=shift_amount, dims=3)
185
+ stereo_adjusted = input_tensor * (1.0 - alpha) + shifted * alpha
186
+ elif eye == 'right':
187
+ shifted = torch.roll(input_tensor, shifts=-shift_amount, dims=3)
188
+ stereo_adjusted = input_tensor * (1.0 - alpha) + shifted * alpha
189
+ else:
190
+ # 'both' симметричный режим
191
+ shifted_left = torch.roll(input_tensor, shifts=shift_amount, dims=3)
192
+ shifted_right = torch.roll(input_tensor, shifts=-shift_amount, dims=3)
193
+ shifted_avg = 0.5 * (shifted_left + shifted_right)
194
+ stereo_adjusted = input_tensor * (1.0 - alpha) + shifted_avg * alpha
195
+
196
+ padded = F.pad(stereo_adjusted, (pad_w, pad_w, pad_h, pad_h), mode='circular')
197
+ return padded
198
+
199
+ def compute_hex_padding_x(input_tensor, pad_l, pad_r):
200
+ """Гексагональный паддинг (из v2.0)"""
201
+ b, c, h, w = input_tensor.shape
202
+ odd_mask = get_or_create_mask(h, w, input_tensor.device).expand(b, c, h, w)
203
+
204
+ shift = w // 2
205
+ input_shifted = torch.roll(input_tensor, shifts=shift, dims=3)
206
+ source = torch.where(odd_mask, input_shifted, input_tensor)
207
+
208
+ left_pad = source[:, :, :, -pad_l:]
209
+ right_pad = source[:, :, :, :pad_r]
210
+
211
+ return torch.cat([left_pad, input_tensor, right_pad], dim=3)
212
+
213
+ def compute_blend_padding(padded, pad_h, pad_w, blend_strength=0.1):
214
+ """
215
+ Мягкое смешивание краев поверх уже примененного паддинга.
216
+ Работает с любым базовым режимом.
217
+ """
218
+ b, c, H, W = padded.shape
219
+
220
+ h = max(H - 2 * pad_h, 0)
221
+ w = max(W - 2 * pad_w, 0)
222
+
223
+ if h <= 0 or w <= 0:
224
+ return padded
225
+
226
+ blend_w = min(int(w * blend_strength), w)
227
+ blend_h = min(int(h * blend_strength), h)
228
+
229
+ if blend_h > 0 and pad_h > 0:
230
+ top_mask = torch.linspace(0.0, 1.0, steps=blend_h, device=padded.device, dtype=padded.dtype)
231
+ top_mask = top_mask.view(1, 1, blend_h, 1).expand(b, c, blend_h, W)
232
+ padded[:, :, pad_h:pad_h + blend_h, :] *= top_mask
233
+
234
+ bottom_mask = torch.linspace(1.0, 0.0, steps=blend_h, device=padded.device, dtype=padded.dtype)
235
+ bottom_mask = bottom_mask.view(1, 1, blend_h, 1).expand(b, c, blend_h, W)
236
+ padded[:, :, pad_h + h - blend_h:pad_h + h, :] *= bottom_mask
237
+
238
+ if blend_w > 0 and pad_w > 0:
239
+ left_mask = torch.linspace(0.0, 1.0, steps=blend_w, device=padded.device, dtype=padded.dtype)
240
+ left_mask = left_mask.view(1, 1, 1, blend_w).expand(b, c, H, blend_w)
241
+ padded[:, :, :, pad_w:pad_w + blend_w] *= left_mask
242
+
243
+ right_mask = torch.linspace(1.0, 0.0, steps=blend_w, device=padded.device, dtype=padded.dtype)
244
+ right_mask = right_mask.view(1, 1, 1, blend_w).expand(b, c, H, blend_w)
245
+ padded[:, :, :, pad_w + w - blend_w:pad_w + w] *= right_mask
246
+
247
+ return padded
248
+
249
+ # ========================================================================
250
+ # ГЛАВНАЯ ФУНКЦИЯ ПАДДИНГА
251
+ # ========================================================================
252
+
253
+ def custom_padding_forward(input_tensor, weight, bias, stride, padding, dilation, groups, params):
254
+ """
255
+ Универсальная функция паддинга с поддержкой всех режимов v3.0
256
+ """
257
+ try:
258
+ current_step = getattr(shared.state, 'sampling_step', 0)
259
+ if not (params['start_step'] <= current_step <= params['end_step']):
260
+ return F.conv2d(input_tensor, weight, bias, stride, padding, dilation, groups)
261
+
262
+ # Если включено "Disable Advanced Tiling during hires pass",
263
+ # отключаем Advanced Tiling на hires-проходе
264
+ script = params.get('script_ref', None)
265
+ if params.get('tiling_disable_hr', False) and script is not None:
266
+ # tiling_enable_hr: включён ли вообще hires.fix
267
+ # tiling_is_hires: находимся ли сейчас на hires-проходе
268
+ if getattr(script, 'tiling_enable_hr', False) and getattr(script, 'tiling_is_hires', False):
269
+ return F.conv2d(input_tensor, weight, bias, stride, padding, dilation, groups)
270
+
271
+ k_h, k_w = weight.shape[2], weight.shape[3]
272
+ d_h, d_w = (dilation, dilation) if isinstance(dilation, int) else dilation
273
+
274
+ if isinstance(padding, str):
275
+ if padding == 'same':
276
+ req_pad_h = ((k_h - 1) * d_h) // 2
277
+ req_pad_w = ((k_w - 1) * d_w) // 2
278
+ elif padding == 'valid':
279
+ req_pad_h = req_pad_w = 0
280
+ else:
281
+ req_pad_h = req_pad_w = 1
282
+ elif isinstance(padding, int):
283
+ req_pad_h = req_pad_w = padding
284
+ elif isinstance(padding, (tuple, list)):
285
+ req_pad_h, req_pad_w = padding[0], padding[1]
286
+ else:
287
+ req_pad_h = req_pad_w = 0
288
+
289
+ x = input_tensor
290
+ mode_x = params['mode_x']
291
+ mode_y = params['mode_y']
292
+
293
+ # ====== СПЕЦИАЛЬНЫЕ РЕЖИМЫ ======
294
+
295
+ # Стереоскопия
296
+ if params.get('stereo_enabled', False):
297
+ eye = params.get('stereo_eye', 'left')
298
+ convergence = params.get('stereo_convergence', 0.5)
299
+ separation = params.get('stereo_separation', 0.065)
300
+ x = compute_stereoscopic_padding(x, req_pad_h, req_pad_w, eye, convergence, separation)
301
+
302
+ # Анизотропный
303
+ elif mode_x == MODE_ANISOTROPIC or mode_y == MODE_ANISOTROPIC:
304
+ angle = params.get('anisotropic_angle', 45)
305
+ x = compute_anisotropic_padding(x, req_pad_h, req_pad_w, angle)
306
+
307
+ # Полярный (сферический)
308
+ elif mode_x == MODE_POLAR or mode_y == MODE_POLAR:
309
+ x = compute_polar_padding(x, req_pad_h, req_pad_w)
310
+
311
+ # Панорама
312
+ elif mode_x == MODE_PANORAMA or mode_y == MODE_PANORAMA:
313
+ x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='circular')
314
+ x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
315
+
316
+ # Стандартные режимы (раздельно по осям)
317
+ else:
318
+ # Ось Y
319
+ if mode_y == MODE_CIRCULAR:
320
+ x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
321
+ elif mode_y == MODE_MIRROR:
322
+ x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='reflect')
323
+ elif mode_y == MODE_HEXAGONAL:
324
+ x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='circular')
325
+ else:
326
+ x = F.pad(x, (0, 0, req_pad_h, req_pad_h), mode='constant', value=0)
327
+
328
+ # Ось X
329
+ if mode_x == MODE_CIRCULAR:
330
+ x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='circular')
331
+ elif mode_x == MODE_MIRROR:
332
+ x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='reflect')
333
+ elif mode_x == MODE_HEXAGONAL:
334
+ x = compute_hex_padding_x(x, req_pad_w, req_pad_w)
335
+ else:
336
+ x = F.pad(x, (req_pad_w, req_pad_w, 0, 0), mode='constant', value=0)
337
+
338
+ # Дополнительный мягкий бленд поверх выбранного режима
339
+ if params.get('blend_enabled', False) and (req_pad_h > 0 or req_pad_w > 0):
340
+ blend_str = params.get('blend_strength', 0.1)
341
+ x = compute_blend_padding(x, req_pad_h, req_pad_w, blend_str)
342
+
343
+ # Мультиразрешение
344
+ if params.get('multires_enabled', False):
345
+ step_ratio = current_step / max(params['end_step'], 1)
346
+ if step_ratio < 0.3:
347
+ x_default = F.pad(input_tensor, (req_pad_w, req_pad_w, req_pad_h, req_pad_h),
348
+ mode='constant', value=0)
349
+ alpha = step_ratio * 3
350
+ x = x * alpha + x_default * (1 - alpha)
351
+
352
+ return F.conv2d(x, weight, bias, stride, 0, dilation, groups)
353
+
354
+ except Exception as e:
355
+ print(f"Advanced Tiling v3 Error: {e}")
356
+ return F.conv2d(input_tensor, weight, bias, stride, padding, dilation, groups)
357
+
358
+ # ========================================================================
359
+ # РЕГИСТРАЦИЯ KOHAKU SAMPLER
360
+ # ========================================================================
361
+
362
+ def register_kohaku_sampler():
363
+ """Регистрирует Kohaku_LoNyu_Yog сэмплер в WebUI"""
364
+ global _SAMPLER_REGISTERED
365
+
366
+ if _SAMPLER_REGISTERED:
367
+ return
368
+
369
+ if any(s.name == 'Kohaku_LoNyu_Yog' for s in sd_samplers.all_samplers):
370
+ _SAMPLER_REGISTERED = True
371
+ return
372
+
373
+ if not hasattr(k_diffusion.sampling, 'sample_kohaku_lonyu_yog'):
374
+ setattr(k_diffusion.sampling, 'sample_kohaku_lonyu_yog', sample_kohaku_lonyu_yog)
375
+
376
+ sampler_data = sd_samplers_common.SamplerData(
377
+ name='Kohaku_LoNyu_Yog',
378
+ constructor=lambda model: sd_samplers_kdiffusion.KDiffusionSampler('sample_kohaku_lonyu_yog', model),
379
+ aliases=['kohaku', 'lonyu'],
380
+ options={'second_order': True}
381
+ )
382
+
383
+ sd_samplers.all_samplers.append(sampler_data)
384
+ sd_samplers.all_samplers_map = {x.name: x for x in sd_samplers.all_samplers}
385
+
386
+ _SAMPLER_REGISTERED = True
387
+ print("✓ Kohaku_LoNyu_Yog sampler registered successfully!")
388
+
389
+ # ========================================================================
390
+ # КЛАСС СКРИПТА (Advanced Tiling + Latent Mirroring)
391
+ # ========================================================================
392
+
393
+ class AdvancedTilingScriptV3(scripts.Script):
394
+ def title(self):
395
+ return "Advanced Tiling v3.0 PRO (Kohaku + Stereo + Aniso + Latent Mirror)"
396
+
397
+ def show(self, is_img2img):
398
+ return scripts.AlwaysVisible
399
+
400
+ def ui(self, is_img2img):
401
+ with gr.Accordion("🚀 Advanced Tiling v3.0 PRO Edition", open=False):
402
+ with gr.Row():
403
+ enabled = gr.Checkbox(label="Enable Tiling", value=False)
404
+ use_kohaku = gr.Checkbox(
405
+ label="Use Kohaku_LoNyu_Yog Sampler",
406
+ value=False,
407
+ info="Geometric second-order method for better seamless quality"
408
+ )
409
+
410
+ with gr.Tabs():
411
+ with gr.Tab("🎨 Basic Modes"):
412
+ with gr.Row():
413
+ mode_x = gr.Dropdown(
414
+ label="Mode X",
415
+ choices=[MODE_OFF, MODE_CIRCULAR, MODE_MIRROR, MODE_HEXAGONAL],
416
+ value=MODE_CIRCULAR
417
+ )
418
+ mode_y = gr.Dropdown(
419
+ label="Mode Y",
420
+ choices=[MODE_OFF, MODE_CIRCULAR, MODE_MIRROR, MODE_HEXAGONAL],
421
+ value=MODE_CIRCULAR
422
+ )
423
+
424
+ with gr.Row():
425
+ multires = gr.Checkbox(label="Multi-Resolution Mode", value=False)
426
+ use_blend = gr.Checkbox(label="Soft Blend Edges", value=False)
427
+ blend_strength = gr.Slider(0.0, 0.5, step=0.05, label="Blend Strength", value=0.1)
428
+
429
+ with gr.Tab("🌐 Advanced Modes"):
430
+ gr.Markdown("**Специальные режимы для сложных топологий**")
431
+
432
+ with gr.Row():
433
+ use_panorama = gr.Checkbox(label="Panorama 360°", value=False)
434
+ use_polar = gr.Checkbox(
435
+ label="Polar (Sphere Correct)",
436
+ value=False,
437
+ info="Correct pole transitions for equirectangular"
438
+ )
439
+
440
+ with gr.Row():
441
+ use_anisotropic = gr.Checkbox(
442
+ label="Anisotropic (Directional)",
443
+ value=False,
444
+ info="Different behavior along diagonals"
445
+ )
446
+ aniso_angle = gr.Slider(
447
+ 0, 360, step=15,
448
+ label="Anisotropic Angle",
449
+ value=45,
450
+ info="Direction of fibers/texture (degrees)"
451
+ )
452
+
453
+ with gr.Tab("🎭 Stereoscopic 3D"):
454
+ gr.Markdown("**Генерация стереопар для 3D контента**")
455
+
456
+ with gr.Row():
457
+ stereo_enabled = gr.Checkbox(label="Enable Stereoscopic Mode", value=False)
458
+ stereo_eye = gr.Radio(
459
+ label="Eye",
460
+ choices=["left", "right", "both"],
461
+ value="left",
462
+ info="Which eye view to generate"
463
+ )
464
+
465
+ with gr.Row():
466
+ stereo_separation = gr.Slider(
467
+ 0.0, 0.15, step=0.005,
468
+ label="IPD (Inter-Pupillary Distance)",
469
+ value=0.065,
470
+ info="Eye separation as fraction of width"
471
+ )
472
+ stereo_convergence = gr.Slider(
473
+ 0.0, 1.0, step=0.05,
474
+ label="Convergence Point",
475
+ value=0.5,
476
+ info="Depth at which eyes converge"
477
+ )
478
+
479
+ gr.Markdown("""
480
+ **💡 Совет для стерео:**
481
+ - Генерируйте сначала левый глаз
482
+ - Затем правый с теми же параметрами
483
+ - Используйте Side-by-Side или Anaglyph компоновку
484
+ """)
485
+
486
+ with gr.Tab("🪞 Latent Mirroring"):
487
+ # Главный переключатель латентного зеркалирования
488
+ enable_mirroring = gr.Checkbox(
489
+ label="Enable Latent Mirroring",
490
+ value=False
491
+ )
492
+
493
+ with gr.Group():
494
+ mirror_mode = gr.Radio(
495
+ label='Latent Mirror mode',
496
+ choices=['None', 'Alternate Steps', 'Blend Average'],
497
+ value='None',
498
+ type="index"
499
+ )
500
+ mirror_style = gr.Radio(
501
+ label='Latent Mirror style',
502
+ choices=[
503
+ 'Horizontal Mirroring',
504
+ 'Vertical Mirroring',
505
+ 'Horizontal+Vertical Mirroring',
506
+ '90 Degree Rotation',
507
+ '180 Degree Rotation',
508
+ 'Roll Channels',
509
+ 'None'
510
+ ],
511
+ value='Horizontal Mirroring',
512
+ type="index"
513
+ )
514
+
515
+ with gr.Row():
516
+ x_pan = gr.Slider(
517
+ minimum=-1.0, maximum=1.0, step=0.01,
518
+ label='X panning', value=0.0
519
+ )
520
+ y_pan = gr.Slider(
521
+ minimum=-1.0, maximum=1.0, step=0.01,
522
+ label='Y panning', value=0.0
523
+ )
524
+
525
+ mirroring_max_step_fraction = gr.Slider(
526
+ minimum=0.0, maximum=1.0, step=0.01,
527
+ label='Maximum steps fraction to mirror at',
528
+ value=0.25
529
+ )
530
+
531
+ # Режим интерпретации шага для mirroring:
532
+ # 0 = Max fraction (оригинал), 1 = Custom range (Start/End)
533
+ mirror_step_mode = gr.Radio(
534
+ label="Mirroring Step Mode",
535
+ choices=["Max fraction (original)", "Custom range"],
536
+ value="Max fraction (original)",
537
+ type="index"
538
+ )
539
+ mirror_start_frac = gr.Slider(
540
+ minimum=0.0, maximum=1.0, step=0.01,
541
+ label="Mirror Start (fraction of total steps)",
542
+ value=0.0
543
+ )
544
+ mirror_end_frac = gr.Slider(
545
+ minimum=0.0, maximum=1.0, step=0.01,
546
+ label="Mirror End (fraction of total steps)",
547
+ value=0.25
548
+ )
549
+
550
+ if not is_img2img:
551
+ disable_hr = gr.Checkbox(label='Disable during hires pass', value=False)
552
+ else:
553
+ disable_hr = gr.State(False)
554
+
555
+ with gr.Row():
556
+ start_step = gr.Slider(0, 150, step=1, label="Start Step", value=0)
557
+ end_step = gr.Slider(0, 150, step=1, label="End Step", value=150)
558
+
559
+ # Режим интерпретации Start/End Step: абсолютные шаги или доли от p.steps
560
+ with gr.Row():
561
+ step_mode = gr.Radio(
562
+ label="Tiling Step Mode",
563
+ choices=["Absolute Steps", "Fraction of total steps"],
564
+ value="Absolute Steps",
565
+ type="index"
566
+ )
567
+ step_start_frac = gr.Slider(
568
+ 0.0, 1.0, step=0.01,
569
+ label="Start (fraction of total steps)",
570
+ value=0.0
571
+ )
572
+ step_end_frac = gr.Slider(
573
+ 0.0, 1.0, step=0.01,
574
+ label="End (fraction of total steps)",
575
+ value=1.0
576
+ )
577
+
578
+ with gr.Row():
579
+ patch_vae = gr.Checkbox(
580
+ label="Patch VAE Decoder",
581
+ value=True,
582
+ info="Fix seams in final pixel decode"
583
+ )
584
+
585
+ with gr.Row():
586
+ tiling_disable_hr = gr.Checkbox(
587
+ label="Disable Advanced Tiling during hires pass",
588
+ value=False,
589
+ info=(
590
+ "When hires fix is enabled, apply Advanced Tiling only on "
591
+ "the first (base) sampling pass"
592
+ )
593
+ )
594
+
595
+ # Preview Tool
596
+ with gr.Accordion("🔍 Preview & Info", open=False):
597
+ with gr.Tabs():
598
+ with gr.Tab("Visual Preview"):
599
+ preview_btn = gr.Button("Generate Preview", variant="primary")
600
+ preview_img = gr.Image(label="Preview Grid (2x2)")
601
+
602
+ preview_btn.click(
603
+ fn=self.generate_preview,
604
+ inputs=[mode_x, mode_y, use_anisotropic, aniso_angle,
605
+ stereo_enabled, stereo_eye],
606
+ outputs=preview_img
607
+ )
608
+
609
+ with gr.Tab("Kohaku Info"):
610
+ gr.Markdown("""
611
+ ## 🔬 Kohaku_LoNyu_Yog Принцип работы
612
+ (описание опущено ради компактности)
613
+ """)
614
+
615
+ # для Latent Mirroring
616
+ self.run_callback = False
617
+
618
+ return [enabled, mode_x, mode_y,
619
+ start_step, end_step,
620
+ step_mode, step_start_frac, step_end_frac,
621
+ multires,
622
+ use_panorama, use_polar, use_blend, blend_strength,
623
+ use_anisotropic, aniso_angle,
624
+ stereo_enabled, stereo_eye, stereo_separation, stereo_convergence,
625
+ patch_vae, tiling_disable_hr, use_kohaku,
626
+ enable_mirroring,
627
+ mirror_mode, mirror_style, x_pan, y_pan,
628
+ mirroring_max_step_fraction,
629
+ mirror_step_mode, mirror_start_frac, mirror_end_frac,
630
+ disable_hr]
631
+
632
+ # ====== LATENT MIRRORING CALLBACK ======
633
+
634
+ def denoise_callback(self, params):
635
+ # --- Детекция hires-прохода для Advanced Tiling ---
636
+ tiling_is_hires = getattr(self, "tiling_is_hires", False)
637
+ if getattr(self, "tiling_enable_hr", False) and params.total_sampling_steps > 0:
638
+ if params.sampling_step >= params.total_sampling_steps - 2:
639
+ # Переключаем флаг между base/hires
640
+ self.tiling_is_hires = not tiling_is_hires
641
+
642
+ # --- Оригинальная логика Latent Mirroring ---
643
+ is_hires = self.is_hires
644
+
645
+ # indices start at -1; params.sampling_step = max(0, real_sampling_step)
646
+ if params.sampling_step >= params.total_sampling_steps - 2:
647
+ self.is_hires = not is_hires and self.enable_hr
648
+
649
+ if not self.run_callback or is_hires:
650
+ return
651
+
652
+ cur_step = params.sampling_step
653
+ total_steps = max(params.total_sampling_steps, 1)
654
+
655
+ # Режим интерпретации шагов mirroring:
656
+ # 0 = Max fraction (оригинал),
657
+ # 1 = Custom range [mirror_start_frac, mirror_end_frac]
658
+ mirror_step_mode = getattr(self, "mirror_step_mode", 0)
659
+
660
+ if mirror_step_mode == 0:
661
+ # Оригинальное поведение: от начала до max_fraction * total_steps
662
+ if cur_step >= total_steps * self.mirroring_max_step_fraction:
663
+ return
664
+ else:
665
+ # Новый режим: собственный диапазон Start/End в долях
666
+ start_step = int(total_steps * getattr(self, "mirror_start_frac", 0.0))
667
+ end_step = int(total_steps * getattr(self, "mirror_end_frac", 1.0))
668
+ if start_step > end_step:
669
+ start_step, end_step = end_step, start_step
670
+ if not (start_step <= cur_step <= end_step):
671
+ return
672
+
673
+ try:
674
+ if self.mirror_mode == 1:
675
+ if self.mirror_style == 0:
676
+ params.x[:, :, :, :] = torch.flip(params.x, [3])
677
+ elif self.mirror_style == 1:
678
+ params.x[:, :, :, :] = torch.flip(params.x, [2])
679
+ elif self.mirror_style == 2:
680
+ params.x[:, :, :, :] = torch.flip(params.x, [3, 2])
681
+ elif self.mirror_style == 3:
682
+ params.x[:, :, :, :] = torch.rot90(params.x, dims=[2, 3])
683
+ elif self.mirror_style == 4:
684
+ params.x[:, :, :, :] = torch.rot90(
685
+ torch.rot90(params.x, dims=[2, 3]), dims=[2, 3]
686
+ )
687
+ elif self.mirror_style == 5:
688
+ params.x[:, :, :, :] = torch.roll(params.x, shifts=1, dims=[1])
689
+
690
+ elif self.mirror_mode == 2:
691
+ if self.mirror_style == 0:
692
+ params.x[:, :, :, :] = (torch.flip(params.x, [3]) + params.x) / 2
693
+ elif self.mirror_style == 1:
694
+ params.x[:, :, :, :] = (torch.flip(params.x, [2]) + params.x) / 2
695
+ elif self.mirror_style == 2:
696
+ params.x[:, :, :, :] = (torch.flip(params.x, [2, 3]) + params.x) / 2
697
+ elif self.mirror_style == 3:
698
+ params.x[:, :, :, :] = (torch.rot90(params.x, dims=[2, 3]) + params.x) / 2
699
+ elif self.mirror_style == 4:
700
+ params.x[:, :, :, :] = (
701
+ torch.rot90(torch.rot90(params.x, dims=[2, 3]), dims=[2, 3]) + params.x
702
+ ) / 2
703
+ elif self.mirror_style == 5:
704
+ params.x[:, :, :, :] = (torch.roll(params.x, shifts=1, dims=[1]) + params.x) / 2
705
+ except RuntimeError as e:
706
+ if self.mirror_style in (3, 4):
707
+ raise RuntimeError('90 Degree Rotation requires a square image.') from e
708
+ else:
709
+ raise RuntimeError('Error transforming image for latent mirroring.') from e
710
+
711
+ if self.x_pan != 0:
712
+ params.x[:, :, :, :] = torch.roll(
713
+ params.x,
714
+ shifts=int(params.x.size()[3] * self.x_pan),
715
+ dims=[3]
716
+ )
717
+ if self.y_pan != 0:
718
+ params.x[:, :, :, :] = torch.roll(
719
+ params.x,
720
+ shifts=int(params.x.size()[2] * self.y_pan),
721
+ dims=[2]
722
+ )
723
+
724
+ # ====== PREVIEW ======
725
+
726
+ def generate_preview(self, mx, my, use_aniso, aniso_angle, stereo, eye):
727
+ """Генерация preview с учетом новых режимов"""
728
+ h, w = 256, 256
729
+ img = np.zeros((h, w, 3), dtype=np.uint8)
730
+
731
+ for i in range(h):
732
+ for j in range(w):
733
+ col = (i + j) % 255
734
+ if i < 5 or i > h - 5 or j < 5 or j > w - 5:
735
+ img[i, j] = [255, 50, 50]
736
+ else:
737
+ img[i, j] = [col, col // 2, 255 - col]
738
+
739
+ def get_tile(r, c):
740
+ tile = img.copy()
741
+
742
+ if use_aniso:
743
+ angle_rad = np.radians(aniso_angle)
744
+ for ii in range(h):
745
+ for jj in range(w):
746
+ dist = abs((jj - w / 2) * np.cos(angle_rad) +
747
+ (ii - h / 2) * np.sin(angle_rad))
748
+ tile[ii, jj] = tile[ii, jj] * (0.5 + 0.5 * np.sin(dist * 0.1))
749
+
750
+ elif stereo:
751
+ shift = 10 if eye == 'left' else -10
752
+ tile = np.roll(tile, shift, axis=1)
753
+
754
+ else:
755
+ if mx == MODE_MIRROR and c % 2 != 0:
756
+ tile = np.fliplr(tile)
757
+ if my == MODE_MIRROR and r % 2 != 0:
758
+ tile = np.flipud(tile)
759
+ if mx == MODE_HEXAGONAL and r % 2 != 0:
760
+ tile = np.roll(tile, w // 2, axis=1)
761
+
762
+ return tile.astype(np.uint8)
763
+
764
+ canvas = np.zeros((h * 2, w * 2, 3), dtype=np.uint8)
765
+ for r in range(2):
766
+ for c in range(2):
767
+ canvas[r * h:(r + 1) * h, c * w:(c + 1) * w] = get_tile(r, c)
768
+
769
+ return canvas
770
+
771
+ # ====== PROCESS (TILING + MIRRORING) ======
772
+
773
+ def process(self, p,
774
+ enabled, mode_x, mode_y,
775
+ start_step, end_step,
776
+ step_mode, step_start_frac, step_end_frac,
777
+ multires,
778
+ use_panorama, use_polar, use_blend, blend_strength,
779
+ use_anisotropic, aniso_angle,
780
+ stereo_enabled, stereo_eye, stereo_separation, stereo_convergence,
781
+ patch_vae, tiling_disable_hr, use_kohaku,
782
+ enable_mirroring,
783
+ mirror_mode, mirror_style, x_pan, y_pan,
784
+ mirroring_max_step_fraction,
785
+ mirror_step_mode, mirror_start_frac, mirror_end_frac,
786
+ disable_hr):
787
+
788
+ # -------- Latent Mirroring часть --------
789
+ self.mirror_mode = mirror_mode
790
+ self.mirror_style = mirror_style
791
+ self.mirroring_max_step_fraction = mirroring_max_step_fraction
792
+ self.x_pan = x_pan
793
+ self.y_pan = y_pan
794
+ self.mirror_step_mode = mirror_step_mode
795
+ self.mirror_start_frac = mirror_start_frac
796
+ self.mirror_end_frac = mirror_end_frac
797
+
798
+ # Mirroring включается, только если:
799
+ # - чекбокс Enable Latent Mirroring включён
800
+ # - и есть хотя бы какой-то эффект (mode или pan)
801
+ need_mirroring = enable_mirroring and (mirror_mode != 0 or x_pan != 0 or y_pan != 0)
802
+
803
+ # denoise_callback нужен либо для mirroring,
804
+ # либо для Advanced Tiling с отключением на hires-��роходе
805
+ need_denoise_cb = need_mirroring or (tiling_disable_hr and getattr(p, "enable_hr", False))
806
+
807
+ # Экспорт параметров только если Latent Mirroring реально активен
808
+ if need_mirroring:
809
+ if mirror_mode != 0:
810
+ p.extra_generation_params["Mirror Mode"] = mirror_mode
811
+ p.extra_generation_params["Mirror Style"] = mirror_style
812
+ if mirror_step_mode == 0:
813
+ # Оригинальный режим — до max_fraction
814
+ p.extra_generation_params["Mirroring Max Step Fraction"] = mirroring_max_step_fraction
815
+ else:
816
+ # Новый режим — диапазон Start/End в долях
817
+ p.extra_generation_params["Mirror Start Fraction"] = mirror_start_frac
818
+ p.extra_generation_params["Mirror End Fraction"] = mirror_end_frac
819
+ if x_pan != 0:
820
+ p.extra_generation_params["X Pan"] = x_pan
821
+ if y_pan != 0:
822
+ p.extra_generation_params["Y Pan"] = y_pan
823
+
824
+ if need_denoise_cb and not hasattr(self, 'callbacks_added'):
825
+ on_cfg_denoiser(self.denoise_callback)
826
+ self.callbacks_added = True
827
+
828
+ # run_callback управляет только Latent Mirroring
829
+ self.run_callback = need_mirroring
830
+
831
+ # hires-логика для Latent Mirroring (оригинальное поведение)
832
+ self.enable_hr = getattr(p, 'enable_hr', False) and not disable_hr
833
+ self.is_hires = False
834
+
835
+ # hires-логика для Advanced Tiling (независимо от mirroring)
836
+ self.tiling_enable_hr = getattr(p, 'enable_hr', False)
837
+ self.tiling_is_hires = False
838
+
839
+ # -------- Advanced Tiling часть --------
840
+ if not enabled:
841
+ return
842
+
843
+
844
+ # -------- Обработка режимов Start/End Step для Tiling --------
845
+ # step_mode: 0 = Absolute Steps, 1 = Fraction of total steps
846
+ if step_mode == 1:
847
+ total_steps = max(getattr(p, "steps", 0), 1)
848
+ # Преобразуем доли в реальные шаги
849
+ start_step = int(total_steps * step_start_frac)
850
+ end_step = int(total_steps * step_end_frac)
851
+
852
+ # Валидация диапазона
853
+ if start_step > end_step:
854
+ start_step, end_step = end_step, start_step
855
+
856
+ # Kohaku
857
+ if use_kohaku:
858
+ register_kohaku_sampler()
859
+ if p.sampler_name != 'Kohaku_LoNyu_Yog':
860
+ print(f"Switching sampler from {p.sampler_name} to Kohaku_LoNyu_Yog")
861
+ p.sampler_name = 'Kohaku_LoNyu_Yog'
862
+
863
+ # Спецрежимы
864
+ if use_panorama:
865
+ mode_x = mode_y = MODE_PANORAMA
866
+ if use_polar:
867
+ mode_x = mode_y = MODE_POLAR
868
+ if use_anisotropic:
869
+ mode_x = mode_y = MODE_ANISOTROPIC
870
+
871
+ params = {
872
+ 'mode_x': mode_x,
873
+ 'mode_y': mode_y,
874
+ 'start_step': start_step,
875
+ 'end_step': end_step,
876
+ 'multires_enabled': multires,
877
+ 'blend_enabled': use_blend,
878
+ 'blend_strength': blend_strength,
879
+ 'anisotropic_angle': aniso_angle,
880
+ 'stereo_enabled': stereo_enabled,
881
+ 'stereo_eye': stereo_eye,
882
+ 'stereo_separation': stereo_separation,
883
+ 'stereo_convergence': stereo_convergence,
884
+ 'tiling_disable_hr': tiling_disable_hr,
885
+ 'script_ref': self,
886
+ }
887
+
888
+ # Патчинг UNet
889
+ unet = self.get_unet(p)
890
+ if unet:
891
+ self.restore_original(unet)
892
+ count_unet = self.patch_conv_layers(unet, params)
893
+ print(f"✓ Patched {count_unet} UNet Conv2d layers")
894
+
895
+ # Патчинг VAE
896
+ if patch_vae and hasattr(p.sd_model, 'first_stage_model'):
897
+ vae = p.sd_model.first_stage_model
898
+ count_vae = self.patch_conv_layers(vae, params)
899
+ print(f"✓ Patched {count_vae} VAE Conv2d layers")
900
+
901
+ mode_str = f"{mode_x}/{mode_y}"
902
+ if stereo_enabled:
903
+ mode_str += f" + Stereo({stereo_eye})"
904
+ if use_blend:
905
+ mode_str += " + Blend"
906
+ if use_kohaku:
907
+ mode_str += " + Kohaku"
908
+ if enable_mirroring and (mirror_mode != 0 or x_pan != 0 or y_pan != 0):
909
+ mode_str += " + LatentMirror"
910
+
911
+ print(f"✓ Advanced Tiling v3.0: Mode={mode_str}, Steps={start_step}-{end_step}")
912
+
913
+ # ====== ПАТЧИНГ СЛОЁВ ======
914
+
915
+ def patch_conv_layers(self, module, params):
916
+ """Патчинг с исправленным замыканием"""
917
+ count = 0
918
+ for layer in module.modules():
919
+ if isinstance(layer, torch.nn.Conv2d):
920
+ if layer.kernel_size == (1, 1) or layer.kernel_size == 1:
921
+ continue
922
+
923
+ if layer not in _ORIGINAL_METHODS_CACHE:
924
+ _ORIGINAL_METHODS_CACHE[layer] = layer._conv_forward
925
+
926
+ def make_patched(mod):
927
+ def patched(input, weight, bias):
928
+ return custom_padding_forward(
929
+ input, weight, bias,
930
+ mod.stride, mod.padding, mod.dilation, mod.groups,
931
+ params
932
+ )
933
+ return patched
934
+
935
+ layer._conv_forward = make_patched(layer)
936
+ count += 1
937
+
938
+ return count
939
+
940
+ def get_unet(self, p):
941
+ """Универсальная детекция UNet"""
942
+ if hasattr(p.sd_model, 'forge_objects') and hasattr(p.sd_model.forge_objects, 'unet'):
943
+ return p.sd_model.forge_objects.unet
944
+ elif hasattr(p.sd_model, 'model') and hasattr(p.sd_model.model, 'diffusion_model'):
945
+ return p.sd_model.model.diffusion_model
946
+ return p.sd_model
947
+
948
+ def postprocess(self, p, processed, *args):
949
+ """Восстановление"""
950
+ unet = self.get_unet(p)
951
+ if unet:
952
+ self.restore_original(unet)
953
+
954
+ if hasattr(p.sd_model, 'first_stage_model'):
955
+ self.restore_original(p.sd_model.first_stage_model)
956
+
957
+ # Отключаем латентный миррор до следующего запуска
958
+ self.run_callback = False
959
+ self.tiling_is_hires = False
960
+
961
+ def restore_original(self, module):
962
+ """Восстановление оригинальных методов"""
963
+ if not _ORIGINAL_METHODS_CACHE:
964
+ return
965
+
966
+ restored = 0
967
+ for layer in list(_ORIGINAL_METHODS_CACHE.keys()):
968
+ if hasattr(layer, '_conv_forward'):
969
+ layer._conv_forward = _ORIGINAL_METHODS_CACHE[layer]
970
+ restored += 1
971
+
972
+ _ORIGINAL_METHODS_CACHE.clear()
973
+ _MASK_CACHE.clear()
974
+
975
+ if restored > 0:
976
  print(f"✓ Restored {restored} layers to original state")