From 8ba0ea96202f6deccdacbc21ced090e71f1da950 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 02:04:42 +0000 Subject: [PATCH] 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 --- __init__.py | 8 +++++++ pyproject.toml | 2 +- save_h3_latent.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 save_h3_latent.py diff --git a/__init__.py b/__init__.py index ce3b755..ac38af5 100644 --- a/__init__.py +++ b/__init__.py @@ -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, } diff --git a/pyproject.toml b/pyproject.toml index 8c838cb..10671d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/save_h3_latent.py b/save_h3_latent.py new file mode 100644 index 0000000..6a5cc10 --- /dev/null +++ b/save_h3_latent.py @@ -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)"}