dikdimon commited on
Commit
d0f39c8
·
verified ·
1 Parent(s): 53b19a8

Upload 3 files

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