ApacheOne commited on
Commit
7798da9
·
verified ·
1 Parent(s): e80aece

Upload 2 files

Browse files
ComfyUI-TAEF2/__init__.py ADDED
@@ -0,0 +1,534 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ import comfy.utils
6
+ import comfy.ops
7
+ import comfy.model_management
8
+ import folder_paths
9
+
10
+
11
+ # ============================================================
12
+ # Layers (Comfy-style: disable_weight_init)
13
+ # ============================================================
14
+
15
+ def conv(n_in, n_out, **kwargs):
16
+ return comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
17
+
18
+
19
+ class Clamp(nn.Module):
20
+ def forward(self, x):
21
+ return torch.tanh(x / 3) * 3
22
+
23
+
24
+ class Block(nn.Module):
25
+ def __init__(self, n_in, n_out, use_midblock_gn=False):
26
+ super().__init__()
27
+ self.conv = nn.Sequential(
28
+ conv(n_in, n_out), nn.ReLU(),
29
+ conv(n_out, n_out), nn.ReLU(),
30
+ conv(n_out, n_out),
31
+ )
32
+ self.skip = comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
33
+ self.fuse = nn.ReLU()
34
+
35
+ self.pool = None
36
+ if use_midblock_gn:
37
+ conv1x1 = lambda a, b: comfy.ops.disable_weight_init.Conv2d(a, b, 1, bias=False)
38
+ n_gn = n_in * 4
39
+ self.pool = nn.Sequential(
40
+ conv1x1(n_in, n_gn),
41
+ comfy.ops.disable_weight_init.GroupNorm(4, n_gn),
42
+ nn.ReLU(inplace=True),
43
+ conv1x1(n_gn, n_in),
44
+ )
45
+
46
+ def forward(self, x):
47
+ if self.pool is not None:
48
+ x = x + self.pool(x)
49
+ return self.fuse(self.conv(x) + self.skip(x))
50
+
51
+
52
+ # ============================================================
53
+ # TAESD scale8/scale16 builders
54
+ # Your file is scale8 (encoder final conv at layers.14)
55
+ # ============================================================
56
+
57
+ def build_encoder_scale8(latent_channels, pool_blocks):
58
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
59
+ return nn.Sequential(
60
+ conv(3, 64), # 0
61
+ B(1), # 1
62
+ conv(64, 64, stride=2, bias=False), # 2
63
+ B(3), B(4), B(5), # 3-5
64
+ conv(64, 64, stride=2, bias=False), # 6
65
+ B(7), B(8), B(9), # 7-9
66
+ conv(64, 64, stride=2, bias=False), # 10
67
+ B(11), B(12), B(13), # 11-13
68
+ conv(64, latent_channels), # 14
69
+ )
70
+
71
+
72
+ def build_decoder_scale8(latent_channels, pool_blocks):
73
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
74
+ return nn.Sequential(
75
+ Clamp(), # 0 (no weights)
76
+ conv(latent_channels, 64), # 1
77
+ nn.ReLU(), # 2
78
+ B(3), B(4), B(5), # 3-5
79
+ nn.Upsample(scale_factor=2), # 6
80
+ conv(64, 64, bias=False), # 7
81
+ B(8), B(9), B(10), # 8-10
82
+ nn.Upsample(scale_factor=2), # 11
83
+ conv(64, 64, bias=False), # 12
84
+ B(13), B(14), B(15), # 13-15
85
+ nn.Upsample(scale_factor=2), # 16
86
+ conv(64, 64, bias=False), # 17
87
+ B(18), # 18
88
+ conv(64, 3), # 19
89
+ )
90
+
91
+
92
+ def build_encoder_scale16(latent_channels, pool_blocks):
93
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
94
+ return nn.Sequential(
95
+ conv(3, 64), # 0
96
+ B(1), # 1
97
+ conv(64, 64, stride=2, bias=False), # 2
98
+ B(3), B(4), B(5), # 3-5
99
+ conv(64, 64, stride=2, bias=False), # 6
100
+ B(7), B(8), B(9), # 7-9
101
+ conv(64, 64, stride=2, bias=False), # 10
102
+ B(11), B(12), B(13), # 11-13
103
+ conv(64, 64, stride=2, bias=False), # 14
104
+ B(15), B(16), B(17), # 15-17
105
+ conv(64, latent_channels), # 18
106
+ )
107
+
108
+
109
+ def build_decoder_scale16(latent_channels, pool_blocks):
110
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
111
+ return nn.Sequential(
112
+ Clamp(), # 0
113
+ conv(latent_channels, 64), # 1
114
+ nn.ReLU(), # 2
115
+ B(3), B(4), B(5), # 3-5
116
+ nn.Upsample(scale_factor=2), # 6
117
+ conv(64, 64, bias=False), # 7
118
+ B(8), B(9), B(10), # 8-10
119
+ nn.Upsample(scale_factor=2), # 11
120
+ conv(64, 64, bias=False), # 12
121
+ B(13), B(14), B(15), # 13-15
122
+ nn.Upsample(scale_factor=2), # 16
123
+ conv(64, 64, bias=False), # 17
124
+ B(18), B(19), B(20), # 18-20
125
+ nn.Upsample(scale_factor=2), # 21
126
+ conv(64, 64, bias=False), # 22
127
+ B(23), # 23
128
+ conv(64, 3), # 24
129
+ )
130
+
131
+
132
+ # ============================================================
133
+ # Packed latents (auto-pad so it never errors)
134
+ # ============================================================
135
+
136
+ def unpack_packed_latents(x, latent_channels):
137
+ # [B, C*4, H, W] -> [B, C, H*2, W*2]
138
+ if x.ndim == 4 and x.shape[1] == latent_channels * 4:
139
+ return (
140
+ x.reshape(x.shape[0], latent_channels, 2, 2, x.shape[-2], x.shape[-1])
141
+ .permute(0, 1, 4, 2, 5, 3)
142
+ .reshape(x.shape[0], latent_channels, x.shape[-2] * 2, x.shape[-1] * 2)
143
+ )
144
+ return x
145
+
146
+
147
+ def pack_packed_latents(z, latent_channels):
148
+ # [B, C, H, W] -> [B, C*4, H//2, W//2]
149
+ if z.ndim == 4 and z.shape[1] == latent_channels:
150
+ h, w = z.shape[-2], z.shape[-1]
151
+ pad_h = h & 1
152
+ pad_w = w & 1
153
+ if pad_h or pad_w:
154
+ z = F.pad(z, (0, pad_w, 0, pad_h), mode="replicate")
155
+ h, w = z.shape[-2], z.shape[-1]
156
+
157
+ return (
158
+ z.reshape(z.shape[0], latent_channels, h // 2, 2, w // 2, 2)
159
+ .permute(0, 1, 3, 5, 2, 4)
160
+ .reshape(z.shape[0], latent_channels * 4, h // 2, w // 2)
161
+ )
162
+ return z
163
+
164
+
165
+ def pad_nchw_to_multiple(x, multiple):
166
+ # replicate pad right/bottom so any size works
167
+ _, _, h, w = x.shape
168
+ pad_h = (multiple - (h % multiple)) % multiple
169
+ pad_w = (multiple - (w % multiple)) % multiple
170
+ if pad_h or pad_w:
171
+ x = F.pad(x, (0, pad_w, 0, pad_h), mode="replicate")
172
+ return x
173
+
174
+
175
+ # ============================================================
176
+ # Key conversion for your file format:
177
+ # encoder.layers.N.* and decoder.layers.N.*
178
+ # decoder layers must shift +1 because our decoder has Clamp() at index 0.
179
+ # ============================================================
180
+
181
+ def normalize_state_dict(sd_raw):
182
+ keys = list(sd_raw.keys())
183
+
184
+ # Already comfy split format?
185
+ if any(k.startswith("taesd_encoder.") for k in keys) or any(k.startswith("taesd_decoder.") for k in keys):
186
+ return sd_raw
187
+
188
+ out = {}
189
+
190
+ # Diffusers "encoder.layers.* / decoder.layers.*"
191
+ if any(k.startswith("encoder.layers.") for k in keys) or any(k.startswith("decoder.layers.") for k in keys):
192
+ for k, v in sd_raw.items():
193
+ if k.startswith("encoder.layers."):
194
+ # encoder.layers.N.xxx -> taesd_encoder.N.xxx
195
+ out["taesd_encoder." + k[len("encoder.layers."):]] = v
196
+ elif k.startswith("decoder.layers."):
197
+ # decoder.layers.N.xxx -> taesd_decoder.(N+1).xxx (Clamp at 0)
198
+ rest = k[len("decoder.layers."):]
199
+ parts = rest.split(".", 1)
200
+ try:
201
+ n = int(parts[0])
202
+ n2 = n + 1
203
+ tail = parts[1] if len(parts) > 1 else ""
204
+ out_key = f"taesd_decoder.{n2}" + (("." + tail) if tail else "")
205
+ out[out_key] = v
206
+ except Exception:
207
+ # fallback, keep
208
+ out[k] = v
209
+ else:
210
+ out[k] = v
211
+ return out
212
+
213
+ # Fallback: encoder./decoder. (numeric) — if decoder.0.weight looks like [64,C,3,3], offset it too
214
+ if any(k.startswith("encoder.") for k in keys) or any(k.startswith("decoder.") for k in keys):
215
+ decoder_needs_offset = False
216
+ w0 = sd_raw.get("decoder.0.weight", None)
217
+ if isinstance(w0, torch.Tensor) and w0.ndim == 4 and w0.shape[0] == 64 and w0.shape[2:] == (3, 3):
218
+ decoder_needs_offset = True
219
+
220
+ for k, v in sd_raw.items():
221
+ if k.startswith("encoder."):
222
+ out["taesd_encoder." + k[len("encoder."):]] = v
223
+ elif k.startswith("decoder."):
224
+ rest = k[len("decoder."):]
225
+ if decoder_needs_offset:
226
+ parts = rest.split(".", 1)
227
+ if parts[0].isdigit():
228
+ n = int(parts[0]) + 1
229
+ tail = parts[1] if len(parts) > 1 else ""
230
+ out_key = f"taesd_decoder.{n}" + (("." + tail) if tail else "")
231
+ out[out_key] = v
232
+ else:
233
+ out["taesd_decoder." + rest] = v
234
+ else:
235
+ out["taesd_decoder." + rest] = v
236
+ else:
237
+ out[k] = v
238
+ return out
239
+
240
+ # Unknown layout: return as-is (Dump node will show keys)
241
+ return sd_raw
242
+
243
+
244
+ def split_encoder_decoder(sd):
245
+ enc = {k[len("taesd_encoder."):]: v for k, v in sd.items() if k.startswith("taesd_encoder.")}
246
+ dec = {k[len("taesd_decoder."):]: v for k, v in sd.items() if k.startswith("taesd_decoder.")}
247
+ return enc, dec
248
+
249
+
250
+ def pool_blocks_from_sd(part_sd):
251
+ blocks = set()
252
+ for k in part_sd.keys():
253
+ if ".pool.0.weight" in k or ".pool.0.bias" in k:
254
+ head = k.split(".", 1)[0]
255
+ if head.isdigit():
256
+ blocks.add(int(head))
257
+ return blocks
258
+
259
+
260
+ def infer_latent_channels_from_decoder(dec_sd):
261
+ # Find smallest-index conv weight that looks like decoder input conv: [64, C, 3, 3]
262
+ candidates = []
263
+ for k, v in dec_sd.items():
264
+ if not isinstance(v, torch.Tensor) or v.ndim != 4:
265
+ continue
266
+ head = k.split(".", 1)[0]
267
+ if head.isdigit() and v.shape[0] == 64 and v.shape[2:] == (3, 3):
268
+ candidates.append((int(head), int(v.shape[1])))
269
+ if not candidates:
270
+ raise RuntimeError("Could not infer latent_channels from decoder weights.")
271
+ candidates.sort(key=lambda t: t[0])
272
+ return candidates[0][1]
273
+
274
+
275
+ def detect_layout(enc_sd, latent_channels):
276
+ # Your file has encoder.layers.14.* -> after normalize it's "14.weight"
277
+ if "14.weight" in enc_sd:
278
+ w = enc_sd["14.weight"]
279
+ if isinstance(w, torch.Tensor) and w.ndim == 4 and w.shape[0] == latent_channels and w.shape[1] == 64:
280
+ return "scale8"
281
+ if "18.weight" in enc_sd:
282
+ w = enc_sd["18.weight"]
283
+ if isinstance(w, torch.Tensor) and w.ndim == 4 and w.shape[0] == latent_channels and w.shape[1] == 64:
284
+ return "scale16"
285
+
286
+ # Fallback: find earliest [C,64,3,3] conv in encoder
287
+ best = None
288
+ for k, v in enc_sd.items():
289
+ if not isinstance(v, torch.Tensor) or v.ndim != 4:
290
+ continue
291
+ head = k.split(".", 1)[0]
292
+ if head.isdigit() and v.shape[0] == latent_channels and v.shape[1] == 64 and v.shape[2:] == (3, 3):
293
+ idx = int(head)
294
+ best = idx if best is None else min(best, idx)
295
+ if best is None:
296
+ raise RuntimeError("Could not detect encoder layout (scale8 vs scale16).")
297
+ return "scale8" if best <= 14 else "scale16"
298
+
299
+
300
+ # ============================================================
301
+ # Core model (PR behavior: decode -> [-1,1], encode -> packed for taef2)
302
+ # ============================================================
303
+
304
+ class TAESDCore(nn.Module):
305
+ def __init__(self, encoder, decoder, latent_channels, is_taef2):
306
+ super().__init__()
307
+ self.encoder = encoder
308
+ self.decoder = decoder
309
+ self.latent_channels = int(latent_channels)
310
+ self.is_taef2 = bool(is_taef2)
311
+
312
+ self.vae_scale = nn.Parameter(torch.tensor(1.0))
313
+ self.vae_shift = nn.Parameter(torch.tensor(0.0))
314
+
315
+ @torch.inference_mode()
316
+ def decode(self, x):
317
+ x = unpack_packed_latents(x, self.latent_channels)
318
+ x = (x - self.vae_shift) * self.vae_scale
319
+ x_sample = self.decoder(x)
320
+ # decoder output in [0,1] -> [-1,1]
321
+ return x_sample.sub(0.5).mul(2.0)
322
+
323
+ @torch.inference_mode()
324
+ def encode(self, x):
325
+ # x is [-1,1] -> encoder expects [0,1]
326
+ z = (self.encoder(x * 0.5 + 0.5) / self.vae_scale) + self.vae_shift
327
+ if self.is_taef2:
328
+ z = pack_packed_latents(z, self.latent_channels)
329
+ return z
330
+
331
+
332
+ def load_core(path, device, dtype):
333
+ sd_raw = comfy.utils.load_torch_file(path, safe_load=True)
334
+ sd = normalize_state_dict(sd_raw)
335
+ enc_sd, dec_sd = split_encoder_decoder(sd)
336
+
337
+ if not enc_sd or not dec_sd:
338
+ sample = list(sd_raw.keys())[:40]
339
+ raise RuntimeError(
340
+ "Could not split encoder/decoder weights.\n"
341
+ "Use Dump VAE Keys node and paste first ~40 keys.\n"
342
+ f"First keys: {sample}"
343
+ )
344
+
345
+ enc_pool = pool_blocks_from_sd(enc_sd)
346
+ dec_pool = pool_blocks_from_sd(dec_sd)
347
+
348
+ latent_channels = infer_latent_channels_from_decoder(dec_sd)
349
+ layout = detect_layout(enc_sd, latent_channels)
350
+
351
+ # Flux2 taef2 packed-latents heuristic (matches your file):
352
+ has_midblock_gn = (len(enc_pool) > 0) or (len(dec_pool) > 0)
353
+ is_taef2 = (latent_channels == 32) and has_midblock_gn
354
+
355
+ if layout == "scale8":
356
+ encoder = build_encoder_scale8(latent_channels, enc_pool)
357
+ decoder = build_decoder_scale8(latent_channels, dec_pool)
358
+ base_downscale = 8
359
+ else:
360
+ encoder = build_encoder_scale16(latent_channels, enc_pool)
361
+ decoder = build_decoder_scale16(latent_channels, dec_pool)
362
+ base_downscale = 16
363
+
364
+ # Load in fp32 first, then cast (more robust)
365
+ core = TAESDCore(encoder, decoder, latent_channels, is_taef2)
366
+ core.encoder.load_state_dict(enc_sd, strict=False)
367
+ core.decoder.load_state_dict(dec_sd, strict=False)
368
+
369
+ core = core.to(device=device, dtype=dtype).eval()
370
+ for p in core.parameters():
371
+ p.requires_grad_(False)
372
+
373
+ core._base_downscale = base_downscale
374
+ return core
375
+
376
+
377
+ # ============================================================
378
+ # Comfy VAE interface object
379
+ # ============================================================
380
+
381
+ class TAEF2VAE:
382
+ def __init__(self, weights_path, device, dtype):
383
+ self.device = device
384
+ self.dtype = dtype
385
+ self.core = load_core(weights_path, device=device, dtype=dtype)
386
+
387
+ # packed latents halves latent H/W again -> effective downscale doubles
388
+ self.downscale_ratio = self.core._base_downscale * (2 if self.core.is_taef2 else 1)
389
+
390
+ print(
391
+ f"[TAEF2] Loaded: {weights_path} | latent_channels={self.core.latent_channels} "
392
+ f"| is_taef2={self.core.is_taef2} | base_downscale={self.core._base_downscale} "
393
+ f"| effective_downscale={self.downscale_ratio}"
394
+ )
395
+
396
+ @torch.inference_mode()
397
+ def decode(self, latents):
398
+ x = latents.to(device=self.device, dtype=self.dtype)
399
+ img = self.core.decode(x) # NCHW in [-1,1]
400
+ img = img.clamp(-1, 1).add(1.0).mul(0.5) # -> [0,1]
401
+ return img.to(torch.float32).permute(0, 2, 3, 1).contiguous() # NHWC float32
402
+
403
+ @torch.inference_mode()
404
+ def encode(self, pixels):
405
+ # pixels NHWC [0,1]
406
+ x = pixels[..., :3].permute(0, 3, 1, 2).contiguous()
407
+ x = x.to(device=self.device, dtype=self.dtype).clamp(0, 1).mul(2.0).sub(1.0) # -> [-1,1]
408
+
409
+ # Make it behave like base VAE: pad to required multiple so any size works
410
+ x = pad_nchw_to_multiple(x, self.downscale_ratio)
411
+
412
+ z = self.core.encode(x) # packed if taef2
413
+ return z.to(torch.float32)
414
+
415
+ def decode_tiled(self, latents, **kwargs):
416
+ return self.decode(latents)
417
+
418
+ def encode_tiled(self, pixels, **kwargs):
419
+ return self.encode(pixels)
420
+
421
+ def spacial_compression_decode(self):
422
+ return self.downscale_ratio
423
+
424
+ def spacial_compression_encode(self):
425
+ return self.downscale_ratio
426
+
427
+ def temporal_compression_decode(self):
428
+ return None
429
+
430
+ def temporal_compression_encode(self):
431
+ return None
432
+
433
+
434
+ # ============================================================
435
+ # Nodes
436
+ # ============================================================
437
+
438
+ def _list_vae_files():
439
+ vae_files = folder_paths.get_filename_list("vae")
440
+ approx_files = folder_paths.get_filename_list("vae_approx")
441
+ return sorted(set(vae_files + approx_files))
442
+
443
+ def _resolve_vae_path(fname):
444
+ path = folder_paths.get_full_path("vae_approx", fname)
445
+ if path is None:
446
+ path = folder_paths.get_full_path("vae", fname)
447
+ return path
448
+
449
+
450
+ class LoadTAEF2VAE:
451
+ @classmethod
452
+ def INPUT_TYPES(cls):
453
+ return {
454
+ "required": {
455
+ "weights": (_list_vae_files(),),
456
+ "dtype": (["bf16", "fp16", "fp32"], {"default": "bf16"}),
457
+ }
458
+ }
459
+
460
+ RETURN_TYPES = ("VAE",)
461
+ FUNCTION = "load"
462
+ CATEGORY = "latent/vae"
463
+
464
+ def load(self, weights, dtype):
465
+ path = _resolve_vae_path(weights)
466
+ if path is None:
467
+ raise FileNotFoundError(f"Could not find weights file: {weights}")
468
+
469
+ device = comfy.model_management.get_torch_device()
470
+ if dtype == "bf16":
471
+ tdtype = torch.bfloat16
472
+ elif dtype == "fp16":
473
+ tdtype = torch.float16
474
+ else:
475
+ tdtype = torch.float32
476
+
477
+ return (TAEF2VAE(path, device=device, dtype=tdtype),)
478
+
479
+
480
+ class DumpVAEKeys:
481
+ @classmethod
482
+ def INPUT_TYPES(cls):
483
+ return {
484
+ "required": {
485
+ "weights": (_list_vae_files(),),
486
+ "include_shapes": ("BOOLEAN", {"default": True}),
487
+ "sort_keys": ("BOOLEAN", {"default": True}),
488
+ "max_lines": ("INT", {"default": 0, "min": 0, "max": 200000}),
489
+ }
490
+ }
491
+
492
+ RETURN_TYPES = ("STRING",)
493
+ FUNCTION = "dump"
494
+ CATEGORY = "utils/debug"
495
+
496
+ def dump(self, weights, include_shapes, sort_keys, max_lines):
497
+ path = _resolve_vae_path(weights)
498
+ if path is None:
499
+ raise FileNotFoundError(f"Could not find weights file: {weights}")
500
+
501
+ sd = comfy.utils.load_torch_file(path, safe_load=True)
502
+ keys = list(sd.keys())
503
+ if sort_keys:
504
+ keys.sort()
505
+
506
+ lines = []
507
+ if include_shapes:
508
+ for k in keys:
509
+ v = sd[k]
510
+ if isinstance(v, torch.Tensor):
511
+ lines.append(f"{k}\t{tuple(v.shape)}\t{str(v.dtype)}")
512
+ else:
513
+ lines.append(f"{k}\t{type(v)}")
514
+ else:
515
+ lines = keys
516
+
517
+ if max_lines and len(lines) > max_lines:
518
+ head = lines[:max_lines]
519
+ head.append(f"... TRUNCATED: total_keys={len(lines)} (showing first {max_lines}) ...")
520
+ lines = head
521
+
522
+ text = "\n".join(lines)
523
+ return {"ui": {"text": [text]}, "result": (text,)}
524
+
525
+
526
+ NODE_CLASS_MAPPINGS = {
527
+ "LoadTAEF2VAE": LoadTAEF2VAE,
528
+ "DumpVAEKeys": DumpVAEKeys,
529
+ }
530
+
531
+ NODE_DISPLAY_NAME_MAPPINGS = {
532
+ "LoadTAEF2VAE": "Load TAEF2 (Flux2 Tiny VAE)",
533
+ "DumpVAEKeys": "Dump VAE Keys (as String)",
534
+ }
ComfyUI-TAEF2/convert_safetensors_fp32_to_fp16.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ import comfy.utils
6
+ import comfy.ops
7
+ import comfy.model_management
8
+ import folder_paths
9
+
10
+
11
+ # ============================================================
12
+ # Layers (Comfy-style: disable_weight_init)
13
+ # ============================================================
14
+
15
+ def conv(n_in, n_out, **kwargs):
16
+ return comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 3, padding=1, **kwargs)
17
+
18
+
19
+ class Clamp(nn.Module):
20
+ def forward(self, x):
21
+ return torch.tanh(x / 3) * 3
22
+
23
+
24
+ class Block(nn.Module):
25
+ def __init__(self, n_in, n_out, use_midblock_gn=False):
26
+ super().__init__()
27
+ self.conv = nn.Sequential(
28
+ conv(n_in, n_out), nn.ReLU(),
29
+ conv(n_out, n_out), nn.ReLU(),
30
+ conv(n_out, n_out),
31
+ )
32
+ self.skip = comfy.ops.disable_weight_init.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity()
33
+ self.fuse = nn.ReLU()
34
+
35
+ self.pool = None
36
+ if use_midblock_gn:
37
+ conv1x1 = lambda a, b: comfy.ops.disable_weight_init.Conv2d(a, b, 1, bias=False)
38
+ n_gn = n_in * 4
39
+ self.pool = nn.Sequential(
40
+ conv1x1(n_in, n_gn),
41
+ comfy.ops.disable_weight_init.GroupNorm(4, n_gn),
42
+ nn.ReLU(inplace=True),
43
+ conv1x1(n_gn, n_in),
44
+ )
45
+
46
+ def forward(self, x):
47
+ if self.pool is not None:
48
+ x = x + self.pool(x)
49
+ return self.fuse(self.conv(x) + self.skip(x))
50
+
51
+
52
+ # ============================================================
53
+ # TAESD scale8/scale16 builders
54
+ # Your file is scale8 (encoder final conv at layers.14)
55
+ # ============================================================
56
+
57
+ def build_encoder_scale8(latent_channels, pool_blocks):
58
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
59
+ return nn.Sequential(
60
+ conv(3, 64), # 0
61
+ B(1), # 1
62
+ conv(64, 64, stride=2, bias=False), # 2
63
+ B(3), B(4), B(5), # 3-5
64
+ conv(64, 64, stride=2, bias=False), # 6
65
+ B(7), B(8), B(9), # 7-9
66
+ conv(64, 64, stride=2, bias=False), # 10
67
+ B(11), B(12), B(13), # 11-13
68
+ conv(64, latent_channels), # 14
69
+ )
70
+
71
+
72
+ def build_decoder_scale8(latent_channels, pool_blocks):
73
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
74
+ return nn.Sequential(
75
+ Clamp(), # 0 (no weights)
76
+ conv(latent_channels, 64), # 1
77
+ nn.ReLU(), # 2
78
+ B(3), B(4), B(5), # 3-5
79
+ nn.Upsample(scale_factor=2), # 6
80
+ conv(64, 64, bias=False), # 7
81
+ B(8), B(9), B(10), # 8-10
82
+ nn.Upsample(scale_factor=2), # 11
83
+ conv(64, 64, bias=False), # 12
84
+ B(13), B(14), B(15), # 13-15
85
+ nn.Upsample(scale_factor=2), # 16
86
+ conv(64, 64, bias=False), # 17
87
+ B(18), # 18
88
+ conv(64, 3), # 19
89
+ )
90
+
91
+
92
+ def build_encoder_scale16(latent_channels, pool_blocks):
93
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
94
+ return nn.Sequential(
95
+ conv(3, 64), # 0
96
+ B(1), # 1
97
+ conv(64, 64, stride=2, bias=False), # 2
98
+ B(3), B(4), B(5), # 3-5
99
+ conv(64, 64, stride=2, bias=False), # 6
100
+ B(7), B(8), B(9), # 7-9
101
+ conv(64, 64, stride=2, bias=False), # 10
102
+ B(11), B(12), B(13), # 11-13
103
+ conv(64, 64, stride=2, bias=False), # 14
104
+ B(15), B(16), B(17), # 15-17
105
+ conv(64, latent_channels), # 18
106
+ )
107
+
108
+
109
+ def build_decoder_scale16(latent_channels, pool_blocks):
110
+ def B(idx): return Block(64, 64, use_midblock_gn=(idx in pool_blocks))
111
+ return nn.Sequential(
112
+ Clamp(), # 0
113
+ conv(latent_channels, 64), # 1
114
+ nn.ReLU(), # 2
115
+ B(3), B(4), B(5), # 3-5
116
+ nn.Upsample(scale_factor=2), # 6
117
+ conv(64, 64, bias=False), # 7
118
+ B(8), B(9), B(10), # 8-10
119
+ nn.Upsample(scale_factor=2), # 11
120
+ conv(64, 64, bias=False), # 12
121
+ B(13), B(14), B(15), # 13-15
122
+ nn.Upsample(scale_factor=2), # 16
123
+ conv(64, 64, bias=False), # 17
124
+ B(18), B(19), B(20), # 18-20
125
+ nn.Upsample(scale_factor=2), # 21
126
+ conv(64, 64, bias=False), # 22
127
+ B(23), # 23
128
+ conv(64, 3), # 24
129
+ )
130
+
131
+
132
+ # ============================================================
133
+ # Packed latents (auto-pad so it never errors)
134
+ # ============================================================
135
+
136
+ def unpack_packed_latents(x, latent_channels):
137
+ # [B, C*4, H, W] -> [B, C, H*2, W*2]
138
+ if x.ndim == 4 and x.shape[1] == latent_channels * 4:
139
+ return (
140
+ x.reshape(x.shape[0], latent_channels, 2, 2, x.shape[-2], x.shape[-1])
141
+ .permute(0, 1, 4, 2, 5, 3)
142
+ .reshape(x.shape[0], latent_channels, x.shape[-2] * 2, x.shape[-1] * 2)
143
+ )
144
+ return x
145
+
146
+
147
+ def pack_packed_latents(z, latent_channels):
148
+ # [B, C, H, W] -> [B, C*4, H//2, W//2]
149
+ if z.ndim == 4 and z.shape[1] == latent_channels:
150
+ h, w = z.shape[-2], z.shape[-1]
151
+ pad_h = h & 1
152
+ pad_w = w & 1
153
+ if pad_h or pad_w:
154
+ z = F.pad(z, (0, pad_w, 0, pad_h), mode="replicate")
155
+ h, w = z.shape[-2], z.shape[-1]
156
+
157
+ return (
158
+ z.reshape(z.shape[0], latent_channels, h // 2, 2, w // 2, 2)
159
+ .permute(0, 1, 3, 5, 2, 4)
160
+ .reshape(z.shape[0], latent_channels * 4, h // 2, w // 2)
161
+ )
162
+ return z
163
+
164
+
165
+ def pad_nchw_to_multiple(x, multiple):
166
+ # replicate pad right/bottom so any size works
167
+ _, _, h, w = x.shape
168
+ pad_h = (multiple - (h % multiple)) % multiple
169
+ pad_w = (multiple - (w % multiple)) % multiple
170
+ if pad_h or pad_w:
171
+ x = F.pad(x, (0, pad_w, 0, pad_h), mode="replicate")
172
+ return x
173
+
174
+
175
+ # ============================================================
176
+ # Key conversion for your file format:
177
+ # encoder.layers.N.* and decoder.layers.N.*
178
+ # decoder layers must shift +1 because our decoder has Clamp() at index 0.
179
+ # ============================================================
180
+
181
+ def normalize_state_dict(sd_raw):
182
+ keys = list(sd_raw.keys())
183
+
184
+ # Already comfy split format?
185
+ if any(k.startswith("taesd_encoder.") for k in keys) or any(k.startswith("taesd_decoder.") for k in keys):
186
+ return sd_raw
187
+
188
+ out = {}
189
+
190
+ # Diffusers "encoder.layers.* / decoder.layers.*"
191
+ if any(k.startswith("encoder.layers.") for k in keys) or any(k.startswith("decoder.layers.") for k in keys):
192
+ for k, v in sd_raw.items():
193
+ if k.startswith("encoder.layers."):
194
+ out["taesd_encoder." + k[len("encoder.layers."):]] = v
195
+ elif k.startswith("decoder.layers."):
196
+ rest = k[len("decoder.layers."):]
197
+ parts = rest.split(".", 1)
198
+ try:
199
+ n = int(parts[0])
200
+ n2 = n + 1
201
+ tail = parts[1] if len(parts) > 1 else ""
202
+ out_key = f"taesd_decoder.{n2}" + (("." + tail) if tail else "")
203
+ out[out_key] = v
204
+ except Exception:
205
+ out[k] = v
206
+ else:
207
+ out[k] = v
208
+ return out
209
+
210
+ # Fallback: encoder./decoder. (numeric)
211
+ if any(k.startswith("encoder.") for k in keys) or any(k.startswith("decoder.") for k in keys):
212
+ decoder_needs_offset = False
213
+ w0 = sd_raw.get("decoder.0.weight", None)
214
+ if isinstance(w0, torch.Tensor) and w0.ndim == 4 and w0.shape[0] == 64 and w0.shape[2:] == (3, 3):
215
+ decoder_needs_offset = True
216
+
217
+ for k, v in sd_raw.items():
218
+ if k.startswith("encoder."):
219
+ out["taesd_encoder." + k[len("encoder."):]] = v
220
+ elif k.startswith("decoder."):
221
+ rest = k[len("decoder."):]
222
+ if decoder_needs_offset:
223
+ parts = rest.split(".", 1)
224
+ if parts[0].isdigit():
225
+ n = int(parts[0]) + 1
226
+ tail = parts[1] if len(parts) > 1 else ""
227
+ out_key = f"taesd_decoder.{n}" + (("." + tail) if tail else "")
228
+ out[out_key] = v
229
+ else:
230
+ out["taesd_decoder." + rest] = v
231
+ else:
232
+ out["taesd_decoder." + rest] = v
233
+ else:
234
+ out[k] = v
235
+ return out
236
+
237
+ return sd_raw
238
+
239
+
240
+ def split_encoder_decoder(sd):
241
+ enc = {k[len("taesd_encoder."):]: v for k, v in sd.items() if k.startswith("taesd_encoder.")}
242
+ dec = {k[len("taesd_decoder."):]: v for k, v in sd.items() if k.startswith("taesd_decoder.")}
243
+ return enc, dec
244
+
245
+
246
+ def pool_blocks_from_sd(part_sd):
247
+ blocks = set()
248
+ for k in part_sd.keys():
249
+ if ".pool.0.weight" in k or ".pool.0.bias" in k or ".pool.1.weight" in k or ".pool.1.bias" in k:
250
+ head = k.split(".", 1)[0]
251
+ if head.isdigit():
252
+ blocks.add(int(head))
253
+ return blocks
254
+
255
+
256
+ def infer_latent_channels_from_decoder(dec_sd):
257
+ candidates = []
258
+ for k, v in dec_sd.items():
259
+ if not isinstance(v, torch.Tensor) or v.ndim != 4:
260
+ continue
261
+ head = k.split(".", 1)[0]
262
+ if head.isdigit() and v.shape[0] == 64 and v.shape[2:] == (3, 3):
263
+ candidates.append((int(head), int(v.shape[1])))
264
+ if not candidates:
265
+ raise RuntimeError("Could not infer latent_channels from decoder weights.")
266
+ candidates.sort(key=lambda t: t[0])
267
+ return candidates[0][1]
268
+
269
+
270
+ def detect_layout(enc_sd, latent_channels):
271
+ if "14.weight" in enc_sd:
272
+ w = enc_sd["14.weight"]
273
+ if isinstance(w, torch.Tensor) and w.ndim == 4 and w.shape[0] == latent_channels and w.shape[1] == 64:
274
+ return "scale8"
275
+ if "18.weight" in enc_sd:
276
+ w = enc_sd["18.weight"]
277
+ if isinstance(w, torch.Tensor) and w.ndim == 4 and w.shape[0] == latent_channels and w.shape[1] == 64:
278
+ return "scale16"
279
+
280
+ best = None
281
+ for k, v in enc_sd.items():
282
+ if not isinstance(v, torch.Tensor) or v.ndim != 4:
283
+ continue
284
+ head = k.split(".", 1)[0]
285
+ if head.isdigit() and v.shape[0] == latent_channels and v.shape[1] == 64 and v.shape[2:] == (3, 3):
286
+ idx = int(head)
287
+ best = idx if best is None else min(best, idx)
288
+ if best is None:
289
+ raise RuntimeError("Could not detect encoder layout (scale8 vs scale16).")
290
+ return "scale8" if best <= 14 else "scale16"
291
+
292
+
293
+ # ============================================================
294
+ # AUTO dtype inference (no unnecessary fp32 cast)
295
+ # ============================================================
296
+
297
+ def infer_checkpoint_dtype(sd_raw):
298
+ # Prefer bf16/fp16 if present; else fp32.
299
+ # Use "most common" among float dtypes to handle odd mixed checkpoints.
300
+ from collections import Counter
301
+
302
+ dts = []
303
+ for v in sd_raw.values():
304
+ if isinstance(v, torch.Tensor):
305
+ if v.dtype in (torch.bfloat16, torch.float16, torch.float32):
306
+ dts.append(v.dtype)
307
+
308
+ if not dts:
309
+ return torch.float32
310
+
311
+ c = Counter(dts)
312
+ # If mixed, pick the most common. If tie, prefer bf16 > fp16 > fp32.
313
+ most = c.most_common()
314
+ top_count = most[0][1]
315
+ top = [dt for dt, cnt in most if cnt == top_count]
316
+
317
+ if torch.bfloat16 in top:
318
+ return torch.bfloat16
319
+ if torch.float16 in top:
320
+ return torch.float16
321
+ return torch.float32
322
+
323
+
324
+ def choose_runtime_dtype(ckpt_dtype, device):
325
+ # Keep checkpoint dtype when possible; fallback if needed.
326
+ if device.type == "cpu":
327
+ # CPU fp16/bf16 can be problematic/slow; safest is fp32.
328
+ return torch.float32
329
+
330
+ if device.type == "cuda":
331
+ if ckpt_dtype == torch.bfloat16:
332
+ # If bf16 isn't supported, fall back to fp16 (or fp32).
333
+ if hasattr(torch.cuda, "is_bf16_supported") and not torch.cuda.is_bf16_supported():
334
+ return torch.float16
335
+ return ckpt_dtype
336
+
337
+ # mps/other: be conservative
338
+ return torch.float32 if ckpt_dtype != torch.float32 else torch.float32
339
+
340
+
341
+ # ============================================================
342
+ # Core model
343
+ # ============================================================
344
+
345
+ class TAESDCore(nn.Module):
346
+ def __init__(self, encoder, decoder, latent_channels, is_taef2):
347
+ super().__init__()
348
+ self.encoder = encoder
349
+ self.decoder = decoder
350
+ self.latent_channels = int(latent_channels)
351
+ self.is_taef2 = bool(is_taef2)
352
+
353
+ self.vae_scale = nn.Parameter(torch.tensor(1.0))
354
+ self.vae_shift = nn.Parameter(torch.tensor(0.0))
355
+
356
+ @torch.inference_mode()
357
+ def decode(self, x):
358
+ x = unpack_packed_latents(x, self.latent_channels)
359
+ x = (x - self.vae_shift) * self.vae_scale
360
+ x_sample = self.decoder(x)
361
+ return x_sample.sub(0.5).mul(2.0) # [0,1] -> [-1,1]
362
+
363
+ @torch.inference_mode()
364
+ def encode(self, x):
365
+ z = (self.encoder(x * 0.5 + 0.5) / self.vae_scale) + self.vae_shift
366
+ if self.is_taef2:
367
+ z = pack_packed_latents(z, self.latent_channels)
368
+ return z
369
+
370
+
371
+ def load_core(path, device):
372
+ sd_raw = comfy.utils.load_torch_file(path, safe_load=True)
373
+ ckpt_dtype = infer_checkpoint_dtype(sd_raw)
374
+ runtime_dtype = choose_runtime_dtype(ckpt_dtype, device)
375
+
376
+ sd = normalize_state_dict(sd_raw)
377
+ enc_sd, dec_sd = split_encoder_decoder(sd)
378
+
379
+ if not enc_sd or not dec_sd:
380
+ sample = list(sd_raw.keys())[:40]
381
+ raise RuntimeError(
382
+ "Could not split encoder/decoder weights.\n"
383
+ "Use Dump VAE Keys node and paste first ~40 keys.\n"
384
+ f"First keys: {sample}"
385
+ )
386
+
387
+ enc_pool = pool_blocks_from_sd(enc_sd)
388
+ dec_pool = pool_blocks_from_sd(dec_sd)
389
+
390
+ latent_channels = infer_latent_channels_from_decoder(dec_sd)
391
+ layout = detect_layout(enc_sd, latent_channels)
392
+
393
+ has_midblock_gn = (len(enc_pool) > 0) or (len(dec_pool) > 0)
394
+ is_taef2 = (latent_channels == 32) and has_midblock_gn
395
+
396
+ if layout == "scale8":
397
+ encoder = build_encoder_scale8(latent_channels, enc_pool)
398
+ decoder = build_decoder_scale8(latent_channels, dec_pool)
399
+ base_downscale = 8
400
+ else:
401
+ encoder = build_encoder_scale16(latent_channels, enc_pool)
402
+ decoder = build_decoder_scale16(latent_channels, dec_pool)
403
+ base_downscale = 16
404
+
405
+ # ---- IMPORTANT: keep dtype aligned to checkpoint (no fp32 detour) ----
406
+ core = TAESDCore(encoder, decoder, latent_channels, is_taef2)
407
+
408
+ # Make params match runtime dtype BEFORE loading so load_state_dict doesn't cast.
409
+ core = core.to(dtype=runtime_dtype)
410
+
411
+ core.encoder.load_state_dict(enc_sd, strict=False)
412
+ core.decoder.load_state_dict(dec_sd, strict=False)
413
+
414
+ # Move to device without changing dtype.
415
+ core = core.to(device=device).eval()
416
+ for p in core.parameters():
417
+ p.requires_grad_(False)
418
+
419
+ core._base_downscale = base_downscale
420
+ core._runtime_dtype = runtime_dtype
421
+ core._ckpt_dtype = ckpt_dtype
422
+ return core
423
+
424
+
425
+ # ============================================================
426
+ # Comfy VAE interface object
427
+ # ============================================================
428
+
429
+ class TAEF2VAE:
430
+ def __init__(self, weights_path, device):
431
+ self.device = device
432
+ self.core = load_core(weights_path, device=device)
433
+
434
+ # dtype is now auto-picked from checkpoint (with fallback)
435
+ self.dtype = self.core._runtime_dtype
436
+
437
+ # packed latents halves latent H/W again -> effective downscale doubles
438
+ self.downscale_ratio = self.core._base_downscale * (2 if self.core.is_taef2 else 1)
439
+
440
+ print(
441
+ f"[TAEF2] Loaded: {weights_path} | ckpt_dtype={self.core._ckpt_dtype} | runtime_dtype={self.dtype} "
442
+ f"| latent_channels={self.core.latent_channels} | is_taef2={self.core.is_taef2} "
443
+ f"| base_downscale={self.core._base_downscale} | effective_downscale={self.downscale_ratio}"
444
+ )
445
+
446
+ @torch.inference_mode()
447
+ def decode(self, latents):
448
+ x = latents.to(device=self.device, dtype=self.dtype)
449
+ img = self.core.decode(x) # NCHW in [-1,1]
450
+ img = img.clamp(-1, 1).add(1.0).mul(0.5) # -> [0,1]
451
+ return img.to(torch.float32).permute(0, 2, 3, 1).contiguous() # NHWC float32
452
+
453
+ @torch.inference_mode()
454
+ def encode(self, pixels):
455
+ # pixels NHWC [0,1]
456
+ x = pixels[..., :3].permute(0, 3, 1, 2).contiguous()
457
+ x = x.to(device=self.device, dtype=self.dtype).clamp(0, 1).mul(2.0).sub(1.0) # -> [-1,1]
458
+
459
+ x = pad_nchw_to_multiple(x, self.downscale_ratio)
460
+
461
+ z = self.core.encode(x) # packed if taef2
462
+ return z.to(torch.float32)
463
+
464
+ def decode_tiled(self, latents, **kwargs):
465
+ return self.decode(latents)
466
+
467
+ def encode_tiled(self, pixels, **kwargs):
468
+ return self.encode(pixels)
469
+
470
+ def spacial_compression_decode(self):
471
+ return self.downscale_ratio
472
+
473
+ def spacial_compression_encode(self):
474
+ return self.downscale_ratio
475
+
476
+ def temporal_compression_decode(self):
477
+ return None
478
+
479
+ def temporal_compression_encode(self):
480
+ return None
481
+
482
+
483
+ # ============================================================
484
+ # Nodes
485
+ # ============================================================
486
+
487
+ def _list_vae_files():
488
+ vae_files = folder_paths.get_filename_list("vae")
489
+ approx_files = folder_paths.get_filename_list("vae_approx")
490
+ return sorted(set(vae_files + approx_files))
491
+
492
+
493
+ def _resolve_vae_path(fname):
494
+ path = folder_paths.get_full_path("vae_approx", fname)
495
+ if path is None:
496
+ path = folder_paths.get_full_path("vae", fname)
497
+ return path
498
+
499
+
500
+ class LoadTAEF2VAE:
501
+ @classmethod
502
+ def INPUT_TYPES(cls):
503
+ # Hidden auto dtype: remove dtype selection from UI
504
+ return {
505
+ "required": {
506
+ "weights": (_list_vae_files(),),
507
+ }
508
+ }
509
+
510
+ RETURN_TYPES = ("VAE",)
511
+ FUNCTION = "load"
512
+ CATEGORY = "latent/vae"
513
+
514
+ def load(self, weights):
515
+ path = _resolve_vae_path(weights)
516
+ if path is None:
517
+ raise FileNotFoundError(f"Could not find weights file: {weights}")
518
+
519
+ device = comfy.model_management.get_torch_device()
520
+ return (TAEF2VAE(path, device=device),)
521
+
522
+
523
+ class DumpVAEKeys:
524
+ @classmethod
525
+ def INPUT_TYPES(cls):
526
+ return {
527
+ "required": {
528
+ "weights": (_list_vae_files(),),
529
+ "include_shapes": ("BOOLEAN", {"default": True}),
530
+ "sort_keys": ("BOOLEAN", {"default": True}),
531
+ "max_lines": ("INT", {"default": 0, "min": 0, "max": 200000}),
532
+ }
533
+ }
534
+
535
+ RETURN_TYPES = ("STRING",)
536
+ FUNCTION = "dump"
537
+ CATEGORY = "utils/debug"
538
+
539
+ def dump(self, weights, include_shapes, sort_keys, max_lines):
540
+ path = _resolve_vae_path(weights)
541
+ if path is None:
542
+ raise FileNotFoundError(f"Could not find weights file: {weights}")
543
+
544
+ sd = comfy.utils.load_torch_file(path, safe_load=True)
545
+ keys = list(sd.keys())
546
+ if sort_keys:
547
+ keys.sort()
548
+
549
+ lines = []
550
+ if include_shapes:
551
+ for k in keys:
552
+ v = sd[k]
553
+ if isinstance(v, torch.Tensor):
554
+ lines.append(f"{k}\t{tuple(v.shape)}\t{str(v.dtype)}")
555
+ else:
556
+ lines.append(f"{k}\t{type(v)}")
557
+ else:
558
+ lines = keys
559
+
560
+ if max_lines and len(lines) > max_lines:
561
+ head = lines[:max_lines]
562
+ head.append(f"... TRUNCATED: total_keys={len(lines)} (showing first {max_lines}) ...")
563
+ lines = head
564
+
565
+ text = "\n".join(lines)
566
+ return {"ui": {"text": [text]}, "result": (text,)}
567
+
568
+
569
+ NODE_CLASS_MAPPINGS = {
570
+ "LoadTAEF2VAE": LoadTAEF2VAE,
571
+ "DumpVAEKeys": DumpVAEKeys,
572
+ }
573
+
574
+ NODE_DISPLAY_NAME_MAPPINGS = {
575
+ "LoadTAEF2VAE": "Load TAEF2 (Flux2 Tiny VAE)",
576
+ "DumpVAEKeys": "Dump VAE Keys (as String)",
577
+ }