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 <noreply@anthropic.com>
This commit is contained in:
43
README.md
43
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.
|
||||
|
||||
@@ -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"]
|
||||
|
||||
100
latent_node.py
Normal file
100
latent_node.py
Normal file
@@ -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)",
|
||||
}
|
||||
Reference in New Issue
Block a user