diff --git a/README.md b/README.md
index 3dcafa6..f5745ef 100644
--- a/README.md
+++ b/README.md
@@ -1,102 +1,136 @@
-# ComfyUI SeedVR2 Tiler
-
-
-
-
-A ComfyUI custom node pack for tiling large images through [SeedVR2](https://github.com/TencentARC/SeedVR) with overlap blending. Allows SeedVR2 to upscale images of any size by splitting them into tiles, processing each tile, and seamlessly stitching them back together.
-
-## Nodes
-
-### SeedVR2 Tile Splitter
-Splits an image into a batch of overlapping tiles sized for SeedVR2's resolution constraints.
-
-**Inputs**
-- `image` — source image
-- `tile_size_mp` — maximum tile size in megapixels (default 1.0). Lower = less VRAM per pass
-- `tile_upscale_mp` — target resolution for SeedVR2 to upscale each tile to, in megapixels
-- `overlap_fraction` — overlap between adjacent tiles as a fraction of tile size (default 0.1)
-- `feather_blend` — blend width for overlap stitching (0–1)
-
-**Outputs**
-- `tiles` — IMAGE batch ready for SeedVR2
-- `tile_metadata` — internal metadata needed by the Stitcher
-- `resolution` — INT hint to wire into SeedVR2's resolution input
-
----
-
-### SeedVR2 Tile Stitcher
-Reassembles the upscaled tile batch back into a single image using feathered blending over the overlap regions. The output is resized to preserve the exact aspect ratio of the original image.
-
-**Inputs**
-- `upscaled_tiles` — IMAGE batch from SeedVR2
-- `tile_metadata` — from the Splitter
-
-**Outputs**
-- `image` — final stitched image at the correct aspect ratio
-
----
-
-## Workflow
-```
-Load Image → Tile Splitter → tiles ──────────────────→ SeedVR2
- → tile_metadata ───────────→ Tile Stitcher
- → resolution ─────────────→ SeedVR2
- SeedVR2 → Tile Stitcher → Save Image
-```
-
-
-
-
-* Connect resolution to both resolution and max_resolution inputs on SeedVR2
-
-For **multi-pass upscaling**, run the pipeline multiple times feeding the output back as input. Each pass progressively increases resolution.
-
----
-
-## Installation
-
-### Via ComfyUI Manager
-Search for **SeedVR2 Tiler** in the ComfyUI Manager node list.
-
-### Manual
-```bash
-cd ComfyUI/custom_nodes
-git clone https://github.com/BacoHubo/ComfyUI_SeedVR2_Tiler
-```
-No additional dependencies — uses only PyTorch and standard ComfyUI libraries.
-
----
-
-## Requirements
-- ComfyUI
-- [SeedVR2](https://github.com/TencentARC/SeedVR) custom node installed separately
-- PyTorch (included with ComfyUI)
-
----
-
-## Notes
-- `tile_size_mp` of 0.5–1.0 works well for most 8GB VRAM GPUs
-- For poor quality source images, setting `tile_upscale_mp` close to `tile_size_mp` causes SeedVR2 to behave more as a restorer than an upscaler, often producing better results
-- Multi-pass upscaling works well — each pass feeds back into the Splitter as the new source image
-
----
-
-## Support
-If you find this useful, consider buying me a coffee!
-
-[](https://ko-fi.com/dbacon/tip)
-
----
-
-## Development
-This node pack was developed with the assistance of [Claude](https://claude.ai) (Anthropic). The architecture, design decisions, and testing were directed by the author; the code was written collaboratively with AI. Shared here in the spirit of transparency.
-
----
-
-## Acknowledgements
-Inspired by tiling approaches in the ComfyUI community, including
-[Moonwhaler](https://github.com/moonwhaler/comfyui-seedvr2-tilingupscaler) and the
-[Steudio](https://civitai.com/models/982985/divide-and-conquer-ultimate-upscaling-workflow-for-comfyui) upscaling workflow.
-SeedVR2 itself is by [TencentARC](https://github.com/TencentARC/SeedVR) — this node pack just makes it easier to use on larger images.
-
-
+# ComfyUI SeedVR2 Tiler
+
+
+
+
+A ComfyUI custom node pack for tiling large images through [SeedVR2](https://github.com/TencentARC/SeedVR) with overlap blending. Allows SeedVR2 to upscale images of any size by splitting them into tiles, processing each tile, and seamlessly stitching them back together.
+
+## Nodes
+
+### SeedVR2 Tile Splitter
+Splits an image into a batch of overlapping tiles sized for SeedVR2's resolution constraints.
+
+**Inputs**
+- `image` — source image
+- `tile_size_mp` — maximum tile size in megapixels (default 1.0). Lower = less VRAM per pass
+- `tile_upscale_mp` — target resolution for SeedVR2 to upscale each tile to, in megapixels
+- `overlap_fraction` — overlap between adjacent tiles as a fraction of tile size (default 0.1)
+- `feather_blend` — blend width for overlap stitching (0–1)
+
+**Outputs**
+- `tiles` — IMAGE batch ready for SeedVR2
+- `tile_metadata` — internal metadata needed by the Stitcher
+- `resolution` — INT hint to wire into SeedVR2's resolution input
+
+---
+
+### SeedVR2 Tile Splitter (Longest Edge)
+Same as the standard Tile Splitter but lets you specify your desired output size as a maximum longest edge in pixels. The resolution hint is computed automatically from your image's actual tile dimensions so it stays accurate regardless of tile count.
+
+**Inputs**
+- `image` — source image
+- `tile_size_mp` — maximum tile size in megapixels. Lower = less VRAM per pass
+- `longest_edge_px` — desired pixel length of the longest edge in the final stitched output
+- `overlap_percent` — overlap between adjacent tiles as a percentage (default 10)
+- `feather_blend` — blend width for overlap stitching (0–1)
+
+---
+
+### SeedVR2 Tile Splitter (Shortest Edge)
+Same as the standard Tile Splitter but lets you specify your desired output size as a minimum shortest edge in pixels.
+
+**Inputs**
+- `image` — source image
+- `tile_size_mp` — maximum tile size in megapixels. Lower = less VRAM per pass
+- `shortest_edge_px` — desired pixel length of the shortest edge in the final stitched output
+- `overlap_percent` — overlap between adjacent tiles as a percentage (default 10)
+- `feather_blend` — blend width for overlap stitching (0–1)
+
+---
+
+### SeedVR2 Tile Splitter (Upscale Factor)
+Same as the standard Tile Splitter but lets you specify a simple upscale multiplier instead of megapixels. 2.0 = double the width and height of the final output.
+
+**Inputs**
+- `image` — source image
+- `tile_size_mp` — maximum tile size in megapixels. Lower = less VRAM per pass
+- `upscale_factor` — upscale multiplier (e.g. 2.0 = 2× output, 1.5 = 1.5× output)
+- `overlap_percent` — overlap between adjacent tiles as a percentage (default 10)
+- `feather_blend` — blend width for overlap stitching (0–1)
+
+---
+
+### SeedVR2 Tile Stitcher
+Reassembles the upscaled tile batch back into a single image using feathered blending over the overlap regions. The output is resized to preserve the exact aspect ratio of the original image.
+
+**Inputs**
+- `upscaled_tiles` — IMAGE batch from SeedVR2
+- `tile_metadata` — from the Splitter
+
+**Outputs**
+- `image` — final stitched image at the correct aspect ratio
+
+---
+
+## Workflow
+```
+Load Image → Tile Splitter → tiles ──────────────────→ SeedVR2
+ → tile_metadata ───────────→ Tile Stitcher
+ → resolution ─────────────→ SeedVR2
+ SeedVR2 → Tile Stitcher → Save Image
+```
+
+
+
+
+* Connect resolution to both resolution and max_resolution inputs on SeedVR2
+
+For **multi-pass upscaling**, run the pipeline multiple times feeding the output back as input. Each pass progressively increases resolution.
+
+---
+
+## Installation
+
+### Via ComfyUI Manager
+Search for **SeedVR2 Tiler** in the ComfyUI Manager node list.
+
+### Manual
+```bash
+cd ComfyUI/custom_nodes
+git clone https://github.com/BacoHubo/ComfyUI_SeedVR2_Tiler
+```
+No additional dependencies — uses only PyTorch and standard ComfyUI libraries.
+
+---
+
+## Requirements
+- ComfyUI
+- [SeedVR2](https://github.com/TencentARC/SeedVR) custom node installed separately
+- PyTorch (included with ComfyUI)
+
+---
+
+## Notes
+- `tile_size_mp` of 0.5–1.0 works well for most 8GB VRAM GPUs
+- For poor quality source images, setting `tile_upscale_mp` close to `tile_size_mp` causes SeedVR2 to behave more as a restorer than an upscaler, often producing better results
+- Multi-pass upscaling works well — each pass feeds back into the Splitter as the new source image
+
+---
+
+## Support
+If you find this useful, consider buying me a coffee!
+
+[](https://ko-fi.com/dbacon/tip)
+
+---
+
+## Development
+This node pack was developed with the assistance of [Claude](https://claude.ai) (Anthropic). The architecture, design decisions, and testing were directed by the author; the code was written collaboratively with AI. Shared here in the spirit of transparency.
+
+---
+
+## Acknowledgements
+Inspired by tiling approaches in the ComfyUI community, including
+[Moonwhaler](https://github.com/moonwhaler/comfyui-seedvr2-tilingupscaler) and the
+[Steudio](https://civitai.com/models/982985/divide-and-conquer-ultimate-upscaling-workflow-for-comfyui) upscaling workflow.
+SeedVR2 itself is by [TencentARC](https://github.com/TencentARC/SeedVR) — this node pack just makes it easier to use on larger images.
diff --git a/nodes.py b/nodes.py
index 0da8931..8a4eefb 100644
--- a/nodes.py
+++ b/nodes.py
@@ -1,449 +1,651 @@
-"""
-SeedVR2 Tile Splitter & Stitcher
-=================================
-Add-on for ComfyUI-SeedVR2_VideoUpscaler that enables tiled upscaling of
-images that would otherwise exceed available VRAM.
-
-Tiling algorithm inspired by the Divide and Conquer Node Suite (Steudio):
- - Compute the minimum NxM grid such that each tile ≤ tile_size_mp
- - Among all grids with the same (minimum) tile count, pick the most square
- one relative to the image aspect ratio
- - Resize the image to an exact canvas (cols×tile_w × rows×tile_h) so every
- tile is IDENTICAL in size — no partial edge tiles
- - Adjacent tiles overlap by overlap_percent on each edge
-
-Workflow:
- [Image]
- → [SeedVR2 Tile Splitter] → IMAGE batch → [SeedVR2 node]
- → TILE_METADATA → [SeedVR2 Tile Stitcher]
- [SeedVR2 node output] → [SeedVR2 Tile Stitcher] → final IMAGE
-"""
-
-import math
-import torch
-import torch.nn.functional as F
-from typing import Dict, Any, List, Tuple
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Utility
-# ─────────────────────────────────────────────────────────────────────────────
-
-def _align(value: float, multiple: int = 8) -> int:
- """Round up to nearest multiple of `multiple` (required by VAE / SeedVR2)."""
- return max(multiple, int(math.ceil(value / multiple)) * multiple)
-
-
-def _resize(img: torch.Tensor, h: int, w: int) -> torch.Tensor:
- """Bilinear resize. img: (H, W, C) -> (h, w, C)."""
- x = img.permute(2, 0, 1).unsqueeze(0).float()
- x = F.interpolate(x, size=(h, w), mode="bilinear", align_corners=False)
- return x.squeeze(0).permute(1, 2, 0).to(img.dtype)
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Core grid algorithm
-# ─────────────────────────────────────────────────────────────────────────────
-
-def compute_tile_grid(
- img_w: int,
- img_h: int,
- tile_mp: float,
- overlap_fraction: float,
-) -> Dict[str, Any]:
- """
- Find the optimal cols x rows tile grid for the given image.
-
- Rules:
- 1. Every tile must be <= tile_mp megapixels (after 8-px alignment).
- 2. The canvas (cols*tile_w x rows*tile_h) must cover the full image.
- 3. Adjacent tiles overlap by overlap_fraction of the tile dimension.
- 4. Among all grids with the minimum viable tile count, prefer the most
- square grid (cols/rows closest to the image aspect ratio).
- 5. A slightly larger tile count is accepted if it gives a much squarer
- grid (combined score = n/min_n + squareness).
-
- Returns a dict with:
- cols, rows, tile_w, tile_h, overlap_w, overlap_h,
- stride_w, stride_h, canvas_w, canvas_h, n_tiles
- """
- target_px = tile_mp * 1_000_000
- aspect = img_w / img_h
-
- def _make_grid(cols: int, rows: int) -> Dict[str, Any]:
- # Tile size formula (see derivation in docstring above):
- # canvas_w = tile_w * (cols - overlap_fraction*(cols-1))
- # => tile_w = img_w / denom_w
- denom_w = cols - overlap_fraction * (cols - 1)
- denom_h = rows - overlap_fraction * (rows - 1)
-
- tile_w = _align(img_w / denom_w, 8)
- tile_h = _align(img_h / denom_h, 8)
-
- overlap_w = min(_align(tile_w * overlap_fraction, 8), tile_w - 8)
- overlap_h = min(_align(tile_h * overlap_fraction, 8), tile_h - 8)
-
- stride_w = tile_w - overlap_w
- stride_h = tile_h - overlap_h
-
- canvas_w = stride_w * (cols - 1) + tile_w
- canvas_h = stride_h * (rows - 1) + tile_h
-
- # Guarantee canvas covers image — alignment can undershoot by 1 step
- if canvas_w < img_w:
- tile_w += 8
- overlap_w = min(_align(tile_w * overlap_fraction, 8), tile_w - 8)
- stride_w = tile_w - overlap_w
- canvas_w = stride_w * (cols - 1) + tile_w
-
- if canvas_h < img_h:
- tile_h += 8
- overlap_h = min(_align(tile_h * overlap_fraction, 8), tile_h - 8)
- stride_h = tile_h - overlap_h
- canvas_h = stride_h * (rows - 1) + tile_h
-
- return dict(
- cols=cols, rows=rows,
- tile_w=tile_w, tile_h=tile_h,
- overlap_w=overlap_w, overlap_h=overlap_h,
- stride_w=stride_w, stride_h=stride_h,
- canvas_w=canvas_w, canvas_h=canvas_h,
- n_tiles=cols * rows,
- )
-
- def _squareness(cols: int, rows: int) -> float:
- """
- Score how well tile aspect ratio matches image aspect ratio.
- 0 = tile shape perfectly matches image; higher = more distorted.
- Scores TILE aspect (tile_w/tile_h vs img aspect), NOT grid ratio
- (cols/rows) -- the latter caused portrait images to get landscape tiles.
- """
- g = _make_grid(cols, rows)
- tile_aspect = g["tile_w"] / g["tile_h"]
- return abs(math.log(tile_aspect / aspect))
-
- # Collect all valid (cols, rows) pairs up to 2x the minimum viable count
- candidates: List[Tuple[int, float, int, int, Dict]] = []
-
- for n in range(1, 512):
- for c in range(1, n + 1):
- if n % c != 0:
- continue
- r = n // c
- g = _make_grid(c, r)
- if g["tile_w"] * g["tile_h"] <= target_px * 1.15:
- candidates.append((n, _squareness(c, r), c, r, g))
-
- if candidates:
- min_valid = candidates[0][0]
- if n >= min_valid * 2 + 4:
- break
-
- if not candidates:
- g = _make_grid(1, 1)
- g["squareness"] = 0.0
- return g
-
- # Score: penalise extra tiles + poor squareness equally
- min_n = candidates[0][0]
- best = None
- best_score = float("inf")
-
- for n, sq, c, r, g in candidates:
- score = (n / min_n) + sq
- if score < best_score:
- best_score = score
- g["squareness"] = sq
- best = g
-
- return best
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Node 1 – Tile Splitter
-# ─────────────────────────────────────────────────────────────────────────────
-
-class SeedVR2TileSplitter:
- """
- Splits a single image into a uniform NxM grid of overlapping tiles,
- each within `tile_size_mp` megapixels.
-
- The image is resized to an exact canvas before splitting so every tile
- sent to SeedVR2 is the same size. No partial or oversized edge tiles.
-
- Outputs
- -------
- tiles : IMAGE batch (N, tile_h, tile_w, C) — connect to SeedVR2
- tile_metadata : TILE_METADATA — pass directly to Tile Stitcher
- """
-
- CATEGORY = "image/upscaling"
- FUNCTION = "split"
- RETURN_TYPES = ("IMAGE", "TILE_METADATA", "INT")
- RETURN_NAMES = ("tiles", "tile_metadata", "resolution")
-
- @classmethod
- def INPUT_TYPES(cls):
- return {
- "required": {
- "image": ("IMAGE",),
- "tile_size_mp": (
- "FLOAT",
- {
- "default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1,
- "display": "number",
- "tooltip": (
- "Maximum size of each tile in megapixels. "
- "The grid is chosen so every tile is AT MOST this size. "
- "Lower = less VRAM per SeedVR2 pass."
- ),
- },
- ),
- "tile_upscale_mp": (
- "FLOAT",
- {
- "default": 2.0, "min": 0.1, "max": 64.0, "step": 0.1,
- "display": "number",
- "tooltip": (
- "Desired output size of each upscaled tile in megapixels. "
- "Used to compute the resolution hint for SeedVR2. "
- "Typically 2x your tile_size_mp."
- ),
- },
- ),
- "overlap_percent": (
- "FLOAT",
- {
- "default": 10.0, "min": 0.0, "max": 40.0, "step": 1.0,
- "display": "number",
- "tooltip": (
- "Overlap between adjacent tiles as a % of tile size. "
- "10% is a safe default. Increase to 15-20% if seams appear."
- ),
- },
- ),
- "feather_blend": (
- "FLOAT",
- {
- "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05,
- "display": "number",
- "tooltip": (
- "Fraction of the overlap zone used for blending during stitch. "
- "1.0 = smooth linear feather. 0.0 = hard cut."
- ),
- },
- ),
- }
- }
-
- def split(self, image, tile_size_mp, tile_upscale_mp, overlap_percent, feather_blend):
- # Normalise to (H, W, C)
- if image.ndim == 4:
- if image.shape[0] != 1:
- raise ValueError(
- f"Tile Splitter expects a single image (batch=1), got {image.shape[0]}."
- )
- img = image[0]
- else:
- img = image
-
- orig_H, orig_W, C = img.shape
- overlap_fraction = overlap_percent / 100.0
-
- grid = compute_tile_grid(orig_W, orig_H, tile_size_mp, overlap_fraction)
- cols = grid["cols"]; rows = grid["rows"]
- tile_w = grid["tile_w"]; tile_h = grid["tile_h"]
- overlap_w= grid["overlap_w"]; overlap_h= grid["overlap_h"]
- stride_w = grid["stride_w"]; stride_h = grid["stride_h"]
- canvas_w = grid["canvas_w"]; canvas_h = grid["canvas_h"]
-
- # Resize image to exact canvas
- if canvas_w != orig_W or canvas_h != orig_H:
- canvas_img = _resize(img, canvas_h, canvas_w)
- else:
- canvas_img = img
-
- # Extract tiles (row-major)
- tiles: List[torch.Tensor] = []
- positions: List[Tuple[int, int]] = []
-
- for row in range(rows):
- for col in range(cols):
- x0 = col * stride_w
- y0 = row * stride_h
- tiles.append(canvas_img[y0 : y0 + tile_h, x0 : x0 + tile_w, :])
- positions.append((x0, y0))
-
- tiles_tensor = torch.stack(tiles, dim=0) # (N, tile_h, tile_w, C)
-
- # resolution hint for SeedVR2
- tile_aspect = tile_w / tile_h
- out_h_raw = math.sqrt(tile_upscale_mp * 1_000_000 / tile_aspect)
- resolution = _align(max(out_h_raw * tile_aspect, out_h_raw), 8)
-
- meta: Dict[str, Any] = {
- "orig_w": orig_W, "orig_h": orig_H, "orig_c": C,
- "canvas_w": canvas_w, "canvas_h": canvas_h,
- "cols": cols, "rows": rows, "n_tiles": cols * rows,
- "tile_w": tile_w, "tile_h": tile_h,
- "overlap_w": overlap_w, "overlap_h": overlap_h,
- "stride_w": stride_w, "stride_h": stride_h,
- "positions": positions,
- "feather_blend": feather_blend,
- "resolution": resolution,
- }
-
- print(
- f"\n[SeedVR2 Tile Splitter]"
- f"\n Original : {orig_W}x{orig_H}"
- f"\n Canvas : {canvas_w}x{canvas_h}"
- f"{' (resized)' if (canvas_w != orig_W or canvas_h != orig_H) else ' (exact)'}"
- f"\n Grid : {cols}x{rows} = {cols*rows} tiles"
- f"\n Tile : {tile_w}x{tile_h} = {tile_w*tile_h/1e6:.2f} MP"
- f"\n Overlap : {overlap_w}x{overlap_h} px | Stride: {stride_w}x{stride_h} px"
- f"\n SeedVR2 resolution hint: {resolution}"
- )
-
- return (tiles_tensor, meta, resolution)
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Node 2 – Tile Stitcher
-# ─────────────────────────────────────────────────────────────────────────────
-
-class SeedVR2TileStitcher:
- """
- Receives the upscaled tile batch (SeedVR2 output) + TILE_METADATA,
- blends overlapping regions, and returns the final stitched image.
-
- Actual upscale factor is detected at runtime, so the node works even if
- SeedVR2 produced a different resolution than suggested.
- """
-
- CATEGORY = "image/upscaling"
- FUNCTION = "stitch"
- RETURN_TYPES = ("IMAGE",)
- RETURN_NAMES = ("image",)
-
- @classmethod
- def INPUT_TYPES(cls):
- return {
- "required": {
- "upscaled_tiles": ("IMAGE",),
- "tile_metadata": ("TILE_METADATA",),
- }
- }
-
- def stitch(self, upscaled_tiles: torch.Tensor, tile_metadata: dict):
- meta = tile_metadata
-
- orig_w = meta["orig_w"]; orig_h = meta["orig_h"]
- canvas_w = meta["canvas_w"]; canvas_h = meta["canvas_h"]
- cols = meta["cols"]; rows = meta["rows"]
- n_tiles = meta["n_tiles"]
- tile_w = meta["tile_w"]; tile_h = meta["tile_h"]
- overlap_w= meta["overlap_w"]; overlap_h= meta["overlap_h"]
- stride_w = meta["stride_w"]; stride_h = meta["stride_h"]
- positions = meta["positions"]
- feather_blend: float = meta["feather_blend"]
-
- if upscaled_tiles.shape[0] != n_tiles:
- raise ValueError(
- f"Expected {n_tiles} upscaled tiles but received "
- f"{upscaled_tiles.shape[0]}. Check batch order is unchanged."
- )
-
- up_h = upscaled_tiles.shape[1]
- up_w = upscaled_tiles.shape[2]
- C = upscaled_tiles.shape[3]
-
- scale_x = up_w / tile_w
- scale_y = up_h / tile_h
-
- out_canvas_w = round(canvas_w * scale_x)
- out_canvas_h = round(canvas_h * scale_y)
-
- feather_px_x = max(1, round(overlap_w * scale_x * feather_blend))
- feather_px_y = max(1, round(overlap_h * scale_y * feather_blend))
-
- print(
- f"\n[SeedVR2 Tile Stitcher]"
- f"\n Scale : {scale_x:.3f}x x {scale_y:.3f}x"
- f"\n Canvas : {out_canvas_w}x{out_canvas_h}"
- f"\n Tiles : {n_tiles} ({cols}x{rows})"
- f"\n Feather: {feather_px_x}x{feather_px_y} px"
- )
-
- device = upscaled_tiles.device
- dtype = upscaled_tiles.dtype
-
- canvas = torch.zeros(out_canvas_h, out_canvas_w, C, dtype=torch.float32, device=device)
- weights = torch.zeros(out_canvas_h, out_canvas_w, 1, dtype=torch.float32, device=device)
-
- for i, (x0_src, y0_src) in enumerate(positions):
- tile = upscaled_tiles[i].float()
- col = i % cols
- row = i // cols
-
- mask = torch.ones(up_h, up_w, dtype=torch.float32, device=device)
-
- if col > 0 and feather_px_x > 0:
- fe = min(feather_px_x, up_w)
- mask[:, :fe] *= torch.linspace(0.0, 1.0, fe, device=device)
- if col < cols - 1 and feather_px_x > 0:
- fe = min(feather_px_x, up_w)
- mask[:, up_w - fe:] *= torch.linspace(1.0, 0.0, fe, device=device)
- if row > 0 and feather_px_y > 0:
- fe = min(feather_px_y, up_h)
- mask[:fe, :] *= torch.linspace(0.0, 1.0, fe, device=device).unsqueeze(1)
- if row < rows - 1 and feather_px_y > 0:
- fe = min(feather_px_y, up_h)
- mask[up_h - fe:, :] *= torch.linspace(1.0, 0.0, fe, device=device).unsqueeze(1)
-
- ox = round(x0_src * scale_x)
- oy = round(y0_src * scale_y)
-
- dx0 = max(0, ox); dy0 = max(0, oy)
- dx1 = min(out_canvas_w, ox + up_w)
- dy1 = min(out_canvas_h, oy + up_h)
- tx0 = dx0 - ox; ty0 = dy0 - oy
- tx1 = tx0 + (dx1 - dx0); ty1 = ty0 + (dy1 - dy0)
-
- if tx1 <= tx0 or ty1 <= ty0:
- continue
-
- mask_crop = mask[ty0:ty1, tx0:tx1].unsqueeze(-1)
- canvas [dy0:dy1, dx0:dx1, :] += tile[ty0:ty1, tx0:tx1, :] * mask_crop
- weights[dy0:dy1, dx0:dx1, :] += mask_crop
-
- result = (canvas / weights.clamp(min=1e-8)).clamp(0.0, 1.0).to(dtype)
-
- # Crop/resize back to exact original aspect ratio at the effective upscale.
- # Use geometric mean of x/y scale factors so the output area is consistent,
- # then derive final_w and final_h directly from orig_w/orig_h — this
- # guarantees the aspect ratio is pixel-perfect regardless of canvas padding.
- eff_scale = math.sqrt((out_canvas_w / orig_w) * (out_canvas_h / orig_h))
- final_w = round(orig_w * eff_scale)
- final_h = round(orig_h * eff_scale)
-
- if final_w != out_canvas_w or final_h != out_canvas_h:
- result = _resize(result, final_h, final_w)
-
- print(f" Output : {final_w}x{final_h} ({final_w/orig_w:.3f}x {final_h/orig_h:.3f}x)")
-
- return (result.unsqueeze(0),)
-
-
-# ─────────────────────────────────────────────────────────────────────────────
-# Registration
-# ─────────────────────────────────────────────────────────────────────────────
-
-NODE_CLASS_MAPPINGS = {
- "SeedVR2TileSplitter": SeedVR2TileSplitter,
- "SeedVR2TileStitcher": SeedVR2TileStitcher,
-}
-
-NODE_DISPLAY_NAME_MAPPINGS = {
- "SeedVR2TileSplitter": "SeedVR2 Tile Splitter",
- "SeedVR2TileStitcher": "SeedVR2 Tile Stitcher",
-}
+"""
+SeedVR2 Tile Splitter & Stitcher
+=================================
+Add-on for ComfyUI-SeedVR2_VideoUpscaler that enables tiled upscaling of
+images that would otherwise exceed available VRAM.
+
+Tiling algorithm inspired by the Divide and Conquer Node Suite (Steudio):
+ - Compute the minimum NxM grid such that each tile ≤ tile_size_mp
+ - Among all grids with the same (minimum) tile count, pick the most square
+ one relative to the image aspect ratio
+ - Resize the image to an exact canvas (cols×tile_w × rows×tile_h) so every
+ tile is IDENTICAL in size — no partial edge tiles
+ - Adjacent tiles overlap by overlap_percent on each edge
+
+Workflow:
+ [Image]
+ → [SeedVR2 Tile Splitter] → IMAGE batch → [SeedVR2 node]
+ → TILE_METADATA → [SeedVR2 Tile Stitcher]
+ [SeedVR2 node output] → [SeedVR2 Tile Stitcher] → final IMAGE
+"""
+
+import math
+import torch
+import torch.nn.functional as F
+from typing import Dict, Any, List, Tuple
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Utility
+# ─────────────────────────────────────────────────────────────────────────────
+
+def _align(value: float, multiple: int = 8) -> int:
+ """Round up to nearest multiple of `multiple` (required by VAE / SeedVR2)."""
+ return max(multiple, int(math.ceil(value / multiple)) * multiple)
+
+
+def _resize(img: torch.Tensor, h: int, w: int) -> torch.Tensor:
+ """Bilinear resize. img: (H, W, C) -> (h, w, C)."""
+ x = img.permute(2, 0, 1).unsqueeze(0).float()
+ x = F.interpolate(x, size=(h, w), mode="bilinear", align_corners=False)
+ return x.squeeze(0).permute(1, 2, 0).to(img.dtype)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Core grid algorithm
+# ─────────────────────────────────────────────────────────────────────────────
+
+def compute_tile_grid(
+ img_w: int,
+ img_h: int,
+ tile_mp: float,
+ overlap_fraction: float,
+) -> Dict[str, Any]:
+ """
+ Find the optimal cols x rows tile grid for the given image.
+
+ Rules:
+ 1. Every tile must be <= tile_mp megapixels (after 8-px alignment).
+ 2. The canvas (cols*tile_w x rows*tile_h) must cover the full image.
+ 3. Adjacent tiles overlap by overlap_fraction of the tile dimension.
+ 4. Among all grids with the minimum viable tile count, prefer the most
+ square grid (cols/rows closest to the image aspect ratio).
+ 5. A slightly larger tile count is accepted if it gives a much squarer
+ grid (combined score = n/min_n + squareness).
+
+ Returns a dict with:
+ cols, rows, tile_w, tile_h, overlap_w, overlap_h,
+ stride_w, stride_h, canvas_w, canvas_h, n_tiles
+ """
+ target_px = tile_mp * 1_000_000
+ aspect = img_w / img_h
+
+ def _make_grid(cols: int, rows: int) -> Dict[str, Any]:
+ # Tile size formula (see derivation in docstring above):
+ # canvas_w = tile_w * (cols - overlap_fraction*(cols-1))
+ # => tile_w = img_w / denom_w
+ denom_w = cols - overlap_fraction * (cols - 1)
+ denom_h = rows - overlap_fraction * (rows - 1)
+
+ tile_w = _align(img_w / denom_w, 8)
+ tile_h = _align(img_h / denom_h, 8)
+
+ overlap_w = min(_align(tile_w * overlap_fraction, 8), tile_w - 8)
+ overlap_h = min(_align(tile_h * overlap_fraction, 8), tile_h - 8)
+
+ stride_w = tile_w - overlap_w
+ stride_h = tile_h - overlap_h
+
+ canvas_w = stride_w * (cols - 1) + tile_w
+ canvas_h = stride_h * (rows - 1) + tile_h
+
+ # Guarantee canvas covers image — alignment can undershoot by 1 step
+ if canvas_w < img_w:
+ tile_w += 8
+ overlap_w = min(_align(tile_w * overlap_fraction, 8), tile_w - 8)
+ stride_w = tile_w - overlap_w
+ canvas_w = stride_w * (cols - 1) + tile_w
+
+ if canvas_h < img_h:
+ tile_h += 8
+ overlap_h = min(_align(tile_h * overlap_fraction, 8), tile_h - 8)
+ stride_h = tile_h - overlap_h
+ canvas_h = stride_h * (rows - 1) + tile_h
+
+ return dict(
+ cols=cols, rows=rows,
+ tile_w=tile_w, tile_h=tile_h,
+ overlap_w=overlap_w, overlap_h=overlap_h,
+ stride_w=stride_w, stride_h=stride_h,
+ canvas_w=canvas_w, canvas_h=canvas_h,
+ n_tiles=cols * rows,
+ )
+
+ def _squareness(cols: int, rows: int) -> float:
+ """
+ Score how well tile aspect ratio matches image aspect ratio.
+ 0 = tile shape perfectly matches image; higher = more distorted.
+ Scores TILE aspect (tile_w/tile_h vs img aspect), NOT grid ratio
+ (cols/rows) -- the latter caused portrait images to get landscape tiles.
+ """
+ g = _make_grid(cols, rows)
+ tile_aspect = g["tile_w"] / g["tile_h"]
+ return abs(math.log(tile_aspect / aspect))
+
+ # Collect all valid (cols, rows) pairs up to 2x the minimum viable count
+ candidates: List[Tuple[int, float, int, int, Dict]] = []
+
+ for n in range(1, 512):
+ for c in range(1, n + 1):
+ if n % c != 0:
+ continue
+ r = n // c
+ g = _make_grid(c, r)
+ if g["tile_w"] * g["tile_h"] <= target_px * 1.15:
+ candidates.append((n, _squareness(c, r), c, r, g))
+
+ if candidates:
+ min_valid = candidates[0][0]
+ if n >= min_valid * 2 + 4:
+ break
+
+ if not candidates:
+ g = _make_grid(1, 1)
+ g["squareness"] = 0.0
+ return g
+
+ # Score: penalise extra tiles + poor squareness equally
+ min_n = candidates[0][0]
+ best = None
+ best_score = float("inf")
+
+ for n, sq, c, r, g in candidates:
+ score = (n / min_n) + sq
+ if score < best_score:
+ best_score = score
+ g["squareness"] = sq
+ best = g
+
+ return best
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Node 1 – Tile Splitter
+# ─────────────────────────────────────────────────────────────────────────────
+
+class SeedVR2TileSplitter:
+ """
+ Splits a single image into a uniform NxM grid of overlapping tiles,
+ each within `tile_size_mp` megapixels.
+
+ The image is resized to an exact canvas before splitting so every tile
+ sent to SeedVR2 is the same size. No partial or oversized edge tiles.
+
+ Outputs
+ -------
+ tiles : IMAGE batch (N, tile_h, tile_w, C) — connect to SeedVR2
+ tile_metadata : TILE_METADATA — pass directly to Tile Stitcher
+ """
+
+ CATEGORY = "image/upscaling"
+ FUNCTION = "split"
+ RETURN_TYPES = ("IMAGE", "TILE_METADATA", "INT")
+ RETURN_NAMES = ("tiles", "tile_metadata", "resolution")
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "tile_size_mp": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1,
+ "display": "number",
+ "tooltip": (
+ "Maximum size of each tile in megapixels. "
+ "The grid is chosen so every tile is AT MOST this size. "
+ "Lower = less VRAM per SeedVR2 pass."
+ ),
+ },
+ ),
+ "tile_upscale_mp": (
+ "FLOAT",
+ {
+ "default": 2.0, "min": 0.1, "max": 64.0, "step": 0.1,
+ "display": "number",
+ "tooltip": (
+ "Desired output size of each upscaled tile in megapixels. "
+ "Used to compute the resolution hint for SeedVR2. "
+ "Typically 2x your tile_size_mp."
+ ),
+ },
+ ),
+ "overlap_percent": (
+ "FLOAT",
+ {
+ "default": 10.0, "min": 0.0, "max": 40.0, "step": 1.0,
+ "display": "number",
+ "tooltip": (
+ "Overlap between adjacent tiles as a % of tile size. "
+ "10% is a safe default. Increase to 15-20% if seams appear."
+ ),
+ },
+ ),
+ "feather_blend": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05,
+ "display": "number",
+ "tooltip": (
+ "Fraction of the overlap zone used for blending during stitch. "
+ "1.0 = smooth linear feather. 0.0 = hard cut."
+ ),
+ },
+ ),
+ }
+ }
+
+ def _split_core(self, image, tile_size_mp, overlap_percent, feather_blend, upscale_ratio=None, tile_upscale_mp=None):
+ """
+ Core split logic shared by all variant nodes.
+ Provide either upscale_ratio (preferred, uses actual tile dims) or tile_upscale_mp.
+ """
+ if image.ndim == 4:
+ if image.shape[0] != 1:
+ raise ValueError(
+ f"Tile Splitter expects a single image (batch=1), got {image.shape[0]}."
+ )
+ img = image[0]
+ else:
+ img = image
+
+ orig_H, orig_W, C = img.shape
+ overlap_fraction = overlap_percent / 100.0
+
+ grid = compute_tile_grid(orig_W, orig_H, tile_size_mp, overlap_fraction)
+ cols = grid["cols"]; rows = grid["rows"]
+ tile_w = grid["tile_w"]; tile_h = grid["tile_h"]
+ overlap_w= grid["overlap_w"]; overlap_h= grid["overlap_h"]
+ stride_w = grid["stride_w"]; stride_h = grid["stride_h"]
+ canvas_w = grid["canvas_w"]; canvas_h = grid["canvas_h"]
+
+ if canvas_w != orig_W or canvas_h != orig_H:
+ canvas_img = _resize(img, canvas_h, canvas_w)
+ else:
+ canvas_img = img
+
+ tiles: List[torch.Tensor] = []
+ positions: List[Tuple[int, int]] = []
+ for row in range(rows):
+ for col in range(cols):
+ x0 = col * stride_w
+ y0 = row * stride_h
+ tiles.append(canvas_img[y0 : y0 + tile_h, x0 : x0 + tile_w, :])
+ positions.append((x0, y0))
+
+ tiles_tensor = torch.stack(tiles, dim=0)
+
+ # Compute resolution hint from actual tile dimensions.
+ # If upscale_ratio provided, derive target tile dims directly — this is
+ # accurate regardless of tile count or tile_size_mp value.
+ # If tile_upscale_mp provided (legacy MP mode), use the old formula.
+ if upscale_ratio is not None:
+ target_tile_w = tile_w * upscale_ratio
+ target_tile_h = tile_h * upscale_ratio
+ resolution = _align(min(target_tile_w, target_tile_h), 8)
+ # Exact target output dimensions for stitcher to hit precisely
+ target_out_w = round(orig_W * upscale_ratio)
+ target_out_h = round(orig_H * upscale_ratio)
+ else:
+ tile_aspect = tile_w / tile_h
+ out_h_raw = math.sqrt(tile_upscale_mp * 1_000_000 / tile_aspect)
+ resolution = _align(min(out_h_raw * tile_aspect, out_h_raw), 8)
+ target_out_w = None
+ target_out_h = None
+
+ meta: Dict[str, Any] = {
+ "orig_w": orig_W, "orig_h": orig_H, "orig_c": C,
+ "target_out_w": target_out_w, "target_out_h": target_out_h,
+ "canvas_w": canvas_w, "canvas_h": canvas_h,
+ "cols": cols, "rows": rows, "n_tiles": cols * rows,
+ "tile_w": tile_w, "tile_h": tile_h,
+ "overlap_w": overlap_w, "overlap_h": overlap_h,
+ "stride_w": stride_w, "stride_h": stride_h,
+ "positions": positions,
+ "feather_blend": feather_blend,
+ "resolution": resolution,
+ }
+
+ print(
+ f"\n[SeedVR2 Tile Splitter]"
+ f"\n Original : {orig_W}x{orig_H}"
+ f"\n Canvas : {canvas_w}x{canvas_h}"
+ f"{' (resized)' if (canvas_w != orig_W or canvas_h != orig_H) else ' (exact)'}"
+ f"\n Grid : {cols}x{rows} = {cols*rows} tiles"
+ f"\n Tile : {tile_w}x{tile_h} = {tile_w*tile_h/1e6:.2f} MP"
+ f"\n Overlap : {overlap_w}x{overlap_h} px | Stride: {stride_w}x{stride_h} px"
+ f"\n SeedVR2 resolution hint: {resolution}"
+ )
+
+ return (tiles_tensor, meta, resolution)
+
+ def split(self, image, tile_size_mp, tile_upscale_mp, overlap_percent, feather_blend):
+ return self._split_core(image, tile_size_mp, overlap_percent, feather_blend,
+ tile_upscale_mp=tile_upscale_mp)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Node 2 – Tile Stitcher
+# ─────────────────────────────────────────────────────────────────────────────
+
+class SeedVR2TileStitcher:
+ """
+ Receives the upscaled tile batch (SeedVR2 output) + TILE_METADATA,
+ blends overlapping regions, and returns the final stitched image.
+
+ Actual upscale factor is detected at runtime, so the node works even if
+ SeedVR2 produced a different resolution than suggested.
+ """
+
+ CATEGORY = "image/upscaling"
+ FUNCTION = "stitch"
+ RETURN_TYPES = ("IMAGE",)
+ RETURN_NAMES = ("image",)
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "upscaled_tiles": ("IMAGE",),
+ "tile_metadata": ("TILE_METADATA",),
+ }
+ }
+
+ def stitch(self, upscaled_tiles: torch.Tensor, tile_metadata: dict):
+ meta = tile_metadata
+
+ orig_w = meta["orig_w"]; orig_h = meta["orig_h"]
+ canvas_w = meta["canvas_w"]; canvas_h = meta["canvas_h"]
+ cols = meta["cols"]; rows = meta["rows"]
+ n_tiles = meta["n_tiles"]
+ tile_w = meta["tile_w"]; tile_h = meta["tile_h"]
+ overlap_w= meta["overlap_w"]; overlap_h= meta["overlap_h"]
+ stride_w = meta["stride_w"]; stride_h = meta["stride_h"]
+ positions = meta["positions"]
+ feather_blend: float = meta["feather_blend"]
+
+ if upscaled_tiles.shape[0] != n_tiles:
+ raise ValueError(
+ f"Expected {n_tiles} upscaled tiles but received "
+ f"{upscaled_tiles.shape[0]}. Check batch order is unchanged."
+ )
+
+ up_h = upscaled_tiles.shape[1]
+ up_w = upscaled_tiles.shape[2]
+ C = upscaled_tiles.shape[3]
+
+ scale_x = up_w / tile_w
+ scale_y = up_h / tile_h
+
+ out_canvas_w = round(canvas_w * scale_x)
+ out_canvas_h = round(canvas_h * scale_y)
+
+ feather_px_x = max(1, round(overlap_w * scale_x * feather_blend))
+ feather_px_y = max(1, round(overlap_h * scale_y * feather_blend))
+
+ print(
+ f"\n[SeedVR2 Tile Stitcher]"
+ f"\n Scale : {scale_x:.3f}x x {scale_y:.3f}x"
+ f"\n Canvas : {out_canvas_w}x{out_canvas_h}"
+ f"\n Tiles : {n_tiles} ({cols}x{rows})"
+ f"\n Feather: {feather_px_x}x{feather_px_y} px"
+ )
+
+ device = upscaled_tiles.device
+ dtype = upscaled_tiles.dtype
+
+ canvas = torch.zeros(out_canvas_h, out_canvas_w, C, dtype=torch.float32, device=device)
+ weights = torch.zeros(out_canvas_h, out_canvas_w, 1, dtype=torch.float32, device=device)
+
+ for i, (x0_src, y0_src) in enumerate(positions):
+ tile = upscaled_tiles[i].float()
+ col = i % cols
+ row = i // cols
+
+ mask = torch.ones(up_h, up_w, dtype=torch.float32, device=device)
+
+ if col > 0 and feather_px_x > 0:
+ fe = min(feather_px_x, up_w)
+ mask[:, :fe] *= torch.linspace(0.0, 1.0, fe, device=device)
+ if col < cols - 1 and feather_px_x > 0:
+ fe = min(feather_px_x, up_w)
+ mask[:, up_w - fe:] *= torch.linspace(1.0, 0.0, fe, device=device)
+ if row > 0 and feather_px_y > 0:
+ fe = min(feather_px_y, up_h)
+ mask[:fe, :] *= torch.linspace(0.0, 1.0, fe, device=device).unsqueeze(1)
+ if row < rows - 1 and feather_px_y > 0:
+ fe = min(feather_px_y, up_h)
+ mask[up_h - fe:, :] *= torch.linspace(1.0, 0.0, fe, device=device).unsqueeze(1)
+
+ ox = round(x0_src * scale_x)
+ oy = round(y0_src * scale_y)
+
+ dx0 = max(0, ox); dy0 = max(0, oy)
+ dx1 = min(out_canvas_w, ox + up_w)
+ dy1 = min(out_canvas_h, oy + up_h)
+ tx0 = dx0 - ox; ty0 = dy0 - oy
+ tx1 = tx0 + (dx1 - dx0); ty1 = ty0 + (dy1 - dy0)
+
+ if tx1 <= tx0 or ty1 <= ty0:
+ continue
+
+ mask_crop = mask[ty0:ty1, tx0:tx1].unsqueeze(-1)
+ canvas [dy0:dy1, dx0:dx1, :] += tile[ty0:ty1, tx0:tx1, :] * mask_crop
+ weights[dy0:dy1, dx0:dx1, :] += mask_crop
+
+ result = (canvas / weights.clamp(min=1e-8)).clamp(0.0, 1.0).to(dtype)
+
+ # If the variant nodes stored an exact target size, use it directly.
+ # Otherwise fall back to aspect-ratio-preserving resize from orig dimensions.
+ target_out_w = meta.get("target_out_w")
+ target_out_h = meta.get("target_out_h")
+
+ if target_out_w and target_out_h:
+ final_w, final_h = target_out_w, target_out_h
+ else:
+ eff_scale = math.sqrt((out_canvas_w / orig_w) * (out_canvas_h / orig_h))
+ final_w = round(orig_w * eff_scale)
+ final_h = round(orig_h * eff_scale)
+
+ if final_w != out_canvas_w or final_h != out_canvas_h:
+ result = _resize(result, final_h, final_w)
+
+ print(f" Output : {final_w}x{final_h} ({final_w/orig_w:.3f}x {final_h/orig_h:.3f}x)")
+
+ return (result.unsqueeze(0),)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Node 3 – Tile Splitter (by Longest Edge)
+# ─────────────────────────────────────────────────────────────────────────────
+
+class SeedVR2TileSplitterByLongestEdge(SeedVR2TileSplitter):
+ """
+ Same as Tile Splitter but lets you specify the desired output size as a
+ maximum longest edge in pixels rather than megapixels.
+ tile_upscale_mp is computed automatically from the image aspect ratio.
+ """
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "tile_size_mp": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1,
+ "display": "number",
+ "tooltip": "Maximum size of each tile in megapixels. Lower = less VRAM per pass.",
+ },
+ ),
+ "longest_edge_px": (
+ "INT",
+ {
+ "default": 2048, "min": 64, "max": 16384, "step": 8,
+ "display": "number",
+ "tooltip": "Desired pixel length of the longest edge in the final stitched output.",
+ },
+ ),
+ "overlap_percent": (
+ "FLOAT",
+ {
+ "default": 10.0, "min": 0.0, "max": 40.0, "step": 1.0,
+ "display": "number",
+ "tooltip": "Overlap between adjacent tiles as a % of tile size.",
+ },
+ ),
+ "feather_blend": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05,
+ "display": "number",
+ "tooltip": "1.0 = smooth feather blend. 0.0 = hard cut.",
+ },
+ ),
+ }
+ }
+
+ def split(self, image, tile_size_mp, longest_edge_px, overlap_percent, feather_blend):
+ img = image[0] if image.ndim == 4 else image
+ orig_H, orig_W = img.shape[:2]
+ upscale_ratio = longest_edge_px / max(orig_W, orig_H)
+ return self._split_core(image, tile_size_mp, overlap_percent, feather_blend,
+ upscale_ratio=upscale_ratio)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Node 4 – Tile Splitter (by Shortest Edge)
+# ─────────────────────────────────────────────────────────────────────────────
+
+class SeedVR2TileSplitterByShortestEdge(SeedVR2TileSplitter):
+ """
+ Same as Tile Splitter but lets you specify the desired output size as a
+ minimum shortest edge in pixels rather than megapixels.
+ tile_upscale_mp is computed automatically from the image aspect ratio.
+ """
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "tile_size_mp": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1,
+ "display": "number",
+ "tooltip": "Maximum size of each tile in megapixels. Lower = less VRAM per pass.",
+ },
+ ),
+ "shortest_edge_px": (
+ "INT",
+ {
+ "default": 2048, "min": 64, "max": 16384, "step": 8,
+ "display": "number",
+ "tooltip": "Desired pixel length of the shortest edge in the final stitched output.",
+ },
+ ),
+ "overlap_percent": (
+ "FLOAT",
+ {
+ "default": 10.0, "min": 0.0, "max": 40.0, "step": 1.0,
+ "display": "number",
+ "tooltip": "Overlap between adjacent tiles as a % of tile size.",
+ },
+ ),
+ "feather_blend": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05,
+ "display": "number",
+ "tooltip": "1.0 = smooth feather blend. 0.0 = hard cut.",
+ },
+ ),
+ }
+ }
+
+ def split(self, image, tile_size_mp, shortest_edge_px, overlap_percent, feather_blend):
+ img = image[0] if image.ndim == 4 else image
+ orig_H, orig_W = img.shape[:2]
+ upscale_ratio = shortest_edge_px / min(orig_W, orig_H)
+ return self._split_core(image, tile_size_mp, overlap_percent, feather_blend,
+ upscale_ratio=upscale_ratio)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Node 5 – Tile Splitter (by Upscale Factor)
+# ─────────────────────────────────────────────────────────────────────────────
+
+class SeedVR2TileSplitterByFactor(SeedVR2TileSplitter):
+ """
+ Same as Tile Splitter but lets you specify the desired upscale as a
+ simple multiplier (e.g. 2.0 = double the image dimensions).
+ tile_upscale_mp is computed automatically.
+ """
+
+ @classmethod
+ def INPUT_TYPES(cls):
+ return {
+ "required": {
+ "image": ("IMAGE",),
+ "tile_size_mp": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.1, "max": 16.0, "step": 0.1,
+ "display": "number",
+ "tooltip": "Maximum size of each tile in megapixels. Lower = less VRAM per pass.",
+ },
+ ),
+ "upscale_factor": (
+ "FLOAT",
+ {
+ "default": 2.0, "min": 1.0, "max": 16.0, "step": 0.25,
+ "display": "number",
+ "tooltip": "Upscale multiplier. 2.0 = double the width and height of the final output.",
+ },
+ ),
+ "overlap_percent": (
+ "FLOAT",
+ {
+ "default": 10.0, "min": 0.0, "max": 40.0, "step": 1.0,
+ "display": "number",
+ "tooltip": "Overlap between adjacent tiles as a % of tile size.",
+ },
+ ),
+ "feather_blend": (
+ "FLOAT",
+ {
+ "default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05,
+ "display": "number",
+ "tooltip": "1.0 = smooth feather blend. 0.0 = hard cut.",
+ },
+ ),
+ }
+ }
+
+ def split(self, image, tile_size_mp, upscale_factor, overlap_percent, feather_blend):
+ return self._split_core(image, tile_size_mp, overlap_percent, feather_blend,
+ upscale_ratio=upscale_factor)
+
+
+# ─────────────────────────────────────────────────────────────────────────────
+# Registration
+# ─────────────────────────────────────────────────────────────────────────────
+
+NODE_CLASS_MAPPINGS = {
+ "SeedVR2TileSplitter": SeedVR2TileSplitter,
+ "SeedVR2TileStitcher": SeedVR2TileStitcher,
+ "SeedVR2TileSplitterByLongestEdge": SeedVR2TileSplitterByLongestEdge,
+ "SeedVR2TileSplitterByShortestEdge": SeedVR2TileSplitterByShortestEdge,
+ "SeedVR2TileSplitterByFactor": SeedVR2TileSplitterByFactor,
+}
+
+NODE_DISPLAY_NAME_MAPPINGS = {
+ "SeedVR2TileSplitter": "SeedVR2 Tile Splitter",
+ "SeedVR2TileStitcher": "SeedVR2 Tile Stitcher",
+ "SeedVR2TileSplitterByLongestEdge": "SeedVR2 Tile Splitter (Longest Edge)",
+ "SeedVR2TileSplitterByShortestEdge": "SeedVR2 Tile Splitter (Shortest Edge)",
+ "SeedVR2TileSplitterByFactor": "SeedVR2 Tile Splitter (Upscale Factor)",
+}