Add nested-safe H3 latent saver (1.27.0)

The stock SaveLatent calls .contiguous() straight on samples["samples"], which
fails on H3 because the latent is a NestedTensor holding the video and audio
components. This unbinds them first and writes each as its own tensor, so an H3
latent can be taken out of the graph and decoded or inspected offline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-21 02:04:42 +00:00
parent 5331c16480
commit 8ba0ea9620
3 changed files with 64 additions and 1 deletions
+8
View File
@@ -34,6 +34,12 @@ try:
except Exception as _e: # noqa
print(f"[BFSNodes] MiniMax-H3 direct decode node not loaded: {_e!r}")
MMDD_NODE_CLASS_MAPPINGS, MMDD_NODE_DISPLAY_NAME_MAPPINGS = {}, {}
try:
from .save_h3_latent import NODE_CLASS_MAPPINGS as SVH3_NODE_CLASS_MAPPINGS
from .save_h3_latent import NODE_DISPLAY_NAME_MAPPINGS as SVH3_NODE_DISPLAY_NAME_MAPPINGS
except Exception as _e: # noqa
print(f"[BFSNodes] Save H3 Latent node not loaded: {_e!r}")
SVH3_NODE_CLASS_MAPPINGS, SVH3_NODE_DISPLAY_NAME_MAPPINGS = {}, {}
try:
from .ltx_identity_multiangle import NODE_CLASS_MAPPINGS as MA_NODE_CLASS_MAPPINGS
from .ltx_identity_multiangle import NODE_DISPLAY_NAME_MAPPINGS as MA_NODE_DISPLAY_NAME_MAPPINGS
@@ -80,6 +86,7 @@ NODE_CLASS_MAPPINGS = {
**MMVAE_NODE_CLASS_MAPPINGS,
**SHARP_NODE_CLASS_MAPPINGS,
**MMDD_NODE_CLASS_MAPPINGS,
**SVH3_NODE_CLASS_MAPPINGS,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -100,4 +107,5 @@ NODE_DISPLAY_NAME_MAPPINGS = {
**MMVAE_NODE_DISPLAY_NAME_MAPPINGS,
**SHARP_NODE_DISPLAY_NAME_MAPPINGS,
**MMDD_NODE_DISPLAY_NAME_MAPPINGS,
**SVH3_NODE_DISPLAY_NAME_MAPPINGS,
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "bfsnodes"
description = "Custom ComfyUI nodes including LTXV Edit Anything, Head-Swap/Anime2Real Bernini conditioning, the AMV Guide Builder, LTX Identity Transfer (IPT2V reference + source-phase RoPE), and MiniMax-H3 single-frame VAE loaders with tile control."
version = "1.26.2"
version = "1.27.0"
license = {file = "LICENSE"}
dependencies = [
"torch",
+55
View File
@@ -0,0 +1,55 @@
"""Salva um latente do MiniMax-H3 em disco.
O `SaveLatent` nativo quebra com o latente do H3: ele chega como NestedTensor (video + audio)
e o node chama `.contiguous()` direto. Aqui os componentes sao desempacotados e salvos
separadamente, para o latente poder ser inspecionado ou decodificado fora do grafo.
"""
import os
import folder_paths
import torch
from safetensors.torch import save_file
class SaveH3Latent:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"samples": ("LATENT",),
"filename_prefix": ("STRING", {"default": "h3_latent"}),
}
}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "BFS/debug"
def save(self, samples, filename_prefix):
z = samples["samples"]
tensores = {}
if getattr(z, "is_nested", False):
for i, parte in enumerate(z.unbind()):
t = parte if parte.dim() == 5 else parte.unsqueeze(0)
tensores[f"component_{i}"] = t.contiguous().cpu()
else:
t = z if z.dim() == 5 else z.unsqueeze(0)
tensores["component_0"] = t.contiguous().cpu()
pasta = folder_paths.get_output_directory()
n = 0
while os.path.exists(os.path.join(pasta, f"{filename_prefix}_{n:05d}.safetensors")):
n += 1
caminho = os.path.join(pasta, f"{filename_prefix}_{n:05d}.safetensors")
meta = {k: str(tuple(v.shape)) for k, v in tensores.items()}
save_file(tensores, caminho, metadata={"format": "pt", **meta})
print(f"[BFSNodes] latente salvo em {caminho}")
for k, v in tensores.items():
print(f"[BFSNodes] {k}: {tuple(v.shape)} {v.dtype}")
return {}
NODE_CLASS_MAPPINGS = {"SaveH3Latent": SaveH3Latent}
NODE_DISPLAY_NAME_MAPPINGS = {"SaveH3Latent": "Save H3 Latent / nested-safe (BFS)"}