dikdimon commited on
Commit
7483aeb
·
verified ·
1 Parent(s): ae90ad4

Upload 3 files

Browse files
asd/libs/advanced_zoom_extension.py ADDED
@@ -0,0 +1,1606 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ╔══════════════════════════════════════════════════════════════════════════════╗
3
+ ║ ADVANCED ZOOM SYSTEM vULTIMATE - ULTIMATE PRODUCTION VERSION ║
4
+ ║ Лучшее из всех версий: V3.1.1 FIXED + Безопасность V3.2.1 FINAL_FIX ║
5
+ ╚══════════════════════════════════════════════════════════════════════════════╝
6
+
7
+ 🎯 vULTIMATE = V3.1.1 FIXED + EXPAND SAFETY
8
+ ═══════════════════════════════════════════════════════════════════════════════
9
+
10
+ ЧТО ВЗЯТО ИЗ V3.1.1 FIXED:
11
+ ✅ create_adaptive_latent_noise - COHERENT OUTPAINTING!
12
+ ✅ ПОЛНЫЙ функционал (spiral_zoom, gradient_radial, noise_blend)
13
+ ✅ Правильная логика convergence positioning
14
+ ✅ Distance-based adaptive noise strength
15
+ ✅ Fade mask для контента
16
+ ✅ get_adaptive_epsilon для float16
17
+ ✅ safe_interpolate для масштабирования
18
+ ✅ apply_variance_correction
19
+
20
+ ЧТО ДОБАВЛЕНО ИЗ V3.2.1 FINAL_FIX:
21
+ ✅ БЕЗОПАСНЫЙ EXPAND - все .expand() обернуты в try/except
22
+ ✅ Fallback через broadcast_to при ошибках
23
+ ✅ НЕТ RuntimeError: "expanded size must match"
24
+ ✅ 100% стабильность без потери функционала
25
+
26
+ КРИТИЧНЫЕ ИСПРАВЛЕНИЯ (vUltimate):
27
+ ═══════════════════════════════════════════════════════════════════════════════
28
+ 🔧 edge_smoothing.expand() - обернут в try/except
29
+ 🔧 gradient.expand() - обернут в try/except (apply_gradient_radial_blend)
30
+ 🔧 blend_mask.expand() - обернут в try/except (apply_noise_blend)
31
+ 🔧 adaptive_strength.expand() - уже был безопасным в V3.1.1
32
+ 🔧 variance_fix.expand() - уже был безопасным в V3.1.1
33
+
34
+ ИСПРАВЛЕНИЯ ИЗ V3.1.1 FIXED (сохранены):
35
+ ✅ FLOAT16 EPSILON FIX - адаптивный epsilon (1e-3 для float16, 1e-6 для float32)
36
+ ✅ EDGE_SMOOTHING FIX - правильный expand до (b,c,H,W) вместо (b,c,1,W)
37
+ ✅ SAFE_INTERPOLATE - безопасная интерполяция для float16
38
+ ✅ ALIGN_CORNERS FIX - правильная обработка без None
39
+ ✅ VARIANCE_CORRECTION - адаптивный epsilon + надежный broadcast
40
+ ✅ SPIRAL_ZOOM - адаптивный epsilon для всех sqrt/division
41
+ ✅ NOISE_BLEND - адаптивный epsilon + оптимизированная формула
42
+ ✅ EXTRA_PARAMS - правильная передача параметров
43
+ ✅ DIVISION BY ZERO - защита во всех критичных местах
44
+
45
+ ИСПРАВЛЕНИЯ ИЗ V3.1 COMPLETE (сохранены):
46
+ ✅ QUANTILE FIX - правильная обработка dtype + умное сэмплирование (>10M)
47
+ ✅ TENSOR SIZE FIX - исправлена ошибка "expanded size must match existing size"
48
+ ✅ SPIRAL ZOOM - полная реализация без багов + валидация параметров
49
+ ✅ GRADIENT_RADIAL - новый режим блендинга с радиальным градиентом
50
+ ✅ NOISE_BLEND - новый режим блендинга с процедурным шумом
51
+ ✅ WARNINGS - всегда видны, детальная диагностика
52
+ ✅ SHAPE VALIDATION - проверка размеров на каждом шаге
53
+
54
+ ФУНКЦИИ (vUltimate):
55
+ ═══════════════════════════════════════════════════════════════════════════════
56
+ 🆕 get_adaptive_epsilon(dtype) - автоматический выбор epsilon
57
+ 🆕 safe_interpolate() - безопасная интерполяция для float16
58
+ 🆕 create_adaptive_latent_noise() - COHERENT NOISE ДЛЯ OUTPAINTING
59
+ 🆕 apply_variance_correction() - устранение серости на швах
60
+ 🆕 apply_spiral_zoom() - спиральный zoom с вращением
61
+ 🆕 apply_gradient_radial_blend() - радиальный градиент (SAFE EXPAND!)
62
+ 🆕 apply_noise_blend() - процедурный шум (SAFE EXPAND!)
63
+
64
+ РЕЖИМЫ ZOOM:
65
+ ════════════════════════════════════════════════════════════════��══════════════
66
+ 🎯 OUTPAINT_ZOOM - оптимизирован для outpainting (COHERENT NOISE!)
67
+ 🎯 SPIRAL_ZOOM - спиральный зум с вращением
68
+ 🎯 GRID_WARP - геометрический zoom
69
+ 🎯 BLEND_TRANSITION - плавный переход
70
+ 🎯 CONVERGENCE_SHIFT - legacy сдвиг
71
+ 🎯 HYBRID - комбинация методов
72
+
73
+ РЕЖИМЫ BLEND:
74
+ ═══════════════════════════════════════════════════════════════════════════════
75
+ 🌈 CIRCULAR_REFLECT - бесшовный + отражение
76
+ 🌈 CIRCULAR_CONSTANT - бесшовный + константа
77
+ 🌈 REFLECT_CONSTANT - отражение + константа
78
+ 🌈 POLAR_CIRCULAR - полярное + бесшовный
79
+ 🌈 MIRROR_CIRCULAR - зеркало + бесшовный
80
+ 🌈 ANISO_CIRCULAR - анизотропный + бесшовный
81
+ 🌈 GRADIENT_RADIAL - радиальный градиент (НОВОЕ!)
82
+ 🌈 NOISE_BLEND - процедурный шум (НОВОЕ!)
83
+ 🌈 CUSTOM - пользовательский
84
+
85
+ ПАРАМЕТРЫ:
86
+ ═══════════════════════════════════════════════════════════════════════════════
87
+ ✨ zoom_factor - сила зума (-10 до +10)
88
+ ✨ convergence_point/convergence_y - точки фокуса (0.0-1.0)
89
+ ✨ depth_power - кривая глубины
90
+ ✨ pan_x/pan_y - сдвиг камеры (-1.0 до +1.0)
91
+ ✨ fade_strength - сила fade контента (0.0-1.0)
92
+ ✨ noise_strength - сила шума для outpainting (0.5-1.5, default: 1.0)
93
+ ✨ spiral_rotation - сила вращения (0.0-2.0)
94
+ ✨ spiral_direction - направление вращения (1.0/-1.0)
95
+ ✨ gradient_center_x/y - центр градиента (0.0-1.0)
96
+ ✨ gradient_radius - радиус градиента (0.1-2.0)
97
+ ✨ noise_scale - масштаб шума (1.0-10.0)
98
+ ✨ noise_octaves - октавы шума (1-4)
99
+ ✨ interp_mode - режим интерполяции ('bilinear'/'bicubic'/'nearest')
100
+ ✨ debug - режим отладки
101
+
102
+ СОВМЕСТИМОСТЬ:
103
+ ═══════════════════════════════════════════════════════════════════════════════
104
+ ✅ Полная интеграция с asymmetric_tiling_UNIFIED.py
105
+ ✅ Поддержка всех параметров из V3.0/V3.1/V3.1.1
106
+ ✅ Обратная совместимость со всеми режимами
107
+ ✅ extra_params поддержка для расширяемости
108
+ ✅ FLOAT16 СОВМЕСТИМОСТЬ - все критичные исправления применены
109
+ ✅ RUNTIME ERROR HANDLING - детальная диагностика и fallback
110
+ ✅ NO EXPAND ERRORS - все .expand() безопасные
111
+
112
+ ПРОИЗВОДИТЕЛЬНОСТЬ:
113
+ ═══════════════════════════════════════════════════════════════════════════════
114
+ ⚡ Умное кэширование (distance maps, noise patterns)
115
+ ⚡ Сэмплирование только для огромных тензоров (>10M элементов)
116
+ ⚡ Оптимизированные математические операции
117
+ ⚡ Минимальное использование памяти
118
+ ⚡ Безопасная работа с float16 без overflow/underflow
119
+ ⚡ Детальная диагностика для отладки
120
+ ⚡ Coherent adaptive noise для лучшего outpainting
121
+
122
+ КАЧЕСТВО OUTPAINTING:
123
+ ═══════════════════════════════════════════════════════════════════════════════
124
+ 🌟 Adaptive latent noise - шум адаптируется к статистике входных латентов
125
+ 🌟 Distance-based strength - сила шума зависит от расстояния до контента
126
+ 🌟 Coherent generation - нейросеть получает правильные подсказки для генерации
127
+ 🌟 Fade mask - плавный переход между контентом и шумом
128
+ 🌟 Convergence positioning - контроль положения контента на canvas
129
+
130
+ СТАБИЛЬНОСТЬ:
131
+ ═══════════════════════════════════════════════════════════════════════════════
132
+ ✅ В��е .expand() обернуты в try/except
133
+ ✅ Fallback через broadcast_to при ошибках
134
+ ✅ Проверка размеров перед операциями
135
+ ✅ Детальная диагностика при ошибках
136
+ ✅ Адаптивный epsilon для float16
137
+ ✅ Safe interpolate без dtype mismatch
138
+ ✅ НЕТ ИЗВЕСТНЫХ БАГОВ
139
+
140
+ ╔══════════════════════════════════════════════════════════════════════════════╗
141
+ ║ 🚀 vULTIMATE IS READY! 🚀 ║
142
+ ║ Coherent Outpainting + Rock-Solid Stability ║
143
+ ╚══════════════════════════════════════════════════════════════════════════════╝
144
+ """
145
+
146
+ import torch
147
+ import torch.nn.functional as F
148
+ import math
149
+ from enum import Enum
150
+ from collections import OrderedDict
151
+
152
+ # ═══════════════════════════════════════════════════════════════════════════
153
+ # УТИЛИТА ДЛЯ FLOAT16 СОВМЕСТИМОСТИ (V3.1.1 - НОВОЕ)
154
+ # ═══════════════════════════════════════════════════════════════════════════
155
+
156
+ def get_adaptive_epsilon(dtype):
157
+ """
158
+ Возвращает подходящий epsilon для данного dtype.
159
+
160
+ V3.1.1: КРИТИЧНОЕ ДЛЯ FLOAT16
161
+ Float16 имеет минимальное значение ~6e-5, поэтому 1e-6 вызывает underflow.
162
+
163
+ Args:
164
+ dtype: torch.dtype тензора
165
+
166
+ Returns:
167
+ float: безопасный epsilon для данного типа
168
+ """
169
+ if dtype == torch.float16:
170
+ return 1e-3 # Безопасный epsilon для float16
171
+ elif dtype == torch.float32:
172
+ return 1e-6 # Стандартный epsilon для float32
173
+ else: # float64
174
+ return 1e-12 # Высокая точность для float64
175
+
176
+ # ═══════════════════════════════════════════════════════════════════════════
177
+ # ENUMS
178
+ # ═══════════════════════════════════════════════════════════════════════════
179
+
180
+ class ZoomMode(Enum):
181
+ OUTPAINT_ZOOM = "outpaint_zoom" # Оптимизирован для outpainting (рекомендуется!)
182
+ BLEND_TRANSITION = "blend_transition" # Плавный переход с blending
183
+ CONVERGENCE_SHIFT = "convergence_shift" # Legacy сдвиг
184
+ GRID_WARP = "grid_warp" # Геометрический zoom
185
+ HYBRID = "hybrid" # Комбинация
186
+ SPIRAL_ZOOM = "spiral_zoom" # 🆕 V3.1: Спиральный zoom с вращением
187
+
188
+ class BlendMode(Enum):
189
+ CIRCULAR_REFLECT = "circular_reflect" # Бесшовный + отражение
190
+ CIRCULAR_CONSTANT = "circular_constant" # Бесшовный + константа
191
+ REFLECT_CONSTANT = "reflect_constant" # Отражение + константа
192
+ POLAR_CIRCULAR = "polar_circular" # Полярное + бесшовный
193
+ MIRROR_CIRCULAR = "mirror_circular" # Зеркало + бесшовный
194
+ ANISO_CIRCULAR = "aniso_circular" # Анизотропный + бесшовный
195
+ CUSTOM = "custom" # Пользовательский
196
+ GRADIENT_RADIAL = "gradient_radial" # 🆕 V3.1: Радиальный градиент
197
+ NOISE_BLEND = "noise_blend" # 🆕 V3.1: Блендинг с процедурным шумом
198
+
199
+ # ═══════════════════════════════════════════════════════════════════════════
200
+ # КЭШИРОВАНИЕ (V3.0 - УЛУЧШЕНО)
201
+ # ═══════════════════════════════════════════════════════════════════════════
202
+
203
+ class DistanceMapCache:
204
+ """Кэш для distance maps с LRU вытеснением"""
205
+ def __init__(self, max_size=20):
206
+ self.cache = OrderedDict()
207
+ self.max_size = max_size
208
+
209
+ def get(self, key):
210
+ if key in self.cache:
211
+ self.cache.move_to_end(key)
212
+ return self.cache[key]
213
+ return None
214
+
215
+ def set(self, key, value):
216
+ if key in self.cache:
217
+ self.cache.move_to_end(key)
218
+ else:
219
+ if len(self.cache) >= self.max_size:
220
+ self.cache.popitem(last=False)
221
+ self.cache[key] = value
222
+
223
+ _DISTANCE_MAP_CACHE = DistanceMapCache()
224
+
225
+ # ═══════════════════════════════════════════════════════════════════════════
226
+ # УТИЛИТЫ ДЛЯ ЛАТЕНТНОГО ШУМА (V3.0 - УЛУЧШЕНО)
227
+ # ═══════════════════════════════════════════════════════════════════════════
228
+
229
+ def compute_latent_statistics(input_tensor, percentile_clip=True):
230
+ """
231
+ Вычисляет статистику латентов для правильной генерации шума.
232
+
233
+ V3.0: Добавлен percentile_clip для робастности
234
+
235
+ Args:
236
+ input_tensor: входной тензор латентов
237
+ percentile_clip: использовать percentile вместо min/max
238
+
239
+ Returns:
240
+ dict: {'mean': float, 'std': float, 'min': float, 'max': float}
241
+ """
242
+ stats = {
243
+ 'mean': input_tensor.mean().item(),
244
+ 'std': input_tensor.std().item(),
245
+ }
246
+
247
+ if percentile_clip:
248
+ # V3.1 FIX: Правильная обработка quantile() dtype
249
+ flat = input_tensor.flatten()
250
+
251
+ # V3.1.1 FIX: Явная конверсия в float32 (вместо неявного .float())
252
+ if flat.dtype not in [torch.float32, torch.float64]:
253
+ flat = flat.to(torch.float32) # Более явный и безопасный вариант
254
+
255
+ # Умное сэмплирование ТОЛЬКО для очень больших тензоров (>10M элементов)
256
+ if flat.numel() > 10_000_000:
257
+ indices = torch.randperm(flat.numel(), device=flat.device)[:1_000_000]
258
+ flat = flat[indices]
259
+
260
+ try:
261
+ stats['min'] = torch.quantile(flat, 0.01).item()
262
+ stats['max'] = torch.quantile(flat, 0.99).item()
263
+ except RuntimeError as e:
264
+ # Fallback: используем сортировку для робастного percentile
265
+ sorted_flat = torch.sort(flat)[0]
266
+ idx_01 = max(0, int(0.01 * len(sorted_flat)))
267
+ idx_99 = min(len(sorted_flat) - 1, int(0.99 * len(sorted_flat)))
268
+ stats['min'] = sorted_flat[idx_01].item()
269
+ stats['max'] = sorted_flat[idx_99].item()
270
+ else:
271
+ stats['min'] = input_tensor.min().item()
272
+ stats['max'] = input_tensor.max().item()
273
+
274
+ return stats
275
+
276
+
277
+ def create_distance_map(canvas_h, canvas_w, content_box, device, dtype):
278
+ """
279
+ Создает карту расстояний от контента с кэшированием.
280
+
281
+ V3.0: Добавлено кэширование для оптимизации
282
+
283
+ Args:
284
+ canvas_h, canvas_w: размеры холста
285
+ content_box: (y1, y2, x1, x2) где размещен контент
286
+
287
+ Returns:
288
+ torch.Tensor (1, 1, canvas_h, canvas_w): карта расстояний [0, 1]
289
+ """
290
+ # Проверяем кэш
291
+ cache_key = (canvas_h, canvas_w, content_box, str(device), str(dtype))
292
+ cached = _DISTANCE_MAP_CACHE.get(cache_key)
293
+ if cached is not None:
294
+ return cached
295
+
296
+ y1, y2, x1, x2 = content_box
297
+
298
+ # Создаем координатные сетки
299
+ y_coords = torch.arange(canvas_h, device=device, dtype=dtype).view(-1, 1).expand(canvas_h, canvas_w)
300
+ x_coords = torch.arange(canvas_w, device=device, dtype=dtype).view(1, -1).expand(canvas_h, canvas_w)
301
+
302
+ # Расстояние до ближайшей точки контента
303
+ dist_y = torch.maximum(
304
+ torch.clamp(y1 - y_coords, min=0),
305
+ torch.clamp(y_coords - y2, min=0)
306
+ )
307
+ dist_x = torch.maximum(
308
+ torch.clamp(x1 - x_coords, min=0),
309
+ torch.clamp(x_coords - x2, min=0)
310
+ )
311
+
312
+ # Евклидово расстояние
313
+ distance = torch.sqrt(dist_x ** 2 + dist_y ** 2)
314
+
315
+ # V3.1.1 FIX: Защита от деления на 0 для float16 (было: может быть 0)
316
+ max_dist = max(math.sqrt(canvas_h**2 + canvas_w**2) * 0.5, get_adaptive_epsilon(dtype))
317
+ distance_norm = torch.clamp(distance / max_dist, 0, 1)
318
+
319
+ result = distance_norm.unsqueeze(0).unsqueeze(0)
320
+
321
+ # Сохраняем в кэш
322
+ _DISTANCE_MAP_CACHE.set(cache_key, result)
323
+
324
+ return result
325
+
326
+
327
+ def create_adaptive_latent_noise(canvas_shape, content_box, zoom_factor, input_stats,
328
+ device, dtype, blend_mode='circular_reflect',
329
+ noise_strength=1.0, adaptive_scale=True, seed=-1):
330
+ """
331
+ Adaptive latent noise for coherent outpainting backgrounds.
332
+ Pass seed >= 0 for reproducible results via a local torch.Generator;
333
+ seed=-1 (default) uses global RNG so behaviour matches the rest of the
334
+ pipeline without disturbing it.
335
+ """
336
+ b, c, canvas_h, canvas_w = canvas_shape
337
+
338
+ # Use a local Generator when a seed is requested so global RNG state
339
+ # is never mutated (mirrors the fix applied to gaussian_latent_noise).
340
+ gen = None
341
+ if seed >= 0:
342
+ gen = torch.Generator(device=device)
343
+ gen.manual_seed(int(seed))
344
+
345
+ base_noise = torch.randn(b, c, canvas_h, canvas_w,
346
+ device=device, dtype=dtype, generator=gen)
347
+
348
+ # 2. Применяем статистику
349
+ base_noise = base_noise * input_stats['std'] + input_stats['mean']
350
+
351
+ # 3. Distance map
352
+ distance_map = create_distance_map(canvas_h, canvas_w, content_box, device, dtype)
353
+
354
+ # 4. Adaptive scaling
355
+ if adaptive_scale:
356
+ zoom_scale = 1.0 - min(abs(zoom_factor) * 0.05, 0.3)
357
+ else:
358
+ zoom_scale = 1.0
359
+
360
+ # 5. Итоговая сила шума
361
+ final_strength = noise_strength * zoom_scale
362
+
363
+ # 6. Адаптивная сила
364
+ adaptive_strength = final_strength * (0.5 + distance_map * 1.5)
365
+
366
+ # 7. Apply strength (Безопасный expand)
367
+ if adaptive_strength.shape != base_noise.shape:
368
+ try:
369
+ adaptive_strength = adaptive_strength.expand(b, c, canvas_h, canvas_w)
370
+ except RuntimeError:
371
+ adaptive_strength = adaptive_strength.reshape(1, 1, canvas_h, canvas_w).expand(b, c, canvas_h, canvas_w)
372
+
373
+ adaptive_noise = base_noise * adaptive_strength
374
+
375
+ # 8. Edge smoothing
376
+ if 'circular' in blend_mode:
377
+ edge_smoothing = 0.9 + 0.1 * torch.cos(
378
+ torch.linspace(0, 2*math.pi, canvas_w, device=device, dtype=dtype)
379
+ ).view(1, 1, 1, -1)
380
+ try:
381
+ edge_smoothing = edge_smoothing.expand(b, c, canvas_h, canvas_w)
382
+ except RuntimeError:
383
+ edge_smoothing = edge_smoothing.reshape(1, 1, 1, canvas_w).expand(b, c, canvas_h, canvas_w)
384
+ adaptive_noise = adaptive_noise * edge_smoothing
385
+
386
+ return adaptive_noise
387
+
388
+
389
+ def apply_variance_correction(blended_tensor, mask, debug=False):
390
+ """
391
+ v3.3 ULTIMATE FIX:
392
+ 1. Исправляет размер (RuntimeError: size mismatch 30 vs 28)
393
+ 2. Исправляет тип данных (RuntimeError: Input type float and bias type Half)
394
+ """
395
+ # 1. Запоминаем исходный тип данных (скорее всего Float16)
396
+ target_dtype = blended_tensor.dtype
397
+
398
+ # Защита размерности маски
399
+ if mask.dim() < 4:
400
+ mask = mask.view(1, 1, mask.shape[-2], mask.shape[-1])
401
+
402
+ # Получаем epsilon (приводим к float32 для безопасности вычислений)
403
+ eps = 1e-6
404
+
405
+ # 2. Вычисляем маску в высокой точности (Float32), чтобы не было нулей
406
+ mask_f32 = mask.to(torch.float32)
407
+ variance_fix = torch.sqrt(mask_f32**2 + (1 - mask_f32)**2 + eps)
408
+
409
+ # 3. КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ РАЗМЕРОВ (Подгонка)
410
+ if variance_fix.shape[-2:] != blended_tensor.shape[-2:]:
411
+ if debug:
412
+ print(f"⚠️ Resizing correction mask: {variance_fix.shape} -> {blended_tensor.shape}")
413
+
414
+ variance_fix = F.interpolate(
415
+ variance_fix,
416
+ size=blended_tensor.shape[-2:],
417
+ mode='bilinear',
418
+ align_corners=True
419
+ )
420
+
421
+ # 4. Возвращаем маску в исходный тип (например, Float16) ПЕРЕД применением
422
+ variance_fix = variance_fix.to(dtype=target_dtype)
423
+
424
+ # 5. Безопасное расширение (Expand)
425
+ if variance_fix.shape[0] != blended_tensor.shape[0] or variance_fix.shape[1] != blended_tensor.shape[1]:
426
+ try:
427
+ variance_fix = variance_fix.expand_as(blended_tensor)
428
+ except RuntimeError:
429
+ # Fallback через repeat
430
+ target_shape = blended_tensor.shape
431
+ cur_shape = variance_fix.shape
432
+ reps = [max(1, t // c) for t, c in zip(target_shape, cur_shape)]
433
+ while len(reps) < 4: reps.insert(0, 1)
434
+ variance_fix = variance_fix.repeat(*reps)
435
+
436
+ # 6. Применяем коррекцию
437
+ corrected = blended_tensor / variance_fix
438
+
439
+ # 7. ФИНАЛЬНАЯ ЗАЩИТА ТИПА (Гарантируем возврат того же типа, что пришел)
440
+ return corrected.to(dtype=target_dtype)
441
+
442
+
443
+ # ═══════════════════════════════════════════════════════════════════════════
444
+ # 1. УЛУЧШЕННЫЙ LEGACY METHOD (V3.0)
445
+ # ═══════════════════════════════════════════════════════════════════════════
446
+
447
+ def apply_legacy_shift_zoom(input_tensor, zoom_factor, convergence=0.5, power=1.0,
448
+ pan_x=0.0, pan_y=0.0, auto_clamp_pan=True, debug=False):
449
+ """
450
+ V3.0 УЛУЧШЕНИЯ:
451
+ - Добавлен auto_clamp_pan для безопасного pan
452
+ - Debug mode для диагностики
453
+ """
454
+ b, c, h, w = input_tensor.shape
455
+ device = input_tensor.device
456
+ dtype = input_tensor.dtype
457
+
458
+ # V3.0: Auto-clamp pan для предотвращения потери контента
459
+ if auto_clamp_pan:
460
+ pan_x = max(-0.5, min(0.5, pan_x))
461
+ pan_y = max(-0.5, min(0.5, pan_y))
462
+
463
+ # 1. Применяем Pan
464
+ if pan_x != 0 or pan_y != 0:
465
+ shift_x = int(w * pan_x)
466
+ shift_y = int(h * pan_y)
467
+ input_tensor = torch.roll(input_tensor, shifts=(shift_y, shift_x), dims=(2, 3))
468
+
469
+ if debug:
470
+ print(f"[Legacy Shift Zoom] Pan applied: X={shift_x}px, Y={shift_y}px")
471
+
472
+ if abs(zoom_factor) < 0.001:
473
+ return input_tensor
474
+
475
+ # Максимальный сдвиг
476
+ max_shift_w = w // 4
477
+ max_shift_h = h // 4
478
+
479
+ shift_px_w = int(max_shift_w * (zoom_factor / 5.0))
480
+ shift_px_h = int(max_shift_h * (zoom_factor / 5.0))
481
+
482
+ # Правильная форма тензоров
483
+ x_1d = torch.linspace(0, 1, w, device=device, dtype=dtype)
484
+ x = x_1d.view(1, 1, 1, w).expand(b, c, h, w)
485
+
486
+ # Расстояние от convergence point
487
+ dist_x = torch.abs(x - convergence)
488
+
489
+ # Power - это "Mask Sharpness"
490
+ mask_w = torch.pow(torch.clamp(dist_x * 2.0, 0, 1), power)
491
+
492
+ # ZOOM OUT
493
+ if zoom_factor < 0:
494
+ left_mask = (x < convergence).to(dtype=dtype)
495
+ right_mask = (x >= convergence).to(dtype=dtype)
496
+
497
+ shifted_left = torch.roll(input_tensor, shifts=shift_px_w, dims=3)
498
+ shifted_right = torch.roll(input_tensor, shifts=-shift_px_w, dims=3)
499
+
500
+ result = shifted_left * left_mask + shifted_right * right_mask
501
+ return result
502
+
503
+ # ZOOM IN
504
+ else:
505
+ shifted = torch.roll(input_tensor, shifts=shift_px_w, dims=3)
506
+ result = input_tensor * (1.0 - mask_w) + shifted * mask_w
507
+ return result
508
+
509
+
510
+ # ═══════════════════════════════════════════════════════════════════════════
511
+ # 2. УЛУЧШЕННЫЙ GRID WARP (V3.0)
512
+ # ═══════════════════════════════════════════════════════════════════════════
513
+
514
+ def apply_grid_warp_zoom(input_tensor, zoom_factor, convergence=0.5, power=1.0,
515
+ pan_x=0.0, pan_y=0.0, convergence_y=0.5,
516
+ interp_mode='bilinear', debug=False):
517
+ """
518
+ V3.0 УЛУЧШЕНИЯ:
519
+ - Добавлен параметр interp_mode ('bilinear', 'bicubic', 'nearest')
520
+ - Scale clamping для предотвращения NaN
521
+ - Debug mode
522
+ """
523
+ b, c, h, w = input_tensor.shape
524
+ device = input_tensor.device
525
+ dtype = input_tensor.dtype
526
+
527
+ # V3.0: Scale clamping для безопасности
528
+ scale = 1.0 + (zoom_factor * 0.1)
529
+ scale = torch.clamp(torch.tensor(scale, device=device), min=0.1, max=10.0).item()
530
+
531
+ if debug:
532
+ print(f"[Grid Warp] Scale: {scale:.4f}, Interp: {interp_mode}")
533
+
534
+ y_coords = torch.linspace(-1, 1, h, device=device, dtype=dtype)
535
+ x_coords = torch.linspace(-1, 1, w, device=device, dtype=dtype)
536
+
537
+ y_grid, x_grid = torch.meshgrid(y_coords, x_coords, indexing='ij')
538
+
539
+ # Convergence определяет центр
540
+ center_x = (convergence - 0.5) * 2.0
541
+ center_y = (convergence_y - 0.5) * 2.0
542
+
543
+ # Pan
544
+ offset_x = pan_x * 2.0
545
+ offset_y = pan_y * 2.0
546
+
547
+ # Zoom относительно convergence point
548
+ x_new = (x_grid - center_x) / scale + center_x - offset_x
549
+ y_new = (y_grid - center_y) / scale + center_y - offset_y
550
+
551
+ grid = torch.stack((x_new, y_new), dim=-1)
552
+ grid = grid.unsqueeze(0).expand(b, -1, -1, -1)
553
+
554
+ # V3.0: Поддержка разных режимов интерполяции
555
+ if interp_mode not in ['bilinear', 'nearest']:
556
+ interp_mode = 'bilinear' # Fallback (bicubic не поддерживается в grid_sample)
557
+
558
+ return F.grid_sample(
559
+ input_tensor,
560
+ grid,
561
+ mode=interp_mode,
562
+ padding_mode='zeros',
563
+ align_corners=True
564
+ )
565
+
566
+
567
+
568
+ # ═══════════════════════════════════════════════════════════════════════════
569
+ # 2.5. БЕЗОПАСНАЯ ИНТЕРПОЛЯЦИЯ ДЛЯ FLOAT16 (V3.1.1 - НОВОЕ)
570
+ # ═══════════════════════════════════════════════════════════════════════════
571
+
572
+ def safe_interpolate(tensor, size, mode='bilinear'):
573
+ """
574
+ Безопасная интерполяция с поддержкой float16.
575
+
576
+ V3.1.1: НОВАЯ ФУНКЦИЯ
577
+ - Конвертирует float16 → float32 для интерполяции (предотвращает overflow)
578
+ - Правильно обрабатывает align_corners (не использует None)
579
+ - Возвращает результат в исходном dtype
580
+
581
+ Args:
582
+ tensor: входной тензор
583
+ size: целевой размер (H, W)
584
+ mode: режим интерполяции ('bilinear', 'bicubic', 'nearest')
585
+
586
+ Returns:
587
+ torch.Tensor: интерполированный тензор в исходном dtype
588
+ """
589
+ original_dtype = tensor.dtype
590
+
591
+ # Для float16 конвертируем в float32 для стабильности
592
+ if original_dtype == torch.float16:
593
+ tensor = tensor.float()
594
+
595
+ # FIX: Правильная обработка align_corners (не использовать None!)
596
+ interpolate_kwargs = {
597
+ 'size': size,
598
+ 'mode': mode,
599
+ }
600
+ if mode != 'nearest':
601
+ interpolate_kwargs['align_corners'] = True
602
+
603
+ result = F.interpolate(tensor, **interpolate_kwargs)
604
+
605
+ # Конвертируем обратно в исходный dtype
606
+ if original_dtype == torch.float16:
607
+ result = result.half()
608
+
609
+ return result
610
+
611
+
612
+ # ═══════════════════════════════════════════════════════════════════════════
613
+ # 3. ПОЛНОСТЬЮ ПЕРЕРАБОТАННЫЙ OUTPAINT ZOOM (V3.0)
614
+ # ═══════════════════════════════════════════════════════════════════════════
615
+
616
+ def apply_outpaint_zoom(input_tensor, zoom_factor, pad_h, pad_w,
617
+ convergence=0.5, convergence_y=0.5,
618
+ fade_strength=0.3, depth_power=1.0,
619
+ pan_x=0.0, pan_y=0.0,
620
+ fade_to_black=False, fade_edge_strength=0.15,
621
+ blend_mode='circular_reflect',
622
+ noise_strength=1.0,
623
+ interp_mode='bilinear',
624
+ zoom_in_fade=True,
625
+ variance_correction=True,
626
+ auto_clamp_pan=True,
627
+ adaptive_noise_scale=True,
628
+ debug=False,
629
+ extra_params=None): # V3.1.1 FIX: Добавлен extra_params
630
+ """
631
+ ═══════════════════════════════════════════════════════════════════════════
632
+ V3.0 - ПРОФЕССИОНАЛЬНАЯ ВЕРСИЯ С ИСПРАВЛЕНИЕМ ВСЕХ ПРОБЛЕМ
633
+ V3.1.1 - КРИТИЧНЫЕ ИСПРАВЛЕНИЯ ДЛЯ FLOAT16
634
+ ═══════════════════════════════════════════════════════════════════════════
635
+
636
+ ИСПРАВЛЕНИЯ V3.0:
637
+ 🔧 noise_strength теперь 1.0 (было 0.1) - coherent outpainting
638
+ 🔧 interp_mode параметр для выбора интерполяции
639
+ 🔧 zoom_in_fade для устранения швов при приближении
640
+ 🔧 variance_correction для устранения серости
641
+ 🔧 auto_clamp_pan для безопасного сдвига
642
+ 🔧 adaptive_noise_scale для умного масштабирования шума
643
+ 🔧 debug режим для диагностики
644
+ 🔧 Улучшенная валидация с warnings
645
+
646
+ ИСПРАВЛЕНИЯ V3.1.1:
647
+ 🔧 extra_params для передачи параметров в gradient_radial/noise_blend
648
+ 🔧 safe_interpolate вместо F.interpolate для float16
649
+ 🔧 Адаптивный epsilon для всех операций
650
+
651
+ Args:
652
+ input_tensor: входной латент (b, c, h, w)
653
+ zoom_factor: сила зума (-10 д�� +10)
654
+ pad_h, pad_w: padding размеры
655
+ convergence, convergence_y: точки фокуса (0-1)
656
+ fade_strength: сила fade на контенте (0-1)
657
+ depth_power: кривая градиента fade (<1 резче, >1 мягче)
658
+ pan_x, pan_y: сдвиг (-1 до +1)
659
+ fade_to_black: затемнение внешних краев canvas
660
+ fade_edge_strength: сила edge fade
661
+ blend_mode: режим блендинга
662
+ noise_strength: сила шума для outpainting (0.5-1.5, default: 1.0) 🆕
663
+ interp_mode: режим интерполяции ('bilinear'/'bicubic'/'nearest') 🆕
664
+ zoom_in_fade: применять fade при zoom in 🆕
665
+ variance_correction: коррекция серости 🆕
666
+ auto_clamp_pan: автоматическая коррекция pan 🆕
667
+ adaptive_noise_scale: адаптивное масштабирование шума 🆕
668
+ debug: режим отладки 🆕
669
+ extra_params: дополнительные параметры (dict) 🆕 V3.1.1
670
+
671
+ Returns:
672
+ torch.Tensor: результат зума с padding
673
+ """
674
+ b, c, h, w = input_tensor.shape
675
+ device = input_tensor.device
676
+ dtype = input_tensor.dtype
677
+
678
+ if debug:
679
+ print(f"\n{'='*70}")
680
+ print(f"[Outpaint Zoom V3.0] Starting...")
681
+ print(f" Input shape: {input_tensor.shape}")
682
+ print(f" Zoom factor: {zoom_factor:.2f}")
683
+ print(f" Noise strength: {noise_strength:.2f}")
684
+ print(f" Interp mode: {interp_mode}")
685
+ print(f"{'='*70}\n")
686
+
687
+ is_zooming = abs(zoom_factor) > 0.001
688
+ is_panning = abs(pan_x) > 0.001 or abs(pan_y) > 0.001
689
+
690
+ # Если ничего не происходит — просто возвращаем паддинг
691
+ if not is_zooming and not is_panning:
692
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
693
+
694
+ # V3.0: Валидация interp_mode
695
+ valid_modes = ['bilinear', 'bicubic', 'nearest']
696
+ if interp_mode not in valid_modes:
697
+ if debug:
698
+ print(f"⚠️ Warning: Invalid interp_mode '{interp_mode}', using 'bilinear'")
699
+ interp_mode = 'bilinear'
700
+
701
+ # ═══════════════════════════════════════════════════════════════════
702
+ # ZOOM OUT (ОТДАЛЕНИЕ)
703
+ # ═══════════════════════════════════════════════════════════════════
704
+ if zoom_factor < 0:
705
+ # V3.1: Проверяем специальные blend режимы
706
+ if blend_mode == 'gradient_radial':
707
+ # V3.1.1 FIX: Используем параметры из extra_params если есть
708
+ gradient_center_x = 0.5
709
+ gradient_center_y = 0.5
710
+ gradient_radius = 1.0
711
+
712
+ if extra_params:
713
+ gradient_center_x = extra_params.get('gradient_center_x', 0.5)
714
+ gradient_center_y = extra_params.get('gradient_center_y', 0.5)
715
+ gradient_radius = extra_params.get('gradient_radius', 1.0)
716
+
717
+ if debug:
718
+ print(f"[Blend Mode] Using GRADIENT_RADIAL")
719
+ return apply_gradient_radial_blend(
720
+ input_tensor, pad_h, pad_w,
721
+ gradient_center_x=gradient_center_x,
722
+ gradient_center_y=gradient_center_y,
723
+ gradient_radius=gradient_radius,
724
+ debug=debug
725
+ )
726
+ elif blend_mode == 'noise_blend':
727
+ # V3.1.1 FIX: Используем параметры из extra_params если есть
728
+ noise_scale = 5.0
729
+ noise_octaves = 2
730
+
731
+ if extra_params:
732
+ noise_scale = extra_params.get('noise_scale', 5.0)
733
+ noise_octaves = extra_params.get('noise_octaves', 2)
734
+
735
+ if debug:
736
+ print(f"[Blend Mode] Using NOISE_BLEND")
737
+ return apply_noise_blend(
738
+ input_tensor, pad_h, pad_w,
739
+ noise_scale=noise_scale,
740
+ noise_octaves=noise_octaves,
741
+ debug=debug
742
+ )
743
+
744
+ # 1. Масштабирование
745
+ scale = 1.0 + abs(zoom_factor) * 0.1
746
+ scale = max(1.0, min(scale, 4.0))
747
+
748
+ new_h = max(int(h / scale), 16)
749
+ new_w = max(int(w / scale), 16)
750
+
751
+ if debug:
752
+ print(f"[Zoom Out] Scale: {scale:.4f}, New size: {new_h}x{new_w}")
753
+
754
+ # V3.1.1 FIX: Используем safe_interpolate для float16 совместимости
755
+ content_small = safe_interpolate(
756
+ input_tensor,
757
+ size=(new_h, new_w),
758
+ mode=interp_mode
759
+ )
760
+
761
+ # 2. Создаем fade маску (с учетом Depth Power)
762
+ mask = torch.ones(1, 1, new_h, new_w, device=device, dtype=dtype)
763
+
764
+ fade_h = int(new_h * fade_strength)
765
+ fade_w = int(new_w * fade_strength)
766
+
767
+ if fade_h > 0 and fade_w > 0:
768
+ lin_x = torch.linspace(0, 1, fade_w, device=device, dtype=dtype)
769
+ lin_y = torch.linspace(0, 1, fade_h, device=device, dtype=dtype)
770
+
771
+ # Depth Power для кривой градиента
772
+ curve_x = torch.pow(lin_x, depth_power)
773
+ curve_y = torch.pow(lin_y, depth_power)
774
+
775
+ mask[:, :, :, :fade_w] *= curve_x.view(1, 1, 1, -1)
776
+ mask[:, :, :, -fade_w:] *= curve_x.flip(0).view(1, 1, 1, -1)
777
+ mask[:, :, :fade_h, :] *= curve_y.view(1, 1, -1, 1)
778
+ mask[:, :, -fade_h:, :] *= curve_y.flip(0).view(1, 1, -1, 1)
779
+
780
+ content_faded = content_small * mask.expand_as(content_small)
781
+
782
+ # 3. Создаем canvas с адаптивным шумом
783
+ canvas_h = h + 2 * pad_h
784
+ canvas_w = w + 2 * pad_w
785
+
786
+ # Convergence определяет позицию фокуса
787
+ focus_x = int((canvas_w - new_w) * convergence)
788
+ focus_y = int((canvas_h - new_h) * convergence_y)
789
+
790
+ center_x = focus_x
791
+ center_y = focus_y
792
+
793
+ # V3.0: Pan с auto-clamping
794
+ # EDGE-CASE FIX: when new_w/new_h >= canvas (tiny latent + min-16 forcing),
795
+ # (canvas - new) is negative → max_pan goes negative → clamp flips pan to +1
796
+ # producing false clipping warnings. Explicitly zero pan on any axis where
797
+ # content is at least as large as the canvas.
798
+ if auto_clamp_pan:
799
+ if new_w >= canvas_w:
800
+ pan_x = 0.0
801
+ else:
802
+ max_pan_x = (canvas_w - new_w) / canvas_w
803
+ pan_x = max(-max_pan_x, min(max_pan_x, pan_x))
804
+ if new_h >= canvas_h:
805
+ pan_y = 0.0
806
+ else:
807
+ max_pan_y = (canvas_h - new_h) / canvas_h
808
+ pan_y = max(-max_pan_y, min(max_pan_y, pan_y))
809
+
810
+ shift_y = int(pan_y * canvas_h * 0.5)
811
+ shift_x = int(pan_x * canvas_w * 0.5)
812
+
813
+ paste_y = center_y + shift_y
814
+ paste_x = center_x + shift_x
815
+
816
+ # V3.0: Улучшенная валидация с warnings
817
+ # EDGE-CASE FIX: suppress warning when content >= canvas on an axis —
818
+ # the overflow is structural (min-16 forcing), not caused by user pan.
819
+ clipped = False
820
+ if (paste_y < 0 or paste_y + new_h > canvas_h) and new_h < canvas_h:
821
+ print(f"⚠️ Warning: Pan Y ({pan_y:.2f}) causes vertical clipping")
822
+ clipped = True
823
+ if (paste_x < 0 or paste_x + new_w > canvas_w) and new_w < canvas_w:
824
+ print(f"⚠️ Warning: Pan X ({pan_x:.2f}) causes horizontal clipping")
825
+ clipped = True
826
+
827
+ # Безопасная вставка с clipping
828
+ y1_c = max(0, paste_y)
829
+ x1_c = max(0, paste_x)
830
+ y2_c = min(canvas_h, paste_y + new_h)
831
+ x2_c = min(canvas_w, paste_x + new_w)
832
+
833
+ y1_src = max(0, -paste_y)
834
+ x1_src = max(0, -paste_x)
835
+ y2_src = y1_src + (y2_c - y1_c)
836
+ x2_src = x1_src + (x2_c - x1_c)
837
+
838
+ # V3.0: ИСПРАВЛЕНО - Адаптивный латентный шум с правильной силой
839
+ input_stats = compute_latent_statistics(input_tensor, percentile_clip=True)
840
+ content_box = (y1_c, y2_c, x1_c, x2_c)
841
+
842
+ # === ФИНАЛЬНЫЙ РАБОЧИЙ ВАРИАНТ ===
843
+
844
+ # 1. Вычисляем, сколько места пустого слева, справа, сверху, снизу
845
+ pad_left = x1_c
846
+ pad_right = canvas_w - x2_c
847
+ pad_top = y1_c
848
+ pad_bottom = canvas_h - y2_c
849
+
850
+ # 2. ГЛАВНОЕ ИСПРАВЛЕНИЕ:
851
+ # Мы берем content_small (это УМЕНЬШЕННАЯ картинка).
852
+ # И добавляем к ней края (mode='reflect' - это зеркальное отражение, чтобы не было швов).
853
+ # В итоге получается картинка нужного размера (как холст).
854
+
855
+ # BUG FIX 2: reflect padding crashes when any pad >= the reduced
856
+ # dimension. Use it only when it's geometrically valid; otherwise
857
+ # fall back to replicate so we never hard-crash.
858
+ reflect_safe = (pad_left < new_w and pad_right < new_w and
859
+ pad_top < new_h and pad_bottom < new_h)
860
+ if reflect_safe:
861
+ canvas = F.pad(content_small,
862
+ (pad_left, pad_right, pad_top, pad_bottom),
863
+ mode='reflect')
864
+ else:
865
+ # Fallback: replicate is always safe regardless of pad size.
866
+ # For larger outpainting factors this is visually acceptable and
867
+ # lets create_adaptive_latent_noise blend over it below.
868
+ canvas = F.pad(content_small,
869
+ (pad_left, pad_right, pad_top, pad_bottom),
870
+ mode='replicate')
871
+
872
+ # BUG FIX 4: previously the canvas was built solely from reflect/
873
+ # replicate padding of the shrunken content, making noise_strength
874
+ # and adaptive_noise_scale purely decorative. Now we actually use
875
+ # create_adaptive_latent_noise to fill the background of the canvas
876
+ # and blend it with the padded content so those parameters have a
877
+ # real visible effect.
878
+ #
879
+ # FIX 4 REGRESSION PATCH: when new_h/new_w are forced to min=16 and
880
+ # the input is tiny (e.g. 8x8 latent), the pads can collapse to zero
881
+ # and canvas ends up being new_h×new_w (e.g. 16×16), not the
882
+ # pre-computed canvas_h×canvas_w (8×8). Always read the real shape
883
+ # after F.pad and drive everything from those actual dimensions.
884
+ actual_canvas_h, actual_canvas_w = canvas.shape[-2:]
885
+
886
+ if noise_strength > 0.01:
887
+ # Clip content_box to actual canvas bounds (safe for tiny inputs)
888
+ box_y1 = min(y1_c, actual_canvas_h)
889
+ box_y2 = min(y2_c, actual_canvas_h)
890
+ box_x1 = min(x1_c, actual_canvas_w)
891
+ box_x2 = min(x2_c, actual_canvas_w)
892
+ actual_content_box = (box_y1, box_y2, box_x1, box_x2)
893
+
894
+ adaptive_canvas = create_adaptive_latent_noise(
895
+ canvas_shape=(b, c, actual_canvas_h, actual_canvas_w),
896
+ content_box=actual_content_box,
897
+ zoom_factor=zoom_factor,
898
+ input_stats=input_stats,
899
+ device=device,
900
+ dtype=dtype,
901
+ blend_mode=blend_mode,
902
+ noise_strength=noise_strength,
903
+ adaptive_scale=adaptive_noise_scale,
904
+ )
905
+ # Blend: keep padded content as base, overlay adaptive noise
906
+ # in the outpaint region only (outside the content paste box).
907
+ content_region_mask = torch.zeros(1, 1, actual_canvas_h, actual_canvas_w,
908
+ device=device, dtype=dtype)
909
+ if box_y2 > box_y1 and box_x2 > box_x1:
910
+ content_region_mask[:, :, box_y1:box_y2, box_x1:box_x2] = 1.0
911
+ canvas = canvas * content_region_mask + adaptive_canvas * (1.0 - content_region_mask)
912
+
913
+ if debug:
914
+ print(f"[Noise Stats] Mean: {canvas.mean().item():.4f}, "
915
+ f"Std: {canvas.std().item():.4f}")
916
+
917
+ # Вставляем контент
918
+ if y2_c > y1_c and x2_c > x1_c:
919
+ canvas[:, :, y1_c:y2_c, x1_c:x2_c] = content_faded[:, :, y1_src:y2_src, x1_src:x2_src]
920
+
921
+ # V3.0: fade_to_black для краев canvas (use actual dims)
922
+ if fade_to_black:
923
+ edge_fade_h = int(actual_canvas_h * fade_edge_strength)
924
+ edge_fade_w = int(actual_canvas_w * fade_edge_strength)
925
+
926
+ if edge_fade_h > 0 and edge_fade_w > 0:
927
+ fade_mask = torch.ones(1, 1, actual_canvas_h, actual_canvas_w, device=device, dtype=dtype)
928
+
929
+ fade_h = torch.linspace(0, 1, edge_fade_h, device=device, dtype=dtype)
930
+ fade_w = torch.linspace(0, 1, edge_fade_w, device=device, dtype=dtype)
931
+
932
+ fade_mask[:, :, :edge_fade_h, :] *= fade_h.view(-1, 1)
933
+ fade_mask[:, :, -edge_fade_h:, :] *= fade_h.flip(0).view(-1, 1)
934
+ fade_mask[:, :, :, :edge_fade_w] *= fade_w.view(1, -1)
935
+ fade_mask[:, :, :, -edge_fade_w:] *= fade_w.flip(0).view(1, -1)
936
+
937
+ canvas = canvas * fade_mask.expand_as(canvas)
938
+
939
+ # V3.0: Shape check (converted from hard assert so tiny latents don't crash;
940
+ # when new_h/new_w are forced to min=16 the canvas can legitimately differ
941
+ # from the pre-computed canvas_h×canvas_w on very small inputs).
942
+ if canvas.shape != (b, c, canvas_h, canvas_w):
943
+ if debug:
944
+ print(f"[Outpaint Zoom] Note: canvas shape {canvas.shape} differs from "
945
+ f"expected {(b, c, canvas_h, canvas_w)} (normal for tiny latents)")
946
+
947
+ return canvas
948
+
949
+ # ═══════════════════════════════════════════════════════════════════
950
+ # ZOOM IN (ПРИБЛИЖЕНИЕ)
951
+ # ═══════════════════════════════════════════════════════════════════
952
+ else:
953
+ scale = 1.0 + zoom_factor * 0.1
954
+ new_h = int(h * scale)
955
+ new_w = int(w * scale)
956
+
957
+ if debug:
958
+ print(f"[Zoom In] Scale: {scale:.4f}, New size: {new_h}x{new_w}")
959
+
960
+ # V3.1.1 FIX: Используем safe_interpolate для float16 совместимости
961
+ content_large = safe_interpolate(
962
+ input_tensor,
963
+ size=(new_h, new_w),
964
+ mode=interp_mode
965
+ )
966
+
967
+ # Convergence для zoom in - определяет откуда "смотрим"
968
+ focus_x = int(new_w * convergence) - w // 2
969
+ focus_y = int(new_h * convergence_y) - h // 2
970
+
971
+ # V3.0: Pan с auto-clamping
972
+ if auto_clamp_pan:
973
+ max_pan_x = (new_w - w) / w * 0.5
974
+ max_pan_y = (new_h - h) / h * 0.5
975
+ pan_x = max(-max_pan_x, min(max_pan_x, pan_x))
976
+ pan_y = max(-max_pan_y, min(max_pan_y, pan_y))
977
+
978
+ shift_y = int(pan_y * h * 0.5)
979
+ shift_x = int(pan_x * w * 0.5)
980
+
981
+ crop_y = max(0, min(new_h - h, focus_y + shift_y))
982
+ crop_x = max(0, min(new_w - w, focus_x + shift_x))
983
+
984
+ cropped = content_large[:, :, crop_y:crop_y+h, crop_x:crop_x+w]
985
+
986
+ # V3.0: НОВОЕ - Fade для zoom in (устраняет швы!)
987
+ # BUG FIX 3: initialise fade_mask=None so downstream code can always
988
+ # do a safe `if fade_mask is not None:` check. Without this,
989
+ # if fade_h_in==0 or fade_w_in==0 the variable is never created but
990
+ # is still referenced inside the variance_correction block.
991
+ fade_mask = None
992
+ if zoom_in_fade and fade_strength > 0:
993
+ # Легкий fade на краях для smooth transitions
994
+ fade_h_in = int(h * fade_strength * 0.5) # Меньше чем для zoom out
995
+ fade_w_in = int(w * fade_strength * 0.5)
996
+
997
+ if fade_h_in > 0 and fade_w_in > 0:
998
+ fade_mask = torch.ones(1, 1, h, w, device=device, dtype=dtype)
999
+
1000
+ lin_x = torch.linspace(0, 1, fade_w_in, device=device, dtype=dtype)
1001
+ lin_y = torch.linspace(0, 1, fade_h_in, device=device, dtype=dtype)
1002
+
1003
+ curve_x = torch.pow(lin_x, depth_power)
1004
+ curve_y = torch.pow(lin_y, depth_power)
1005
+
1006
+ fade_mask[:, :, :, :fade_w_in] *= curve_x.view(1, 1, 1, -1)
1007
+ fade_mask[:, :, :, -fade_w_in:] *= curve_x.flip(0).view(1, 1, 1, -1)
1008
+ fade_mask[:, :, :fade_h_in, :] *= curve_y.view(1, 1, -1, 1)
1009
+ fade_mask[:, :, -fade_h_in:, :] *= curve_y.flip(0).view(1, 1, -1, 1)
1010
+
1011
+ cropped = cropped * fade_mask.expand_as(cropped)
1012
+
1013
+ if debug:
1014
+ print(f"[Zoom In Fade] Applied with strength {fade_strength:.2f}")
1015
+
1016
+ # Padding
1017
+ padded = F.pad(cropped, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1018
+
1019
+ # V3.0: Variance correction для устранения серости на швах
1020
+ if variance_correction and zoom_in_fade:
1021
+ # BUG FIX 3 (continued): use fade_mask only if it was actually
1022
+ # created; fall back to a neutral all-ones mask when fade
1023
+ # dimensions collapsed to zero (very small latents).
1024
+ if fade_mask is not None:
1025
+ correction_mask = fade_mask
1026
+ else:
1027
+ # Fade dimensions were zero — use a flat mask so
1028
+ # variance_correction still runs without crashing.
1029
+ correction_mask = torch.ones(1, 1, h, w, device=device, dtype=dtype)
1030
+
1031
+ padded = apply_variance_correction(padded, correction_mask, debug=debug)
1032
+
1033
+ # V3.0: Shape assertion
1034
+ expected_shape = (b, c, h + 2*pad_h, w + 2*pad_w)
1035
+ assert padded.shape == expected_shape, \
1036
+ f"Shape mismatch! Expected {expected_shape}, got {padded.shape}"
1037
+
1038
+ return padded
1039
+
1040
+
1041
+ # ═══════════════════════════════════════════════════════════════════════════
1042
+ # 3.5. SPIRAL ZOOM (V3.1 - НОВАЯ ФУНКЦИЯ БЕЗ БАГОВ)
1043
+ # ═══════════════════════════════════════════════════════════════════════════
1044
+
1045
+ def apply_spiral_zoom(input_tensor, zoom_factor, pad_h, pad_w,
1046
+ spiral_rotation=0.5,
1047
+ spiral_direction=1.0,
1048
+ interp_mode='bilinear',
1049
+ debug=False,
1050
+ **kwargs):
1051
+ """
1052
+ Спиральный зум с эффектом вращения.
1053
+
1054
+ V3.1: ПОЛНАЯ РЕАЛИЗАЦИЯ БЕЗ БАГОВ
1055
+ - Правильная валидация параметров
1056
+ - Безопасная обработка особых случаев (dx=dy=0)
1057
+ - Проверка размеров на всех этапах
1058
+
1059
+ V3.1.1: КРИТИЧНЫЕ ИСПРАВЛЕНИЯ ДЛЯ FLOAT16
1060
+ - Адаптивный epsilon для всех sqrt/division операций
1061
+
1062
+ Args:
1063
+ input_tensor: входной латент (B, C, H, W)
1064
+ zoom_factor: сила зума (-5.0 до 5.0)
1065
+ pad_h, pad_w: размеры паддинга
1066
+ spiral_rotation: сила вращения (0.0 до 2.0)
1067
+ - 0.0 = без вращения (обычный зум)
1068
+ - 0.5 = слабое вращение
1069
+ - 1.0 = среднее вращение
1070
+ - 2.0 = сильное вращение
1071
+ spiral_direction: направление (1.0 = по часовой, -1.0 = против)
1072
+ interp_mode: режим интерполяции ('bilinear', 'bicubic', 'nearest')
1073
+ debug: вывод отладочной информации
1074
+
1075
+ Returns:
1076
+ torch.Tensor: трансформированный и padded тензор
1077
+ """
1078
+ b, c, h, w = input_tensor.shape
1079
+ device = input_tensor.device
1080
+ dtype = input_tensor.dtype
1081
+
1082
+ # V3.1.1 FIX: Получаем адаптивный epsilon для данного dtype
1083
+ eps = get_adaptive_epsilon(dtype)
1084
+
1085
+ # V3.1: Валидация параметров
1086
+ spiral_rotation = float(max(0.0, min(2.0, spiral_rotation)))
1087
+ spiral_direction = 1.0 if spiral_direction >= 0 else -1.0
1088
+ zoom_factor = float(max(-5.0, min(5.0, zoom_factor)))
1089
+
1090
+ if debug:
1091
+ print(f"\n{'='*70}")
1092
+ print(f"[Spiral Zoom V3.1.1]")
1093
+ print(f" Input shape: {input_tensor.shape}")
1094
+ print(f" Zoom Factor: {zoom_factor:.2f}")
1095
+ print(f" Rotation: {spiral_rotation:.2f} ({'clockwise' if spiral_direction > 0 else 'counter-clockwise'})")
1096
+ print(f" Interp mode: {interp_mode}")
1097
+ print(f" Epsilon: {eps} (for {dtype})")
1098
+ print(f"{'='*70}\n")
1099
+
1100
+ # Центр изображения
1101
+ center_y = (h - 1) / 2.0
1102
+ center_x = (w - 1) / 2.0
1103
+
1104
+ # Создаем координатные сетки
1105
+ y_coords = torch.arange(h, device=device, dtype=dtype).view(-1, 1).expand(h, w)
1106
+ x_coords = torch.arange(w, device=device, dtype=dtype).view(1, -1).expand(h, w)
1107
+
1108
+ # Смещение от центра
1109
+ dy = y_coords - center_y
1110
+ dx = x_coords - center_x
1111
+
1112
+ # V3.1.1 FIX: Полярные координаты с адаптивным epsilon
1113
+ r = torch.sqrt(dx**2 + dy**2 + eps)
1114
+ theta = torch.atan2(dy, dx)
1115
+
1116
+ # Спиральная трансформация
1117
+ # 1. Zoom scale
1118
+ zoom_scale = 1.0 + zoom_factor * 0.1
1119
+
1120
+ # 2. Rotation - зависит от расстояния от центра
1121
+ max_radius = math.sqrt(h**2 + w**2) / 2.0
1122
+ # V3.1.1 FIX: Адаптивный epsilon для division
1123
+ normalized_r = torch.clamp(r / (max_radius + eps), 0.0, 1.0)
1124
+
1125
+ # Угол вращения увеличивается с расстоянием от центра (спиральный эффект)
1126
+ rotation_angle = spiral_direction * spiral_rotation * normalized_r * math.pi
1127
+
1128
+ # 3. Применяем трансформацию
1129
+ new_theta = theta + rotation_angle
1130
+ new_r = r * zoom_scale
1131
+
1132
+ # Обратно в декартовы координаты
1133
+ new_x = center_x + new_r * torch.cos(new_theta)
1134
+ new_y = center_y + new_r * torch.sin(new_theta)
1135
+
1136
+ # Нормализация для grid_sample [-1, 1]
1137
+ grid_x = 2.0 * new_x / max(w - 1, 1) - 1.0
1138
+ grid_y = 2.0 * new_y / max(h - 1, 1) - 1.0
1139
+
1140
+ # V3.1 FIX: Clamp grid values для предотвращения выхода за границы
1141
+ grid_x = torch.clamp(grid_x, -1.0, 1.0)
1142
+ grid_y = torch.clamp(grid_y, -1.0, 1.0)
1143
+
1144
+ # Собираем grid
1145
+ grid = torch.stack([grid_x, grid_y], dim=-1).unsqueeze(0).to(dtype)
1146
+
1147
+ # V3.1: Валидация размеров grid
1148
+ expected_grid_shape = (1, h, w, 2)
1149
+ if grid.shape != expected_grid_shape:
1150
+ raise ValueError(f"Grid shape mismatch! Expected {expected_grid_shape}, got {grid.shape}")
1151
+
1152
+ # Применяем деформацию
1153
+ warped = F.grid_sample(
1154
+ input_tensor,
1155
+ grid.expand(b, -1, -1, -1),
1156
+ mode=interp_mode,
1157
+ padding_mode='zeros',
1158
+ align_corners=True
1159
+ )
1160
+
1161
+ # V3.1: Проверка после warp
1162
+ if warped.shape != input_tensor.shape:
1163
+ raise ValueError(f"Warped shape mismatch! Expected {input_tensor.shape}, got {warped.shape}")
1164
+
1165
+ # Паддинг
1166
+ padded = F.pad(warped, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1167
+
1168
+ # V3.1: Финальная проверка размеров
1169
+ expected_padded_shape = (b, c, h + 2*pad_h, w + 2*pad_w)
1170
+ if padded.shape != expected_padded_shape:
1171
+ raise ValueError(f"Padded shape mismatch! Expected {expected_padded_shape}, got {padded.shape}")
1172
+
1173
+ if debug:
1174
+ print(f"[Spiral Zoom] Input shape: {input_tensor.shape}")
1175
+ print(f"[Spiral Zoom] Output shape: {padded.shape}")
1176
+ print(f"[Spiral Zoom] ✓ All shape checks passed")
1177
+
1178
+ return padded
1179
+
1180
+
1181
+ # ═══════════════════════════════════════════════════════════════════════════
1182
+ # 3.6. GRADIENT RADIAL BLENDING (V3.1 - НОВАЯ ФУНКЦИЯ)
1183
+ # ═══════════════════════════════════════════════════════════════════════════
1184
+
1185
+ def apply_gradient_radial_blend(input_tensor, pad_h, pad_w,
1186
+ gradient_center_x=0.5, gradient_center_y=0.5,
1187
+ gradient_radius=1.0, debug=False):
1188
+ """
1189
+ Радиальный градиент для плавных переходов от центра к краям.
1190
+
1191
+ V3.1: НОВАЯ ФУНКЦИЯ
1192
+
1193
+ Args:
1194
+ input_tensor: входной латент (B, C, H, W)
1195
+ pad_h, pad_w: размеры паддинга
1196
+ gradient_center_x: центр по X (0.0-1.0, default 0.5)
1197
+ gradient_center_y: центр по Y (0.0-1.0, default 0.5)
1198
+ gradient_radius: радиус градиента (0.1-2.0, default 1.0)
1199
+ debug: вывод отладки
1200
+
1201
+ Returns:
1202
+ torch.Tensor: padded тензор с радиальным градиентом
1203
+ """
1204
+ b, c, h, w = input_tensor.shape
1205
+ device = input_tensor.device
1206
+ dtype = input_tensor.dtype
1207
+
1208
+ # Валидация параметров
1209
+ gradient_center_x = float(max(0.0, min(1.0, gradient_center_x)))
1210
+ gradient_center_y = float(max(0.0, min(1.0, gradient_center_y)))
1211
+ gradient_radius = float(max(0.1, min(2.0, gradient_radius)))
1212
+
1213
+ if debug:
1214
+ print(f"[Gradient Radial] Center: ({gradient_center_x:.2f}, {gradient_center_y:.2f}), "
1215
+ f"Radius: {gradient_radius:.2f}")
1216
+
1217
+ # Размеры с паддингом
1218
+ canvas_h = h + 2 * pad_h
1219
+ canvas_w = w + 2 * pad_w
1220
+
1221
+ # Координаты центра градиента
1222
+ center_y = gradient_center_y * canvas_h
1223
+ center_x = gradient_center_x * canvas_w
1224
+
1225
+ # Создаем координатную сетку
1226
+ y = torch.arange(canvas_h, device=device, dtype=dtype).view(-1, 1)
1227
+ x = torch.arange(canvas_w, device=device, dtype=dtype).view(1, -1)
1228
+
1229
+ # Расстояние от центра (нормализованное)
1230
+ max_dist = math.sqrt(canvas_h**2 + canvas_w**2) / 2.0
1231
+ dist = torch.sqrt((y - center_y)**2 + (x - center_x)**2) / max_dist
1232
+
1233
+ # Радиальный градиент [0, 1]
1234
+ gradient = torch.clamp(1.0 - (dist / gradient_radius), 0.0, 1.0)
1235
+ gradient = gradient.unsqueeze(0).unsqueeze(0) # (1, 1, canvas_h, canvas_w)
1236
+
1237
+ # Circular padding для входного тензора
1238
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1239
+
1240
+ # vUltimate FIX: Безопасный expand с try/except
1241
+ try:
1242
+ gradient_expanded = gradient.expand(b, c, canvas_h, canvas_w)
1243
+ except RuntimeError as e:
1244
+ if debug:
1245
+ print(f"⚠️ [Gradient Radial] Expand failed: {e}, using broadcast_to")
1246
+ # Fallback: используем broadcast_to
1247
+ gradient_expanded = torch.broadcast_to(gradient, (b, c, canvas_h, canvas_w))
1248
+
1249
+ # Применяем градиент (плавный переход к circular padding на краях)
1250
+ result = padded * gradient_expanded
1251
+
1252
+ if debug:
1253
+ print(f"[Gradient Radial] Gradient range: [{gradient.min().item():.3f}, {gradient.max().item():.3f}]")
1254
+
1255
+ return result
1256
+
1257
+
1258
+ # ═══════════════════════════════════════════════════════════════════════════
1259
+ # 3.7. NOISE BLEND (V3.1 - НОВАЯ ФУНКЦИЯ)
1260
+ # ═══════════════════════════════════════════════════════════════════════════
1261
+
1262
+ def apply_noise_blend(input_tensor, pad_h, pad_w, noise_scale=5.0,
1263
+ noise_octaves=2, debug=False):
1264
+ """
1265
+ Блендинг с процедурным шумом для органичных границ.
1266
+
1267
+ V3.1: НОВАЯ ФУНКЦИЯ
1268
+ V3.1.1: КРИТИЧНЫЕ ИСПРАВЛЕНИЯ ДЛЯ FLOAT16
1269
+ - Адаптивный epsilon для нормализации
1270
+ - Упрощенная формула для blend (оптимизация)
1271
+
1272
+ Args:
1273
+ input_tensor: входной латент (B, C, H, W)
1274
+ pad_h, pad_w: размеры паддинга
1275
+ noise_scale: масшта�� шума (1.0-10.0, default 5.0)
1276
+ noise_octaves: количество октав шума (1-4, default 2)
1277
+ debug: вывод отладки
1278
+
1279
+ Returns:
1280
+ torch.Tensor: padded тензор с noise blending
1281
+ """
1282
+ b, c, h, w = input_tensor.shape
1283
+ device = input_tensor.device
1284
+ dtype = input_tensor.dtype
1285
+
1286
+ # V3.1.1 FIX: Получаем адаптивный epsilon
1287
+ eps = get_adaptive_epsilon(dtype)
1288
+
1289
+ # Валидация параметров
1290
+ noise_scale = float(max(1.0, min(10.0, noise_scale)))
1291
+ noise_octaves = int(max(1, min(4, noise_octaves)))
1292
+
1293
+ if debug:
1294
+ print(f"[Noise Blend] Scale: {noise_scale:.2f}, Octaves: {noise_octaves}, Epsilon: {eps}")
1295
+
1296
+ # Размеры с паддингом
1297
+ canvas_h = h + 2 * pad_h
1298
+ canvas_w = w + 2 * pad_w
1299
+
1300
+ # Создаем координатную сетку
1301
+ y = torch.arange(canvas_h, device=device, dtype=dtype).view(-1, 1)
1302
+ x = torch.arange(canvas_w, device=device, dtype=dtype).view(1, -1)
1303
+
1304
+ # Многооктавный Perlin-style шум
1305
+ noise_mask = torch.zeros(canvas_h, canvas_w, device=device, dtype=dtype)
1306
+ amplitude = 1.0
1307
+ frequency = 1.0
1308
+
1309
+ for octave in range(noise_octaves):
1310
+ # Простой процедурный шум через sin/cos
1311
+ phase_x = x * frequency * noise_scale / canvas_w * 2 * math.pi
1312
+ phase_y = y * frequency * noise_scale / canvas_h * 2 * math.pi
1313
+
1314
+ octave_noise = torch.sin(phase_x + octave) * torch.cos(phase_y + octave * 0.7)
1315
+ noise_mask = noise_mask + octave_noise * amplitude
1316
+
1317
+ amplitude *= 0.5
1318
+ frequency *= 2.0
1319
+
1320
+ # V3.1.1 FIX: Нормализация в [0, 1] с адаптивным epsilon
1321
+ noise_mask = (noise_mask - noise_mask.min()) / (noise_mask.max() - noise_mask.min() + eps)
1322
+
1323
+ # Расстояние от контента (для комбинирования с шумом)
1324
+ content_box = (pad_h, pad_h + h, pad_w, pad_w + w)
1325
+ distance = create_distance_map(canvas_h, canvas_w, content_box, device, dtype)
1326
+
1327
+ # V3.1.1 FIX: Явное преобразование размеров для ясности
1328
+ distance_2d = distance.squeeze(0).squeeze(0) # (canvas_h, canvas_w)
1329
+
1330
+ # Комбинируем distance с noise (больше шума на краях)
1331
+ blend_mask = 0.7 * distance_2d + 0.3 * noise_mask
1332
+ blend_mask = torch.clamp(blend_mask, 0.0, 1.0).unsqueeze(0).unsqueeze(0)
1333
+
1334
+ # Circular padding
1335
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1336
+
1337
+ # vUltimate FIX: Безопасный expand с try/except
1338
+ try:
1339
+ blend_mask_expanded = blend_mask.expand(b, c, canvas_h, canvas_w)
1340
+ except RuntimeError as e:
1341
+ if debug:
1342
+ print(f"⚠️ [Noise Blend] Expand failed: {e}, using broadcast_to")
1343
+ # Fallback: используем broadcast_to
1344
+ blend_mask_expanded = torch.broadcast_to(blend_mask, (b, c, canvas_h, canvas_w))
1345
+
1346
+ # V3.1.1 FIX: Упрощенная формула для blend
1347
+ # Было: padded * (1.0 - blend_mask) + padded * blend_mask * 0.5
1348
+ # Упрощено: padded * (1.0 - 0.5 * blend_mask)
1349
+ result = padded * (1.0 - 0.5 * blend_mask_expanded)
1350
+
1351
+ if debug:
1352
+ print(f"[Noise Blend] Mask range: [{blend_mask.min().item():.3f}, {blend_mask.max().item():.3f}]")
1353
+
1354
+ return result
1355
+
1356
+
1357
+ # ═══════════════════════════════════════════════════════════════════════════
1358
+ # 4. ГЛАВНАЯ ФУНКЦИЯ (V3.0 - УЛУЧШЕНО)
1359
+ # ═══════════════════════════════════════════════════════════════════════════
1360
+
1361
+ def validate_zoom_params(params):
1362
+ """
1363
+ V3.0: Добавлены новые параметры
1364
+ """
1365
+ z_mode = params.get('zoom_mode', 'outpaint_zoom')
1366
+ try:
1367
+ zoom_mode = ZoomMode(z_mode)
1368
+ except:
1369
+ zoom_mode = ZoomMode.OUTPAINT_ZOOM
1370
+
1371
+ b_mode = params.get('blend_mode', 'circular_reflect')
1372
+ try:
1373
+ blend_mode = BlendMode(b_mode)
1374
+ except:
1375
+ blend_mode = BlendMode.CIRCULAR_REFLECT
1376
+
1377
+ return {
1378
+ # Базовые параметры
1379
+ 'zoom_factor': float(params.get('zoom_factor', 0.0)),
1380
+ 'zoom_mode': zoom_mode,
1381
+ 'blend_mode': blend_mode,
1382
+ 'convergence_point': float(params.get('convergence_point', 0.5)),
1383
+ 'convergence_y': float(params.get('convergence_y', 0.5)),
1384
+ 'depth_power': float(params.get('depth_power', 1.0)),
1385
+ 'blend_falloff': str(params.get('blend_falloff', 'smoothstep')),
1386
+ 'blend_sharpness': float(params.get('blend_sharpness', 1.0)),
1387
+ 'blend_width': params.get('blend_width', None),
1388
+ # BUG FIX 1: support both naming styles (pan_x/pan_y from main dict
1389
+ # and legacy x_pan/y_pan) so values are never silently dropped.
1390
+ 'pan_x': float(params.get('pan_x', params.get('x_pan', 0.0))),
1391
+ 'pan_y': float(params.get('pan_y', params.get('y_pan', 0.0))),
1392
+ 'fade_to_black': bool(params.get('zoom_fade_to_black', False)),
1393
+ 'fade_strength': float(params.get('zoom_fade_strength', 0.3)),
1394
+ 'fade_edge_strength': float(params.get('fade_edge_strength', 0.15)),
1395
+
1396
+ # V3.0: Новые параметры
1397
+ 'noise_strength': float(params.get('noise_strength', 1.0)),
1398
+ 'interp_mode': str(params.get('interp_mode', 'bilinear')),
1399
+ 'zoom_in_fade': bool(params.get('zoom_in_fade', True)),
1400
+ 'variance_correction': bool(params.get('variance_correction', True)),
1401
+ 'auto_clamp_pan': bool(params.get('auto_clamp_pan', True)),
1402
+ 'adaptive_noise_scale': bool(params.get('adaptive_noise_scale', True)),
1403
+ 'debug': bool(params.get('debug_mode', False)),
1404
+
1405
+ # V3.1: Новые параметры для spiral_zoom
1406
+ 'spiral_rotation': float(params.get('spiral_rotation', 0.5)),
1407
+ 'spiral_direction': float(params.get('spiral_direction', 1.0)),
1408
+
1409
+ # V3.1: Новые параметры для gradient_radial
1410
+ 'gradient_center_x': float(params.get('gradient_center_x', 0.5)),
1411
+ 'gradient_center_y': float(params.get('gradient_center_y', 0.5)),
1412
+ 'gradient_radius': float(params.get('gradient_radius', 1.0)),
1413
+
1414
+ # V3.1: Новые параметры для noise_blend
1415
+ 'noise_scale': float(params.get('noise_scale', 5.0)),
1416
+ 'noise_octaves': int(params.get('noise_octaves', 2)),
1417
+ }
1418
+
1419
+
1420
+ def apply_unified_zoom(input_tensor, pad_h, pad_w, zoom_factor=0.0,
1421
+ zoom_mode=ZoomMode.OUTPAINT_ZOOM,
1422
+ blend_mode=BlendMode.CIRCULAR_REFLECT,
1423
+ convergence_point=0.5, convergence_y=0.5,
1424
+ depth_power=1.0,
1425
+ blend_falloff='smoothstep', blend_sharpness=1.0,
1426
+ blend_width=None,
1427
+ pan_x=0.0, pan_y=0.0,
1428
+ fade_to_black=False, fade_strength=0.3,
1429
+ fade_edge_strength=0.15,
1430
+ noise_strength=1.0,
1431
+ interp_mode='bilinear',
1432
+ zoom_in_fade=True,
1433
+ variance_correction=True,
1434
+ auto_clamp_pan=True,
1435
+ adaptive_noise_scale=True,
1436
+ debug=False,
1437
+ extra_params=None):
1438
+
1439
+ # 1. Запоминаем оригинальный тип (скорее всего float16)
1440
+ original_dtype = input_tensor.dtype
1441
+
1442
+ # 2. ПРИНУДИТЕЛЬНО ПЕРЕВОДИМ В FLOAT32 для вычислений
1443
+ # Это предотвращает появление "кислотного шума" (NaN/Inf)
1444
+ input_tensor = input_tensor.float()
1445
+
1446
+ # Защита от нулевого zoom
1447
+ is_active = (abs(zoom_factor) > 0.001) or (abs(pan_x) > 0.001) or (abs(pan_y) > 0.001)
1448
+
1449
+ if not is_active:
1450
+ result = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1451
+ # Возвращаем в исходном типе
1452
+ return result.to(dtype=original_dtype)
1453
+
1454
+ if debug:
1455
+ print(f"[Unified Zoom] Mode: {zoom_mode}, Dtype safe cast: {original_dtype} -> float32")
1456
+
1457
+ # Переменная для результата
1458
+ result = None
1459
+
1460
+ # ═══════════════════════════════════════════════════════════════════
1461
+ # ВЫЗОВ ФУНКЦИЙ (Теперь напрямую, так как они в этом же файле)
1462
+ # ═══════════════════════════════════════════════════════════════════
1463
+
1464
+ if zoom_mode == ZoomMode.OUTPAINT_ZOOM:
1465
+ result = apply_outpaint_zoom(
1466
+ input_tensor,
1467
+ zoom_factor,
1468
+ pad_h, pad_w,
1469
+ convergence=convergence_point,
1470
+ convergence_y=convergence_y,
1471
+ fade_strength=fade_strength,
1472
+ depth_power=depth_power,
1473
+ pan_x=pan_x, pan_y=pan_y,
1474
+ fade_to_black=fade_to_black,
1475
+ fade_edge_strength=fade_edge_strength,
1476
+ blend_mode=blend_mode.value if isinstance(blend_mode, BlendMode) else str(blend_mode),
1477
+ noise_strength=noise_strength,
1478
+ interp_mode=interp_mode,
1479
+ zoom_in_fade=zoom_in_fade,
1480
+ variance_correction=variance_correction,
1481
+ auto_clamp_pan=auto_clamp_pan,
1482
+ adaptive_noise_scale=adaptive_noise_scale,
1483
+ debug=debug,
1484
+ extra_params=extra_params
1485
+ )
1486
+
1487
+ elif zoom_mode == ZoomMode.GRID_WARP:
1488
+ x_warped = apply_grid_warp_zoom(
1489
+ input_tensor,
1490
+ zoom_factor,
1491
+ convergence_point,
1492
+ depth_power,
1493
+ pan_x, pan_y,
1494
+ convergence_y,
1495
+ interp_mode=interp_mode,
1496
+ debug=debug
1497
+ )
1498
+ result = F.pad(x_warped, (pad_w, pad_w, pad_h, pad_h), mode='constant', value=0)
1499
+
1500
+ elif zoom_mode == ZoomMode.SPIRAL_ZOOM:
1501
+ spiral_rotation = 0.5
1502
+ spiral_direction = 1.0
1503
+
1504
+ if extra_params:
1505
+ spiral_rotation = extra_params.get('spiral_rotation', 0.5)
1506
+ spiral_direction = extra_params.get('spiral_direction', 1.0)
1507
+
1508
+ result = apply_spiral_zoom(
1509
+ input_tensor,
1510
+ zoom_factor,
1511
+ pad_h, pad_w,
1512
+ spiral_rotation=spiral_rotation,
1513
+ spiral_direction=spiral_direction,
1514
+ interp_mode=interp_mode,
1515
+ debug=debug
1516
+ )
1517
+
1518
+ elif zoom_mode in [ZoomMode.CONVERGENCE_SHIFT, ZoomMode.HYBRID, ZoomMode.BLEND_TRANSITION]:
1519
+ x_shifted = apply_legacy_shift_zoom(
1520
+ input_tensor,
1521
+ zoom_factor,
1522
+ convergence_point,
1523
+ depth_power,
1524
+ pan_x, pan_y,
1525
+ auto_clamp_pan=auto_clamp_pan,
1526
+ debug=debug
1527
+ )
1528
+
1529
+ if zoom_mode == ZoomMode.CONVERGENCE_SHIFT:
1530
+ padded = F.pad(x_shifted, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1531
+ if fade_to_black:
1532
+ # Импорт только если нужен (для совместимости)
1533
+ try:
1534
+ from improved_tiling_functions import compute_blend_fade_to_black
1535
+ padded = compute_blend_fade_to_black(padded, pad_h, pad_w, fade_strength)
1536
+ except ImportError:
1537
+ pass
1538
+ result = padded
1539
+ else:
1540
+ # Fallback для остальных режимов
1541
+ try:
1542
+ from improved_tiling_functions import compute_advanced_blend_padding
1543
+ mode_str = blend_mode.value if isinstance(blend_mode, BlendMode) else str(blend_mode)
1544
+ mode_adv = mode_str.split('_')[0] if '_' in mode_str else 'circular'
1545
+
1546
+ result = compute_advanced_blend_padding(
1547
+ x_shifted, pad_h, pad_w,
1548
+ mode_simple='replicate',
1549
+ mode_advanced=mode_adv,
1550
+ blend_strength=0.7,
1551
+ blend_width=blend_width,
1552
+ falloff_curve=blend_falloff,
1553
+ edge_sharpness=blend_sharpness,
1554
+ fade_to_black=fade_to_black,
1555
+ fade_strength=fade_strength
1556
+ )
1557
+
1558
+ # ═══════════════════════════════════════════════════════════════
1559
+ # КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Защита от переполнения float16
1560
+ # ═══════════════════════════════════════════════════════════════
1561
+ # Это исправление устраняет "серый шум"!
1562
+ #
1563
+ # Проблема: compute_advanced_blend_padding может создать значения
1564
+ # за пределами диапазона float16 (-65504 до 65504).
1565
+ # При конвертации float32→float16 они превращаются в Inf/NaN → шум
1566
+ #
1567
+ # Решение: Сначала очищаем и ограничиваем В FLOAT32,
1568
+ # затем безопасно конвертируем в float16
1569
+ # ═══════════════════════════════════════════════════════════════
1570
+
1571
+ if original_dtype == torch.float16:
1572
+ # Шаг 1: Очищаем NaN/Inf в float32 (пока значения не испорчены)
1573
+ result = torch.nan_to_num(result, nan=0.0, posinf=65504.0, neginf=-65504.0)
1574
+
1575
+ # Шаг 2: Ограничиваем диапазон значений ПЕРЕД конвертацией
1576
+ # float16 range: -65504 to 65504
1577
+ result = torch.clamp(result, min=-65504.0, max=65504.0)
1578
+
1579
+ if debug:
1580
+ print(f"[Unified Zoom] Float16 safety: clamped to [-65504, 65504]")
1581
+
1582
+ except ImportError:
1583
+ result = F.pad(x_shifted, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1584
+
1585
+ # Fallback если режим не найден
1586
+ if result is None:
1587
+ result = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
1588
+
1589
+ # ═══════════════════════════════════════════════════════════════════
1590
+ # 3. БЕЗОПАСНАЯ КОНВЕРТАЦИЯ ОБРАТНО В ИСХОДНЫЙ ТИП
1591
+ # ═══════════════════════════════════════════════════════════════════
1592
+ # Теперь, когда все опасные значения очищены и ограничены,
1593
+ # можно безопасно конвертировать в float16
1594
+ # ═══════════════════════════════════════════════════════════════════
1595
+
1596
+ if torch.is_tensor(result) and result.dtype != original_dtype:
1597
+ # Если мы ещё не применили защиту (для других режимов кроме HYBRID/BLEND_TRANSITION)
1598
+ if original_dtype == torch.float16:
1599
+ # Финальная очистка перед конвертацией (на всякий случай)
1600
+ result = torch.nan_to_num(result, nan=0.0, posinf=65504.0, neginf=-65504.0)
1601
+ result = torch.clamp(result, min=-65504.0, max=65504.0)
1602
+
1603
+ # Теперь конвертируем - безопасно!
1604
+ result = result.to(dtype=original_dtype)
1605
+
1606
+ return result
asd/libs/improved_tiling_functions.py ADDED
@@ -0,0 +1,609 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import math
4
+ import numpy as np
5
+ from collections import OrderedDict
6
+
7
+ # =======================================================================
8
+ # vUltimate - REAL Deep Code Audit
9
+ # Based on v13 (THE_last_version) with CRITICAL FIXES
10
+ # =======================================================================
11
+
12
+ # =======================================================================
13
+ # 1. SMART CACHING (Speed optimization ~15-20%)
14
+ # =======================================================================
15
+ class SmartMaskCache:
16
+ """LRU cache for blend masks to avoid regeneration"""
17
+ def __init__(self, max_size=50):
18
+ self.cache = OrderedDict()
19
+ self.max_size = max_size
20
+
21
+ def get(self, key):
22
+ if key in self.cache:
23
+ self.cache.move_to_end(key)
24
+ return self.cache[key]
25
+ return None
26
+
27
+ def set(self, key, value):
28
+ if key in self.cache:
29
+ self.cache.move_to_end(key)
30
+ self.cache[key] = value
31
+ if len(self.cache) > self.max_size:
32
+ self.cache.popitem(last=False)
33
+
34
+ # Global cache instance
35
+ _MASK_CACHE = SmartMaskCache()
36
+
37
+
38
+ # =======================================================================
39
+ # 2. SAFE EPSILON (🔴 CRITICAL FIX: Infinite recursion bug in v13!)
40
+ # =======================================================================
41
+ def get_safe_epsilon(tensor_or_dtype):
42
+ """
43
+ Float16-safe epsilon - CRITICAL for half precision!
44
+
45
+ 🔴 vUltimate Fix: v13 had INFINITE RECURSION bug:
46
+ Line 51: return get_safe_epsilon(torch.float16) # INFINITE LOOP!
47
+
48
+ Args:
49
+ tensor_or_dtype: torch.Tensor or torch.dtype
50
+
51
+ Returns:
52
+ float: safe epsilon for given dtype
53
+ """
54
+ if isinstance(tensor_or_dtype, torch.Tensor):
55
+ dtype = tensor_or_dtype.dtype
56
+ else:
57
+ dtype = tensor_or_dtype
58
+
59
+ # Float16 minimum value ~6e-5, so 1e-6 causes underflow
60
+ if dtype in (torch.float16, torch.bfloat16):
61
+ return 1e-3 # Safe for half precision
62
+ elif dtype == torch.float32:
63
+ return 1e-6 # 🔴 FIX: Changed from recursive call to direct value
64
+ else:
65
+ return 1e-12 # High precision for float64
66
+
67
+
68
+ # =======================================================================
69
+ # 3. LATENT COLOR FIX (Variance-preserving blend)
70
+ # =======================================================================
71
+ def blend_with_variance_fix(a, b, mask):
72
+ """
73
+ Mathematically correct blending of latent noise.
74
+ ✅ FLOAT16 FIX: Safe sqrt with adaptive epsilon
75
+
76
+ Args:
77
+ a: Primary layer (active where mask=1) -> Advanced/Circular
78
+ b: Background layer (active where mask=0) -> Simple/Replicate
79
+ mask: Blend mask [0,1]
80
+
81
+ 🔴 IMPORTANT: From v11+ the mask semantics are:
82
+ mask=1.0 on EDGES (where Advanced padding is needed)
83
+ mask=0.0 in CENTER (where content or Simple padding is)
84
+ """
85
+ # 1. Linear blend
86
+ blended = a * mask + b * (1 - mask)
87
+
88
+ # 2. Variance correction with adaptive epsilon
89
+ eps_val = get_safe_epsilon(mask.dtype)
90
+ variance_fix = torch.sqrt(mask**2 + (1 - mask)**2 + eps_val)
91
+
92
+ return blended / variance_fix
93
+
94
+
95
+ # =======================================================================
96
+ # 4. LEGACY FADE TO BLACK (For ZOOM effect) ✅
97
+ # =======================================================================
98
+ def compute_blend_fade_to_black(padded, pad_h, pad_w, fade_strength=0.1):
99
+ """
100
+ ⚡ LEGACY MODE for Zoom effect (V3.5 logic from v11-v13) ⚡
101
+
102
+ Gradient now covers ENTIRE padding + part of content.
103
+ Result: Beautiful vignette from 0 (edge) to 1 (center).
104
+
105
+ 🔴 vUltimate Note: This is v13 logic (NOT v7 logic).
106
+ v7 applied fade only to content inside padding zones.
107
+ v13 applies fade to ENTIRE image including padding.
108
+
109
+ Args:
110
+ padded: Already padded tensor [B, C, H, W]
111
+ pad_h: Vertical padding size
112
+ pad_w: Horizontal padding size
113
+ fade_strength: Fade depth into content (0.0-1.0), typically 0.05-0.2
114
+
115
+ Returns:
116
+ Tensor with darkened edges
117
+ """
118
+ b, c, H, W = padded.shape
119
+
120
+ # Calculate content size (without padding)
121
+ h_content = max(H - 2 * pad_h, 0)
122
+ w_content = max(W - 2 * pad_w, 0)
123
+
124
+ # Fade depth inside content
125
+ blend_in_h = int(h_content * fade_strength)
126
+ blend_in_w = int(w_content * fade_strength)
127
+
128
+ # Total fade zone = Padding + Entry into content
129
+ total_fade_h = pad_h + blend_in_h
130
+ total_fade_w = pad_w + blend_in_w
131
+
132
+ result = padded.clone()
133
+
134
+ # ═══════════════════════════════════════════════════════════════════
135
+ # VERTICAL EDGES
136
+ # ═══════════════════════════════════════════════════════════════════
137
+ if total_fade_h > 0:
138
+ # Create gradient 0 -> 1
139
+ fade = torch.linspace(0, 1, steps=total_fade_h,
140
+ device=padded.device, dtype=padded.dtype)
141
+ fade = fade.view(1, 1, -1, 1) # Shape: (1,1,H,1)
142
+
143
+ # Top (from 0 to total_fade_h)
144
+ safe_h = min(total_fade_h, H)
145
+ result[:, :, :safe_h, :] *= fade[:, :, :safe_h, :]
146
+
147
+ # Bottom (from H-total_fade_h to H) - use flipped gradient
148
+ result[:, :, -safe_h:, :] *= fade[:, :, :safe_h, :].flip(2)
149
+
150
+ # ═══════════════════════════════════════════════════════════════════
151
+ # HORIZONTAL EDGES
152
+ # ═══════════════════════════════════════════════════════════════════
153
+ if total_fade_w > 0:
154
+ # Create gradient 0 -> 1
155
+ fade = torch.linspace(0, 1, steps=total_fade_w,
156
+ device=padded.device, dtype=padded.dtype)
157
+ fade = fade.view(1, 1, 1, -1) # Shape: (1,1,1,W)
158
+
159
+ # Left
160
+ safe_w = min(total_fade_w, W)
161
+ result[:, :, :, :safe_w] *= fade[:, :, :, :safe_w]
162
+
163
+ # Right
164
+ result[:, :, :, -safe_w:] *= fade[:, :, :, :safe_w].flip(3)
165
+
166
+ return result
167
+
168
+
169
+ # =======================================================================
170
+ # 5. ADVANCED BLEND MASK (For modern tiling mode)
171
+ # =======================================================================
172
+ def create_advanced_blend_mask(h, w, blend_width, device, dtype=torch.float32,
173
+ falloff_curve="smoothstep", edge_sharpness=1.0):
174
+ """
175
+ Creates cached edge blend mask.
176
+
177
+ 🔴 MASK SEMANTICS (v11+ convention):
178
+ 1.0 = on the very EDGE (where Advanced Padding is needed)
179
+ 0.0 = in CENTER (where content or Simple Padding is)
180
+
181
+ Args:
182
+ h, w: Mask dimensions
183
+ blend_width: Transition zone width (pixels)
184
+ device: Torch device
185
+ dtype: Data type
186
+ falloff_curve: Curve type ('linear', 'smoothstep', 'cosine')
187
+ edge_sharpness: Edge sharpness (1.0 = normal, >1 = sharper, <1 = softer)
188
+
189
+ Returns:
190
+ Mask of size [1, 1, h, w]
191
+ """
192
+ if blend_width <= 0:
193
+ return torch.ones(1, 1, h, w, device=device, dtype=dtype)
194
+
195
+ # BUG FIX 6a: normalise falloff_curve to a known value; warn loudly if
196
+ # the UI has sent something the backend doesn't actually implement.
197
+ _KNOWN_FALLOFFS = {'linear', 'smoothstep', 'cosine'}
198
+ if falloff_curve not in _KNOWN_FALLOFFS:
199
+ print(f"[AdvancedBlend] Warning: unsupported falloff_curve '{falloff_curve}' "
200
+ f"— falling back to 'smoothstep'. Supported: {sorted(_KNOWN_FALLOFFS)}")
201
+ falloff_curve = 'smoothstep'
202
+
203
+ blend_w = min(blend_width, w // 2)
204
+ blend_h = min(blend_width, h // 2)
205
+
206
+ mask = torch.zeros((1, 1, h, w), device=device, dtype=dtype)
207
+
208
+ def get_ramp(size):
209
+ """Generate gradient with configurable curve"""
210
+ t = torch.linspace(0, 1, steps=size, device=device, dtype=dtype)
211
+ if edge_sharpness != 1.0:
212
+ t = torch.pow(t, edge_sharpness)
213
+
214
+ if falloff_curve == 'smoothstep':
215
+ return t * t * (3 - 2 * t)
216
+ elif falloff_curve == 'cosine':
217
+ return (1 - torch.cos(t * math.pi)) / 2
218
+ elif falloff_curve == 'linear':
219
+ return t
220
+ return t
221
+
222
+ # Fill edges
223
+ if blend_w > 0:
224
+ ramp = get_ramp(blend_w)
225
+ # Left edge
226
+ mask[:, :, :, :blend_w] = torch.maximum(mask[:, :, :, :blend_w],
227
+ ramp.flip(0).view(1,1,1,-1))
228
+ # Right edge
229
+ mask[:, :, :, -blend_w:] = torch.maximum(mask[:, :, :, -blend_w:],
230
+ ramp.view(1,1,1,-1))
231
+
232
+ if blend_h > 0:
233
+ ramp = get_ramp(blend_h)
234
+ # Top edge
235
+ mask[:, :, :blend_h, :] = torch.maximum(mask[:, :, :blend_h, :],
236
+ ramp.flip(0).view(1,1,-1,1))
237
+ # Bottom edge
238
+ mask[:, :, -blend_h:, :] = torch.maximum(mask[:, :, -blend_h:, :],
239
+ ramp.view(1,1,-1,1))
240
+
241
+ return mask
242
+
243
+
244
+ # =======================================================================
245
+ # 6. IMPROVED BLEND PADDING (Main tiling function)
246
+ # =======================================================================
247
+ def compute_advanced_blend_padding(input_tensor, pad_h, pad_w,
248
+ mode_simple='replicate',
249
+ mode_advanced='circular',
250
+ blend_strength=0.5,
251
+ blend_width=None,
252
+ falloff_curve='smoothstep',
253
+ edge_sharpness=1.0,
254
+ fade_to_black=False,
255
+ fade_strength=0.1):
256
+ """
257
+ IMPROVED PADDING MODE
258
+
259
+ Two operation modes:
260
+
261
+ 1. FADE TO BLACK (fade_to_black=True) - for Zoom effect:
262
+ - Applies one padding (mode_advanced)
263
+ - DARKENS edges, creating zoom out effect
264
+ - Uses legacy compute_blend_fade_to_black function
265
+
266
+ 2. BLEND TWO PADDINGS (fade_to_black=False) - for quality edges:
267
+ - Creates two different paddings (simple and advanced)
268
+ - Blends them via mask
269
+ - Applies variance fix for color correction
270
+ - Does NOT create zoom effect
271
+
272
+ Args:
273
+ input_tensor: Original tensor WITHOUT padding [B, C, H, W]
274
+ pad_h, pad_w: Padding sizes
275
+ mode_simple: Mode for "simple" padding ('replicate', 'constant')
276
+ mode_advanced: Mode for "advanced" padding ('circular', 'reflect')
277
+ blend_strength: Blend strength (0.0-1.0)
278
+ blend_width: Transition width (None = auto)
279
+ falloff_curve: Gradient curve type
280
+ edge_sharpness: Edge sharpness
281
+ fade_to_black: If True, uses legacy darkening mode
282
+ fade_strength: Darkening strength for fade_to_black mode
283
+
284
+ Returns:
285
+ Padded tensor [B, C, H+2*pad_h, W+2*pad_w]
286
+ """
287
+
288
+ # ═══════════════════════════════════════════════════════════════════
289
+ # MODE 1: FADE TO BLACK (for Zoom)
290
+ # ═══════════════════════════════════════════════════════════════════
291
+ if fade_to_black:
292
+ # Apply ONE padding first
293
+ if isinstance(mode_advanced, str):
294
+ if mode_advanced == 'reflect':
295
+ b, c, h, w = input_tensor.shape
296
+ if pad_w < w and pad_h < h:
297
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='reflect')
298
+ else:
299
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='replicate')
300
+ else: # circular
301
+ padded = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode=mode_advanced)
302
+ else:
303
+ padded = mode_advanced # Pre-computed tensor
304
+
305
+ # Darken edges (now works correctly!)
306
+ return compute_blend_fade_to_black(padded, pad_h, pad_w, fade_strength)
307
+
308
+ # ═══════════════════════════════════════════════════════════════════
309
+ # MODE 2: BLEND TWO PADDINGS (Tiling)
310
+ # ═══════════════════════════════════════════════════════════════════
311
+
312
+ # If disabled
313
+ if blend_strength <= 0.001:
314
+ if mode_simple == 'constant':
315
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='constant', value=0)
316
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode=mode_simple)
317
+
318
+ # If 100% strength
319
+ if blend_strength >= 0.999:
320
+ if isinstance(mode_advanced, str):
321
+ if mode_advanced == 'reflect':
322
+ b, c, h, w = input_tensor.shape
323
+ if pad_w < w and pad_h < h:
324
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='reflect')
325
+ else:
326
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='replicate')
327
+ return F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode=mode_advanced)
328
+ return mode_advanced # Pre-computed tensor
329
+
330
+ # 1. Prepare layers
331
+ if mode_simple == 'constant':
332
+ simple = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='constant', value=0)
333
+ else:
334
+ simple = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode=mode_simple)
335
+
336
+ if isinstance(mode_advanced, str):
337
+ if mode_advanced == 'reflect':
338
+ b, c, h, w = input_tensor.shape
339
+ if pad_w < w and pad_h < h:
340
+ advanced = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='reflect')
341
+ else:
342
+ advanced = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='replicate')
343
+ else: # circular
344
+ advanced = F.pad(input_tensor, (pad_w, pad_w, pad_h, pad_h), mode='circular')
345
+ else:
346
+ advanced = mode_advanced # Pre-computed
347
+
348
+ # 2. Get mask from cache
349
+ if blend_width is None:
350
+ blend_width = max(pad_h, pad_w)
351
+
352
+ b, c, h, w = input_tensor.shape
353
+ device = input_tensor.device
354
+ dtype = input_tensor.dtype
355
+
356
+ # 🔴 vUltimate Fix: Enhanced cache key WITH dtype (v11+ feature)
357
+ cache_key = (h, w, pad_h, pad_w, blend_width, falloff_curve, edge_sharpness,
358
+ str(device), str(dtype))
359
+ mask = _MASK_CACHE.get(cache_key)
360
+
361
+ if mask is None:
362
+ H_pad, W_pad = simple.shape[2:]
363
+ mask = create_advanced_blend_mask(H_pad, W_pad, blend_width, device,
364
+ dtype, falloff_curve, edge_sharpness)
365
+ _MASK_CACHE.set(cache_key, mask)
366
+
367
+ # 3. Blend with variance fix
368
+ final_mask = mask * blend_strength
369
+
370
+ # 🔴 vUltimate: v11+ semantics: advanced (mask=1 on edges) / simple (mask=0 in center)
371
+ return blend_with_variance_fix(advanced, simple, final_mask)
372
+
373
+
374
+ # =======================================================================
375
+ # 7. MULTI-RESOLUTION (Temporal strategy)
376
+ # =======================================================================
377
+ class BlendStrategy:
378
+ """Interpolation strategies for multi-resolution transitions"""
379
+ LINEAR = "linear"
380
+ COSINE = "cosine"
381
+ EXPONENTIAL = "exponential"
382
+ SIGMOID = "sigmoid"
383
+
384
+ class MultiResStrategy:
385
+ """Handles temporal blending curves for progressive detail addition"""
386
+ def __init__(self, strategy_type=BlendStrategy.COSINE):
387
+ self.strategy_type = strategy_type
388
+
389
+ def get_factor(self, progress, sharpness=1.0):
390
+ """Calculate blend factor based on progress (0.0 to 1.0)"""
391
+ t = max(0.0, min(1.0, progress))
392
+ if sharpness != 1.0:
393
+ t = math.pow(t, sharpness)
394
+
395
+ if self.strategy_type == BlendStrategy.LINEAR:
396
+ return t
397
+ elif self.strategy_type == BlendStrategy.COSINE:
398
+ return (1.0 - math.cos(t * math.pi)) / 2.0
399
+ elif self.strategy_type == BlendStrategy.EXPONENTIAL:
400
+ return math.pow(t, 2)
401
+ elif self.strategy_type == BlendStrategy.SIGMOID:
402
+ if t <= 0: return 0.0
403
+ if t >= 1: return 1.0
404
+ return 1.0 / (1.0 + math.exp(-12.0 * (t - 0.5)))
405
+ return t
406
+
407
+ def apply_multires_blend(tensor_simple, tensor_advanced, current_step,
408
+ start_step, end_step,
409
+ strategy="cosine",
410
+ transition_start=0.0,
411
+ transition_end=0.3,
412
+ sharpness=1.0,
413
+ enabled=False):
414
+ """
415
+ Progressive blending from simple to advanced over denoising steps.
416
+
417
+ Args:
418
+ tensor_simple: Low-detail padding result
419
+ tensor_advanced: High-detail padding result
420
+ current_step: Current denoising step
421
+ start_step, end_step: Denoising range
422
+ strategy: Interpolation curve type
423
+ transition_start, transition_end: Transition window (0.0-1.0)
424
+ sharpness: Curve adjustment
425
+ enabled: Master switch
426
+
427
+ Returns:
428
+ Blended tensor
429
+ """
430
+ if not enabled:
431
+ return tensor_advanced
432
+
433
+ # BUG FIX 6b: normalise strategy to a supported value before use.
434
+ _KNOWN_STRATEGIES = {BlendStrategy.LINEAR, BlendStrategy.COSINE,
435
+ BlendStrategy.EXPONENTIAL, BlendStrategy.SIGMOID}
436
+ _STRATEGY_ALIASES = {
437
+ 'linear': BlendStrategy.LINEAR,
438
+ 'cosine': BlendStrategy.COSINE,
439
+ 'exponential': BlendStrategy.EXPONENTIAL,
440
+ 'sigmoid': BlendStrategy.SIGMOID,
441
+ }
442
+ if isinstance(strategy, str):
443
+ strategy_key = strategy.lower()
444
+ if strategy_key not in _STRATEGY_ALIASES:
445
+ print(f"[MultiRes] Warning: unsupported strategy '{strategy}' "
446
+ f"— falling back to 'cosine'. "
447
+ f"Supported: {sorted(_STRATEGY_ALIASES.keys())}")
448
+ strategy = BlendStrategy.COSINE
449
+ else:
450
+ strategy = _STRATEGY_ALIASES[strategy_key]
451
+ elif strategy not in _KNOWN_STRATEGIES:
452
+ print(f"[MultiRes] Warning: unknown strategy {strategy!r} — falling back to cosine")
453
+ strategy = BlendStrategy.COSINE
454
+
455
+ total_steps = end_step - start_step
456
+ if total_steps <= 0:
457
+ return tensor_advanced
458
+
459
+ step_frac = (current_step - start_step) / total_steps
460
+ step_frac = max(0.0, min(1.0, step_frac))
461
+
462
+ if step_frac < transition_start:
463
+ local_progress = 0.0
464
+ elif step_frac > transition_end:
465
+ local_progress = 1.0
466
+ else:
467
+ duration = transition_end - transition_start
468
+ if duration <= 0:
469
+ local_progress = 1.0
470
+ else:
471
+ local_progress = (step_frac - transition_start) / duration
472
+
473
+ strat = MultiResStrategy(strategy)
474
+ alpha = strat.get_factor(local_progress, sharpness)
475
+
476
+ if alpha <= 0.001:
477
+ return tensor_simple
478
+ if alpha >= 0.999:
479
+ return tensor_advanced
480
+
481
+ # Standard lerp (temporal blend, not spatial)
482
+ return tensor_simple * (1.0 - alpha) + tensor_advanced * alpha
483
+
484
+
485
+ # =======================================================================
486
+ # 8. HELPER FUNCTIONS (From v13 for compatibility)
487
+ # =======================================================================
488
+
489
+ def create_circular_mask(h, w, center_x=0.5, center_y=0.5, radius=0.5,
490
+ device='cpu', dtype=torch.float32):
491
+ """
492
+ Creates circular mask (white circle on black background).
493
+ ✅ FLOAT16 FIX: Safe sqrt
494
+
495
+ NOTE: This is v13 version (radial distance mask).
496
+ Different from v1/exp which don't have this function.
497
+ """
498
+ eps_val = get_safe_epsilon(dtype)
499
+
500
+ # Create coordinate grid
501
+ y, x = torch.meshgrid(
502
+ torch.linspace(-1, 1, h, device=device, dtype=dtype),
503
+ torch.linspace(-1, 1, w, device=device, dtype=dtype),
504
+ indexing='ij'
505
+ )
506
+
507
+ # Shift center
508
+ x = x - (center_x - 0.5) * 2
509
+ y = y - (center_y - 0.5) * 2
510
+
511
+ # Calculate distance from center with protection
512
+ dist = torch.sqrt(x*x + y*y + eps_val)
513
+
514
+ # Create soft mask (smooth edges 0.1)
515
+ mask = 1.0 - torch.clamp((dist - (radius - 0.1)) / 0.2, 0, 1)
516
+
517
+ # Add channel and batch dimensions
518
+ if len(mask.shape) == 2:
519
+ mask = mask.unsqueeze(0).unsqueeze(0)
520
+
521
+ return mask
522
+
523
+ def create_fade_to_black_mask(h, w, strength=0.1, device='cpu', dtype=torch.float32):
524
+ """
525
+ Creates vignette (darkening towards edges).
526
+ ✅ FLOAT16 FIX: Safe sqrt
527
+
528
+ NOTE: This is v13 version (radial vignette).
529
+ Different from v1/exp which don't have this function.
530
+ """
531
+ eps_val = get_safe_epsilon(dtype)
532
+
533
+ y, x = torch.meshgrid(
534
+ torch.linspace(-1, 1, h, device=device, dtype=dtype),
535
+ torch.linspace(-1, 1, w, device=device, dtype=dtype),
536
+ indexing='ij'
537
+ )
538
+
539
+ # sqrt with protection
540
+ dist = torch.sqrt(x*x + y*y + eps_val)
541
+
542
+ # Normalize so corners are 1.0 (max distance ~1.41)
543
+ dist = dist / 1.4142
544
+
545
+ # Invert: center white (1), edges black (0)
546
+ threshold = 1.0 - strength
547
+ mask = 1.0 - torch.clamp((dist - threshold) / strength, 0, 1)
548
+
549
+ if len(mask.shape) == 2:
550
+ mask = mask.unsqueeze(0).unsqueeze(0)
551
+
552
+ return mask
553
+
554
+
555
+ # =======================================================================
556
+ # 9. PARAMETER VALIDATION HELPERS
557
+ # =======================================================================
558
+
559
+ def validate_blend_params(params):
560
+ """Extract and validate blend parameters from dict.
561
+ Unsupported falloff values are normalised here with a warning so callers
562
+ never silently receive a mode the backend cannot honour.
563
+ Supported: 'linear', 'smoothstep', 'cosine'
564
+ """
565
+ falloff = params.get('blend_falloff', 'smoothstep')
566
+ _SUPPORTED_FALLOFFS = {'linear', 'smoothstep', 'cosine'}
567
+ if falloff not in _SUPPORTED_FALLOFFS:
568
+ print(f"[validate_blend_params] Warning: unsupported blend_falloff '{falloff}' "
569
+ f"— falling back to 'smoothstep'. Supported: {sorted(_SUPPORTED_FALLOFFS)}")
570
+ falloff = 'smoothstep'
571
+
572
+ return {
573
+ 'strength': float(params.get('blend_strength', 0.5)),
574
+ 'width': int(params.get('blend_width', 0)) if params.get('blend_width') else None,
575
+ 'falloff': falloff,
576
+ 'sharpness': float(params.get('blend_sharpness', 1.0)),
577
+ 'fade_to_black': bool(params.get('blend_fade_to_black', False)),
578
+ 'fade_strength': float(params.get('blend_fade_strength', 0.1))
579
+ }
580
+
581
+ def validate_multires_params(params):
582
+ """Extract and validate multi-resolution parameters from dict.
583
+ Unsupported strategy values are normalised here with a warning.
584
+ Supported: 'linear', 'cosine', 'exponential', 'sigmoid'
585
+ """
586
+ strategy = params.get('multires_strategy', 'cosine')
587
+ _SUPPORTED_STRATEGIES = {'linear', 'cosine', 'exponential', 'sigmoid'}
588
+ if strategy not in _SUPPORTED_STRATEGIES:
589
+ print(f"[validate_multires_params] Warning: unsupported multires_strategy '{strategy}' "
590
+ f"— falling back to 'cosine'. Supported: {sorted(_SUPPORTED_STRATEGIES)}")
591
+ strategy = 'cosine'
592
+
593
+ return {
594
+ 'strategy': strategy,
595
+ 'transition_start': float(params.get('multires_start', 0.0)),
596
+ 'transition_end': float(params.get('multires_end', 0.3)),
597
+ 'sharpness': float(params.get('multires_sharpness', 1.0))
598
+ }
599
+
600
+ # =======================================================================
601
+ # vUltimate - End of File
602
+ # CRITICAL FIXES APPLIED:
603
+ # ✅ Fix 1: Infinite recursion in get_safe_epsilon (v13 bug at line 51/65)
604
+ # ✅ Fix 2: Correct v11+ mask semantics (advanced first, mask=1.0 on edges)
605
+ # ✅ Fix 3: Cache key includes dtype (v11+ improvement)
606
+ # ✅ Fix 4: Safe reflect with size validation
607
+ # ✅ Fix 5: v13 fade_to_black logic (not v7 logic)
608
+ # ✅ Fix 6: v13 circular/vignette masks (not v1/exp)
609
+ # =======================================================================
asd/scripts/asymmetric_tiling_UNIFIED.py ADDED
The diff for this file is too large to render. See raw diff