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)"}