commit 291a0a1f1c23757902e58092247f87a1cdf3fda3 Author: ethanfel Date: Sat Jun 27 13:25:47 2026 +0200 Add Latent Split node (2/4 spatial split) for ComfyUI Splits a [B,C,H,W] latent into left/right or top/bottom halves, or four quadrants. Model-agnostic (SD, SDXL, Flux / FLUX.1 Krea); preserves batch dimension and noise_mask. Co-Authored-By: Claude Opus 4.8 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..faa3900 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5a5471 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# ComfyUI-Latent-splitter + +A ComfyUI custom node that splits a latent into **2** (left/right or top/bottom) +or **4** tiles along its spatial axes. + +It is model-agnostic — it only slices the height/width of the `[B, C, H, W]` +latent tensor, so it works with SD/SDXL (4-channel) and Flux / **FLUX.1 Krea** +(16-channel) latents alike. A `1024×1024` image (a `128×128` latent) splits into +two `512×1024` halves or four `512×512` quadrants. + +## Node: Latent Split (2 / 4) + +**Category:** `latent` + +**Inputs** + +| Input | Type | Notes | +|--------|--------|-------| +| `samples` | LATENT | The latent to split. | +| `mode` | choice | `left / right`, `top / bottom`, or `quad (4)`. | + +**Outputs** — filled in reading (row-major) order: + +| Mode | latent_1 | latent_2 | latent_3 | latent_4 | +|------|----------|----------|----------|----------| +| `left / right` | left | right | — | — | +| `top / bottom` | top | bottom | — | — | +| `quad (4)` | top-left | top-right | bottom-left | bottom-right | + +Unused outputs (3 and 4 in the two-way modes) return `None` — leave them +unconnected. Odd latent dimensions split with the extra row/column going to the +second tile. The batch dimension and any `noise_mask` are preserved (the mask is +sliced to match each tile). + +## Install + +Clone into your ComfyUI `custom_nodes` directory: + +```bash +cd ComfyUI/custom_nodes +git clone https://github.com/ethanfel/ComfyUI-Split-Latent.git +``` + +No extra dependencies — it uses the PyTorch that ships with ComfyUI. Restart +ComfyUI and find the node under **Add Node → latent → Latent Split (2 / 4)**. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..39a8c6b --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..5e156d3 --- /dev/null +++ b/nodes.py @@ -0,0 +1,121 @@ +"""Latent splitter nodes for ComfyUI. + +Splits a latent along its spatial dimensions into halves (left/right or +top/bottom) or quarters. The latent tensor has shape ``[B, C, H, W]`` where H/W +are 1/8 of the image size, so this is model-agnostic: it only touches the H/W +axes and works for SD/SDXL (4 channels) and Flux / FLUX.1 Krea (16 channels) +alike. + +Outputs are filled in row-major (reading) order: + + left / right -> latent_1 = left, latent_2 = right + top / bottom -> latent_1 = top, latent_2 = bottom + quad (4) -> latent_1 = top-left, latent_2 = top-right, + latent_3 = bottom-left, latent_4 = bottom-right + +Unused slots (3 and 4 in the two-way modes) return ``None``; leave them +unconnected. +""" + +SPLIT_MODES = ["left / right", "top / bottom", "quad (4)"] + + +def _halve(t, axis): + """Split a tensor in two along ``axis`` at its midpoint (floor division). + + Returns views; the caller makes them contiguous when wrapping into a latent. + """ + n = t.shape[axis] + mid = n // 2 + return t.narrow(axis, 0, mid), t.narrow(axis, mid, n - mid) + + +def _wrap(latent, samples, mask): + """Copy the latent dict, swapping in a sub-tile of samples (and its mask).""" + out = {k: v for k, v in latent.items() if k not in ("samples", "noise_mask")} + out["samples"] = samples.contiguous() + if mask is not None: + out["noise_mask"] = mask.contiguous() + return out + + +class LatentSplit: + """Split one latent into 2 (left/right or top/bottom) or 4 tiles.""" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "samples": ("LATENT", {"tooltip": "Latent to split."}), + "mode": ( + SPLIT_MODES, + { + "default": "quad (4)", + "tooltip": "left/right splits the width, top/bottom " + "splits the height, quad splits both into 4 tiles.", + }, + ), + } + } + + RETURN_TYPES = ("LATENT", "LATENT", "LATENT", "LATENT") + RETURN_NAMES = ("latent_1", "latent_2", "latent_3", "latent_4") + OUTPUT_TOOLTIPS = ( + "left / top / top-left tile", + "right / bottom / top-right tile", + "bottom-left tile (quad only, else None)", + "bottom-right tile (quad only, else None)", + ) + FUNCTION = "split" + CATEGORY = "latent" + DESCRIPTION = ( + "Split a latent into halves or quarters along its spatial axes.\n" + "Model-agnostic (works with SD, SDXL, Flux / FLUX.1 Krea).\n\n" + "Outputs fill in reading order:\n" + " left/right -> 1=left, 2=right\n" + " top/bottom -> 1=top, 2=bottom\n" + " quad -> 1=TL, 2=TR, 3=BL, 4=BR\n" + "Odd latent dimensions split with the extra row/column going to the " + "second tile." + ) + + def split(self, samples, mode): + latent = samples + s = latent["samples"] # [B, C, H, W] + mask = latent.get("noise_mask") # optional [B, 1, H', W'] + + if mode == "left / right": + s_parts = _halve(s, -1) + m_parts = _halve(mask, -1) if mask is not None else (None, None) + tiles = list(zip(s_parts, m_parts)) + elif mode == "top / bottom": + s_parts = _halve(s, -2) + m_parts = _halve(mask, -2) if mask is not None else (None, None) + tiles = list(zip(s_parts, m_parts)) + else: # quad (4) + top_s, bot_s = _halve(s, -2) + tl, tr = _halve(top_s, -1) + bl, br = _halve(bot_s, -1) + s_parts = [tl, tr, bl, br] + if mask is not None: + top_m, bot_m = _halve(mask, -2) + ml, mr = _halve(top_m, -1) + mbl, mbr = _halve(bot_m, -1) + m_parts = [ml, mr, mbl, mbr] + else: + m_parts = [None, None, None, None] + tiles = list(zip(s_parts, m_parts)) + + outputs = [_wrap(latent, sub_s, sub_m) for sub_s, sub_m in tiles] + while len(outputs) < 4: + outputs.append(None) + return tuple(outputs) + + +NODE_CLASS_MAPPINGS = { + "LatentSplit": LatentSplit, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "LatentSplit": "Latent Split (2 / 4)", +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2da5cea --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "comfyui-latent-splitter" +description = "Split a latent into 2 (left/right or top/bottom) or 4 tiles. Model-agnostic (SD, SDXL, Flux / FLUX.1 Krea)." +version = "1.0.0" +license = { text = "MIT" } +requires-python = ">=3.9" + +[project.urls] +Repository = "https://github.com/ethanfel/ComfyUI-Split-Latent.git" + +[tool.comfy] +PublisherId = "ethanfel" +DisplayName = "ComfyUI-Latent-splitter"