From d2f00d1141f41e44a6adf897dd47f6c88e83d734 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 19 Feb 2026 16:50:24 +0100 Subject: [PATCH] Add Save/Load Latent (Absolute Path) nodes from Comfyui-save-latent Consolidates SaveLatentAbsolute and LoadLatentAbsolute into this project as latent_node.py. Saves and loads LATENT data to absolute file paths in safetensors format, preserving device info and non-tensor metadata. Co-Authored-By: Claude Opus 4.6 --- README.md | 43 +++++++++++++++++++++ __init__.py | 6 +++ latent_node.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 latent_node.py diff --git a/README.md b/README.md index 4c57ce4..391c8dc 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,49 @@ Saves a WanVideo diffusion model (with merged LoRAs) as a `.safetensors` file. F - Clones all tensors before saving to handle shared/aliased weights safely. - Automatically avoids overwriting existing files by appending `_1`, `_2`, etc. +--- + +## Node: Save Latent (Absolute Path) + +Saves a LATENT to an absolute file path as `.latent` (safetensors format). Found under the **latent** category. + +### Inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `samples` | LATENT | — | Latent samples to save. | +| `path` | STRING | `/path/to/latent.latent` | Absolute file path. `.latent` extension is appended if missing. | +| `overwrite` | BOOLEAN | `False` | If false, appends `_1`, `_2`, etc. to avoid overwriting. | + +### Outputs + +| Output | Description | +|---|---| +| `LATENT` | Pass-through of the input samples (for chaining). | + +### Behavior + +- Saves all tensor data via safetensors, with device info and non-tensor metadata stored in the file header. +- Creates parent directories automatically. + +--- + +## Node: Load Latent (Absolute Path) + +Loads a LATENT from an absolute file path. Found under the **latent** category. + +### Inputs + +| Input | Type | Default | Description | +|---|---|---|---| +| `path` | STRING | `/path/to/latent.latent` | Absolute path to a `.latent` file previously saved by Save Latent. | + +### Outputs + +| Output | Description | +|---|---| +| `LATENT` | Restored latent samples with original devices and non-tensor data. | + ## Dependencies PyTorch and safetensors, both bundled with ComfyUI. diff --git a/__init__.py b/__init__.py index 1d1ef9e..7eddfca 100644 --- a/__init__.py +++ b/__init__.py @@ -3,8 +3,14 @@ from .save_node import ( NODE_CLASS_MAPPINGS as SAVE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SAVE_DISPLAY_MAPPINGS, ) +from .latent_node import ( + NODE_CLASS_MAPPINGS as LATENT_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as LATENT_DISPLAY_MAPPINGS, +) NODE_CLASS_MAPPINGS.update(SAVE_CLASS_MAPPINGS) +NODE_CLASS_MAPPINGS.update(LATENT_CLASS_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(SAVE_DISPLAY_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(LATENT_DISPLAY_MAPPINGS) __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/latent_node.py b/latent_node.py new file mode 100644 index 0000000..177b812 --- /dev/null +++ b/latent_node.py @@ -0,0 +1,100 @@ +import os +import json +import torch +import safetensors.torch + + +class SaveLatentAbsolute: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "samples": ("LATENT",), + "path": ("STRING", {"default": "/path/to/latent.latent"}), + }, + "optional": { + "overwrite": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "save" + CATEGORY = "latent" + OUTPUT_NODE = True + + def save(self, samples, path, overwrite=False): + path = os.path.expanduser(path) + if not path.endswith(".latent"): + path += ".latent" + os.makedirs(os.path.dirname(path), exist_ok=True) + + if not overwrite and os.path.exists(path): + base, ext = os.path.splitext(path) + counter = 1 + while os.path.exists(f"{base}_{counter}{ext}"): + counter += 1 + path = f"{base}_{counter}{ext}" + + tensors = {} + non_tensors = {} + devices = {} + for key, value in samples.items(): + if isinstance(value, torch.Tensor): + devices[key] = str(value.device) + tensors[key] = value.contiguous() + else: + non_tensors[key] = value + + metadata = {"devices": json.dumps(devices)} + if non_tensors: + metadata["non_tensor_data"] = json.dumps(non_tensors) + + safetensors.torch.save_file(tensors, path, metadata=metadata) + return (samples,) + + +class LoadLatentAbsolute: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "path": ("STRING", {"default": "/path/to/latent.latent"}), + } + } + + RETURN_TYPES = ("LATENT",) + FUNCTION = "load" + CATEGORY = "latent" + + def load(self, path): + path = os.path.expanduser(path) + + samples = safetensors.torch.load_file(path, device="cpu") + + with safetensors.safe_open(path, framework="pt") as f: + meta = f.metadata() + + # Restore original devices + if meta and "devices" in meta: + devices = json.loads(meta["devices"]) + for key, device in devices.items(): + if key in samples: + samples[key] = samples[key].to(device) + + # Restore non-tensor data + if meta and "non_tensor_data" in meta: + non_tensors = json.loads(meta["non_tensor_data"]) + samples.update(non_tensors) + + return (samples,) + + +NODE_CLASS_MAPPINGS = { + "SaveLatentAbsolute": SaveLatentAbsolute, + "LoadLatentAbsolute": LoadLatentAbsolute, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SaveLatentAbsolute": "Save Latent (Absolute Path)", + "LoadLatentAbsolute": "Load Latent (Absolute Path)", +}