Author SHA1 Message Date
jetthuangai 3c053e09df Bump version 1.7.0 2026-10-18 12:10:29 +07:00
jetthuangai 5a617560a3 docs: SAM 3 and NH_SEGS nodes in the EN/VI guides
- vision guide (EN/VI): requirements and install, how the two passes work
  and their limits, the NH_SEGS type, the six nodes, three workflows and
  troubleshooting
- READMEs (75 nodes), CHANGELOG 1.7.0, THIRD_PARTY_NOTICES (SAM License,
  user-installed sam3 and checkpoint)
- six node cards, sam3-segment.jpg example from the public-domain astronaut
  photo; build_examples.py builds it with --gpu
2026-10-18 12:10:18 +07:00
jetthuangai 5389e93994 feat: SAM 3 segmentation and NH_SEGS instance nodes (Vision v2 P0)
Six new Vision nodes, 69 -> 75:
- SAM3 Segment: every instance of the concepts you name, with Meta's SAM 3.
  The whole image is encoded once for all concepts (pass 1); in balanced mode
  each instance is segmented again on a full-resolution close-up with the
  box-prompt decoder (pass 2), so small and medium objects keep crisp edges
  on 4K-8K photos. A close-up that does not show the pass-1 instance is
  dropped (tolerant check that keeps thin poles and cables).
- Segs Select, Segs Compose (label formulas: | - & ~ all image none),
  Segs To Masks, Segs To Impact SEGS, Segs Preview.

NH_SEGS keeps per-instance masks only inside each box, so dozens of
instances on an 8K image stay small; full-size masks are built only on
output (with a RAM guard in Segs To Masks).

Self-contained: no other custom node pack is used. Meta's sam3 package is
imported lazily with an API check (install hint pinned to the tested
commit), TF32 switches changed by its import are restored, and the gated
sam3.pt checkpoint in models/sam3 is checked before loading and never
downloaded. CUDA only. Weights and sam3's GPU-side tables go back to RAM
after each run unless keep_model_loaded.
2026-10-18 12:10:18 +07:00
jetthuangai f4843d30d3 docs: bilingual EN/VI guide, node cards, examples and new README
- README rewritten: banner, highlights with real outputs, install incl.
  ONNX Runtime and extra_model_paths keys, quick start, full catalog,
  licence summary, PDF guide links
- docs/en and docs/vi: guide home + 13 pages covering all 69 nodes (card,
  inputs/outputs from the live definitions, behaviour, recipes, tips)
- docs/assets: 69 node cards generated from INPUT_TYPES, 14 example images
  and a banner rendered from real node outputs on public-domain/CC0 photos
- docs/_tools: card, example, spec and PDF builders (reportlab) so the docs
  can be regenerated from the code
2026-10-18 09:17:48 +07:00
jetthuangai 8eb38a81bf fix: matching context base is the parent of the search roots 2026-10-18 09:12:33 +07:00
jetthuangai e9ff4c2390 fix: score matching context against one base per request
The base was the common folder of whatever happened to match, so an unrelated
folder elsewhere in the library could move it and flip the pick. It is now the
common folder of the request's search roots, passed explicitly to every
ranking. Adds tests for the unrelated-folder and twin-root cases.
2026-10-18 09:12:33 +07:00
jetthuangai 1687e14d08 fix: Load images matching ranks context below shared ancestor folders (1.6.2)
The context score compared absolute paths, so folders the source and the
library share (D:/Photos) counted as much as the Nu/Nam group and a source in
Source/Nu matched Library/Nam/<subject> by alphabetical tie-break. Candidates
are now scored on their path below the folder they all share.
2026-10-18 09:12:33 +07:00
jetthuangai e28b9c3810 Bump version 1.6.1 2026-10-18 08:37:20 +07:00
jetthuangai 5d2e6c5b9c refactor: split image tools, share helpers, harden routes (no node changes)
- nodes/image/image_tools_nodes.py (1776 lines) split into save_image_path,
  folder_loaders, layer_composite, image_label, image_compare and
  image_resize_unit; folder helpers in core/image_folders.py (set-based
  dedupe), image helpers in core/image_ops.py
- One tensor -> PIL converter (core.utils.tensor_to_pil, rgb=/keep_alpha=)
  that keeps the tensor's dtype; bf16 supported; duplicates removed from grid,
  large save and preview cache
- core/mask_ops.mask_bbox replaces five bbox copies; model helpers moved to
  core/model_files.py; Pillow fromarray(mode=) uses removed
- Routes moved to core/folder_routes.py: no absolute paths in responses,
  recursive honoured, proper status codes, scans off the event loop; large
  preview routes reject malformed JSON and hide paths
- web/nh_widget_utils.js shared by all extensions; widgets hidden via the
  frontend's `hidden` property when available; folder image count honours
  recursive
- A wired Load Images Folder path no longer fingerprints ComfyUI's input folder
- Node ids, inputs and outputs unchanged (69 nodes); new tests for routes,
  web helpers, mask ops, channel handling and Pillow usage
2026-10-18 08:37:20 +07:00
jetthuangai e8a0f8c005 Bump version 1.6.0 2026-10-18 07:31:58 +07:00
jetthuangai d128024dff perf: lazy switches, faster morphology, smarter caching and VTON VRAM release
- If/Else, Switch N and Random Choice use lazy inputs: only the chosen
  branch is executed (same choice as before; connected inputs count even
  when their upstream returns nothing)
- Mask Morphology rewritten on torch: soft masks kept, replicate borders,
  even kernels act as the next odd size; large kernels ~100x faster
- Layer Stack is cached like a pure node; Load Images Matching caches on a
  file signature, searches only inside its root, never walks drive/share
  roots or the models folder, and on a miss returns found=False plus the
  new optional lazy fallback_images (or blocks images)
- Indexed loaders reuse the model ComfyUI still holds via a weak reference
  (strong only with --cache-classic); VAEs keep file metadata
- VTON: keep_model_loaded (default off) releases ONNX Runtime GPU sessions
  after runs that really used CUDA; ComfyUI is asked to make room first
- SegFormer free_memory now targets ComfyUI's device (cuda:0) so models are
  actually unloaded, and adds extra_reserved_memory
- Model downloads pinned to tested Hugging Face revisions with
  allow_patterns; partial VTON folders are repaired
- nh_nodes (.safetensors) and vton_mask (.onnx) folder names honour
  extra_model_paths.yaml; existing installs keep working
- Large preview/compare viewer: batch navigation with buttons and arrow keys
- Real PromptExecutor integration tests (CPU) for lazy and blocking paths
2026-10-18 07:31:58 +07:00
jetthuangai e21e2ee7ef Bump version 1.5.0 2026-10-18 04:40:24 +07:00
jetthuangai 71e42b303b fix: correctness fixes across image, resolution, logic, text and mask nodes
Image
- VTON, Face Paste and Agnostic Image process the whole batch (batch-1
  inputs broadcast); VTON converts input to RGB and names the batch index
  when no person is detected
- Values are clipped before 8-bit conversion (no wrap-around to black)
- Mask-Aware Resize honours the mode with an empty mask and clips Lanczos
  overshoot; Agnostic Image resizes/accepts 2-D masks, keeps soft masks and
  uses deterministic noise
- Layer Layout/Stack keep RGBA layer alpha; pad colours accept #RRGGBBAA
  (opaque by default instead of copying the blue value into alpha)
- Save Image Path: prefix/new folder cannot escape the chosen folder
  (Windows colons refused); position expressions use the shared safe
  evaluator (no more 9**9**9 freeze, no ast.Num)
- Save Large Image checks WEBP/JPEG limits before writing and respects
  --disable-metadata; Image Label caches fonts; Grid guards on free memory;
  Cache Manager re-runs every queue

Resolution
- Remove Resize Padding no longer stretches cover_crop results: with
  original_size the image is scaled back uniformly into the original frame
- Lanczos/box/hamming keep float precision, clipped between passes
- Picker reports stale presets/model names in info

Logic, text, lists, masks
- New core/safe_eval.py: float-only arithmetic, whitelist, no runaway ints;
  Math Eval errors name the expression, x/0 -> 0 with a warning
- Random Choice weights follow slots; literal seeds in four seeded nodes
- Prompt Template no longer glues words around empty variables; Regex
  Extract handles unmatched groups; Pack Universal packs by slot; Compare
  'in' is a plain membership test; Batch Merge aligns RGB/RGBA and never
  rounds a side to 0
- Mask Aspect Ratio Match placement fixed and batched; VTON garment boxes
  ignore undetected keypoints and fall back to the garment contour
- Universal Slider Builder marked DEPRECATED

Adds docs/CHANGELOG.md and ~460 regression tests.
2026-10-18 04:40:24 +07:00
201 changed files with 29229 additions and 2754 deletions
+124 -319
View File
@@ -1,354 +1,159 @@
<p align="center"><img src="docs/assets/banner.jpg" alt="NH-Nodes — image, mask, resolution and workflow nodes for ComfyUI" width="100%"></p>
<p align="center">
<a href="https://registry.comfy.org/nodes/nh-nodes"><img src="https://img.shields.io/badge/ComfyUI_Registry-nh--nodes-2563eb" alt="ComfyUI Registry"></a>
<a href="https://github.com/jetthuangai/NH-Nodes/actions/workflows/tests.yml"><img src="https://github.com/jetthuangai/NH-Nodes/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT_(see_notices)-16a34a" alt="License"></a>
<a href="docs/en/README.md"><img src="https://img.shields.io/badge/Docs-EN_%7C_VI-0f766e" alt="Documentation"></a>
</p>
<p align="center"><b>English</b> · <a href="docs/vi/README.md">Tiếng Việt</a></p>
# NH-Nodes for ComfyUI
Production-focused custom nodes for ComfyUI workflows: image loading, tiling,
compositing, mask utilities, smart resize, logic routing, prompt/text tooling,
batch helpers, indexed model loaders, garment/portrait segmentation, and VTON
preprocessing.
**75 production-focused nodes** for ComfyUI: load and save image sets with their metadata, tile and composite large
images, pick the right resolution for current models and put results back into the original frame, refine masks,
segment garments and faces, find every person, window or sky by name with SAM 3, build VTON masks, route workflows
with lazy switches, generate prompts, and test many LoRAs or models from one node.
[![ComfyUI Registry](https://img.shields.io/badge/ComfyUI_Registry-nh--nodes-2563eb)](https://registry.comfy.org/nodes/nh-nodes)
[![GitHub](https://img.shields.io/badge/GitHub-jetthuangai%2FNH--Nodes-111827)](https://github.com/jetthuangai/NH-Nodes)
[![License: MIT](https://img.shields.io/badge/License-MIT-16a34a.svg)](LICENSE)
Every node is documented with a visual card, its inputs and outputs, and examples — in
[English](docs/en/README.md) and [Vietnamese](docs/vi/README.md).
- **Author:** [jetthuang.com](https://jetthuang.com)
- **Hugging Face:** [nhathoangfoto](https://huggingface.co/nhathoangfoto)
- **Support:** [PayPal](https://paypal.me/nhathoangfoto)
## Highlights
---
## Why NH-Nodes
NH-Nodes is built for practical workflow construction. It focuses on reusable
building blocks that remove repetitive graph work:
- Load images with filename/path metadata.
- Split images into tiles, upscale or process tiles, then stitch them back.
- Build image grids, layer composites, comparisons, and labeled previews.
- Resize images and masks with predictable aspect-ratio behavior.
- Route workflow branches with boolean logic and generic data switches.
- Generate prompts from templates, regex, lists, schedules, and text utilities.
- Pick indexed LoRA/diffusion models for larger configurable workflows.
- Prepare VTON masks, agnostic images, DensePose/parsing outputs, and related masks.
---
<table>
<tr>
<td width="50%"><a href="docs/en/resolution.md"><img src="docs/assets/examples/resize-modes.jpg" alt="Resize modes"></a><br>
<b>Resolution done right</b> — presets for FLUX.2, Qwen-Image and Z-Image, cover / contain / stretch resizing, and an
exact round trip back to the original frame.</td>
<td width="50%"><a href="docs/en/image-compose.md"><img src="docs/assets/examples/tile-untile.jpg" alt="Tile and untile"></a><br>
<b>Large images</b> — split into overlapping tiles, process them, and blend them back seamlessly; deep-zoom preview
for images too big for the canvas.</td>
</tr>
<tr>
<td><a href="docs/en/vision.md"><img src="docs/assets/examples/garment-segment.jpg" alt="Garment segmentation"></a><br>
<b>Segmentation</b> — name what you want (people, windows, the sky) and SAM 3 returns every instance as its own
mask, with crisp edges on small and medium objects even in 4K–8K photos; garment and face-part masks from
SegFormer models; VTON garment masks with pose and parsing.</td>
<td><a href="docs/en/mask.md"><img src="docs/assets/examples/mask-morphology.jpg" alt="Mask morphology"></a><br>
<b>Mask tools</b> — fast grow / shrink on the CPU or the GPU, hole filling, soft edges, bounding boxes and
aspect-ratio matching.</td>
</tr>
<tr>
<td><a href="docs/en/image-compose.md"><img src="docs/assets/examples/layer-layout.jpg" alt="Layer layout"></a><br>
<b>Compositing</b> — layouts, layer stacks with alpha, labelled grids and side-by-side comparisons.</td>
<td><a href="docs/en/logic.md"><img src="docs/assets/examples/grid-composite.jpg" alt="Grid composite"></a><br>
<b>Workflow logic</b> — lazy If/Else and switches that run only the chosen branch, safe math, counters, lists and
prompt tools.</td>
</tr>
</table>
## Installation
### ComfyUI Manager
**ComfyUI Manager** — search for `NH-Nodes`, install, restart ComfyUI.
Search for `NH-Nodes`, install, then restart ComfyUI.
### Comfy CLI
**Comfy CLI**
```bash
comfy node install nh-nodes
```
### Manual
**Manual**
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/NH-Nodes.git
cd NH-Nodes
pip install -r requirements.txt
python -m pip install -r requirements.txt
```
Restart ComfyUI after installation.
Restart ComfyUI afterwards. Requirements: a recent ComfyUI (tested with 0.41) and Python 3.10+. **SAM3 Segment** has
more requirements, listed below.
---
### Optional: VTON and segmentation models
## Featured Workflows
- **VTON Ultimate Processor** needs ONNX Runtime, which is not installed automatically (the CPU wheel would overwrite an
existing GPU build). Install one of them into ComfyUI's Python:
`python -m pip install onnxruntime-gpu` (NVIDIA) or `python -m pip install onnxruntime` (CPU).
- **Garment Segment**, **Portrait Segment** and **VTON** download their models from Hugging Face on first use (pinned
revisions, only the needed files). To keep models elsewhere, add these keys to `extra_model_paths.yaml`:
### Tile, Process, Untile
```yaml
my_models:
base_path: D:/ai-models/
nh_nodes: NH-Nodes/ # SegFormer models (garment, face parsing)
vton_mask: ComfyUI-Vton-Mask/ # VTON pose + parsing ONNX models
sam3: sam3/ # SAM 3 checkpoint (you download sam3.pt yourself)
```
- **Requirements for SAM 3** (SAM3 Segment only): an NVIDIA GPU with CUDA, Meta's `sam3` package and the `sam3.pt`
checkpoint. NH-Nodes installs and downloads none of them: you install the package yourself and request the weights
from Meta. The steps, and what to do if another SAM 3 pack is already installed, are in
[Before you start with SAM 3](docs/en/vision.md#before-you-start-with-sam-3).
Use this when an image is too large for a model, or when every tile needs the
same enhancement/upscale step.
## Quick start
```text
Load Image -> Image Tile (NH) -> upscale/process tiles -> Image Untile (NH)
```
| Goal | Chain | Guide |
|---|---|---|
| Upscale or refine a huge image piece by piece | Load Image → **Image Tile** → your sampler/upscaler per tile → **Image Untile** | [image-compose](docs/en/image-compose.md#image-tile-nh) |
| Generate at the model's native size, then restore the original framing | Load Image → **NH Smart Ratio Image Resize** → edit → **NH Restore Image Geometry** | [resolution](docs/en/resolution.md) |
| Inpaint only the clothes | Load Image → **Garment Segment** → **Mask Morphology** (grow) → inpaint | [vision](docs/en/vision.md) |
| Mask every person, window or the sky in a large photo | Load Image → **SAM3 Segment** → **Segs Select** or **Segs Compose** → mask → edit or inpaint | [vision](docs/en/vision.md#sam-3-workflows) |
| Compare many LoRAs, one per queue | **Counter** → **Load LoRA Model Index** (index) → sampler → **Image Label** (prints the LoRA name) → Save Image | [loaders](docs/en/loaders.md) |
| Run one of two branches without executing the other | condition → **If/Else** (lazy) → … | [logic](docs/en/logic.md#ifelse-nh) |
`Image Tile (NH)` supports:
## Node catalog
- `original_ratio`: derive tile dimensions from the original image aspect ratio.
- `custom`: use explicit tile width and height.
- `square`: force square tiles, for example `1024 x 1024`.
- Automatic overlap calculation.
- Edge padding so square/custom tile outputs keep the requested tile size.
| Group | Nodes | Guide |
|---|---|---|
| Image — load & save | Load Image Info, Load Images Folder, Load images matching, Save Image Path, NH Save Large Image | [EN](docs/en/image-io.md) · [VI](docs/vi/image-io.md) |
| Image — compose | Image Tile, Image Untile, Image Grid Composite, Layer Layout Composite, Layer Stack Composite, Image Compare, Image Label, Image Resize Unit | [EN](docs/en/image-compose.md) · [VI](docs/vi/image-compose.md) |
| Image — large preview | NH Large Image Preview, NH Large Image Compare, NH Large Preview Cache Manager | [EN](docs/en/image-preview.md) · [VI](docs/vi/image-preview.md) |
| Image — mask-based retouch | NH Mask-Aware Resize, NH Agnostic Image Generator, NH Simple Face Paste | [EN](docs/en/image-retouch.md) · [VI](docs/vi/image-retouch.md) |
| Resolution | NH Smart Resolution Picker, NH Smart Ratio Image Resize, NH Ratio Preset Image Resize, NH Remove Resize Padding, NH Restore Image Geometry | [EN](docs/en/resolution.md) · [VI](docs/vi/resolution.md) |
| Mask | Mask Morphology, Mask Properties, NH Create Box Mask, NH Mask Aspect Ratio Match | [EN](docs/en/mask.md) · [VI](docs/vi/mask.md) |
| Vision | Garment Segment, Portrait Segment, SAM3 Segment, Segs Select, Segs Compose, Segs To Masks, Segs To Impact SEGS, Segs Preview | [EN](docs/en/vision.md) · [VI](docs/vi/vision.md) |
| VTON | VTON Ultimate Processor | [EN](docs/en/vton.md) · [VI](docs/vi/vton.md) |
| Logic | Compare, Logic Gate, If/Else, Switch N, Any Switch, Any Branch Switch, Gate Switch, Value Match Index, Math Eval, Random Choice | [EN](docs/en/logic.md) · [VI](docs/vi/logic.md) |
| Text | Prompt Template, Prompt Scheduler, String Operations, Prompt Join, Text Split, Regex Extract, Text Index, Text Concatenate, Text Split Lines, Text Random Line | [EN](docs/en/text.md) · [VI](docs/vi/text.md) |
| Batch & lists | Batch Index, Batch Merge, Counter, List Create, List Index, List Filter, Any List Split | [EN](docs/en/batch-lists.md) · [VI](docs/vi/batch-lists.md) |
| Loaders | Load LoRA Model Index, Load LoRA Clip Index, Load Diffusion Model Index, Load VAE Index, Load Clip Index | [EN](docs/en/loaders.md) · [VI](docs/vi/loaders.md) |
| Utils | Multi-Slider (FLOAT/INT), Boolean Switch, Pack Universal, Unpack Universal, Universal Slider Builder *(deprecated)* | [EN](docs/en/utils.md) · [VI](docs/vi/utils.md) |
`Image Untile (NH)` uses `tile_data` from the tile node to stitch results back.
It also handles tiles that were resized or upscaled, such as 2x or 3x per tile.
All nodes live under **NH-Nodes** in the node menu; search by name or by node id (for example `NH_ImageTile`).
### Grid Contact Sheet
## Documentation
```text
Image batch -> Image Grid Composite (NH) -> Preview Image
```
Use manual cell size or first-image cell size. Resize modes include `stretch`,
`pad`, and `fill`.
### Metadata-Aware Image Load
```text
Load Image Info (NH) -> image / mask / file_name / path
```
Useful for naming outputs, logging source images, or routing by filename.
### Smart Resolution Resize
```text
Load Image -> NH Smart Ratio Image Resize -> (your pipeline) -> NH Remove Resize Padding
```
`NH Smart Ratio Image Resize` picks the closest model preset itself from
`model_family` + `resolution_level`, so it does not need `NH Smart Resolution
Picker` upstream. Resize with cover crop, contain pad, or stretch behavior.
In `contain_pad` mode it also emits `resize_data` describing the content region,
padding, and scale factor. Feed that into `NH Remove Resize Padding` at the end
of the pipeline to crop the padded border back off (`content_region`) or to
restore the original input size (`original_size`).
For `cover_crop`, use `NH Restore Image Geometry` after processing. Connect the
processed image to `image`, the source image to `original_image`, and the resize
node's `resize_data` to the matching input. It returns an image at the exact
original dimensions, with processed content placed back in its original region
and cropped-away borders retained from the source. Its `mask` is white in the
restored region and black in the retained borders; use it for masked blending
with the original, optionally softening the mask edges first.
Processed images may be upscaled, including by a non-integer factor, provided
their framing is unchanged. Additional crops, rotations, or model-generated
changes in geometry cannot be reversed by this metadata. The restore node also
accepts `contain_pad` and `stretch` metadata and supports matching batches or
singleton batch broadcasting. Restoration uses bicubic (default), bilinear, or
nearest sampling; it does not recover details lost during downsampling.
Use `NH Smart Resolution Picker` when you need target dimensions and a latent
for text-to-image, instead of resizing an existing image.
### Garment and Portrait Segmentation
```text
Load Image -> Garment Segment (NH) [part=upper_garment] -> mask / cutout_rgba / mask_image
Load Image -> Portrait Segment (NH) [hair + face] -> mask / cutout_rgba / mask_image
```
SegFormer-based masks at input resolution. `cutout_rgba` is the input image
with the mask as alpha (RGB is left untouched outside the mask); `mask_image`
is the mask as an RGB preview. Dresses and jumpsuits belong to `upper_garment`. Both nodes share `mask_expand`, `mask_blur`, and `fill_holes`.
Choose `part=all_garments` to combine upper/lower garments, footwear and headwear
in one mask, for example a shirt and pants together. `attach_parts` still controls
whether touching pockets, zippers and other garment decorations are included.
Models are auto-downloaded to `models/NH-Nodes/` on first use (existing
`models/face_parsing` or `models/RMBG/segformer_fashion` copies are reused).
---
## Node Catalog
### Image Loading and Saving
| Node | Purpose |
|---|---|
| `Load Image Info (NH)` | Load one image and output `IMAGE`, `MASK`, file name, and path. |
| `Save Image Path (NH)` | Save image batches to a chosen folder and output saved filenames/path/count. |
| `NH Save Large Image` | Save full-resolution images without attaching heavy full-res previews to the canvas. |
| `Load Images Folder (NH)` | Load images from a folder by index/step, with filename and path outputs. |
| `Load images matching` | Load source/matching images by text, folder context, and matching rules. |
### Image Tiling, Grid, and Composition
| Node | Purpose |
|---|---|
| `Image Tile (NH)` | Split image batches into overlapping tiles with metadata for reconstruction. |
| `Image Untile (NH)` | Rebuild full images from processed/upscaled tiles and tile metadata. |
| `Image Grid Composite (NH)` | Combine an image batch into a configurable grid. |
| `NH Large Image Preview` | Preview large images with lightweight canvas thumbnails and passthrough output. |
| `NH Large Image Compare` | Compare large A/B images with lightweight canvas proxy and tiled slider/side/difference viewer. |
| `NH Large Preview Cache Manager` | Inspect and clean the NH large preview tile cache by age, size, or cache id. |
| `Layer Layout Composite (NH)` | Place layers into preset, grid, or custom layout slots. |
| `Layer Stack Composite (NH)` | Paste multiple layers onto a background using explicit coordinates. |
| `Image Resize Unit (NH)` | Resize by pixel/mm/cm using stretch, pad, crop, or lock-ratio modes. |
| `Image Compare (NH)` | Compare two images side-by-side, top/bottom, split, difference, or overlay. |
| `Image Label (NH)` | Add header/bottom labels with font, padding, color, and outline controls. |
| `NH Agnostic Image Generator` | Create agnostic/masked/composite images for inpaint and VTON workflows. |
| `NH Simple Face Paste` | Paste a source face region into a target using mask feathering. |
### Smart Resolution
| Node | Purpose |
|---|---|
| `NH Smart Resolution Picker` | Pick model/preset-aware dimensions and latent setup. |
| `NH Smart Ratio Image Resize` | Auto-pick the closest model preset for the source aspect, then resize with cover, contain, or stretch; outputs `resize_data` for padding removal. |
| `NH Ratio Preset Image Resize` | Resize to a chosen model preset ratio with crop, pad, or fill behavior. |
| `NH Remove Resize Padding` | Remove the padded border from processed images using resize metadata. |
| `NH Restore Image Geometry` | Restore processed or upscaled images to original coordinates with a blend mask; keep source pixels outside the crop. |
### Vision Segmentation
| Node | Purpose |
|---|---|
| `Garment Segment (NH)` | Mask `upper_garment` (tops, jackets, coats, dresses, ties, scarves, collars, sleeves), `lower_garment` (pants, shorts, skirts, belts, tights; no shoes), `footwear`, `headwear`, or combine all four with `all_garments`. Attached parts (pockets, zippers, bows...) merge only when touching the selected garments. |
| `Portrait Segment (NH)` | Checklist mask of face, neck, hair, eyes, lips, nose, eyebrows, ears (+ optional mouth interior). `face` is the solid face region including features. |
### Mask Tools
| Node | Purpose |
|---|---|
| `Mask Morphology (NH)` | Expand/shrink masks, fill holes, and blur edges. |
| `Mask Properties (NH)` | Output mask bounding box, dimensions, and related measurements. |
| `NH Create Box Mask` | Convert a mask area into a rectangular box mask. |
| `NH Mask Aspect Ratio Match` | Adjust one mask to match another mask's aspect ratio. |
| `NH Mask-Aware Resize` | Resize an image/mask pair while preserving the masked subject region. |
### Logic and Flow Control
| Node | Purpose |
|---|---|
| `Compare (NH)` | Compare values with typed operators. |
| `Logic Gate (NH)` | AND, OR, NOT, XOR, NAND, and NOR boolean logic. |
| `If/Else (NH)` | Select between two inputs by condition. |
| `Switch N (NH)` | Pick one of several generic inputs by index. |
| `Any Switch (NH)` | Boolean switch helper. |
| `Any Branch Switch (NH)` | Route generic data through selected branches. |
| `Gate Switch (NH)` | Pass or block a generic input by boolean state. |
| `Value Match Index (NH)` | Return index of matching configured value. |
| `Math Eval (NH)` | Safe math expression evaluator with numeric/string outputs. |
| `Random Choice (NH)` | Weighted random choice from generic inputs. |
### Text and Prompt Tools
| Node | Purpose |
|---|---|
| `String Operations (NH)` | Strip, case convert, replace, slice, inspect, and test strings. |
| `Prompt Join (NH)` | Join text fragments with separators and empty-line handling. |
| `Text Split (NH)` | Split text by delimiter and expose count/first/last values. |
| `Regex Extract (NH)` | Extract, replace, split, and match with regex patterns. |
| `Text Index (NH)` | Select a line/item from text by index. |
| `Text Concatenate (NH)` | Concatenate multiple text inputs. |
| `Text Split Lines (NH)` | Split multiline text into selected parts and metadata. |
| `Text Random Line (NH)` | Pick a random line from multiline text. |
| `Prompt Template (NH)` | Fill `{placeholder}` variables from connected text values. |
| `Prompt Scheduler (NH)` | Select prompts by step, sequence, ping-pong, or seed. |
### Lists and Batch Helpers
| Node | Purpose |
|---|---|
| `List Create (NH)` | Build an `NH_LIST` from multiline or delimited text. |
| `List Index (NH)` | Select an item from an `NH_LIST`. |
| `List Filter (NH)` | Filter lists with contains, regex, equals, length, and related conditions. |
| `Batch Index (NH)` | Extract one or more images from an image batch. |
| `Batch Merge (NH)` | Merge image batches with resize handling. |
| `Counter (NH)` | Stateful counter for repeated queue runs. |
| `Any List Split (NH)` | Split any incoming list into an `All` output plus up to 10 indexed outputs. |
### Indexed Loaders
| Node | Purpose |
|---|---|
| `Load LoRA Model Index (NH)` | Apply one selected LoRA to `MODEL` by 1-based index. |
| `Load LoRA Clip Index (NH)` | Apply one selected LoRA to `CLIP` by 1-based index. |
| `Load Diffusion Model Index (NH)` | Load one diffusion model by 1-based index. |
| `Load VAE Index (NH)` | Load one VAE by 1-based index or boolean toggle. |
| `Load Clip Index (NH)` | Load one CLIP by 1-based index or boolean toggle, with CLIP type selection. |
### Workflow Utilities
| Node | Purpose |
|---|---|
| `Multi-Slider (FLOAT)` | Five float controls in one node. |
| `Multi-Slider (INT)` | Five integer controls in one node. |
| `Universal Slider Builder (NH)` | Generate dynamic slider outputs from text config. |
| `Boolean Switch (NH)` | Simple boolean toggle. |
| `Pack Universal (NH)` | Bundle generic values into an `NH_UNIVERSAL_PIPE`. |
| `Unpack Universal (NH)` | Extract values from an `NH_UNIVERSAL_PIPE`. |
### VTON Preprocessing
| Node | Purpose |
|---|---|
| `VTON Ultimate Processor (NH)` | Generate masks, agnostic image data, DensePose/parsing outputs, hair/hands masks, and related VTON preprocessing assets. |
---
## Menu Structure
```text
NH-Nodes/
Image/
Load/save helpers
Tile/untile
Grid and layer composition
Resize, compare, label
Mask/
Morphology, properties, bbox, aspect matching
Vision/
Garment and portrait segmentation
Logic/
Compare, gates, switches, branching, math, random choice
Text/
String, regex, split, concatenate, templates, scheduling
Batch/
Image batch, list helpers, any-list split
Resolution/
Smart resolution picker, ratio resize, preset resize, padding removal
Loaders/
Indexed LoRA, diffusion model, VAE, and CLIP loaders
Utils/
Sliders, booleans, universal pipe
VTON/
VTON Ultimate Processor
```
---
## Requirements
```text
numpy
Pillow
scipy
opencv-python
scikit-image>=0.18.0
transformers>=4.20.0
tqdm>=4.62.0
huggingface_hub>=0.16.0
```
`VTON Ultimate Processor (NH)` also needs ONNX Runtime, which is not installed
automatically (the CPU wheel would overwrite an existing GPU build). Install one:
`python -m pip install onnxruntime-gpu` (NVIDIA) or `python -m pip install onnxruntime` (CPU).
Most image/text/logic nodes use common ComfyUI dependencies. VTON and Vision
nodes download model assets on first use (`transformers` SegFormer weights for
Vision: ~190 MB fashion, ~340 MB face).
---
## Development Notes
- Layout: `core/` holds shared helpers and model backends (no nodes); `nodes/<category>/`
mirrors the ComfyUI menu, and every module there that defines `NODE_CLASS_MAPPINGS`
is registered automatically by `__init__.py` - drop a file in the right folder
and restart. VTON's vendored preprocessing lives in `nodes/vton/preprocess/`.
- Tests: `python -m pytest custom_nodes/NH-Nodes/tests -q` from the ComfyUI root.
- Restart ComfyUI after installing or updating nodes.
- If a node appears cached in the browser, refresh the ComfyUI page after restart.
- `Image Tile (NH)` and `Image Untile (NH)` must pass `tile_data` directly through
the workflow so reconstruction uses the original tile layout.
---
- **Guide:** [English](docs/en/README.md) · [Tiếng Việt](docs/vi/README.md)
- **PDF guides:** [English](https://github.com/jetthuangai/NH-Nodes/releases/latest/download/NH-Nodes-Guide-EN.pdf) ·
[Tiếng Việt](https://github.com/jetthuangai/NH-Nodes/releases/latest/download/NH-Nodes-Huong-dan-VI.pdf)
- **Changelog:** [docs/CHANGELOG.md](docs/CHANGELOG.md) — read it when updating: some fixes change outputs on purpose.
## License
NH-Nodes' own code is [MIT](LICENSE). `VTON Ultimate Processor (NH)` and
`Portrait Segment (NH)` depend on third-party code or models that are licensed for
**non-commercial use only**, and the model behind `Garment Segment (NH)` declares no
license. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) before using these
three nodes commercially.
NH-Nodes' own code is [MIT](LICENSE). Some features rely on third-party code or models with other terms — see
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md):
If NH-Nodes helps your workflow, consider starring the repository:
[github.com/jetthuangai/NH-Nodes](https://github.com/jetthuangai/NH-Nodes)
- **VTON Ultimate Processor** and **Portrait Segment** are for **non-commercial use only** (CC BY-NC-SA 4.0 code and
parsing models; non-commercial face-parsing model).
- **Garment Segment**'s model does not declare a licence.
- **SAM3 Segment** uses Meta's SAM 3 under the SAM License: royalty-free, with no non-commercial clause, but it bars
military or warfare, nuclear, espionage, and gun or illegal-weapon uses. The weights and the `sam3` package are not
included; you get them from Meta.
## Development
- Layout: `core/` holds shared helpers and model backends (no nodes); `nodes/<category>/` mirrors the menu, and every
module there that defines `NODE_CLASS_MAPPINGS` is registered automatically; `web/` holds the frontend extensions.
- Tests and lint (run from the ComfyUI root / the pack folder):
```bash
python -m pytest custom_nodes/NH-Nodes/tests -q
ruff check .
```
- Docs assets are generated from the live code: `docs/_tools/build_node_cards.py` (node cards) and
`docs/_tools/build_examples.py` (example images), both run from the ComfyUI root.
## Author
[jetthuang.com](https://jetthuang.com) · [Hugging Face](https://huggingface.co/nhathoangfoto) ·
[Support via PayPal](https://paypal.me/nhathoangfoto). If NH-Nodes helps your work, a ⭐ on GitHub is appreciated.
+38 -5
View File
@@ -16,6 +16,7 @@ MIT license.
| `VTON Ultimate Processor (NH)` | **No** | Its code and parsing models are CC BY-NC-SA 4.0 (FitDiT / OOTDiffusion) |
| `Portrait Segment (NH)` | **No** | The face-parsing model card restricts use to non-commercial research and education |
| `Garment Segment (NH)` | Unclear | The model uploader does not declare a license |
| `SAM3 Segment (NH)` | Yes, within trade-control limits | Meta's SAM License is royalty-free and has no non-commercial clause, but it forbids ITAR-regulated activities and uses that trade controls prohibit (military or warfare, nuclear, espionage, guns and illegal weapons). The model and the `sam3` package are not part of NH-Nodes: you get them from Meta |
## Source code bundled in this repository
@@ -44,17 +45,49 @@ example, ONNX Runtime sessions in the vendored code are created through
## Models downloaded at runtime
These files are **not** stored in this repository. The nodes download them from
Hugging Face on first use.
Hugging Face on first use, pinned to the revisions below (only the files the nodes need).
They are looked up first in the folders registered as `vton_mask` and `nh_nodes`
(configurable in `extra_model_paths.yaml`).
| Node | Hugging Face repository | Contents | Terms |
|------|-------------------------|----------|-------|
| VTON Ultimate Processor | [`kg-09/kg-vton-mask`](https://huggingface.co/kg-09/kg-vton-mask) | `dwpose/yolox_l.onnx`, `dwpose/dw-ll_ucoco_384.onnx` | DWPose / YOLOX weights, Apache-2.0 upstream |
| VTON Ultimate Processor | [`kg-09/kg-vton-mask`](https://huggingface.co/kg-09/kg-vton-mask) @ `4648778` | `dwpose/yolox_l.onnx`, `dwpose/dw-ll_ucoco_384.onnx` | DWPose / YOLOX weights, Apache-2.0 upstream |
| VTON Ultimate Processor | same | `humanparsing/parsing_atr.onnx`, `humanparsing/parsing_lip.onnx` | From the OOTDiffusion/FitDiT checkpoints (CC BY-NC-SA 4.0); trained on the ATR and LIP datasets, which are distributed for research use |
| Portrait Segment | [`jonathandinu/face-parsing`](https://huggingface.co/jonathandinu/face-parsing) | SegFormer fine-tuned on CelebAMask-HQ | Model card: "non-commercial research and educational purposes" |
| Garment Segment | [`1038lab/segformer_fashion`](https://huggingface.co/1038lab/segformer_fashion) | SegFormer trained for Fashionpedia classes | No license declared by the uploader (Fashionpedia annotations are CC BY 4.0) |
| Portrait Segment | [`jonathandinu/face-parsing`](https://huggingface.co/jonathandinu/face-parsing) @ `758b82e` | SegFormer fine-tuned on CelebAMask-HQ | Model card: "non-commercial research and educational purposes" |
| Garment Segment | [`1038lab/segformer_fashion`](https://huggingface.co/1038lab/segformer_fashion) @ `e5bae2d` | SegFormer trained for Fashionpedia classes | No license declared by the uploader (Fashionpedia annotations are CC BY 4.0) |
## Software and models you install yourself
`SAM3 Segment (NH)` runs Meta's SAM 3. NH-Nodes neither bundles nor downloads it, and the
nodes only load it when you use them. You get both parts from Meta and accept Meta's terms
yourself.
| What | Where it comes from | Terms |
|------|---------------------|-------|
| `sam3` package (code) | [`facebookresearch/sam3`](https://github.com/facebookresearch/sam3), installed by the user with pip | SAM License |
| SAM 3 checkpoint `sam3.pt` | [`facebook/sam3`](https://huggingface.co/facebook/sam3) on Hugging Face, a gated repository: access is granted after the user accepts the license. It goes in the `sam3` model folder (`models/sam3`, or any location added under `sam3` in `extra_model_paths.yaml`), which other SAM 3 packs share | SAM License |
The [SAM License](https://github.com/facebookresearch/sam3/blob/main/LICENSE) (Meta, last
updated 19 November 2025) is a non-exclusive, worldwide, non-transferable, royalty-free
limited license to use, reproduce, distribute, copy, modify and create derivative works of
the SAM Materials. It does not mention commercial use. What matters to a user of NH-Nodes:
- **Trade controls.** You must not be a target of trade controls and must comply with them.
The materials may not be used for activities subject to the International Traffic in Arms
Regulations (ITAR), or for end uses that trade controls prohibit, including military or
warfare purposes, nuclear industries or applications, espionage, and the development or
use of guns or illegal weapons.
- Whoever redistributes the SAM Materials must include a copy of the license. NH-Nodes does
not redistribute them.
- Research publications that use the SAM Materials must acknowledge it.
- The materials may not be used to reverse engineer or decompile them, nor to encourage
others to.
- They are provided "as is", and Meta may terminate the license if you breach it.
## Python dependencies
Dependencies listed in `requirements.txt` are installed from PyPI and are not
redistributed here; each keeps its own license. ONNX Runtime (MIT) is optional
and only needed by the VTON node.
and only needed by the VTON node. The `sam3` package (SAM License) and its extras
(`timm`, `ftfy`, `regex`, `iopath`) are optional too: only `SAM3 Segment (NH)` needs
them, and you install them yourself.
+3 -45
View File
@@ -45,58 +45,16 @@ for _category in pkgutil.iter_modules(_nodes_pkg.__path__):
NODE_DISPLAY_NAME_MAPPINGS.update(getattr(_module, "NODE_DISPLAY_NAME_MAPPINGS", {}))
_module_count += 1
# Route-only module: registers /nh-nodes/large-preview/* on import.
# Route-only modules: they register the /nh-nodes/* HTTP routes on import.
try:
from .core import large_preview_routes # noqa: F401
except Exception as e:
print(f"[NH-Nodes] Failed to import core.large_preview_routes: {e}")
try:
from aiohttp import web
from server import PromptServer
@PromptServer.instance.routes.get("/nh-nodes/load-images-folder/count")
async def nh_load_images_folder_count(request):
folder_path = request.query.get("folder_path", "")
recursive = request.query.get("recursive", "").lower() in {"1", "true", "yes", "on"}
try:
from .nodes.image.image_tools_nodes import _collect_image_files, _resolve_load_dir
resolved_folder = _resolve_load_dir(folder_path)
image_count = len(_collect_image_files(resolved_folder, recursive=recursive))
return web.json_response({"count": image_count, "path": resolved_folder})
except Exception as exc:
return web.json_response({"count": 0, "error": str(exc)})
@PromptServer.instance.routes.get("/nh-nodes/smart-resolution-picker/presets")
async def nh_smart_resolution_picker_presets(request):
try:
from .core.resolution_data import DEFAULT_MODEL_LABEL, DEFAULT_PRESET, MODEL_LABELS, PRESET_LABELS_BY_MODEL
from .nodes.resolution.smart_resolution_picker import TARGET_RESOLUTION_LEVELS, _resolution_candidates
ratios_by_model_level = {}
for model_label in MODEL_LABELS:
ratios_by_model_level[model_label] = {}
for resolution_level in TARGET_RESOLUTION_LEVELS:
candidates = _resolution_candidates(model_label, resolution_level)
ratios = []
for entry in candidates:
if entry["aspect"] not in ratios:
ratios.append(entry["aspect"])
ratios_by_model_level[model_label][resolution_level] = ratios
return web.json_response({
"models": MODEL_LABELS,
"default_model": DEFAULT_MODEL_LABEL,
"default_preset": DEFAULT_PRESET,
"presets_by_model": PRESET_LABELS_BY_MODEL,
"resolution_levels": TARGET_RESOLUTION_LEVELS,
"ratios_by_model_level": ratios_by_model_level,
})
except Exception as exc:
return web.json_response({"error": str(exc)}, status=500)
from .core import folder_routes # noqa: F401
except Exception as e:
print(f"[NH-Nodes] Failed to register web routes: {e}")
print(f"[NH-Nodes] Failed to import core.folder_routes: {e}")
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+103
View File
@@ -0,0 +1,103 @@
"""HTTP routes behind the NH-Nodes frontend extensions.
GET /nh-nodes/load-images-folder/count how many images a folder loader sees (auto increment / wrap)
GET /nh-nodes/smart-resolution-picker/presets the preset lists for the resolution pickers' dropdowns
Importing this module registers both on ComfyUI's PromptServer, like core.large_preview_routes. The handlers are plain
module-level functions, so a test can serve them from any aiohttp app through ``register_routes``.
"""
import asyncio
import os
from aiohttp import web
from .image_folders import collect_image_files, resolve_load_dir
COUNT_PATH = "/nh-nodes/load-images-folder/count"
PRESETS_PATH = "/nh-nodes/smart-resolution-picker/presets"
_MAX_FOLDER_PATH_CHARS = 4096
# Spellings of the ``recursive`` query parameter. Anything else is a bad request, not a silent "false".
_FLAGS = {"1": True, "true": True, "yes": True, "on": True, "": False, "0": False, "false": False, "no": False, "off": False}
def _error(status, message):
# Fixed short texts only: ComfyUI can be reachable from the network (--listen), so an answer never carries a
# resolved path or the text of an exception.
return web.json_response({"error": message}, status=status)
def _count_images(folder_path, recursive):
"""How many images a loader with these settings sees. Blocking (a recursive walk): run it in a worker thread."""
folder = resolve_load_dir(folder_path)
if not os.path.isdir(folder):
raise FileNotFoundError("folder not found")
try:
return len(collect_image_files(folder, recursive=recursive))
except ValueError:
return 0 # the folder exists but holds no image
async def nh_load_images_folder_count(request):
folder_path = request.query.get("folder_path", "")
recursive = _FLAGS.get(request.query.get("recursive", "").strip().lower())
if recursive is None:
return _error(400, "recursive must be true or false")
if len(folder_path) > _MAX_FOLDER_PATH_CHARS or "\0" in folder_path:
return _error(400, "invalid folder_path")
try:
count = await asyncio.to_thread(_count_images, folder_path, recursive)
except FileNotFoundError:
return _error(404, "folder not found")
except PermissionError:
return _error(403, "folder is not readable")
except Exception as exc:
print(f"[NH-Nodes] load-images-folder/count failed: {type(exc).__name__}: {exc}")
return _error(500, "could not read the folder")
return web.json_response({"count": count})
async def nh_smart_resolution_picker_presets(request):
try:
# Imported here: core must not depend on the node modules at import time.
from ..nodes.resolution.smart_resolution_picker import TARGET_RESOLUTION_LEVELS, _resolution_candidates
from .resolution_data import DEFAULT_MODEL_LABEL, DEFAULT_PRESET, MODEL_LABELS, PRESET_LABELS_BY_MODEL
ratios_by_model_level = {}
for model_label in MODEL_LABELS:
ratios_by_model_level[model_label] = {}
for resolution_level in TARGET_RESOLUTION_LEVELS:
candidates = _resolution_candidates(model_label, resolution_level)
ratios = []
for entry in candidates:
if entry["aspect"] not in ratios:
ratios.append(entry["aspect"])
ratios_by_model_level[model_label][resolution_level] = ratios
return web.json_response({
"models": MODEL_LABELS,
"default_model": DEFAULT_MODEL_LABEL,
"default_preset": DEFAULT_PRESET,
"presets_by_model": PRESET_LABELS_BY_MODEL,
"resolution_levels": TARGET_RESOLUTION_LEVELS,
"ratios_by_model_level": ratios_by_model_level,
})
except Exception as exc:
print(f"[NH-Nodes] smart-resolution-picker/presets failed: {type(exc).__name__}: {exc}")
return _error(500, "could not build the preset lists")
def register_routes(routes):
"""Attach the handlers to an aiohttp RouteTableDef (ComfyUI's ``PromptServer.instance.routes``)."""
routes.get(COUNT_PATH)(nh_load_images_folder_count)
routes.get(PRESETS_PATH)(nh_smart_resolution_picker_presets)
try:
from server import PromptServer
register_routes(PromptServer.instance.routes)
except Exception as exc:
print(f"[NH-Nodes] Failed to register folder routes: {exc}")
+78
View File
@@ -0,0 +1,78 @@
"""Image folder helpers shared by the folder loader nodes and the folder-count HTTP route.
``resolve_load_dir`` turns the folder text of a loader into a directory, ``collect_image_files`` lists the images in
it in natural order. Nothing here needs torch or Pillow.
"""
import os
import re
from pathlib import Path
import folder_paths
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".webp", ".avif", ".bmp", ".tif", ".tiff"}
def natural_key(value):
"""Sort key that puts 'img2' before 'img10' and ignores case."""
return [int(part) if part.isdigit() else part.lower() for part in re.split(r"(\d+)", value)]
def resolve_load_dir(folder_path, image="", upload_probe=""):
"""The folder a loader reads.
``folder_path`` when given (relative to ComfyUI's input folder unless absolute; a file stands for its folder),
else the folder of the uploaded ``image`` / ``upload_probe``, else the input folder.
"""
folder_path = (folder_path or "").strip()
if folder_path:
resolved = folder_path
if not os.path.isabs(resolved):
resolved = os.path.join(folder_paths.get_input_directory(), resolved)
if os.path.isfile(resolved):
resolved = os.path.dirname(resolved)
return resolved
upload_name = image or upload_probe
if upload_name:
return os.path.dirname(folder_paths.get_annotated_filepath(upload_name))
return folder_paths.get_input_directory()
def collect_image_files(folder_path, recursive=False):
"""The images of ``folder_path`` in natural order, as names (paths relative to the folder below it).
Sub-folders are read when ``recursive`` is set, and also when the folder holds no image itself. ValueError for a
folder that does not exist or has no images.
"""
if not os.path.isdir(folder_path):
raise ValueError(f"Folder does not exist: {folder_path}")
files = []
for entry in os.listdir(folder_path):
full_path = os.path.join(folder_path, entry)
if os.path.isfile(full_path) and Path(entry).suffix.lower() in IMAGE_EXTENSIONS:
files.append(entry)
if recursive or not files:
seen = set(files)
models_dir = os.path.normcase(os.path.abspath(folder_paths.models_dir))
for current_root, dir_names, file_names in os.walk(folder_path):
# Natural order, and never into the ComfyUI models folder (a walk from one of its parents would crawl it).
dir_names[:] = sorted(
(name for name in dir_names if os.path.normcase(os.path.abspath(os.path.join(current_root, name))) != models_dir),
key=natural_key,
)
for file_name in sorted(file_names, key=natural_key):
full_path = os.path.join(current_root, file_name)
if os.path.isfile(full_path) and Path(file_name).suffix.lower() in IMAGE_EXTENSIONS:
rel_path = os.path.relpath(full_path, folder_path)
if rel_path not in seen:
seen.add(rel_path)
files.append(rel_path)
files.sort(key=natural_key)
if not files:
raise ValueError(f"No images found in folder: {folder_path}")
return files
+47
View File
@@ -0,0 +1,47 @@
"""Pillow / tensor helpers shared by the image nodes (save, load, compare, label, resize, layer composite).
IMAGE tensors become PIL images with core.utils.tensor_to_pil (rgb=True, plus keep_alpha=True for layers);
``pil_to_image_tensor`` converts back in ComfyUI's intermediate dtype.
"""
from PIL import Image, ImageColor
import comfy.model_management
from .utils import parse_hex_color, pil_to_tensor
RESAMPLING = getattr(Image, "Resampling", Image)
def parse_color(value, default):
"""Parse a colour into an (r, g, b, a) tuple of 0-255 ints; ``default`` when the text is not a colour.
'#RGB', '#RRGGBB' and '#RRGGBBAA' go through core.utils.parse_hex_color (opaque unless AA is given). Other CSS
colours Pillow understands ('red', 'rgb(1, 2, 3)') keep working, as they did before.
"""
text = str(value or "").strip()
color = parse_hex_color(text, ())
if color:
return color
try:
return ImageColor.getcolor(text, "RGBA")
except ValueError:
return tuple(default) + (255,) * (4 - len(default))
def pil_to_image_tensor(image):
"""A PIL image as a [1, H, W, C] float IMAGE in ComfyUI's intermediate dtype."""
return pil_to_tensor(image).to(dtype=comfy.model_management.intermediate_dtype())
def resize_pil(image, size, method):
"""``image`` resized to ``size`` (width, height) with the 'lanczos', 'bicubic', 'bilinear' or 'nearest' filter."""
resample = {
"lanczos": RESAMPLING.LANCZOS,
"bicubic": RESAMPLING.BICUBIC,
"bilinear": RESAMPLING.BILINEAR,
"nearest": RESAMPLING.NEAREST,
}[method]
return image.resize(size, resample)
+5 -17
View File
@@ -10,10 +10,11 @@ import uuid
from pathlib import Path
import folder_paths
import numpy as np
import torch
from PIL import Image, ImageChops, ImageDraw
from .utils import tensor_to_pil
CACHE_SUBDIR = "nh_large_preview"
CACHE_ID_RE = re.compile(r"^[a-f0-9]{32}$")
@@ -139,21 +140,6 @@ def delete_cache(cache_id):
}
def _tensor_to_pil(image_tensor):
if not isinstance(image_tensor, torch.Tensor):
raise TypeError(f"Expected IMAGE tensor, got {type(image_tensor).__name__}")
array = image_tensor.detach().cpu().numpy()
array = np.clip(array * 255.0, 0, 255).astype(np.uint8)
if array.ndim == 2:
return Image.fromarray(array).convert("RGB")
if array.ndim == 3 and array.shape[-1] == 1:
return Image.fromarray(array[:, :, 0]).convert("RGB")
if array.ndim == 3 and array.shape[-1] == 4:
return Image.fromarray(array).convert("RGBA")
return Image.fromarray(array).convert("RGB")
def _preview_dimensions(width, height, preview_megapixels):
width = max(1, int(width))
height = max(1, int(height))
@@ -292,7 +278,9 @@ def create_preview_cache(
batch_index=0,
batch_count=1,
):
source = _tensor_to_pil(image_tensor)
if not isinstance(image_tensor, torch.Tensor):
raise TypeError(f"Expected IMAGE tensor, got {type(image_tensor).__name__}")
source = tensor_to_pil(image_tensor, rgb=True, keep_alpha=True)
source_width, source_height = source.size
preview_width, preview_height = _preview_dimensions(source_width, source_height, preview_megapixels)
preview = source.copy()
+95 -56
View File
@@ -1,67 +1,106 @@
"""HTTP routes for NH large image preview cache."""
"""HTTP routes for NH large image preview cache.
Importing this module registers the routes on ComfyUI's PromptServer. The handlers are plain module-level functions,
so a test can serve them from any aiohttp app through ``register_routes``.
"""
import asyncio
import json
from aiohttp import web
from .large_preview_cache import cache_info, cleanup_cache, delete_cache, get_thumb_path, get_tile_path, read_manifest
def _without_paths(value):
"""Answers go to the browser: drop absolute filesystem paths (the Cache Manager node still shows them)."""
if isinstance(value, dict):
return {key: _without_paths(item) for key, item in value.items() if key not in ("path", "root")}
if isinstance(value, list):
return [_without_paths(item) for item in value]
return value
async def _read_json_object(request):
"""The JSON object in the request body ({} when the body is empty). ValueError for anything else."""
try:
text = await request.text()
payload = json.loads(text) if text.strip() else {}
except ValueError: # not JSON, or not UTF-8 text: json.JSONDecodeError and UnicodeDecodeError both are ValueErrors
raise ValueError("request body is not valid JSON") from None
if not isinstance(payload, dict):
raise ValueError("request body must be a JSON object")
return payload
async def nh_large_preview_manifest(request):
try:
manifest = read_manifest(request.match_info["cache_id"])
return web.json_response(manifest)
except FileNotFoundError:
return web.json_response({"error": "manifest not found"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
async def nh_large_preview_thumb(request):
try:
return web.FileResponse(get_thumb_path(request.match_info["cache_id"]))
except FileNotFoundError:
return web.json_response({"error": "thumbnail not found"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
async def nh_large_preview_tile(request):
try:
return web.FileResponse(get_tile_path(
request.match_info["cache_id"],
request.match_info["level"],
request.match_info["tile"],
))
except FileNotFoundError:
return web.json_response({"error": "tile not available in thumbnail-only preview"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
async def nh_large_preview_cache_info(request):
try:
# Directory walks run in a worker thread so a large cache never stalls the server's event loop.
return web.json_response(_without_paths(await asyncio.to_thread(cache_info)))
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
async def nh_large_preview_cleanup(request):
try:
payload = await _read_json_object(request)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
try:
cache_id = payload.get("cache_id", "")
if cache_id:
result = await asyncio.to_thread(delete_cache, cache_id)
else:
result = await asyncio.to_thread(cleanup_cache, payload.get("max_age_hours", 24), payload.get("max_total_mb", 0))
return web.json_response(_without_paths(result))
except (ValueError, TypeError, OverflowError) as exc: # a bad id, or a limit that is not a number
return web.json_response({"error": str(exc)}, status=400)
def register_routes(routes):
"""Attach the handlers to an aiohttp RouteTableDef (ComfyUI's ``PromptServer.instance.routes``)."""
routes.get("/nh-nodes/large-preview/manifest/{cache_id}")(nh_large_preview_manifest)
routes.get("/nh-nodes/large-preview/thumb/{cache_id}")(nh_large_preview_thumb)
routes.get("/nh-nodes/large-preview/tile/{cache_id}/{level}/{tile}")(nh_large_preview_tile)
routes.get("/nh-nodes/large-preview/cache-info")(nh_large_preview_cache_info)
routes.post("/nh-nodes/large-preview/cleanup")(nh_large_preview_cleanup)
try:
from server import PromptServer
@PromptServer.instance.routes.get("/nh-nodes/large-preview/manifest/{cache_id}")
async def nh_large_preview_manifest(request):
try:
manifest = read_manifest(request.match_info["cache_id"])
return web.json_response(manifest)
except FileNotFoundError:
return web.json_response({"error": "manifest not found"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
@PromptServer.instance.routes.get("/nh-nodes/large-preview/thumb/{cache_id}")
async def nh_large_preview_thumb(request):
try:
return web.FileResponse(get_thumb_path(request.match_info["cache_id"]))
except FileNotFoundError:
return web.json_response({"error": "thumbnail not found"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
@PromptServer.instance.routes.get("/nh-nodes/large-preview/tile/{cache_id}/{level}/{tile}")
async def nh_large_preview_tile(request):
try:
return web.FileResponse(get_tile_path(
request.match_info["cache_id"],
request.match_info["level"],
request.match_info["tile"],
))
except FileNotFoundError:
return web.json_response({"error": "tile not available in thumbnail-only preview"}, status=404)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
@PromptServer.instance.routes.get("/nh-nodes/large-preview/cache-info")
async def nh_large_preview_cache_info(request):
try:
return web.json_response(cache_info())
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
@PromptServer.instance.routes.post("/nh-nodes/large-preview/cleanup")
async def nh_large_preview_cleanup(request):
try:
payload = await request.json()
except Exception:
payload = {}
try:
cache_id = payload.get("cache_id", "")
if cache_id:
result = delete_cache(cache_id)
else:
result = cleanup_cache(payload.get("max_age_hours", 24), payload.get("max_total_mb", 0))
return web.json_response(result)
except ValueError as exc:
return web.json_response({"error": str(exc)}, status=400)
register_routes(PromptServer.instance.routes)
except Exception as exc:
print(f"[NH-Nodes] Failed to register large preview routes: {exc}")
+25
View File
@@ -0,0 +1,25 @@
"""Mask helpers shared by the NH mask and image nodes.
NH-Nodes' own code (MIT). It is deliberately not part of core/utils_mask.py: that file is third-party
FitDiT code under CC BY-NC-SA 4.0 (see THIRD_PARTY_NOTICES.md), and plain MIT nodes must not depend on it.
"""
import numpy as np
def mask_bbox(mask, threshold=0.5):
"""Inclusive bounding box ``(x1, y1, x2, y2)`` of the pixels of a 2-D mask that are above ``threshold``.
``mask`` is an [H, W] array (a torch mask goes through ``.cpu().numpy()`` first). A pixel counts only when it is
strictly greater than ``threshold``, so a value of exactly 0.5 is outside, as in every NH mask node. The result is
four Python ints; (x2, y2) is the last pixel inside, so width = x2 - x1 + 1 and height = y2 - y1 + 1.
Returns None when no pixel is above the threshold, i.e. for an empty mask. Anything that is not 2-D raises ValueError.
"""
inside = np.asarray(mask) > threshold
if inside.ndim != 2:
raise ValueError(f"mask_bbox expects a 2-D [H, W] mask, got shape {inside.shape}")
rows = np.flatnonzero(inside.any(axis=1))
if rows.size == 0:
return None
cols = np.flatnonzero(inside.any(axis=0))
return int(cols[0]), int(rows[0]), int(cols[-1]), int(rows[-1])
+131
View File
@@ -0,0 +1,131 @@
"""Model-folder, download and VRAM helpers shared by the SegFormer and VTON model loaders.
Both model families follow the same rules:
* Model folders are registered with ComfyUI's ``folder_paths`` under the names
``nh_nodes`` (SegFormer) and ``vton_mask`` (VTON). Users can add more
locations with those names in ``extra_model_paths.yaml``; every registered
location is searched before anything is downloaded. Each name is registered
with the extension of its model files (``.safetensors``, ``.onnx``), so
ComfyUI's model lists show those files and not the Hugging Face cache
metadata that downloads leave in the same folder.
* Downloads always go to the default folder under the models dir, pinned to a
fixed Hugging Face revision.
"""
import os
import torch
try:
import folder_paths
except Exception: # running outside ComfyUI (tests)
folder_paths = None
try:
import comfy.model_management as _mm
except Exception:
_mm = None
def models_dir():
"""ComfyUI's models directory, read on every call (--models-directory, tests)."""
if folder_paths is not None:
return folder_paths.models_dir
return os.path.join(os.path.dirname(__file__), "models")
def unique_dirs(paths):
"""Drop repeats (same folder spelled differently) but keep the first spelling and the order."""
seen, unique = set(), []
for path in paths:
key = os.path.normcase(os.path.abspath(path))
if key not in seen:
seen.add(key)
unique.append(path)
return unique
def _list_only(folder_name, extensions):
"""Add `extensions` to the file types ComfyUI lists for the registered `folder_name`.
add_model_folder_path registers a name with an empty extension set, which ComfyUI reads as "list
every file": /models and the Model Library then show the .cache/huggingface metadata of our
downloads and a repo's .gitattributes. The set is replaced, never changed in place (ComfyUI shares
one set between many folder names), and the locations list stays the very same object, so every
extra_model_paths.yaml entry survives.
"""
if not extensions:
return
registry = folder_paths.folder_names_and_paths
paths, current = registry[folder_name]
merged = {*current, *extensions}
if merged != set(current):
registry[folder_name] = (paths, merged)
def register_model_folder(folder_name, default_dir, extensions=()):
"""Register `folder_name` with ComfyUI and return every folder searched for it, best first.
ComfyUI applies extra_model_paths.yaml before custom nodes load, so locations the user
configured under `folder_name` are already registered; they come first. `default_dir`
is added too (a no-op when already present) and is always part of the result, so models
that live there are found whether or not the user configured anything. Safe to call repeatedly.
`extensions` (".onnx") are the file types of the models in these folders: ComfyUI's lists for
`folder_name` then show only those, also for the locations the user added.
"""
registered = []
if folder_paths is not None:
try:
folder_paths.add_model_folder_path(folder_name, default_dir)
registered = folder_paths.get_folder_paths(folder_name)
_list_only(folder_name, extensions) # last: if it fails, the locations above are still returned
except Exception as exc: # an unusual ComfyUI build: fall back to the default location alone
print(f"[NH-Nodes] Could not register model folder '{folder_name}' with ComfyUI: {exc}")
return unique_dirs([*registered, default_dir])
def download_pinned(repo_id, revision, target, allow_patterns):
"""snapshot_download of `allow_patterns` from `repo_id` at `revision` into `target`.
Finished files are kept and partial ones resume, so calling it again after an interrupted
download completes the folder. Failures are re-raised with where to get the files by hand.
"""
from huggingface_hub import snapshot_download
os.makedirs(target, exist_ok=True)
print(f"[NH-Nodes] Downloading {repo_id}@{revision[:8]} -> {target}")
try:
snapshot_download(repo_id=repo_id, revision=revision, local_dir=target, allow_patterns=list(allow_patterns))
except Exception as exc:
raise RuntimeError(
f"[NH-Nodes] Downloading {repo_id} (revision {revision[:8]}) into {target} failed: {exc}\n"
f"Run again once the connection works (finished files are kept), or download "
f"{', '.join(allow_patterns)} from https://huggingface.co/{repo_id}/tree/{revision} into that folder."
) from exc
def make_room_on_gpu(nbytes):
"""Ask ComfyUI to unload its own models until about `nbytes` of VRAM are free.
ComfyUI's own loaders ask for their size plus the VRAM it keeps in reserve (extra_reserved_memory(),
set by --reserve-vram or its built-in default), so that reserve is added here as well: asking for
`nbytes` alone would leave us that much short. Also hands PyTorch's cached blocks back to the
driver: libraries that allocate outside PyTorch (ONNX Runtime) cannot use them. Nothing happens
without a CUDA device, when ComfyUI itself runs on the CPU, or outside ComfyUI. A failure is
reported, not raised: the caller still tries its allocation.
"""
if _mm is None or not torch.cuda.is_available():
return
try:
# ComfyUI's loaded models carry an indexed device (cuda:0); an index-less
# torch.device("cuda") compares unequal to it and would match none of them.
device = _mm.get_torch_device()
if device.type != "cuda":
return
reserved = getattr(_mm, "extra_reserved_memory", None) # absent in older ComfyUI builds
_mm.free_memory(nbytes + (reserved() if reserved else 0), device)
_mm.soft_empty_cache()
except Exception as exc:
print(f"[NH-Nodes] Could not ask ComfyUI to free VRAM ({exc}); continuing")
+4
View File
@@ -2,6 +2,10 @@
Data is kept separate from node logic so new model families can be added
without changing execution code.
min_res / max_res are the clamp range for *custom* sizes. Curated presets may go
beyond max_res (e.g. 4 MP tiers); their own reliability/warning fields say how
safe they are, and the node reports that warning in its info output.
"""
+126
View File
@@ -0,0 +1,126 @@
"""Arithmetic expression evaluator shared by Math Eval (NH) and layer position expressions.
Accepted: numbers, named variables, + - * / // % **, unary + and -, and an optional
whitelist of functions. Everything is computed in floating point, so no expression can
build a huge integer and freeze the worker (``9**9**9`` simply overflows and is reported).
"""
import ast
import math
import numbers
import operator
# Python's ** returns a complex number for a negative base with a fractional exponent;
# math.pow raises instead, which is what a node input needs.
_BINARY = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.FloorDiv: operator.floordiv,
ast.Mod: operator.mod,
ast.Pow: math.pow,
}
_UNARY = {
ast.UAdd: operator.pos,
ast.USub: operator.neg,
}
_DIVISIONS = (ast.Div, ast.FloorDiv, ast.Mod)
MATH_FUNCTIONS = {
"min": min,
"max": max,
"abs": abs,
"round": lambda value, digits=0: round(value, int(digits)),
"sqrt": math.sqrt,
"floor": math.floor,
"ceil": math.ceil,
"clamp": lambda value, low, high: max(low, min(high, value)),
}
class ExpressionError(ValueError):
"""Raised for any expression that cannot be evaluated to a finite number."""
def evaluate(expression, variables=None, functions=None, *, zero_division="zero",
case_insensitive=False, on_warning=None):
"""Evaluate ``expression`` and return a finite float.
zero_division: "zero" makes x/0, x//0 and x%0 evaluate to 0 (reported through
``on_warning``); "error" raises ExpressionError.
case_insensitive: match variable names with str.casefold (keys must already be folded).
"""
variables = variables or {}
functions = functions or {}
text = str(expression).strip()
if not text:
raise ExpressionError("Expression is empty")
def warn(message):
if on_warning is not None:
on_warning(message)
def number(value, what):
# numpy scalars and 0-d torch tensors arrive from upstream nodes on FLOAT/INT sockets.
if getattr(value, "ndim", None) == 0 and callable(getattr(value, "item", None)):
value = value.item()
if not isinstance(value, numbers.Real): # bool is a Real (True + 1 == 2, as before)
raise ExpressionError(f"{what} must be a number, got {type(value).__name__}")
try:
return float(value)
except OverflowError as exc:
raise ExpressionError(f"{what} is too large") from exc
def lookup(name):
key = name.casefold() if case_insensitive else name
if key in variables:
return number(variables[key], f"Variable '{name}'")
raise ExpressionError(f"Unknown variable '{name}'")
def visit(node):
if isinstance(node, ast.Constant):
return number(node.value, f"Constant {node.value!r}")
if isinstance(node, ast.Name):
return lookup(node.id)
if isinstance(node, ast.BinOp):
op = _BINARY.get(type(node.op))
if op is None:
raise ExpressionError(f"Operator '{type(node.op).__name__}' is not allowed")
left, right = visit(node.left), visit(node.right)
if isinstance(node.op, _DIVISIONS) and right == 0:
if zero_division == "zero":
warn("division by zero, using 0")
return 0.0
raise ExpressionError("Division by zero")
return float(op(left, right))
if isinstance(node, ast.UnaryOp):
op = _UNARY.get(type(node.op))
if op is None:
raise ExpressionError(f"Operator '{type(node.op).__name__}' is not allowed")
return op(visit(node.operand))
if isinstance(node, ast.Call):
name = node.func.id if isinstance(node.func, ast.Name) else None
if name not in functions or node.keywords:
raise ExpressionError(f"Function '{name}' is not allowed")
return number(functions[name](*[visit(arg) for arg in node.args]), f"{name}()")
raise ExpressionError(f"'{type(node).__name__}' is not allowed in an expression")
try:
result = visit(ast.parse(text, mode="eval").body)
except ExpressionError:
raise
except SyntaxError as exc:
raise ExpressionError(f"Invalid expression: {text}") from exc
except OverflowError as exc:
raise ExpressionError(f"Result is too large: {text}") from exc
except (ValueError, TypeError, ZeroDivisionError) as exc:
# math domain errors, wrong argument counts, 0 ** -1 ...
raise ExpressionError(f"Cannot evaluate {text}: {exc}") from exc
except (RecursionError, MemoryError) as exc:
# The parser raises MemoryError ("parser stack overflowed") for absurdly nested input.
raise ExpressionError("Expression is nested too deeply") from exc
if not math.isfinite(result):
raise ExpressionError(f"Result is not a finite number: {text}")
return result
+626
View File
@@ -0,0 +1,626 @@
"""SAM 3 for the NH vision nodes: finding the model, loading it, and segmenting concepts in two passes.
NH-Nodes does not ship SAM 3. It uses Meta's ``sam3`` package, installed separately (see ``INSTALL_HINT``), and the
``sam3.pt`` checkpoint, which Hugging Face hands out only after Meta's license is accepted, so it is never
downloaded automatically. Checkpoints are looked up in the ``sam3`` model folder (``models/sam3`` by default, more
locations in ``extra_model_paths.yaml``); other SAM 3 packs use the same folder, so one copy serves all of them.
Pass 1 looks at the whole image once: the image is resized to the model's square input, encoded once and asked for
every concept. Each instance's mask is sampled back to full resolution inside its own box only.
Pass 2 (``detail_level="balanced"``) cuts each instance out of the full-resolution image with a margin and segments it
again with SAM 3's box-prompt decoder, the pass-1 mask given as a hint. Objects that are small or medium in the frame
then get their edges from the original pixels instead of a ~1000 px view of the whole frame. SAM 3 draws masks on a
grid of about 288 cells across whatever it looks at, so a subject that fills most of an 8K frame still has edges at
about 1/300 of its size; hair-level edges need matting. A close-up that does not show the pass-1 instance is dropped
and the pass-1 mask kept.
The official sam3 code builds some of its lookup tables on the GPU, so SAM 3 runs on CUDA only.
"""
import contextlib
import importlib
import importlib.util
import inspect
import itertools
import math
import os
import cv2
import numpy as np
import torch
import torch.nn.functional as F
from .model_files import make_room_on_gpu, models_dir, register_model_folder
from .segs import SegsFrame, make_segment
try:
import folder_paths
except Exception: # running outside ComfyUI (tests)
folder_paths = None
try:
import comfy.model_management as _mm
except Exception:
_mm = None
FOLDER_NAME = "sam3" # shared with the other SAM 3 packs
# Listed in ComfyUI for the folder: SAM 3 checkpoints in the formats SAM 3 packs use, not Hugging Face cache files.
FOLDER_EXTENSIONS = (".pt", ".pth", ".safetensors")
CHECKPOINT_EXTENSIONS = (".pt", ".pth") # what sam3's own loader reads (torch.load)
DEFAULT_CHECKPOINT = "sam3.pt"
DETAIL_LEVELS = ("balanced", "fast")
SAM3_COMMIT = "b26a5f330e05d321afb39d01d3d4881f258f65ff" # the sam3 build NH-Nodes is tested with
INSTALL_HINT = (
"SAM 3 needs Meta's sam3 package, which NH-Nodes does not install for you. In ComfyUI's Python environment run:\n"
f" pip install --no-deps git+https://github.com/facebookresearch/sam3@{SAM3_COMMIT}\n"
" pip install timm ftfy regex iopath\n"
"--no-deps keeps your numpy: sam3 asks for numpy<2, which ComfyUI does not need. "
"Restart ComfyUI afterwards."
)
CHECKPOINT_HINT = (
"Ask for access at https://huggingface.co/facebook/sam3 (Meta has you accept the SAM License first), "
"download sam3.pt from that page and put it in {folder}. Then refresh ComfyUI (press R) and pick it."
)
DEDUP_IOU = 0.9 # a second instance of the same concept overlapping a better one this much is a duplicate
# A close-up replaces the pass-1 mask when it shows the same object: at least REFINE_MIN_INSIDE of it lies within the
# pass-1 blur of the pass-1 mask, and it covers at least REFINE_MIN_CORE of the pass-1 core (see close_up_agrees).
REFINE_MIN_INSIDE = 0.8
REFINE_MIN_CORE = 0.6
_PASS1_GRID = 288 # SAM 3's detector draws masks on a 288 x 288 grid of its square view of the whole image
_AGREE_PIXELS = 1 << 21 # masks larger than this are compared at a reduced scale
_WIDE_ASPECT = 4.0 # beyond this aspect ratio SAM 3's square view squeezes the picture enough to miss small things
_CROP_MARGIN = 0.2 # pass-2 crop margin on each side, as a fraction of the instance's longer side
_CROP_MIN_MARGIN = 16 # px
_CROP_MAX_ASPECT = 2.0 # pass-2 crops are widened to at most 2:1, the model squeezes them into a square
_SAMPLE_CHUNK = 1 << 22 # output pixels per grid_sample call when resampling masks
_WORKING_BYTES = 3 << 30 # VRAM for activations and the full-resolution image, on top of the weights
_API = None
_CACHE = {}
# ---------------------------------------------------------------------------
# Checkpoints
# ---------------------------------------------------------------------------
def default_folder():
"""<models dir>/sam3, the folder ComfyUI and the other SAM 3 packs use."""
return os.path.join(models_dir(), FOLDER_NAME)
def checkpoint_folders():
"""Every folder searched for checkpoints, best first (registers the folder name with ComfyUI)."""
return register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS)
def checkpoint_choices():
"""Checkpoint names for the node's list: .pt/.pth files in the sam3 folders, or the default name if none yet."""
names = set()
folders = checkpoint_folders()
if folder_paths is not None:
try:
names.update(folder_paths.get_filename_list(FOLDER_NAME))
except Exception as exc:
print(f"[NH-Nodes] Could not list the {FOLDER_NAME} model folder: {exc}")
else:
for folder in folders:
if os.path.isdir(folder):
names.update(os.listdir(folder))
names = sorted(name for name in names if name.lower().endswith(CHECKPOINT_EXTENSIONS))
return names or [DEFAULT_CHECKPOINT]
def resolve_checkpoint(name):
"""Full path of checkpoint ``name`` in the sam3 folders; a RuntimeError says where to get it when it is missing."""
name = str(name or DEFAULT_CHECKPOINT)
parts = name.replace("\\", "/").split("/")
if os.path.isabs(name) or ".." in parts:
raise RuntimeError(f"[NH-Nodes] SAM 3 checkpoint name must be a file in the {FOLDER_NAME} folder: {name!r}")
if not name.lower().endswith(CHECKPOINT_EXTENSIONS):
raise RuntimeError(
f"[NH-Nodes] {name} is not a SAM 3 .pt checkpoint. The safetensors export on Hugging Face is for the "
"transformers library and uses other weight names; download sam3.pt instead."
)
folders = checkpoint_folders()
path = folder_paths.get_full_path(FOLDER_NAME, name) if folder_paths is not None else None
if path is None:
path = next((os.path.join(folder, *parts) for folder in folders
if os.path.isfile(os.path.join(folder, *parts))), None)
if path is None or not os.path.isfile(path):
raise RuntimeError(
f"[NH-Nodes] SAM 3 checkpoint {name} not found in: {', '.join(folders)}.\n"
+ CHECKPOINT_HINT.format(folder=folders[0] if folders else default_folder())
)
return path
# Make the folder name known to ComfyUI as soon as the pack loads.
checkpoint_folders()
# ---------------------------------------------------------------------------
# The sam3 package and the model
# ---------------------------------------------------------------------------
def _tf32_flags():
"""(matmul, cudnn) TF32 switches, or None when torch refuses to report them (its newer precision API in use)."""
try:
return torch.backends.cuda.matmul.allow_tf32, torch.backends.cudnn.allow_tf32
except Exception:
return None
def _restore_tf32(flags):
if flags is not None:
try:
torch.backends.cuda.matmul.allow_tf32, torch.backends.cudnn.allow_tf32 = flags
except Exception:
pass
def _missing_parameters(func, names):
"""Names from ``names`` that ``func`` does not accept (none when its signature cannot be read or takes **kwargs)."""
try:
parameters = inspect.signature(func).parameters
except (TypeError, ValueError):
return []
if any(p.kind == p.VAR_KEYWORD for p in parameters.values()):
return []
return [name for name in names if name not in parameters]
def ensure_sam3():
"""(build_sam3_image_model, Sam3Processor) from the installed sam3 package.
A RuntimeError explains what to do when the package is missing, fails to import or lacks the image API
NH-Nodes uses. Importing sam3's model builder switches TF32 on for the whole process; the previous setting is
put back, so ComfyUI's own models keep theirs.
"""
global _API
if _API is not None:
return _API
if importlib.util.find_spec("sam3") is None:
raise RuntimeError("[NH-Nodes] " + INSTALL_HINT)
flags = _tf32_flags()
try:
builder = importlib.import_module("sam3.model_builder")
processors = importlib.import_module("sam3.model.sam3_image_processor")
except Exception as exc:
raise RuntimeError(f"[NH-Nodes] The sam3 package is installed but failed to load: {exc}\n{INSTALL_HINT}") from exc
finally:
_restore_tf32(flags)
build = getattr(builder, "build_sam3_image_model", None)
processor_cls = getattr(processors, "Sam3Processor", None)
missing = []
if not callable(build):
missing.append("build_sam3_image_model")
else:
missing += [f"build_sam3_image_model({name}=)" for name in _missing_parameters(
build, ("checkpoint_path", "load_from_HF", "enable_inst_interactivity", "device", "eval_mode"))]
if processor_cls is None:
missing.append("Sam3Processor")
else:
missing += [f"Sam3Processor({name}=)" for name in
_missing_parameters(processor_cls, ("device", "confidence_threshold"))]
missing += [f"Sam3Processor.{name}" for name in ("set_image", "set_text_prompt", "reset_all_prompts")
if not callable(getattr(processor_cls, name, None))]
if missing:
raise RuntimeError(
f"[NH-Nodes] The installed sam3 package lacks {', '.join(missing)}; it is older or newer than the "
f"versions NH-Nodes supports. Reinstall it:\n{INSTALL_HINT}"
)
_API = (build, processor_cls)
return _API
def sam3_device():
"""ComfyUI's torch device, which must be a CUDA GPU for SAM 3."""
if _mm is not None:
device = _mm.get_torch_device()
else:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if device.type != "cuda":
raise RuntimeError(
f"[NH-Nodes] SAM 3 needs an NVIDIA GPU with CUDA (ComfyUI runs on {device}): the official sam3 code "
"builds part of the model on the GPU."
)
return device
def check_checkpoint(path):
"""Fail early, with the download advice, when ``path`` is not a readable SAM 3 checkpoint.
sam3 loads weights non-strictly and only prints the names it misses, so another model's .pt in the shared folder
would run with random weights, and a truncated download fails deep inside torch. Reading the names is quick:
the file is memory-mapped, not loaded.
"""
advice = CHECKPOINT_HINT.format(folder=os.path.dirname(path))
try:
state = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
except Exception as exc:
raise RuntimeError(f"[NH-Nodes] {path} cannot be read as a SAM 3 checkpoint ({exc}); it may be an "
f"incomplete download.\n{advice}") from exc
if isinstance(state, dict) and isinstance(state.get("model"), dict):
state = state["model"]
names = list(state.keys()) if isinstance(state, dict) else []
del state
if not any(name.startswith("detector.") for name in names) or not any(name.startswith("tracker.") for name in names):
raise RuntimeError(f"[NH-Nodes] {path} is not a SAM 3 checkpoint (it has no SAM 3 detector and tracker "
f"weights).\n{advice}")
def load_model(checkpoint):
"""The SAM 3 image model for ``checkpoint`` (a name in the sam3 folders); kept in RAM between runs."""
path = resolve_checkpoint(checkpoint)
stamp = (os.path.normcase(os.path.abspath(path)), os.path.getmtime(path), os.path.getsize(path))
if _CACHE.get("model") is not None and _CACHE.get("stamp") == stamp:
return _CACHE["model"]
unload()
build, _ = ensure_sam3()
device = sam3_device()
check_checkpoint(path)
print(f"[NH-Nodes] Loading SAM 3 from {path}")
with torch.cuda.device(device): # the tables sam3 builds on "cuda" go to ComfyUI's GPU
model = build(checkpoint_path=path, load_from_HF=False, enable_inst_interactivity=True,
device="cpu", eval_mode=True)
if not callable(getattr(model, "predict_inst", None)) or getattr(model, "inst_interactive_predictor", None) is None:
raise RuntimeError("[NH-Nodes] This sam3 build has no box-prompt decoder (predict_inst). " + INSTALL_HINT)
transforms = getattr(model.inst_interactive_predictor, "_transforms", None)
if getattr(transforms, "max_hole_area", 0):
# sam3 fills holes of close-up masks by default; keep real ones (between an arm and the body), as pass 1 does
transforms.max_hole_area = 0.0
model.requires_grad_(False)
_CACHE.update(model=model, stamp=stamp)
return model
def _model_device(model):
return next(model.parameters()).device
def _is_on(current, wanted):
"""True when ``current`` satisfies ``wanted``; an index-less "cuda" accepts any cuda:N."""
return current.type == wanted.type and (wanted.index is None or current.index == wanted.index)
_TABLE_ATTRIBUTES = ("freqs_cis", "freqs_cis_real", "freqs_cis_imag") # sam3's rotary tables, plain attributes
def _all_on(model, device):
return all(_is_on(p.device, device) for p in model.parameters())
def _move_tables(model, device):
"""Move sam3's rotary tables, which model.to() leaves behind because they are not buffers."""
for module in model.modules():
for name in _TABLE_ATTRIBUTES:
value = getattr(module, name, None)
if isinstance(value, torch.Tensor) and not _is_on(value.device, device):
setattr(module, name, value.to(device))
def move_model(model, device):
"""Put the weights on ``device``, asking ComfyUI to free VRAM first; back to the CPU if the move fails."""
if device.type == "cuda":
weights = 0 if _all_on(model, device) else sum(
t.numel() * t.element_size() for t in itertools.chain(model.parameters(), model.buffers()))
make_room_on_gpu(weights + _WORKING_BYTES)
try:
model.to(device)
_move_tables(model, device)
except Exception:
offload(model) # a half-moved model would fail every later run
raise
def offload(model):
"""Move the weights and sam3's GPU-side tables to the CPU, so nothing holds VRAM between runs.
The position-encoding caches sam3 fills at build time (about 200 MB on the GPU) are emptied; sam3 rebuilds them
on the GPU when it needs them again.
"""
cpu = torch.device("cpu")
if not _all_on(model, cpu):
model.to(cpu)
_move_tables(model, cpu)
for module in model.modules():
if type(module).__name__ == "PositionEmbeddingSine" and isinstance(getattr(module, "cache", None), dict):
module.cache.clear()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def unload():
"""Forget the cached model (another checkpoint was chosen)."""
model = _CACHE.pop("model", None)
_CACHE.pop("stamp", None)
if model is not None:
offload(model)
def _native_bf16():
try:
return torch.cuda.is_bf16_supported(including_emulation=False)
except TypeError: # torch without the including_emulation switch
return torch.cuda.is_bf16_supported()
def _autocast(device):
"""bfloat16 autocast on GPUs with native bfloat16 (Ampere and newer), as Meta runs SAM 3; full precision elsewhere."""
if device.type == "cuda" and _native_bf16():
return torch.autocast("cuda", dtype=torch.bfloat16)
return contextlib.nullcontext()
# ---------------------------------------------------------------------------
# Geometry helpers (model-free, unit tested)
# ---------------------------------------------------------------------------
def to_logits(masks):
"""sam3 returns its masks after a sigmoid (under the key "masks_logits"); turn them back into logits.
Values outside [0, 1] are taken as logits already, in case a later sam3 returns them as the key says.
"""
masks = masks.float()
if masks.numel() and (float(masks.min()) < 0.0 or float(masks.max()) > 1.0):
return masks
masks = masks.clamp(1e-6, 1.0 - 1e-6)
return torch.log(masks) - torch.log1p(-masks)
def sample_region(logits, image_size, region, out_size):
"""Resample the square model-space map ``logits`` [S, S] over ``region`` of the image, bilinearly.
``region`` is (x1, y1, x2, y2) in pixels of an image of ``image_size`` (height, width), which the model saw
squeezed into S x S; the result is [h, w] for ``out_size`` (h, w), each value taken at the centre of its output
pixel. Runs in row chunks, so a full 8K region needs a few hundred MB at most.
"""
height, width = image_size
size = logits.shape[-1]
x1, y1, x2, y2 = region
out_h, out_w = int(out_size[0]), int(out_size[1])
device = logits.device
xs = x1 + (torch.arange(out_w, device=device, dtype=torch.float32) + 0.5) * ((x2 - x1) / out_w)
ys = y1 + (torch.arange(out_h, device=device, dtype=torch.float32) + 0.5) * ((y2 - y1) / out_h)
# model-map pixel index of each centre, as grid_sample coordinates (align_corners=True: -1/+1 = first/last centre)
grid_x = (xs * (size / width) - 0.5) * (2.0 / max(1, size - 1)) - 1.0
grid_y = (ys * (size / height) - 0.5) * (2.0 / max(1, size - 1)) - 1.0
source = logits.float()[None, None]
result = torch.empty((out_h, out_w), device=device, dtype=torch.float32)
rows = max(1, _SAMPLE_CHUNK // max(1, out_w))
for start in range(0, out_h, rows):
gy = grid_y[start:start + rows]
grid = torch.stack(torch.broadcast_tensors(grid_x[None, :], gy[:, None]), dim=-1)[None]
result[start:start + rows] = F.grid_sample(source, grid, mode="bilinear", padding_mode="border",
align_corners=True)[0, 0]
return result
def mask_box(mask):
"""Tight (x1, y1, x2, y2) box of a bool [H, W] tensor with exclusive ends; None when empty."""
rows = torch.nonzero(mask.any(dim=1)).flatten()
if rows.numel() == 0:
return None
cols = torch.nonzero(mask.any(dim=0)).flatten()
return int(cols[0]), int(rows[0]), int(cols[-1]) + 1, int(rows[-1]) + 1
def model_box_to_image(box, size, image_size, margin=1):
"""An S x S model-space box as an image-space region, ``margin`` model pixels wider on each side, in the image."""
height, width = image_size
sx, sy = width / size, height / size
x1, y1, x2, y2 = box
return (max(0, int((x1 - margin) * sx)), max(0, int((y1 - margin) * sy)),
min(width, int(math.ceil((x2 + margin) * sx))), min(height, int(math.ceil((y2 + margin) * sy))))
def _fit_span(center, size, limit):
size = int(round(min(size, limit)))
start = int(round(center - size / 2.0))
start = min(max(0, start), limit - size)
return start, start + size
def crop_region(box, image_size):
"""Pass-2 crop around an image-space box: a margin on each side, widened to at most 2:1, kept inside the image."""
height, width = image_size
x1, y1, x2, y2 = box
margin = max(_CROP_MIN_MARGIN, _CROP_MARGIN * max(x2 - x1, y2 - y1))
crop_w, crop_h = (x2 - x1) + 2 * margin, (y2 - y1) + 2 * margin
crop_w, crop_h = max(crop_w, crop_h / _CROP_MAX_ASPECT), max(crop_h, crop_w / _CROP_MAX_ASPECT)
left, right = _fit_span((x1 + x2) / 2.0, crop_w, width)
top, bottom = _fit_span((y1 + y2) / 2.0, crop_h, height)
# the box itself always stays inside, whatever the rounding did
return min(left, x1), min(top, y1), max(right, x2), max(bottom, y2)
def drop_stray_parts(refined, coarse):
"""``refined`` without the connected parts that do not touch ``coarse`` at all (specks around the object)."""
count, labels = cv2.connectedComponents(refined.astype(np.uint8), connectivity=8)
if count <= 2:
return refined
touching = np.bincount(labels[coarse], minlength=count) > 0
touching[0] = False
return touching[labels]
def close_up_agrees(refined, coarse, tolerance):
"""True when the close-up mask ``refined`` shows the same object as the pass-1 mask ``coarse`` (bool arrays).
The pass-1 mask is blurred by up to ``tolerance`` px, so plain IoU would turn down a sharp mask of a thin pole or
cable. Instead most of the close-up must lie within ``tolerance`` of the pass-1 mask, and it must cover most of
the pass-1 core (pixels deeper than twice ``tolerance`` inside it); thin objects have no core and need only the
first. Large masks are compared at a reduced scale.
"""
if not refined.any() or not coarse.any():
return False
step = max(1, int(math.sqrt(refined.size / _AGREE_PIXELS)))
if step > 1 and not refined[::step, ::step].any():
step = 1 # something thinner than the step: compare at full scale
refined, coarse = refined[::step, ::step], coarse[::step, ::step]
tolerance = max(1.0, tolerance / step)
to_coarse = cv2.distanceTransform((~coarse).astype(np.uint8), cv2.DIST_L2, 3)
inside = np.count_nonzero(refined & (to_coarse <= tolerance)) / np.count_nonzero(refined)
core = cv2.distanceTransform(coarse.astype(np.uint8), cv2.DIST_L2, 3) > 2.0 * tolerance
covered = np.count_nonzero(refined & core) / np.count_nonzero(core) if core.any() else 1.0
return inside >= REFINE_MIN_INSIDE and covered >= REFINE_MIN_CORE
def _box_overlap(a, b):
"""Intersection box of two (x1, y1, x2, y2) boxes, or None."""
x1, y1, x2, y2 = max(a[0], b[0]), max(a[1], b[1]), min(a[2], b[2]), min(a[3], b[3])
return (x1, y1, x2, y2) if x1 < x2 and y1 < y2 else None
def find_instances(processor, state, concept, max_instances=0):
"""[(score, logits [S, S], model-space box)] for one concept on an encoded image, best first, duplicates dropped."""
processor.reset_all_prompts(state)
output = processor.set_text_prompt(concept, state)
masks, scores = output["masks_logits"], output["scores"]
if masks is None or len(scores) == 0:
return []
scores = scores.float().flatten()
found, kept = [], [] # kept: (bool mask, box, area) of accepted instances
for i in torch.argsort(scores, descending=True).tolist():
logits = to_logits(masks[i].reshape(masks.shape[-2], masks.shape[-1]))
inside = logits > 0
box = mask_box(inside)
if box is None:
continue
area = int(inside.sum())
duplicate = False
for other, other_box, other_area in kept:
overlap = _box_overlap(box, other_box)
if overlap is None:
continue
ox1, oy1, ox2, oy2 = overlap
inter = int((inside[oy1:oy2, ox1:ox2] & other[oy1:oy2, ox1:ox2]).sum())
if inter / max(1, area + other_area - inter) >= DEDUP_IOU:
duplicate = True
break
if duplicate:
continue
kept.append((inside, box, area))
found.append((float(scores[i]), logits, box))
if max_instances and len(found) >= max_instances:
break
return found
# ---------------------------------------------------------------------------
# Segmenting
# ---------------------------------------------------------------------------
def _hint_size(model):
"""Side of the low-resolution mask the box-prompt decoder takes as a hint (288 for SAM 3)."""
encoder = getattr(getattr(getattr(model, "inst_interactive_predictor", None), "model", None),
"sam_prompt_encoder", None)
size = getattr(encoder, "mask_input_size", (288, 288))
return int(size[0]), int(size[1])
def _upsample(concept, score, logits, box, image_size):
"""Pass 1 only: the instance's mask at full resolution, inside its own region."""
region = model_box_to_image(box, logits.shape[-1], image_size)
x1, y1, x2, y2 = region
mask = sample_region(logits, image_size, region, (y2 - y1, x2 - x1)) > 0
return make_segment(concept, score, mask, origin=(x1, y1)), False
def _refine(model, processor, image, concept, score, logits, box, image_size, hint_size):
"""Pass 2: segment the instance again on a full-resolution crop, the pass-1 mask as a hint.
Returns (segment, refined) where ``refined`` is False when the pass-1 mask was kept.
"""
target = model_box_to_image(box, logits.shape[-1], image_size)
cx1, cy1, cx2, cy2 = crop_region(target, image_size)
coarse = (sample_region(logits, image_size, (cx1, cy1, cx2, cy2), (cy2 - cy1, cx2 - cx1)) > 0).cpu().numpy()
hint = sample_region(logits, image_size, (cx1, cy1, cx2, cy2), hint_size).clamp(-20.0, 20.0)
state = processor.set_image(image[:, cy1:cy2, cx1:cx2])
x1, y1, x2, y2 = target
prompt_box = np.array([x1 - cx1, y1 - cy1, x2 - cx1, y2 - cy1], dtype=np.float32)
masks, _, _ = model.predict_inst(state, box=prompt_box, mask_input=hint[None], multimask_output=False,
return_logits=True)
refined = np.asarray(masks).reshape(-1, cy2 - cy1, cx2 - cx1)[0] > 0
refined = drop_stray_parts(refined, coarse)
accepted = close_up_agrees(refined, coarse, max(image_size) / _PASS1_GRID + 2.0)
return make_segment(concept, score, refined if accepted else coarse, origin=(cx1, cy1)), accepted
def _image_chw(image):
"""One ComfyUI image [H, W, C] -> float [3, H, W] in [0, 1]."""
channels = image.shape[-1]
if channels == 1:
image = image.expand(-1, -1, 3)
elif channels not in (3, 4):
raise ValueError(f"[NH-Nodes] SAM 3 expects an IMAGE with 1, 3 or 4 channels, got {channels}")
return image[..., :3].permute(2, 0, 1).float().clamp(0.0, 1.0)
def segment_images(model, make_processor, images, concepts, threshold=0.5, detail_level="balanced",
max_per_concept=0, device=None, interrupt=None, progress=None):
"""NH_SEGS (one SegsFrame per image) for ``concepts`` found in the ComfyUI IMAGE batch ``images``.
``make_processor(threshold)`` returns a Sam3Processor for ``model`` on ``device``; the model must already be
there. Instances come in concept order, best score first within a concept. ``interrupt()`` runs before each
concept and each close-up (it raises to stop), ``progress(done, total)`` after each concept of each image.
"""
device = device or _model_device(model)
refine = detail_level != "fast"
hint_size = _hint_size(model) if refine else None
frames, kept_pass1, warned_wide = [], 0, False
done, total = 0, len(images) * len(concepts)
with torch.inference_mode(), _autocast(device):
for image in images:
height, width = int(image.shape[0]), int(image.shape[1])
if not warned_wide and max(height, width) > _WIDE_ASPECT * min(height, width):
warned_wide = True
print(f"[NH-Nodes] SAM 3 sees the whole {width}x{height} picture squeezed into a square, so on images "
"wider than 4:1 it can miss small things; split such a picture into tiles for better results")
pixels = _image_chw(image).to(device)
processor = make_processor(threshold)
size = int(getattr(processor, "resolution", 1008))
square = F.interpolate(pixels[None], size=(size, size), mode="bilinear", antialias=True,
align_corners=False)[0].clamp_(0.0, 1.0)
state = processor.set_image(square)
segments = []
for concept in concepts:
if interrupt is not None:
interrupt()
for score, logits, box in find_instances(processor, state, concept, max_per_concept):
if refine:
if interrupt is not None:
interrupt()
seg, refined = _refine(model, processor, pixels, concept, score, logits, box,
(height, width), hint_size)
kept_pass1 += 0 if refined else 1
else:
seg, _ = _upsample(concept, score, logits, box, (height, width))
if seg is not None:
segments.append(seg)
done += 1
if progress is not None:
progress(done, total)
frames.append(SegsFrame(height, width, tuple(segments)))
del pixels, square, state
if kept_pass1:
print(f"[NH-Nodes] SAM 3: kept the first-pass mask for {kept_pass1} instance(s) whose close-up disagreed")
return tuple(frames)
def segment(images, concepts, threshold=0.5, detail_level="balanced", max_per_concept=0, checkpoint=DEFAULT_CHECKPOINT,
keep_model_loaded=False, interrupt=None, progress=None):
"""Load SAM 3 and run ``segment_images`` on ComfyUI's GPU; the weights go back to RAM afterwards unless kept."""
if images.shape[0] == 0:
raise ValueError("[NH-Nodes] SAM 3 got an empty image batch")
model = load_model(checkpoint)
_, processor_cls = ensure_sam3()
device = sam3_device()
try:
move_model(model, device)
return segment_images(
model, lambda value: processor_cls(model, device=device, confidence_threshold=value), images, concepts,
threshold, detail_level, max_per_concept, device, interrupt, progress,
)
finally:
if not keep_model_loaded:
offload(model)
+62 -31
View File
@@ -3,6 +3,14 @@
Handles model discovery (reuse dirs already on disk before downloading),
per-model caching with CPU offload between runs, and full-resolution label
map prediction.
The model-folder, download and VRAM helpers it shares with the VTON node live
in model_files.py, so both model families follow the same rules. SegFormer
models are registered with ComfyUI's ``folder_paths`` under the name
``nh_nodes``; users can add more locations with that name in
``extra_model_paths.yaml`` and every registered location is searched before
anything is downloaded. Downloads always go to the default folder under the
models dir, pinned to a fixed Hugging Face revision.
"""
import os
@@ -12,35 +20,34 @@ import numpy as np
import torch
import torch.nn.functional as F
try:
import folder_paths
_MODELS_DIR = folder_paths.models_dir
except Exception: # running outside ComfyUI (tests)
_MODELS_DIR = os.path.join(os.path.dirname(__file__), "models")
try:
import comfy.model_management as _mm
except Exception:
_mm = None
from .model_files import download_pinned, make_room_on_gpu, models_dir, register_model_folder, unique_dirs
_REQUIRED_FILES = ("config.json", "model.safetensors", "preprocessor_config.json")
# Logits are interpolated at most to this size before argmax; the label map is
# then nearest-resized to the input. Keeps VRAM bounded on very large inputs.
_MAX_LOGIT_SIDE = 2048
# name -> HF repo, local dirs to probe before downloading (relative to models
# dir; other packs may already hold the same weights) and a label signature
# that guards against a different checkpoint sitting in a probed dir.
# extra_model_paths.yaml key for the SegFormer models. Each registered location holds
# one sub-folder per model (see MODEL_SPECS), e.g. <location>/segformer_fashion/config.json.
FOLDER_NAME = "nh_nodes"
FOLDER_EXTENSIONS = (".safetensors",) # what ComfyUI lists for FOLDER_NAME: the weights, not config.json or cache metadata
_DEFAULT_FOLDER = "NH-Nodes" # under the models dir: the download target, searched first by default
# name -> HF repo + pinned revision, legacy local dirs to probe after the registered
# folders (relative to the models dir; older versions of this pack and other packs may
# already hold the same weights) and a label signature that guards against a different
# checkpoint sitting in a probed dir.
MODEL_SPECS = {
"segformer_fashion": {
"repo_id": "1038lab/segformer_fashion",
"probe_dirs": ("NH-Nodes/segformer_fashion", "RMBG/segformer_fashion"),
"revision": "e5bae2d8e3333be9f12eb6aae454c0542c1371d3",
"legacy_dirs": ("RMBG/segformer_fashion",),
"signature": {1: "shirt, blouse", 24: "shoe", 46: "tassel"},
},
"face_parsing": {
"repo_id": "jonathandinu/face-parsing",
"probe_dirs": ("NH-Nodes/face_parsing", "face_parsing", "RMBG/segformer_face"),
"revision": "758b82e15a0178c9db39c1ff666a8b56e3a550c8",
"legacy_dirs": ("face_parsing", "RMBG/segformer_face"),
"signature": {1: "skin", 13: "hair", 17: "neck"},
},
}
@@ -49,35 +56,54 @@ _LOADED = {}
_warned_no_cuda = False
# ---------------------------------------------------------------------------
# Model folders and downloads (the shared helpers are in model_files.py)
# ---------------------------------------------------------------------------
def default_folder():
"""Where SegFormer models are downloaded and searched first: <models dir>/NH-Nodes."""
return os.path.join(models_dir(), _DEFAULT_FOLDER)
def _has_required_files(path):
return all(os.path.isfile(os.path.join(path, name)) for name in _REQUIRED_FILES)
def model_search_dirs(model_name):
"""Folders that may hold `model_name`, best match first.
The registered ``nh_nodes`` locations come first (models/NH-Nodes by default, plus whatever
extra_model_paths.yaml adds), then the legacy dirs under the models dir.
"""
spec = MODEL_SPECS[model_name]
dirs = [os.path.join(base, model_name)
for base in register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS)]
dirs += [os.path.normpath(os.path.join(models_dir(), *rel.split("/"))) for rel in spec["legacy_dirs"]]
return unique_dirs(dirs)
def resolve_model_dir(model_name, download=True):
"""Return a local dir holding the model, downloading into models/NH-Nodes if absent."""
spec = MODEL_SPECS[model_name]
for rel in spec["probe_dirs"]:
candidate = os.path.join(_MODELS_DIR, rel)
for candidate in model_search_dirs(model_name):
if _has_required_files(candidate):
return candidate
target = os.path.join(_MODELS_DIR, spec["probe_dirs"][0])
if not download:
return None
from huggingface_hub import snapshot_download
os.makedirs(target, exist_ok=True)
print(f"[NH-Nodes] Downloading {spec['repo_id']} -> {target}")
snapshot_download(
repo_id=spec["repo_id"],
local_dir=target,
allow_patterns=list(_REQUIRED_FILES),
)
target = os.path.join(default_folder(), model_name)
download_pinned(spec["repo_id"], spec["revision"], target, _REQUIRED_FILES)
if not _has_required_files(target):
raise RuntimeError(f"[NH-Nodes] Model files missing after download: {target}")
return target
# Make the folder name known to ComfyUI as soon as the pack loads.
register_model_folder(FOLDER_NAME, default_folder(), FOLDER_EXTENSIONS)
# ---------------------------------------------------------------------------
# SegFormer inference
# ---------------------------------------------------------------------------
def _torch_device(device):
global _warned_no_cuda
if device == "cuda":
@@ -121,14 +147,19 @@ def load_segformer(model_name, device="cuda"):
return bundle
def _is_on(model_device, wanted):
"""True when `model_device` satisfies `wanted`; an index-less "cuda" accepts any cuda:N."""
return model_device.type == wanted.type and (wanted.index is None or model_device.index == wanted.index)
def _ensure_on_device(bundle):
model, device = bundle["model"], bundle["device"]
if model.device == device:
if _is_on(model.device, device):
return
if device.type == "cuda" and _mm is not None:
if device.type == "cuda":
# Let ComfyUI evict its own models first instead of OOM-ing on ours.
size = sum(p.numel() * p.element_size() for p in model.parameters())
_mm.free_memory(size * 2, device)
make_room_on_gpu(size * 2)
model.to(device)
+515
View File
@@ -0,0 +1,515 @@
"""NH_SEGS: the instances the NH vision nodes pass between each other.
An ``NH_SEGS`` value is a tuple with one ``SegsFrame`` per image of a batch. A frame holds the image size and the
``Segment`` instances found in that image. A segment keeps its label, score and box, and only the mask pixels inside
its box, so dozens of instances on an 8K image stay small. Full-size masks are built only when a node outputs a MASK.
Plain torch/numpy, no model: the select, compose, preview and convert nodes and their tests run anywhere.
"""
import fnmatch
import functools
from collections import namedtuple
from dataclasses import dataclass
import cv2
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageFont
SEGS_TYPE = "NH_SEGS"
@dataclass(frozen=True, eq=False)
class Segment:
"""One instance found in an image.
``box`` is (x1, y1, x2, y2) in image pixels with exclusive ends, so the instance covers ``image[y1:y2, x1:x2]``;
it is the tight box around the mask. ``mask`` is a bool [y2 - y1, x2 - x1] CPU tensor over that box.
"""
label: str
score: float
box: tuple
mask: torch.Tensor
source: str = "sam3"
@property
def area(self):
"""Number of mask pixels."""
return int(self.mask.sum())
@property
def center(self):
x1, y1, x2, y2 = self.box
return (x1 + x2) / 2.0, (y1 + y2) / 2.0
@dataclass(frozen=True, eq=False)
class SegsFrame:
"""The instances found in one image of ``height`` x ``width`` pixels, in the order they were found."""
height: int
width: int
segments: tuple = ()
def with_segments(self, segments):
return SegsFrame(self.height, self.width, tuple(segments))
def _comfy_cache_tensors(self):
"""The tensors this frame holds, so ComfyUI's RAM-pressure cache can size it."""
return [seg.mask for seg in self.segments]
def make_segment(label, score, mask, origin=(0, 0), source="sam3"):
"""Segment for the True pixels of the bool [h, w] ``mask`` whose top-left pixel sits at ``origin`` (x, y) of the image.
The stored box is the tight box around those pixels and the stored mask a compact CPU copy of that box. Works on
any device, cropping before the copy to the CPU. Returns None when the mask is empty.
"""
mask = torch.as_tensor(mask)
if mask.dtype != torch.bool:
mask = mask > 0.5
rows = torch.nonzero(mask.any(dim=1)).flatten()
if rows.numel() == 0:
return None
cols = torch.nonzero(mask.any(dim=0)).flatten()
y1, y2 = int(rows[0]), int(rows[-1]) + 1
x1, x2 = int(cols[0]), int(cols[-1]) + 1
crop = mask[y1:y2, x1:x2].to("cpu").clone(memory_format=torch.contiguous_format)
ox, oy = int(origin[0]), int(origin[1])
return Segment(str(label), float(score), (ox + x1, oy + y1, ox + x2, oy + y2), crop, source)
def frame_mask(frame, segments=None):
"""Bool [H, W] union of ``segments`` (every segment of ``frame`` by default)."""
canvas = torch.zeros((frame.height, frame.width), dtype=torch.bool)
for seg in frame.segments if segments is None else segments:
x1, y1, x2, y2 = seg.box
canvas[y1:y2, x1:x2] |= seg.mask
return canvas
def segs_union(segs):
"""MASK [B, H, W] float: the union of every instance, one mask per image."""
return torch.stack([frame_mask(frame) for frame in segs]).float()
def check_memory(nbytes, what):
"""Raise MemoryError with advice when ``nbytes`` would take more than half of the RAM that is free now."""
try:
import psutil
available = psutil.virtual_memory().available
except Exception: # no psutil: nothing to compare with
return
if nbytes > available // 2:
raise MemoryError(
f"[NH-Nodes] {what} would need {nbytes / 2**30:.1f} GiB but only {available / 2**30:.1f} GiB of RAM is "
"free. Keep fewer instances with Segs Select, or combine them into one mask with Segs Compose."
)
def instance_masks(segs):
"""(MASK [N, H, W] float with one full-size mask per instance of every image, in order; the labels).
With no instance at all the MASK holds a single empty mask, as nodes downstream may not accept an empty batch.
Each full-size mask takes H x W x 4 bytes (about 145 MB on an 8K image), so a MemoryError explains what to do
when they would not fit in RAM.
"""
labels = [seg.label for frame in segs for seg in frame.segments]
height, width = segs[0].height, segs[0].width
check_memory(max(1, len(labels)) * height * width * 4, f"{len(labels)} full-size instance masks")
masks = torch.zeros((max(1, len(labels)), height, width))
index = 0
for frame in segs:
for seg in frame.segments:
x1, y1, x2, y2 = seg.box
masks[index, y1:y2, x1:x2] = seg.mask.float()
index += 1
return masks, labels
def count_segments(segs):
return sum(len(frame.segments) for frame in segs)
# ---------------------------------------------------------------------------
# Labels
# ---------------------------------------------------------------------------
def normalize_label(text):
"""Lower case, '_' as a space, single spaces: "Red_Car " and "red car" both become "red car"."""
return " ".join(str(text).replace("_", " ").lower().split())
def label_matches(label, pattern):
"""True when ``label`` matches ``pattern``, ignoring case; the pattern may use * and ? wildcards."""
return fnmatch.fnmatchcase(normalize_label(label), normalize_label(pattern))
def parse_label_list(text):
"""Commas and new lines separate entries: "person, red car" -> ["person", "red car"].
Blank entries and lines starting with '#' are dropped; a repeated entry (ignoring case and spacing) is kept once,
with its first spelling.
"""
entries, seen = [], set()
for line in str(text or "").splitlines():
if line.strip().startswith("#"):
continue
for part in line.split(","):
entry = " ".join(part.split())
key = normalize_label(entry)
if entry and key not in seen:
seen.add(key)
entries.append(entry)
return entries
# ---------------------------------------------------------------------------
# Select
# ---------------------------------------------------------------------------
SORT_ORDERS = ("score", "area", "left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top",
"center_first", "as_found")
def sort_segments(segments, order, frame):
"""``segments`` in ``order`` (one of SORT_ORDERS); positions use each box's centre. Ties keep their found order."""
segments = list(segments)
if order == "score":
return sorted(segments, key=lambda s: -s.score)
if order == "area":
return sorted(segments, key=lambda s: -s.area)
if order == "left_to_right":
return sorted(segments, key=lambda s: s.center[0])
if order == "right_to_left":
return sorted(segments, key=lambda s: -s.center[0])
if order == "top_to_bottom":
return sorted(segments, key=lambda s: s.center[1])
if order == "bottom_to_top":
return sorted(segments, key=lambda s: -s.center[1])
if order == "center_first":
cx, cy = frame.width / 2.0, frame.height / 2.0
return sorted(segments, key=lambda s: (s.center[0] - cx) ** 2 + (s.center[1] - cy) ** 2)
return segments
def select_segments(frame, labels=(), min_score=0.0, min_area=0.0, max_area=1.0, order="score", start=0, count=0):
"""Frame with the segments that pass the filters, sorted by ``order``, from position ``start`` (0 = first).
``labels`` are patterns (any one must match; none = every label). Areas are fractions of the image. ``count`` = 0
keeps every segment from ``start`` on.
"""
total = max(1, frame.height * frame.width)
kept = []
for seg in frame.segments:
if labels and not any(label_matches(seg.label, pattern) for pattern in labels):
continue
if seg.score < min_score:
continue
if not min_area <= seg.area / total <= max_area:
continue
kept.append(seg)
kept = sort_segments(kept, order, frame)
start = max(0, int(start))
kept = kept[start:] if count <= 0 else kept[start:start + int(count)]
return frame.with_segments(kept)
# ---------------------------------------------------------------------------
# Compose: a small boolean language over labels
# ---------------------------------------------------------------------------
class ExpressionError(ValueError):
"""An NH_SEGS mask expression that cannot be read."""
_OPERATORS = {"+": "|", "|": "|", "-": "-", "&": "&", "~": "~", "!": "~", "(": "(", ")": ")"}
_WORD_OPERATORS = {"or": "|", "and": "&", "not": "~"}
_KEYWORDS = ("all", "image", "none")
_QUOTES = "\"'"
def _is_word_char(ch):
return ch.isalnum() or ch == "_"
def _tokenize(text):
"""[(kind, value, position, written)] with kinds "op", "keyword", "label"; positions count from 1.
A quote opens a quoted label only where a token starts, and a '-' between two word characters is part of the
word, so ``women's`` and ``t-shirt`` are plain labels while ``person - hair`` subtracts.
"""
tokens, i, n = [], 0, len(text)
while i < n:
ch = text[i]
if ch.isspace():
i += 1
elif ch in _OPERATORS:
tokens.append(("op", _OPERATORS[ch], i + 1, ch))
i += 1
elif ch in _QUOTES:
end = text.find(ch, i + 1)
if end < 0:
raise ExpressionError(f"quote opened at position {i + 1} is never closed")
tokens.append(("label", text[i + 1:end], i + 1, text[i:end + 1]))
i = end + 1
else:
j = i
while j < n and not text[j].isspace():
hyphen_in_word = (text[j] == "-" and _is_word_char(text[j - 1])
and j + 1 < n and _is_word_char(text[j + 1]))
if text[j] in _OPERATORS and not hyphen_in_word:
break
j += 1
word = text[i:j]
lowered = word.lower()
if lowered in _WORD_OPERATORS:
tokens.append(("op", _WORD_OPERATORS[lowered], i + 1, word))
elif lowered in _KEYWORDS:
tokens.append(("keyword", lowered, i + 1, word))
else:
tokens.append(("label", word, i + 1, word))
i = j
return tokens
class _Parser:
"""expr := term (('|' | '-') term)* ; term := unary ('&' unary)* ; unary := '~' unary | atom ;
atom := label | keyword | '(' expr ')'. Builds a tuple tree evaluated by ``evaluate``."""
def __init__(self, text):
self.tokens = _tokenize(text)
self.pos = 0
def _peek(self):
return self.tokens[self.pos] if self.pos < len(self.tokens) else None
def _where(self):
token = self._peek()
return f"'{token[3]}' at position {token[2]}" if token else "the end"
def parse(self):
if not self.tokens:
raise ExpressionError("the expression is empty; use 'all' for every instance")
tree = self._expr()
if self._peek() is not None:
raise ExpressionError(f"unexpected {self._where()}")
return tree
def _expr(self):
tree = self._term()
while (token := self._peek()) is not None and token[0] == "op" and token[1] in ("|", "-"):
self.pos += 1
tree = (token[1], tree, self._term())
return tree
def _term(self):
tree = self._unary()
while (token := self._peek()) is not None and token[0] == "op" and token[1] == "&":
self.pos += 1
tree = ("&", tree, self._unary())
return tree
def _unary(self):
token = self._peek()
if token is not None and token[0] == "op" and token[1] == "~":
self.pos += 1
return ("~", self._unary())
return self._atom()
def _atom(self):
token = self._peek()
if token is None:
raise ExpressionError("the expression ends where a label, 'all', 'image', 'none' or '(' was expected")
kind, value, position, written = token
self.pos += 1
if kind == "label":
return ("label", value)
if kind == "keyword":
return ("keyword", value)
if value == "(":
tree = self._expr()
closing = self._peek()
if closing is None or closing[:2] != ("op", ")"):
raise ExpressionError(f"'(' at position {position} is not closed (found {self._where()})")
self.pos += 1
return tree
raise ExpressionError(f"unexpected '{written}' at position {position}")
@functools.lru_cache(maxsize=64)
def parse_expression(text):
"""Parse a mask expression (see ``evaluate``) into a tree; raises ExpressionError with the position on mistakes."""
return _Parser(str(text)).parse()
def expression_labels(tree):
"""Label patterns used in a parsed expression, in order."""
if tree[0] == "label":
return [tree[1]]
if tree[0] == "keyword":
return []
return [label for child in tree[1:] for label in expression_labels(child)]
def evaluate(tree, frame):
"""Bool [H, W] mask of a parsed expression for one frame.
A label (wildcards allowed; quotes or '_' for spaces; a '-' inside a word is part of it) is the union of the
matching instances,
``all`` every instance, ``image`` the whole frame, ``none`` nothing. ``a | b`` (also ``+``, ``or``) is the union,
``a - b`` the difference, ``a & b`` (``and``) the intersection, ``~a`` (``!``, ``not``) everything outside ``a``.
"""
kind = tree[0]
if kind == "label":
return frame_mask(frame, [seg for seg in frame.segments if label_matches(seg.label, tree[1])])
if kind == "keyword":
if tree[1] == "all":
return frame_mask(frame)
if tree[1] == "image":
return torch.ones((frame.height, frame.width), dtype=torch.bool)
return torch.zeros((frame.height, frame.width), dtype=torch.bool)
if kind == "~":
return ~evaluate(tree[1], frame)
left, right = evaluate(tree[1], frame), evaluate(tree[2], frame)
if kind == "|":
return left | right
if kind == "&":
return left & right
return left & ~right
def expression_masks(tree, segs):
"""MASK [B, H, W] float of a parsed expression, one mask per image of ``segs``."""
return torch.stack([evaluate(tree, frame) for frame in segs]).float()
# ---------------------------------------------------------------------------
# Impact Pack SEGS
# ---------------------------------------------------------------------------
# The field names of Impact Pack's SEG, so its detailers read these items; NH-Nodes does not import Impact Pack.
IMPACT_SEG = namedtuple("SEG", ["cropped_image", "cropped_mask", "confidence", "crop_region", "bbox", "label",
"control_net_wrapper"], defaults=[None])
def _centred_span(lo, hi, factor, limit):
"""[start, end) of ``factor`` times the length of [lo, hi), centred on it and slid inside [0, limit).
It is never shorter than [lo, hi) and only shorter than asked when the image is, so it always contains [lo, hi).
"""
size = min(limit, max(hi - lo, round((hi - lo) * factor)))
start = min(max(0, round((lo + hi - size) / 2)), limit - size)
return start, start + size
def detail_crop_region(width, height, box, crop_factor):
"""[x1, y1, x2, y2] a detailer works on around ``box``: ``crop_factor`` times its size, centred, inside the image."""
rx1, rx2 = _centred_span(box[0], box[2], crop_factor, width)
ry1, ry2 = _centred_span(box[1], box[3], crop_factor, height)
return [rx1, ry1, rx2, ry2]
def to_impact_segs(frame, crop_factor=3.0):
"""Impact Pack SEGS ``((height, width), [SEG, ...])`` for one frame; each mask is placed in its crop region."""
items = []
for seg in frame.segments:
region = detail_crop_region(frame.width, frame.height, seg.box, crop_factor)
rx1, ry1, rx2, ry2 = region
x1, y1, x2, y2 = seg.box
cropped = np.zeros((ry2 - ry1, rx2 - rx1), dtype=np.float32)
cropped[y1 - ry1:y2 - ry1, x1 - rx1:x2 - rx1] = seg.mask.numpy()
items.append(IMPACT_SEG(None, cropped, float(seg.score), region, [x1, y1, x2, y2], seg.label, None))
return (frame.height, frame.width), items
# ---------------------------------------------------------------------------
# Preview
# ---------------------------------------------------------------------------
_PALETTE = (
(230, 25, 75), (60, 180, 75), (255, 225, 25), (0, 130, 200), (245, 130, 48), (145, 30, 180),
(70, 240, 240), (240, 50, 230), (210, 245, 60), (250, 190, 212), (0, 128, 128), (170, 110, 40),
)
@functools.lru_cache(maxsize=16)
def _label_font(size):
for name in ("arial.ttf", "DejaVuSans.ttf"): # both cover Vietnamese
try:
return ImageFont.truetype(name, size)
except Exception:
continue
try:
return ImageFont.load_default(size=size)
except TypeError: # Pillow < 10.1
return ImageFont.load_default()
def _free_spot(x, y, width, height, placed, limit):
"""``y`` moved down (or up near the bottom ``limit``) until a ``width`` x ``height`` label at ``x`` overlaps none
of the ``placed`` (x1, y1, x2, y2) rectangles; labels of instances sharing a top edge (person, hair) then stack."""
for _ in range(len(placed)):
hit = next((r for r in placed if x < r[2] and r[0] < x + width and y < r[3] and r[1] < y + height), None)
if hit is None:
break
y = hit[3] + 1 if hit[3] + 1 + height <= limit else max(0, hit[1] - height - 1)
return y
def render_preview(image, frame, fill=0.45):
"""uint8 HxWx3 copy of ``image`` with each instance tinted, outlined and labelled "label score"."""
canvas = np.ascontiguousarray(image[:, :, :3]).copy()
height, width = canvas.shape[:2]
if not frame.segments:
return canvas
thickness = max(1, round(min(height, width) / 700))
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * thickness + 1, 2 * thickness + 1))
for index, seg in enumerate(frame.segments):
color = np.array(_PALETTE[index % len(_PALETTE)], dtype=np.float32)
x1, y1, x2, y2 = seg.box
region = canvas[y1:y2, x1:x2]
inside = seg.mask.numpy()
region[inside] = (region[inside] * (1.0 - fill) + color * fill).astype(np.uint8)
padded = np.pad(inside.astype(np.uint8), thickness) # so a box edge counts as an outline too
edge = (padded > 0) & (cv2.erode(padded, kernel) == 0)
region[edge[thickness:-thickness, thickness:-thickness]] = color.astype(np.uint8)
font = _label_font(max(12, round(min(height, width) / 45)))
picture = Image.fromarray(canvas)
draw = ImageDraw.Draw(picture)
placed = [] # label rectangles drawn so far, so a label never hides another one
for index, seg in enumerate(frame.segments):
color = _PALETTE[index % len(_PALETTE)]
text = f"{seg.label} {seg.score:.2f}"
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
pad = max(2, (bottom - top) // 6)
tw, th = right - left + 2 * pad, bottom - top + 2 * pad
# centred over the instance's topmost pixels (a head, a roof), not at a box corner that may be far from it
top_columns = torch.nonzero(seg.mask[0]).flatten()
anchor = seg.box[0] + int(top_columns[top_columns.numel() // 2]) if top_columns.numel() else seg.box[0]
x = min(max(0, anchor - tw // 2), max(0, width - tw))
y = _free_spot(x, seg.box[1] - th if seg.box[1] - th >= 0 else seg.box[1], tw, th, placed, height)
placed.append((x, y, x + tw, y + th))
draw.rectangle((x, y, x + tw, y + th), fill=color)
dark_text = (0.299 * color[0] + 0.587 * color[1] + 0.114 * color[2]) > 150
draw.text((x + pad - left, y + pad - top), text, font=font, fill=(0, 0, 0) if dark_text else (255, 255, 255))
return np.asarray(picture)
def preview_batch(image, segs):
"""IMAGE batch with every instance of ``segs`` drawn on its image (tinted, outlined, labelled)."""
if len(segs) != image.shape[0]:
raise ValueError(
f"[NH-Nodes] The image batch has {image.shape[0]} image(s) but the segs belong to {len(segs)}; "
"connect the image the segs were found in."
)
pictures = []
for picture, frame in zip(image, segs):
if (picture.shape[0], picture.shape[1]) != (frame.height, frame.width):
raise ValueError(
f"[NH-Nodes] The image is {picture.shape[1]}x{picture.shape[0]} but the segs were found on a "
f"{frame.width}x{frame.height} image; connect the image the segs were found in."
)
rgb = picture[..., :3] if picture.shape[-1] >= 3 else picture[..., :1].expand(-1, -1, 3)
pixels = (rgb.float().clamp(0.0, 1.0) * 255.0).round().to(torch.uint8).cpu().numpy()
pictures.append(torch.from_numpy(render_preview(pixels, frame).astype(np.float32) / 255.0))
return torch.stack(pictures)
+45 -6
View File
@@ -11,12 +11,34 @@ def resize_image(image, target_size=768):
new_h, new_w = target_size, int(w * target_size / h)
return image.resize((new_w, new_h), Image.LANCZOS)
def tensor_to_pil(tensor_image):
"""Convert tensor to PIL image"""
def tensor_to_pil(tensor_image, *, rgb=False, keep_alpha=False):
"""Convert ONE ComfyUI image ([H,W,C] or [1,H,W,C]) to PIL.
Values are clipped to [0, 1] first, so overshoot from resampling cannot wrap around
in uint8. 1/3/4 channels become L/RGB/RGBA. Batches must be converted per image.
``rgb=True`` always gives an RGB picture: grey is expanded and alpha is dropped. With ``keep_alpha=True`` as
well, a 4-channel image stays RGBA instead (every other image is still RGB).
"""
if tensor_image.dim() == 4:
tensor_image = tensor_image.squeeze(0)
np_image = (tensor_image.cpu().numpy() * 255).astype(np.uint8)
return Image.fromarray(np_image)
if tensor_image.shape[0] != 1:
raise ValueError(
f"tensor_to_pil expects a single image but got a batch of {tensor_image.shape[0]}; "
"convert each image of the batch separately"
)
tensor_image = tensor_image[0]
pixels = tensor_image.detach().cpu()
if pixels.dtype == torch.bfloat16: # numpy has no bfloat16; other dtypes keep their own rounding
pixels = pixels.float()
np_image = np.clip(pixels.numpy() * 255.0, 0, 255).astype(np.uint8)
if np_image.ndim == 3 and np_image.shape[2] == 1:
np_image = np_image[:, :, 0]
pil_image = Image.fromarray(np_image)
if rgb:
kept_modes = ("RGB", "RGBA") if keep_alpha else ("RGB",)
if pil_image.mode not in kept_modes:
pil_image = pil_image.convert("RGB")
return pil_image
def pil_to_tensor(pil_image):
"""Convert PIL image to tensor in ComfyUI format for IMAGE"""
@@ -32,4 +54,21 @@ def pil_to_mask(pil_image):
if len(np_image.shape) == 3: # Ensure single channel
np_image = np_image[:,:,0]
tensor = torch.from_numpy(np_image).unsqueeze(0)
return tensor
return tensor
def parse_hex_color(value, default=(0, 0, 0, 255)):
"""Parse '#RGB', '#RRGGBB' or '#RRGGBBAA' (the '#' is optional) into an (r, g, b, a) tuple of 0-255 ints.
Without an alpha part the colour is opaque. Text that is not a valid colour returns ``default``.
"""
text = str(value or "").strip().lstrip("#")
if len(text) == 3:
text = "".join(ch * 2 for ch in text)
if len(text) == 6:
text += "ff"
if len(text) != 8:
return tuple(default)
try:
return tuple(int(text[i:i + 2], 16) for i in range(0, 8, 2))
except ValueError:
return tuple(default)
+92 -103
View File
@@ -1,7 +1,8 @@
# Garment mask construction for VTON Ultimate Processor (NH).
# Third-party code: FitDiT src/utils_mask.py (https://github.com/BoyuanJiang/FitDiT),
# received via ComfyUI-Vton-Mask. License: CC BY-NC-SA 4.0 (non-commercial), not the
# pack's MIT license. NH-Nodes changes: lint cleanups and bug fixes. See THIRD_PARTY_NOTICES.md.
# pack's MIT license. NH-Nodes changes: lint cleanups, bug fixes and Image.fromarray calls without the mode
# argument that Pillow 11.3 deprecated. See THIRD_PARTY_NOTICES.md.
import numpy as np
import cv2
from PIL import Image, ImageDraw
@@ -15,11 +16,27 @@ def remove_small(binary_mask, min_area=10000):
small_white_regions = [region for region in regions if region.area < min_area]
for region in small_white_regions:
min_row, min_col, max_row, max_col = region.bbox
binary_mask[min_row:max_row, min_col:max_col] = 0
# Only this region's pixels: its bounding box can overlap a large region.
binary_mask[tuple(region.coords.T)] = 0
return binary_mask
def _merge_bound(keypoint_bound, cloth_bound, pick):
"""Combine a keypoint bound (None when the whole keypoint group is missing) with the garment contour bound."""
if keypoint_bound is None:
return cloth_bound
return pick(keypoint_bound, cloth_bound)
def _fallback_box(x1, y1, x2, y2, shape):
"""Whole-image edges only for bounds that neither keypoints nor the garment contour provided."""
height, width = shape[:2]
return (0 if x1 is None else x1, 0 if y1 is None else y1,
width if x2 is None else x2, height if y2 is None else y2)
# pose_data rows: body 0-17, foot 18-23, face 24-91, hands 92-133. A keypoint that was not detected is
# negative (DWPose marks it -1 and the node then scales it by the width/height), so only values > 0 count.
def get_img_agnostic_upper_rectangle(im_parse, pose_data, offset_top, offset_bottom, offset_left, offset_right):
faces = pose_data[24:92]
@@ -69,48 +86,37 @@ def get_img_agnostic_upper_rectangle(im_parse, pose_data, offset_top, offset_bot
except Exception:
cloth_exist = False
x1 = [body[i, 0] for i in [2, 3, 4] if body[i, 0] != 0]+\
[hands2[i, 0] for i in [5,9,13] if hands2[i, 0] != 0]+\
[hands1[i, 0] for i in [5,9,13] if hands1[i, 0] != 0]
if x1:
x1 = np.min(x1)
else:
x1 = 0
y1 = [body[i, 1] for i in [2, 5] if body[i, 1] != 0]+\
[hands2[i, 1] for i in [5,9,13] if hands2[i, 1] != 0]+\
[hands1[i, 1] for i in [5,9,13] if hands1[i, 1] != 0]
if y1:
y1 = np.min(y1)
else:
y1 = 0
x2 = [body[i, 0] for i in [5, 6, 7] if body[i, 0] != 0]+\
[hands1[i, 0] for i in [5,9,13] if hands1[i, 0] != 0]+\
[hands2[i, 0] for i in [5,9,13] if hands2[i, 0] != 0]
if x2:
x2 = np.max(x2)
else:
x2 = parse_array.shape[1]
y2 = [body[i, 1] for i in [8, 11] if body[i, 1] != 0]+\
[hands2[i, 1] for i in [5,9,13] if hands2[i, 1] != 0]+\
[hands1[i, 1] for i in [5,9,13] if hands1[i, 1] != 0]
if y2:
y2 = np.max(y2)
else:
y2 = parse_array.shape[0]
x1 = [body[i, 0] for i in [2, 3, 4] if body[i, 0] > 0]+\
[hands2[i, 0] for i in [5,9,13] if hands2[i, 0] > 0]+\
[hands1[i, 0] for i in [5,9,13] if hands1[i, 0] > 0]
x1 = np.min(x1) if x1 else None
y1 = [body[i, 1] for i in [2, 5] if body[i, 1] > 0]+\
[hands2[i, 1] for i in [5,9,13] if hands2[i, 1] > 0]+\
[hands1[i, 1] for i in [5,9,13] if hands1[i, 1] > 0]
y1 = np.min(y1) if y1 else None
x2 = [body[i, 0] for i in [5, 6, 7] if body[i, 0] > 0]+\
[hands1[i, 0] for i in [5,9,13] if hands1[i, 0] > 0]+\
[hands2[i, 0] for i in [5,9,13] if hands2[i, 0] > 0]
x2 = np.max(x2) if x2 else None
y2 = [body[i, 1] for i in [8, 11] if body[i, 1] > 0]+\
[hands2[i, 1] for i in [5,9,13] if hands2[i, 1] > 0]+\
[hands1[i, 1] for i in [5,9,13] if hands1[i, 1] > 0]
y2 = np.max(y2) if y2 else None
pad_y1 = 20
pad_y2 = 10
pad_x1 = pad_x2 = 25
if cloth_exist:
x1 = min(x1, _x1)
x2 = max(x2, _x2)
y1 = min(y1, _y1)
if y2>_y2:
x1 = _merge_bound(x1, _x1, min)
x2 = _merge_bound(x2, _x2, max)
y1 = _merge_bound(y1, _y1, min)
if y2 is not None and y2>_y2:
pad_y2 = 0
y2 = max(y2, _y2)
y2 = _merge_bound(y2, _y2, max)
x1, y1, x2, y2 = _fallback_box(x1, y1, x2, y2, parse_array.shape)
y_face = [faces[i, 1] for i in [5, 11] if faces[i, 1] != 0]
y_face = [faces[i, 1] for i in [5, 11] if faces[i, 1] > 0]
if y_face:
y_face = np.mean(y_face)
@@ -120,8 +126,8 @@ def get_img_agnostic_upper_rectangle(im_parse, pose_data, offset_top, offset_bot
agnostic_draw.rectangle((max(0, x1-pad_x1+offset_left), max(0, y1-pad_y1+offset_top), min(x2+pad_x2+offset_right, parse_array.shape[1]), min(y2+pad_y2+offset_bottom, parse_array.shape[0])), 'gray', 'gray')
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255), 'L'))
# agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255)))
# agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255)))
mask_gray = agnostic.copy()
mask = agnostic.point(lambda p: p == 128 and 255)
@@ -180,33 +186,27 @@ def get_img_agnostic_lower_rectangle(im_parse, pose_data, offset_top, offset_bot
except Exception:
all_cloth_exist = False
x1 = [body[i, 0] for i in [8, 9, 10] if body[i, 0] != 0]+[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] != 0]+\
[body[i, 0] for i in [11, 12, 13] if body[i, 0] != 0]+[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] != 0]
if x1:
x1 = np.min(x1)
else:
x1=0
y1 = min(body[8, 1], body[11, 1])
x2 = [body[i, 0] for i in [11, 12, 13] if body[i, 0] != 0]+[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] != 0]+\
[body[i, 0] for i in [8, 9, 10] if body[i, 0] != 0]+[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] != 0]
if x2:
x2 = np.max(x2)
else:
x2 = parse_array.shape[1]
y2 = [body[i, 1] for i in [10, 13] if body[i, 1] != 0]+[foot1[i, 1] for i in [2] if foot2[i, 1] != 0]+\
[foot2[i, 1] for i in [2] if foot2[i, 1] != 0]
if y2:
y2 = np.max(y2)
else:
y2 = parse_array.shape[0]
x1 = [body[i, 0] for i in [8, 9, 10] if body[i, 0] > 0]+[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] > 0]+\
[body[i, 0] for i in [11, 12, 13] if body[i, 0] > 0]+[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] > 0]
x1 = np.min(x1) if x1 else None
# Keypoints that were not detected are negative (DWPose marks them -1): use only the detected hips.
y1 = [body[i, 1] for i in [8, 11] if body[i, 1] > 0]
y1 = np.min(y1) if y1 else None
x2 = [body[i, 0] for i in [11, 12, 13] if body[i, 0] > 0]+[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] > 0]+\
[body[i, 0] for i in [8, 9, 10] if body[i, 0] > 0]+[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] > 0]
x2 = np.max(x2) if x2 else None
y2 = [body[i, 1] for i in [10, 13] if body[i, 1] > 0]+[foot1[i, 1] for i in [2] if foot1[i, 1] > 0]+\
[foot2[i, 1] for i in [2] if foot2[i, 1] > 0]
y2 = np.max(y2) if y2 else None
if cloth_exist:
x1 = min(x1, _x1)
x2 = max(x2, _x2)
y1 = min(y1, _y1)
y2 = max(y2, _y2)
x1 = _merge_bound(x1, _x1, min)
x2 = _merge_bound(x2, _x2, max)
y1 = _merge_bound(y1, _y1, min)
y2 = _merge_bound(y2, _y2, max)
elif all_cloth_exist:
x1 = min(x1, _x1)
x2 = max(x2, _x2)
x1 = _merge_bound(x1, _x1, min)
x2 = _merge_bound(x2, _x2, max)
x1, y1, x2, y2 = _fallback_box(x1, y1, x2, y2, parse_array.shape)
pad_y1 = 5 if cloth_exist else 50
pad_y2 = 10
@@ -214,8 +214,8 @@ def get_img_agnostic_lower_rectangle(im_parse, pose_data, offset_top, offset_bot
agnostic_draw.rectangle((max(0, x1-pad_x1+offset_left), max(0, y1-pad_y1+offset_top), min(x2+pad_x2+offset_right, parse_array.shape[1]), min(y2+pad_y2+offset_bottom, parse_array.shape[0])), 'gray', 'gray')
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255)))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255)))
mask_gray = agnostic.copy()
mask = agnostic.point(lambda p: p == 128 and 255)
@@ -266,48 +266,37 @@ def get_img_agnostic_dresses_rectangle(im_parse, pose_data, offset_top, offset_b
except Exception:
cloth_exist = False
x1 = [body[i, 0] for i in [2, 3, 4] if body[i, 0] != 0]+\
[body[i, 0] for i in [8, 9, 10] if body[i, 0] != 0]+\
[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] != 0]+\
[hands2[i, 0] for i in [5, 9, 13, 17] if hands2[i, 0] != 0]
if x1:
x1 = np.min(x1)
else:
x1 = 0
y1 = [body[2, 1], body[5, 1]]+\
[hands2[i, 1] for i in [5, 9, 13, 17] if hands2[i, 1] != 0]+\
[hands1[i, 1] for i in [5, 9, 13, 17] if hands1[i, 1] != 0]
if y1:
y1 = np.min(y1)
else:
y1 = 0
x2 = [body[i, 0] for i in [5, 6, 7] if body[i, 0] != 0]+\
[body[i, 0] for i in [11, 12, 13] if body[i, 0] != 0]+\
[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] != 0]+\
[hands1[i, 0] for i in [5, 9, 13, 17] if hands1[i, 0] != 0]
if x2:
x2 = np.max(x2)
else:
x2 = parse_array.shape[1]
y2 = [body[i, 1] for i in [10, 13] if body[i, 1] != 0]+[foot1[i, 1] for i in [0, 1, 2] if foot1[i, 1] != 0]+\
[foot2[i, 1] for i in [0, 1, 2] if foot2[i, 1] != 0]
if y2:
y2 = np.max(y2)
else:
y2 = parse_array.shape[0]
x1 = [body[i, 0] for i in [2, 3, 4] if body[i, 0] > 0]+\
[body[i, 0] for i in [8, 9, 10] if body[i, 0] > 0]+\
[foot2[i, 0] for i in [0, 1, 2] if foot2[i, 0] > 0]+\
[hands2[i, 0] for i in [5, 9, 13, 17] if hands2[i, 0] > 0]
x1 = np.min(x1) if x1 else None
y1 = [body[i, 1] for i in [2, 5] if body[i, 1] > 0]+\
[hands2[i, 1] for i in [5, 9, 13, 17] if hands2[i, 1] > 0]+\
[hands1[i, 1] for i in [5, 9, 13, 17] if hands1[i, 1] > 0]
y1 = np.min(y1) if y1 else None
x2 = [body[i, 0] for i in [5, 6, 7] if body[i, 0] > 0]+\
[body[i, 0] for i in [11, 12, 13] if body[i, 0] > 0]+\
[foot1[i, 0] for i in [0, 1, 2] if foot1[i, 0] > 0]+\
[hands1[i, 0] for i in [5, 9, 13, 17] if hands1[i, 0] > 0]
x2 = np.max(x2) if x2 else None
y2 = [body[i, 1] for i in [10, 13] if body[i, 1] > 0]+[foot1[i, 1] for i in [0, 1, 2] if foot1[i, 1] > 0]+\
[foot2[i, 1] for i in [0, 1, 2] if foot2[i, 1] > 0]
y2 = np.max(y2) if y2 else None
if cloth_exist:
x1 = min(x1, _x1)
x2 = max(x2, _x2)
y1 = min(y1, _y1)
y2 = max(y2, _y2)
x1 = _merge_bound(x1, _x1, min)
x2 = _merge_bound(x2, _x2, max)
y1 = _merge_bound(y1, _y1, min)
y2 = _merge_bound(y2, _y2, max)
x1, y1, x2, y2 = _fallback_box(x1, y1, x2, y2, parse_array.shape)
pad_y1 = 20
pad_y2 = 10
pad_x1 = pad_x2 = 50
y_face = [faces[i, 1] for i in [5, 11] if faces[i, 1] != 0]
y_face = [faces[i, 1] for i in [5, 11] if faces[i, 1] > 0]
if y_face:
y_face = np.mean(y_face)
@@ -317,9 +306,9 @@ def get_img_agnostic_dresses_rectangle(im_parse, pose_data, offset_top, offset_b
agnostic_draw.rectangle((max(0, x1-pad_x1+offset_left), max(0, y1-pad_y1+offset_top), min(x2+pad_x2+offset_right, parse_array.shape[1]), min(y2+pad_y2+offset_bottom, parse_array.shape[0])), 'gray', 'gray')
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_shoes * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255), 'L'))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_head * 255)))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_shoes * 255)))
agnostic.paste(img_black, None, Image.fromarray(np.uint8(parse_fixed * 255)))
mask_gray = agnostic.copy()
mask = agnostic.point(lambda p: p == 128 and 255)
+111
View File
@@ -0,0 +1,111 @@
# Changelog
All notable changes to NH-Nodes. Versions follow the Comfy Registry releases.
"Behaviour change" marks fixes that can change the output of an existing workflow.
## 1.7.0 — SAM 3 segmentation and instance nodes (2026-10-18)
### Added
Six nodes under **NH-Nodes → Vision** (75 nodes in total) and a new data type, `NH_SEGS`. Nothing in the existing nodes changes.
- `SAM3 Segment (NH)`: finds every instance of the concepts you name (short English noun phrases, one per line or separated by commas) with Meta's SAM 3. Outputs the instances (`segs`), their union `mask` and the `count`; `Segs Preview (NH)` shows what it found. `detail_level` `balanced` (default) segments each instance a second time on a close-up cut from the full-resolution image, so small and medium objects get crisp edges even on 4K to 8K photos; `fast` keeps the whole-image pass only. Other inputs: `threshold`, `max_per_concept`, `checkpoint` and `keep_model_loaded`.
- `NH_SEGS`: for every image of a batch, a list of instances with a label, a score, a tight box and the mask inside that box only, so dozens of instances on an 8K photo stay light. Full-size masks are built only when a node outputs a `MASK`.
- `Segs Select (NH)`: keeps instances by label (`*` and `?` wildcards), score and size, sorts them (score, area, position, centre first, as found) and takes a slice, for example the second person from the left.
- `Segs Compose (NH)`: one mask from a formula over labels: `|` `+` `or` (union), `-` (minus), `&` `and` (overlap), `~` `!` `not` (everything except), parentheses, and `all`, `image`, `none`. `~person` is the background, `(wall | ceiling) - window` the walls without the windows. A hyphen inside a word belongs to the label (`t-shirt` needs no quotes), so put spaces around a minus: `person - hair`.
- `Segs To Masks (NH)`: one mask per instance, the union per image, the labels and the count. It stops with a message when the masks would need more than about half of the free RAM, for example hundreds of instances on an 8K photo.
- `Segs To Impact SEGS (NH)`: Impact Pack's `SEGS` for one image of the batch (`crop_factor` 3.0 like Impact Pack). Impact Pack is not needed for anything else in NH-Nodes.
- `Segs Preview (NH)`: draws the instances (tint, outline, label and score) on the picture they were found in.
### Notes
- SAM3 Segment needs an NVIDIA GPU with CUDA: the official sam3 code builds part of the model on the GPU, so there is no CPU mode.
- NH-Nodes does not install or download anything for it. You install Meta's `sam3` package yourself (the Vision guide gives the command, pinned to the commit NH-Nodes was tested with; newer commits with the same image API also work), and the weights are gated on Hugging Face: request access, accept Meta's SAM License and put `sam3.pt` in `models/sam3` (or a folder registered as `sam3` in `extra_model_paths.yaml`). The folder is shared with other SAM 3 packs, so an existing copy is found. The licence is in `THIRD_PARTY_NOTICES.md`.
- The model stays in system memory between runs and moves to the GPU only while the node runs, unless `keep_model_loaded` is on.
- `detail_level` has two levels for now, `balanced` and `fast`.
- SAM 3 describes whatever it looks at on a grid of about 288 cells. A subject that fills most of an 8K frame therefore gets edges at about 1/300 of its size: fine for the outline of a person, soft on hair, lace and fur. Hair-level edges are not part of this release.
- SAM 3 first sees the whole picture squeezed into a square, so on a picture wider or taller than 4:1 it can miss small things. The console says so; split such a picture into tiles with `Image Tile (NH)`.
- A file in the `sam3` folder that is not a SAM 3 checkpoint, or an incomplete download, is refused with the download advice before anything runs.
### Planned
Not part of this release, and the names may change: scene parsing that labels the whole picture (sky, wall, floor…), automatic concept tagging with ready presets and a report, matte refinement for hair and fur edges (it will add a `max` detail level), and parsing of each person's parts.
## 1.6.2 — Load images matching picks the right group
- `Load images matching` (behaviour change): when the source and the matching library share parent folders (for example
`D:/Photos/Source/Nu/…` and `D:/Photos/Library/{Nam,Nu}/…`), the shared folders no longer outweigh the source's
group, so a source in `Nu` picks `Nu/<match>` instead of the first folder in alphabetical order. Same-drive and
cross-drive setups now rank identically, and folders elsewhere in the library no longer change which match wins.
## 1.6.1 — Cleanup and hardening
- Internal restructuring with no change to node ids, inputs or outputs: the large image-tools module is split into one
module per node family, shared helpers live in `core/` (`image_folders`, `image_ops`, `mask_ops`, `model_files`), and
the frontend extensions share one widget helper module.
- Widgets are hidden with the frontend's own `hidden` property when available (older frontends keep the previous
method); values are still saved with the workflow.
- `Load Images Folder`: the image count used for auto increment now honours `recursive`; a wired `folder_path` no
longer fingerprints ComfyUI's input folder by mistake.
- HTTP routes: the folder-count and large-preview routes no longer return absolute paths, answer with proper status
codes (malformed JSON → 400) and do their file work off the server's event loop.
- `NH Save Large Image` accepts single-channel images; bfloat16 IMAGE tensors convert correctly; image conversions no
longer use Pillow's deprecated `fromarray(mode=…)`.
## 1.6.0 — Performance, caching and VRAM
### Faster graphs
- **Lazy switches:** `If/Else (NH)`, `Switch N (NH)` and `Random Choice (NH)` now ask ComfyUI only for the branch they pick, so the other branches (for example a second sampler) are never executed. The choice itself is unchanged. A connected input whose upstream returns nothing still counts as connected.
- `Layer Stack Composite (NH)` is cached like any pure node instead of re-running on every queue.
- `Load images matching`: results are cached until the matched files change; the search stays inside the matching root (no more scanning parent folders, drive roots or the models folder). When nothing matches, `found` is `False` and `images` returns the new optional `fallback_images` input (evaluated only on a miss); without a fallback, `images` is blocked with a message and the rest of the workflow keeps running (behaviour change: it used to raise an error). Use `fallback_images` rather than routing `images` through a switch — a blocked input stops the switch too. A drive or network-share root cannot be searched recursively any more.
- Indexed loaders (`Load Diffusion Model Index`, `Load VAE Index`, `Load Clip Index`) reuse the model ComfyUI still holds, so editing a slot you did not select no longer reloads it from disk; they keep only a weak reference, so ComfyUI's RAM-pressure cache can still free the model (`--cache-classic` keeps a normal reference). VAEs are loaded with their file metadata and validated like the core loader does.
- `Mask Morphology (NH)`: rewritten on torch (CPU or GPU). Soft masks keep their gradient, masks touching the border no longer shrink from the edge, and large kernels are up to 100× faster. Even kernel lengths act as the next odd length (behaviour change).
### VRAM and models
- `VTON Ultimate Processor (NH)`: new optional `keep_model_loaded` (default **off**). Off frees the ONNX Runtime GPU memory after each run that really used the GPU (about 1.5 s slower per run); on keeps today's speed and is much faster for image lists, which ComfyUI runs one item at a time. ComfyUI is asked to make room before the models are loaded on the GPU, except once ONNX Runtime has fallen back to the CPU.
- SegFormer nodes now really make ComfyUI unload models when VRAM is tight (a device-index mismatch prevented it).
- Model downloads are pinned to tested Hugging Face revisions and only fetch the needed files; a half-downloaded VTON folder is repaired instead of trusted.
- `extra_model_paths.yaml` support: the folder names `nh_nodes` (SegFormer models, `.safetensors`) and `vton_mask` (VTON models, `.onnx`) can point to extra locations; existing installs keep working without moving anything.
### Viewer
- `NH Large Image Preview` / `NH Large Image Compare`: browse every image of a batch with ◀ n / N ▶ or the ←/→ keys.
## 1.5.0 — Correctness fixes
### Image
- **Batches everywhere:** `VTON Ultimate Processor`, `NH Simple Face Paste` and `NH Agnostic Image Generator` now process every image of a batch (smaller inputs with batch 1 are reused for every item). Behaviour change: Face Paste returns the full destination batch instead of one image. VTON stops with a clear error naming the batch index when no person is detected (it used to return a meaningless mask or crash).
- **Values are clipped before 8-bit conversion**, so overshoot from resampling no longer wraps bright pixels to black (Face Paste, Mask-Aware Resize, shared helpers).
- `NH Mask-Aware Resize`: an empty mask now respects the mode (pad = fit and pad with `pad_color_hex`, crop = cover and centre) instead of stretching.
- `NH Agnostic Image Generator`: masks of a different size are resized to the image, 2-D masks are accepted, soft masks keep their gradient, and the `noise` fill is deterministic (behaviour change).
- `Layer Layout Composite` / `Layer Stack Composite`: RGBA layers are pasted with their own alpha instead of as opaque rectangles. RGBA backgrounds give RGBA output.
- Padding colours accept `#RRGGBBAA`; without an alpha part the padding is opaque (it used to copy the blue value into alpha).
- `Save Image Path (NH)`: `filename_prefix` and `new_folder_name` can no longer write outside the chosen folder (on Windows a prefix may not contain `:`, which blocks drive prefixes such as `D:name`); a prefix such as `renders/img` creates the sub-folder. An absolute `folder_path` is still allowed.
- `NH Save Large Image`: WEBP (16383 px) and JPEG (65500 px) size limits are checked before anything is written; `--disable-metadata` is respected.
- `Image Label (NH)`: much faster font fitting (cached fonts + binary search), same sizes as before.
- `Image Grid Composite (NH)`: refuses a canvas that would not fit in the available memory with a clear message instead of crashing.
- `NH Large Preview Cache Manager`: actions run on every queue (it used to be cached).
### Resolution
- `NH Remove Resize Padding`: images resized with `cover_crop` are no longer stretched back (that distorted them). With `original_size` the picture is scaled back uniformly into the original frame and the cropped-away borders are filled from the edges; use `NH Restore Image Geometry` to composite onto the original instead.
- Resize nodes keep float precision for Lanczos/box/hamming (no 8-bit quantisation).
- `NH Smart Resolution Picker`: a stale preset or model name is reported as a warning in `info`.
### Logic, text and lists
- `Math Eval (NH)`: uses a shared safe evaluator. Behaviour change: an invalid expression now shows an error that names the expression instead of silently returning 0. `x / 0`, `x // 0` and `x % 0` give 0 with a warning. numpy/torch scalar inputs work.
- `Random Choice (NH)`: the weight list follows the input slots (`weights` item *i* belongs to `input_i`). Behaviour change when some slots are empty.
- Seeds are used literally in `Random Choice`, `Prompt Scheduler`, `Text Split Lines` and `Text Random Line`, so seed 0 is reproducible (behaviour change).
- `Prompt Template (NH)`: an empty variable no longer glues the neighbouring words together.
- `Regex Extract (NH)`: optional groups that did not match give an empty string instead of "no match".
- `Pack Universal (NH)`: values are stored by slot, so an empty `input_0` no longer shifts the other indices (behaviour change).
- `Compare (NH)` operator `in`: plain membership / substring test (no more fuzzy numeric matching).
- `Batch Merge (NH)`: RGB and RGBA batches can be merged; letterbox bars are transparent for both; very thin inputs no longer crash `pad`.
- `NH Mask Aspect Ratio Match`: `pad`/`crop` placement fixed (no crash at the image edge, requested size honoured) and the whole batch is processed.
- VTON garment masks: undetected keypoints and missing keypoint groups no longer stretch the mask to the image edges; small parsing islands are removed without cutting rectangles out of the garment.
- `Universal Slider Builder (NH)` is marked deprecated (it never produced output); existing workflows still load.
## 1.4.3 — Tooling, licensing and dependencies
- CI runs ruff and the test suite on every pull request; publishing to the Comfy Registry only happens when it passes.
- `onnxruntime` is no longer installed automatically (the CPU wheel overwrote `onnxruntime-gpu`). `VTON Ultimate Processor` explains how to install it and falls back to the CPU with a warning.
- License notices: the VTON code and parsing models are CC BY-NC-SA 4.0 and the face-parsing model is non-commercial — see `THIRD_PARTY_NOTICES.md`.
## 1.4.2
- `NH Smart Resolution Picker`: FLUX.2 latents are 128 channels at 1/16 (images came out twice the size).
- `Load Clip Index (NH)`: works with current ComfyUI (`CLIPType`) and lists every CLIP type.
- `Counter (NH)`: counts on every queue.
- `NH Mask-Aware Resize`: `crop` keeps the whole mask.
File diff suppressed because it is too large Load Diff
+241
View File
@@ -0,0 +1,241 @@
"""Render a ComfyUI-style SVG card for every NH-Nodes node from its INPUT_TYPES / RETURN_TYPES.
Run with ComfyUI's Python from the ComfyUI root:
python custom_nodes/NH-Nodes/docs/_tools/build_node_cards.py [output_dir]
Cards are generated from the live node definitions, so they never drift from the code.
"""
import importlib
import json
import os
import re
import sys
from html import escape
COMFY_ROOT = os.getcwd()
sys.path[:0] = [COMFY_ROOT, os.path.join(COMFY_ROOT, "custom_nodes")]
import torch # noqa: E402
if not torch.cuda.is_available():
from comfy.cli_args import args # noqa: E402
args.cpu = True
PACK_DIR = os.path.join(COMFY_ROOT, "custom_nodes", "NH-Nodes")
OUT_DIR = sys.argv[1] if len(sys.argv) > 1 else os.path.join(PACK_DIR, "docs", "assets", "nodes")
# ComfyUI default dark palette (frontend 1.55) for link types; NH custom types get amber.
TYPE_COLORS = {
"IMAGE": "#64B5F6", "MASK": "#81C784", "LATENT": "#FF9CF9", "MODEL": "#B39DDB",
"CLIP": "#FFD500", "VAE": "#FF6E6E", "CONDITIONING": "#FFA931", "CONTROL_NET": "#6EE7B7",
"INT": "#9CC2E5", "FLOAT": "#9CC2E5", "STRING": "#C5E1A5", "BOOLEAN": "#F48FB1", "*": "#CFD8DC",
}
CUSTOM_TYPE_COLOR = "#E0B272"
WIDGET_TYPES = {"INT", "FLOAT", "STRING", "BOOLEAN"}
W = 360
PAD = 12
TITLE_H = 46
ROW_H = 22
WIDGET_H = 24
WIDGET_GAP = 5
FONT = "Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif"
MONO = "'JetBrains Mono', Consolas, 'Courier New', monospace"
FILE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif", ".safetensors", ".ckpt",
".pt", ".pth", ".bin", ".gguf", ".onnx", ".sft")
def is_file_combo(options, opts):
"""Combos listing files from the user's folders (inputs, models): never publish those names."""
if opts.get("image_upload") or opts.get("video_upload"):
return True
return any(str(option).lower().endswith(FILE_EXTENSIONS) for option in options)
def type_name(spec_type):
if isinstance(spec_type, (list, tuple)):
return "COMBO"
return str(spec_type)
def type_color(name):
return TYPE_COLORS.get(name, CUSTOM_TYPE_COLOR)
def short(value, limit=24):
text = str(value).replace("\n", " ⏎ ")
return text if len(text) <= limit else text[: limit - 1] + "…"
def fmt_number(value, opts):
if isinstance(value, float):
step = opts.get("step", 0.01) or 0.01
decimals = max(0, min(4, len(str(step).split(".")[1]) if "." in str(step) else 0))
return f"{value:.{decimals}f}"
return str(value)
def collapse_numbered(entries):
"""Fold long numbered families (lora_1..lora_64, enable_1..) into first two + a summary row."""
families = {}
for name, *_ in entries:
match = re.match(r"^(.*?)(\d+)$", name)
if match:
families.setdefault(match.group(1), []).append(int(match.group(2)))
folded = {prefix: max(nums) for prefix, nums in families.items() if len(nums) > 4}
result, emitted = [], set()
for entry in entries:
match = re.match(r"^(.*?)(\d+)$", entry[0])
if match and match.group(1) in folded:
prefix, num = match.group(1), int(match.group(2))
if num <= 2:
result.append(entry)
elif prefix not in emitted:
emitted.add(prefix)
result.append((f"{prefix}3 … {prefix}{folded[prefix]}", "FOLD", {}, entry[3]))
continue
result.append(entry)
return result
def node_entries(cls):
spec = cls.INPUT_TYPES()
entries = []
for section in ("required", "optional"):
for name, value in (spec.get(section) or {}).items():
spec_type = value[0] if isinstance(value, (list, tuple)) and value else value
opts = value[1] if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[1], dict) else {}
entries.append((name, spec_type, opts, section == "optional"))
return collapse_numbered(entries)
def split_entries(entries):
sockets, widgets = [], []
for name, spec_type, opts, optional in entries:
tname = type_name(spec_type)
is_widget = tname == "COMBO" or tname == "FOLD" or (tname in WIDGET_TYPES and not opts.get("forceInput"))
(widgets if is_widget else sockets).append((name, spec_type, opts, optional))
return sockets, widgets
def widget_value(spec_type, opts):
tname = type_name(spec_type)
if tname == "COMBO":
options = list(spec_type)
if is_file_combo(options, opts):
return "(choose a file)", ""
default = opts.get("default", options[0] if options else "")
return (short(default) if default != "" else "—"), f"{len(options)} options" if len(options) > 1 else ""
default = opts.get("default", "")
if tname == "BOOLEAN":
return ("true" if default else "false"), ""
if tname in ("INT", "FLOAT"):
return fmt_number(default, opts), ""
if tname == "STRING":
return short(default, 30) if default != "" else "", "multiline" if opts.get("multiline") else ""
return short(default), ""
def svg_card(node_id, display, category, cls):
entries = node_entries(cls)
sockets, widgets = split_entries(entries)
return_types = [str(t) for t in getattr(cls, "RETURN_TYPES", ())]
return_names = list(getattr(cls, "RETURN_NAMES", None) or return_types)
is_list = list(getattr(cls, "OUTPUT_IS_LIST", ()) or [])
output_node = getattr(cls, "OUTPUT_NODE", False)
io_rows = max(len(sockets), len(return_types))
multiline_rows = sum(1 for _, t, o, _ in widgets if type_name(t) == "STRING" and o.get("multiline"))
widgets_h = len(widgets) * (WIDGET_H + WIDGET_GAP) + multiline_rows * 22
height = TITLE_H + 10 + io_rows * ROW_H + (8 if io_rows and widgets else 0) + widgets_h + PAD
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{height}" viewBox="0 0 {W} {height}" '
f'font-family="{FONT}" role="img" aria-label="{escape(display)} node">',
f'<rect x="0.5" y="0.5" width="{W - 1}" height="{height - 1}" rx="10" fill="#26282c" stroke="#45484f"/>',
f'<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h{W - 21} a10 10 0 0 1 10 10 v{TITLE_H - 10} h-{W - 1} z" fill="#34373d"/>',
f'<circle cx="{PAD + 5}" cy="17" r="4.5" fill="{"#F6AD55" if output_node else "#8FD18F"}"/>',
f'<text x="{PAD + 16}" y="22" font-size="14" font-weight="600" fill="#EEF0F3">{escape(short(display, 40))}</text>',
f'<text x="{PAD + 16}" y="38" font-size="10.5" fill="#9AA0A8" font-family="{MONO}">'
f'{escape(node_id)} · {escape(category.replace("NH-Nodes/", ""))}</text>',
]
y = TITLE_H + 10
for index in range(io_rows):
cy = y + index * ROW_H + ROW_H / 2
if index < len(sockets):
name, spec_type, opts, optional = sockets[index]
tname = type_name(spec_type)
color = type_color(tname)
parts.append(f'<circle cx="{PAD}" cy="{cy}" r="5" fill="{color}" stroke="#1b1c1f" stroke-width="1.5"/>')
label = escape(name) + (' <tspan fill="#7d838c" font-style="italic">(optional)</tspan>' if optional else "")
parts.append(f'<text x="{PAD + 12}" y="{cy + 4}" font-size="12" fill="#D7DADF">{label}</text>')
if index < len(return_types):
tname = return_types[index]
color = type_color(tname)
name = return_names[index] if index < len(return_names) else tname
list_mark = " ≡" if index < len(is_list) and is_list[index] else ""
parts.append(f'<text x="{W - PAD - 12}" y="{cy + 4}" font-size="12" fill="#D7DADF" text-anchor="end">'
f'{escape(name)}{list_mark}</text>')
parts.append(f'<circle cx="{W - PAD}" cy="{cy}" r="5" fill="{color}" stroke="#1b1c1f" stroke-width="1.5"/>')
y += io_rows * ROW_H + (8 if io_rows and widgets else 0)
for name, spec_type, opts, optional in widgets:
tname = type_name(spec_type)
multiline = tname == "STRING" and opts.get("multiline")
h = WIDGET_H + (22 if multiline else 0)
parts.append(f'<rect x="{PAD}" y="{y}" width="{W - 2 * PAD}" height="{h}" rx="7" fill="#1d1f22" stroke="#3a3d43"/>')
if tname == "FOLD":
parts.append(f'<text x="{W / 2}" y="{y + 16}" font-size="11" fill="#9AA0A8" text-anchor="middle" '
f'font-style="italic">{escape(name)}</text>')
y += h + WIDGET_GAP
continue
value, hint = widget_value(spec_type, opts)
label = escape(name) + (' <tspan fill="#6f747c" font-style="italic">opt</tspan>' if optional else "")
if multiline:
parts.append(f'<text x="{PAD + 10}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
parts.append(f'<text x="{PAD + 10}" y="{y + 36}" font-size="11.5" fill="#E6E8EB" font-family="{MONO}">'
f'{escape(value) or "…"}</text>')
elif tname == "BOOLEAN":
on = value == "true"
parts.append(f'<text x="{PAD + 10}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
tx = W - PAD - 40
parts.append(f'<rect x="{tx}" y="{y + 6}" width="30" height="13" rx="6.5" fill="{"#4C8BF5" if on else "#4a4d53"}"/>')
parts.append(f'<circle cx="{tx + (23 if on else 7)}" cy="{y + 12.5}" r="5" fill="#F5F6F7"/>')
else:
arrows = tname in ("INT", "FLOAT", "COMBO")
if arrows:
parts.append(f'<text x="{PAD + 8}" y="{y + 16}" font-size="10" fill="#6f747c">◀</text>')
parts.append(f'<text x="{W - PAD - 16}" y="{y + 16}" font-size="10" fill="#6f747c">▶</text>')
lx = PAD + (22 if arrows else 10)
rx = W - PAD - (22 if arrows else 10)
parts.append(f'<text x="{lx}" y="{y + 16}" font-size="11.5" fill="#9AA0A8">{label}</text>')
parts.append(f'<text x="{rx}" y="{y + 16}" font-size="11.5" fill="#E6E8EB" text-anchor="end">{escape(value)}</text>')
y += h + WIDGET_GAP
parts.append("</svg>")
return "\n".join(parts) + "\n"
def main():
pack = importlib.import_module("NH-Nodes")
os.makedirs(OUT_DIR, exist_ok=True)
index = []
for node_id, cls in sorted(pack.NODE_CLASS_MAPPINGS.items()):
display = pack.NODE_DISPLAY_NAME_MAPPINGS.get(node_id, node_id)
category = getattr(cls, "CATEGORY", "")
svg = svg_card(node_id, display, category, cls)
with open(os.path.join(OUT_DIR, f"{node_id}.svg"), "w", encoding="utf-8", newline="\n") as handle:
handle.write(svg)
index.append({"id": node_id, "name": display, "category": category,
"deprecated": bool(getattr(cls, "DEPRECATED", False))})
with open(os.path.join(OUT_DIR, "index.json"), "w", encoding="utf-8", newline="\n") as handle:
json.dump(index, handle, ensure_ascii=False, indent=1)
print(f"wrote {len(index)} cards to {OUT_DIR}")
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
"""Dump every NH node's live definition (inputs with defaults/ranges/options/tooltips, outputs) to JSON.
Run with ComfyUI's Python from the ComfyUI root: python dump_node_spec.py <out.json>
"""
import importlib
import json
import os
import sys
sys.path[:0] = [os.getcwd(), os.path.join(os.getcwd(), "custom_nodes")]
import torch # noqa: E402
if not torch.cuda.is_available():
from comfy.cli_args import args # noqa: E402
args.cpu = True
pack = importlib.import_module("NH-Nodes")
FILE_EXTENSIONS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff", ".gif", ".safetensors", ".ckpt",
".pt", ".pth", ".bin", ".gguf", ".onnx", ".sft")
def is_file_combo(options, opts):
"""Combos listing files from the user's folders (inputs, models): never publish those names."""
if opts.get("image_upload") or opts.get("video_upload"):
return True
return any(str(option).lower().endswith(FILE_EXTENSIONS) for option in options)
def jsonable(value):
try:
json.dumps(value)
return value
except TypeError:
return str(value)
spec = []
for node_id, cls in sorted(pack.NODE_CLASS_MAPPINGS.items()):
inputs = []
for section, entries in (cls.INPUT_TYPES() or {}).items():
if section == "hidden":
continue
for name, value in (entries or {}).items():
spec_type = value[0] if isinstance(value, (list, tuple)) and value else value
opts = value[1] if isinstance(value, (list, tuple)) and len(value) > 1 and isinstance(value[1], dict) else {}
entry = {"name": name, "section": section}
if isinstance(spec_type, (list, tuple)):
entry["type"] = "COMBO"
options = [str(o) for o in spec_type]
if is_file_combo(options, opts):
entry["options"] = ["(files from the user's folder)"]
opts = {k: v for k, v in opts.items() if k != "default"}
else:
entry["options"] = options if len(options) <= 40 else options[:40] + [f"... ({len(options)} total)"]
else:
entry["type"] = str(spec_type)
for key in ("default", "min", "max", "step", "multiline", "tooltip", "forceInput", "lazy"):
if key in opts:
entry[key] = jsonable(opts[key])
inputs.append(entry)
names = list(getattr(cls, "RETURN_NAMES", None) or getattr(cls, "RETURN_TYPES", ()))
types = [str(t) for t in getattr(cls, "RETURN_TYPES", ())]
is_list = list(getattr(cls, "OUTPUT_IS_LIST", ()) or [])
outputs = [{"name": n, "type": t, "is_list": bool(is_list[i]) if i < len(is_list) else False}
for i, (n, t) in enumerate(zip(names, types))]
spec.append({
"id": node_id,
"name": pack.NODE_DISPLAY_NAME_MAPPINGS.get(node_id, node_id),
"category": getattr(cls, "CATEGORY", ""),
"module": cls.__module__.replace("NH-Nodes.", ""),
"description": getattr(cls, "DESCRIPTION", "") or (cls.__doc__ or "").strip(),
"output_node": bool(getattr(cls, "OUTPUT_NODE", False)),
"deprecated": bool(getattr(cls, "DEPRECATED", False)),
"inputs": inputs,
"outputs": outputs,
})
with open(sys.argv[1], "w", encoding="utf-8") as handle:
json.dump(spec, handle, ensure_ascii=False, indent=1)
print(f"{len(spec)} nodes -> {sys.argv[1]}")
Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="287" viewBox="0 0 360 287" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Agnostic Image Generator node">
<rect x="0.5" y="0.5" width="359" height="286" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Agnostic Image Generator</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AgnosticImageGenerator · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">agnostic_img</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">masked_img</text>
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">composite</text>
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">fill_mode</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">gray</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">blur_radius</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">65</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">noise_strength</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">gray_value</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">feathering</text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+36
View File
@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="347" viewBox="0 0 360 347" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any List Split (NH) node">
<rect x="0.5" y="0.5" width="359" height="346" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any List Split (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnyListSplit · Batch</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">All ≡</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">index 1</text>
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index 2</text>
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">index 3</text>
<circle cx="348" cy="133.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">index 4</text>
<circle cx="348" cy="155.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">index 5</text>
<circle cx="348" cy="177.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">index 6</text>
<circle cx="348" cy="199.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="225.0" font-size="12" fill="#D7DADF" text-anchor="end">index 7</text>
<circle cx="348" cy="221.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="247.0" font-size="12" fill="#D7DADF" text-anchor="end">index 8</text>
<circle cx="348" cy="243.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="269.0" font-size="12" fill="#D7DADF" text-anchor="end">index 9</text>
<circle cx="348" cy="265.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="291.0" font-size="12" fill="#D7DADF" text-anchor="end">index 10</text>
<circle cx="348" cy="287.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="306" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="322" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="322" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="322" font-size="11.5" fill="#9AA0A8">index_count</text>
<text x="326" y="322" font-size="11.5" fill="#E6E8EB" text-anchor="end">4</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="237" viewBox="0 0 360 237" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any Branch Switch (NH) node">
<rect x="0.5" y="0.5" width="359" height="236" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any Branch Switch (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnySwitch · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">output 0</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">output 1</text>
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">output 2</text>
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">output 3</text>
<circle cx="348" cy="133.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">output 4</text>
<circle cx="348" cy="155.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="196" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="212" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="212" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="212" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="212" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Any Switch (NH) node">
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Any Switch (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_AnySwitchBoolean · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_true</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_false</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">condition</text>
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">condition</text>
<rect x="308" y="114" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="120.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+28
View File
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="207" viewBox="0 0 360 207" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Batch Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="206" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Batch Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BatchIndex · Batch</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">batch</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">original_count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">end_index <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">-1</text>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">step <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Batch Merge (NH) node">
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Batch Merge (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BatchMerge · Batch</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">batch_a</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">batch_b <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">batch_c <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">resize_mode <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="127" viewBox="0 0 360 127" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Boolean Switch (NH) node">
<rect x="0.5" y="0.5" width="359" height="126" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Boolean Switch (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_BooleanSwitch · Utils</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">boolean</text>
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">boolean_switch</text>
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="98.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+55
View File
@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Clip Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Clip Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ClipIndex · Loaders</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">clip</text>
<circle cx="348" cy="67.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">type</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">stable_diffusion</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">clip_count</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">clip_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">clip_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">clip_3 … clip_64</text>
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+27
View File
@@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Compare (NH) node">
<rect x="0.5" y="0.5" width="359" height="199" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Compare (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_Compare · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">a</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">b</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">a_passthrough</text>
<circle cx="348" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">b_passthrough</text>
<circle cx="348" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">op</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">is</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">type_cast <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">auto</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="280" viewBox="0 0 360 280" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Counter (NH) node">
<rect x="0.5" y="0.5" width="359" height="279" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Counter (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_Counter · Batch</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">current</text>
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">is_done</text>
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">progress</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">remaining</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">start</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">step</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">max_value</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">9</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="255" font-size="11.5" fill="#9AA0A8">reset</text>
<rect x="308" y="245" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="251.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,55 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Diffusion Model Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Diffusion Model Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_DiffusionModelIndex · Loaders</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">model_count</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">weight_dtype</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">default</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">diffusion_model_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">—</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">diffusion_model_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">—</text>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">diffusion_model_3 … diffusion_model_64</text>
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+43
View File
@@ -0,0 +1,43 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="316" viewBox="0 0 360 316" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Garment Segment (NH) node">
<rect x="0.5" y="0.5" width="359" height="315" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Garment Segment (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_GarmentSegment · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">cutout_rgba</text>
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">mask_image</text>
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">part</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">upper_garment</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">attach_parts</text>
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="171.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">mask_expand</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">mask_blur</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">fill_holes</text>
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="258.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">device</text>
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">cuda</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="127" viewBox="0 0 360 127" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Gate Switch (NH) node">
<rect x="0.5" y="0.5" width="359" height="126" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Gate Switch (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_GateSwitch · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">output</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">enabled</text>
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="98.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="If/Else (NH) node">
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">If/Else (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_IfElse · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">if_true</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">if_false</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">condition_out</text>
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">condition</text>
<rect x="308" y="114" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="120.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="236" viewBox="0 0 360 236" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Compare (NH) node">
<rect x="0.5" y="0.5" width="359" height="235" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Compare (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageCompare · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image_a</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">image_b</text>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">compare_mode</text>
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">side_by_side</text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">split_position</text>
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">overlay_opacity</text>
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="211" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="211" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="211" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="425" viewBox="0 0 360 425" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Grid Composite (NH) node">
<rect x="0.5" y="0.5" width="359" height="424" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Grid Composite (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageGridComposite · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">slot_count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">grid_info</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">columns</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">rows</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">cell_size_mode</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">manual</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">cell_width</text>
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">cell_height</text>
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">resize_mode</text>
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="342" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="342" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="342" font-size="11.5" fill="#9AA0A8">gap_x</text>
<text x="326" y="342" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">gap_y</text>
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="400" font-size="11.5" fill="#9AA0A8">background_color_hex</text>
<text x="338" y="400" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="526" viewBox="0 0 360 526" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Label (NH) node">
<rect x="0.5" y="0.5" width="359" height="525" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Label (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageLabel · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">label_text</text>
<text x="22" y="122" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH Label</text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">position</text>
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">header</text>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">frame_height</text>
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">80</text>
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="211" font-size="11.5" fill="#9AA0A8">fill_enabled</text>
<rect x="308" y="201" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="207.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="224" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="240" font-size="11.5" fill="#9AA0A8">frame_color_hex</text>
<text x="338" y="240" font-size="11.5" fill="#E6E8EB" text-anchor="end">#111111</text>
<rect x="12" y="253" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="269" font-size="11.5" fill="#9AA0A8">text_color_hex</text>
<text x="338" y="269" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
<rect x="12" y="282" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="298" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="298" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="298" font-size="11.5" fill="#9AA0A8">font_name</text>
<text x="326" y="298" font-size="11.5" fill="#E6E8EB" text-anchor="end">default</text>
<rect x="12" y="311" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="327" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="327" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="327" font-size="11.5" fill="#9AA0A8">font_size</text>
<text x="326" y="327" font-size="11.5" fill="#E6E8EB" text-anchor="end">32</text>
<rect x="12" y="340" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="356" font-size="11.5" fill="#9AA0A8">fit_text</text>
<rect x="308" y="346" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="352.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="369" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="385" font-size="11.5" fill="#9AA0A8">overlay_on_image</text>
<rect x="308" y="375" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="381.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="398" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="414" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="414" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="414" font-size="11.5" fill="#9AA0A8">align</text>
<text x="326" y="414" font-size="11.5" fill="#E6E8EB" text-anchor="end">left</text>
<rect x="12" y="427" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="443" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="443" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="443" font-size="11.5" fill="#9AA0A8">padding_x</text>
<text x="326" y="443" font-size="11.5" fill="#E6E8EB" text-anchor="end">20</text>
<rect x="12" y="456" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="472" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="472" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="472" font-size="11.5" fill="#9AA0A8">outline_width</text>
<text x="326" y="472" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="485" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="501" font-size="11.5" fill="#9AA0A8">outline_color_hex</text>
<text x="338" y="501" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Resize Unit (NH) node">
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Resize Unit (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageResizeByUnit · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width_px</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height_px</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">unit</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">pixel</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">width_value</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024.0</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">height_value</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024.0</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">dpi</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">300</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">resize_mode</text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">nearest</text>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="320" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
<text x="338" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="324" viewBox="0 0 360 324" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Tile (NH) node">
<rect x="0.5" y="0.5" width="359" height="323" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Tile (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageTile · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">tiles</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_data</text>
<circle cx="348" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">overlap_x</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">overlap_y</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">tile_info</text>
<circle cx="348" cy="177.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="196" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="212" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="212" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="212" font-size="11.5" fill="#9AA0A8">tile_mode</text>
<text x="326" y="212" font-size="11.5" fill="#E6E8EB" text-anchor="end">original_ratio</text>
<rect x="12" y="225" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="241" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="241" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="241" font-size="11.5" fill="#9AA0A8">tile_size</text>
<text x="326" y="241" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
<rect x="12" y="254" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="270" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="270" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="270" font-size="11.5" fill="#9AA0A8">tile_width</text>
<text x="326" y="270" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
<rect x="12" y="283" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="299" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="299" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="299" font-size="11.5" fill="#9AA0A8">tile_height</text>
<text x="326" y="299" font-size="11.5" fill="#E6E8EB" text-anchor="end">1024</text>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="112" viewBox="0 0 360 112" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Image Untile (NH) node">
<rect x="0.5" y="0.5" width="359" height="111" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Image Untile (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ImageUntile · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">tiles</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">tile_data</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">untile_info</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Image Compare node">
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Image Compare</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargeImageCompare · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image_a</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image_a</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">image_b</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">image_b</text>
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">manifest</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">compare_mode</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">slider</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">sync_pan_zoom</text>
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="171.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">preview_megapixels</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">preview_format</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">preview_quality</text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">title</text>
<text x="338" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH Large Image Compare</text>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">tile_size <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="349" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="349" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="349" font-size="11.5" fill="#9AA0A8">tile_format <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="349" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="378" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="378" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="378" font-size="11.5" fill="#9AA0A8">tile_quality <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="378" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="407" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="407" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="407" font-size="11.5" fill="#9AA0A8">cache_policy <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="407" font-size="11.5" fill="#E6E8EB" text-anchor="end">temp</text>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,56 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="381" viewBox="0 0 360 381" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Image Preview node">
<rect x="0.5" y="0.5" width="359" height="380" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Image Preview</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargeImagePreview · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">manifest</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">preview_megapixels</text>
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">preview_format</text>
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">preview_quality</text>
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="211" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="211" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="211" font-size="11.5" fill="#9AA0A8">viewer_mode</text>
<text x="326" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">modal</text>
<rect x="12" y="224" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="240" font-size="11.5" fill="#9AA0A8">title</text>
<text x="338" y="240" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH Large Image Preview</text>
<rect x="12" y="253" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="269" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="269" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="269" font-size="11.5" fill="#9AA0A8">tile_size <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="269" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="282" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="298" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="298" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="298" font-size="11.5" fill="#9AA0A8">tile_format <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="298" font-size="11.5" fill="#E6E8EB" text-anchor="end">jpg</text>
<rect x="12" y="311" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="327" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="327" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="327" font-size="11.5" fill="#9AA0A8">tile_quality <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="327" font-size="11.5" fill="#E6E8EB" text-anchor="end">90</text>
<rect x="12" y="340" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="356" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="356" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="356" font-size="11.5" fill="#9AA0A8">cache_policy <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="356" font-size="11.5" fill="#E6E8EB" text-anchor="end">temp</text>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="280" viewBox="0 0 360 280" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Large Preview Cache Manager node">
<rect x="0.5" y="0.5" width="359" height="279" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Large Preview Cache Manager</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LargePreviewCacheManager · Image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">summary</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">removed</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">freed_mb</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">cache_root</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">action</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">info</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">max_age_hours</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">24.0</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">max_total_mb</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">2048.0</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="255" font-size="11.5" fill="#9AA0A8">cache_id</text>
<text x="338" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,90 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="621" viewBox="0 0 360 621" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Layer Layout Composite (NH) node">
<rect x="0.5" y="0.5" width="359" height="620" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Layer Layout Composite (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LayerLayoutComposite · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">background_image <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">slot_count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">layout_info</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">canvas_width</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">2480</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">canvas_height</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">3508</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="226" font-size="11.5" fill="#9AA0A8">background_color</text>
<text x="338" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">layout_mode</text>
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">preset</text>
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">layout_preset</text>
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">2x3</text>
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">resize_mode</text>
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">cover</text>
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="342" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="342" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="342" font-size="11.5" fill="#9AA0A8">columns</text>
<text x="326" y="342" font-size="11.5" fill="#E6E8EB" text-anchor="end">2</text>
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">rows</text>
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">3</text>
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="400" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="400" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="400" font-size="11.5" fill="#9AA0A8">margin_x</text>
<text x="326" y="400" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
<rect x="12" y="413" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="429" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="429" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="429" font-size="11.5" fill="#9AA0A8">margin_y</text>
<text x="326" y="429" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
<rect x="12" y="442" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="458" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="458" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="458" font-size="11.5" fill="#9AA0A8">gap_x</text>
<text x="326" y="458" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
<rect x="12" y="471" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="487" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="487" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="487" font-size="11.5" fill="#9AA0A8">gap_y</text>
<text x="326" y="487" font-size="11.5" fill="#E6E8EB" text-anchor="end">150</text>
<rect x="12" y="500" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="516" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="516" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="516" font-size="11.5" fill="#9AA0A8">custom_units</text>
<text x="326" y="516" font-size="11.5" fill="#E6E8EB" text-anchor="end">auto</text>
<rect x="12" y="529" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="545" font-size="11.5" fill="#9AA0A8">custom_layout</text>
<text x="22" y="565" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">0.06,0.04,0.39,0.20 ⏎ 0.55,0.…</text>
<rect x="12" y="580" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="596" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="596" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="596" font-size="11.5" fill="#9AA0A8">background_fit</text>
<text x="326" y="596" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
</svg>

After

Width:  |  Height:  |  Size: 6.9 KiB

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="251" viewBox="0 0 360 251" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Layer Stack Composite (NH) node">
<rect x="0.5" y="0.5" width="359" height="250" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Layer Stack Composite (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LayerStackComposite · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">background_image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">layer_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">used_count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">layer_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">layer_info</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">layer_count</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="159" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">positions</text>
<text x="22" y="195" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">0,0</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="226" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">layer_3 … layer_64</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="244" viewBox="0 0 360 244" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Create (NH) node">
<rect x="0.5" y="0.5" width="359" height="243" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Create (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListCreate · Batch</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">items</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">first</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">last</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">text</text>
<text x="22" y="188" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">item 1 ⏎ item 2 ⏎ item 3</text>
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">delimiter <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">\n</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+25
View File
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Filter (NH) node">
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Filter (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListFilter · Batch</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">passed</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">rejected</text>
<circle cx="348" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">passed_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">rejected_count</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">condition</text>
<text x="338" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">contains:dress</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">mode</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">include</text>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+26
View File
@@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="List Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">List Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_ListIndex · Batch</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">items</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">item</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">is_first</text>
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">is_last</text>
<circle cx="348" cy="111.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">wrap <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+28
View File
@@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="251" viewBox="0 0 360 251" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Image Info (NH) node">
<rect x="0.5" y="0.5" width="359" height="250" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Image Info (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImageInfo · Image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">file_name</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">image</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="197" font-size="11.5" fill="#9AA0A8">filename_extension</text>
<rect x="308" y="187" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="193.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="226" font-size="11.5" fill="#9AA0A8">path_include_filename</text>
<rect x="308" y="216" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="222.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load Images Folder (NH) node">
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load Images Folder (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImagesFromFolder · Image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">images</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">folder_path</text>
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">step</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">seed_mode</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">fixed</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">recursive</text>
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="258.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">filename_extension</text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">path_output</text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">file_path</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="520" viewBox="0 0 360 520" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load images matching node">
<rect x="0.5" y="0.5" width="359" height="519" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load images matching</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoadImagesMatching · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">fallback_images <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">images</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">matched_text</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">found</text>
<circle cx="348" cy="155.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="190" font-size="11.5" fill="#9AA0A8">source_folder_path</text>
<text x="338" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">matching_root_path</text>
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="248" font-size="11.5" fill="#9AA0A8">match_text</text>
<text x="338" y="248" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="261" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="277" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="277" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="277" font-size="11.5" fill="#9AA0A8">match_mode</text>
<text x="326" y="277" font-size="11.5" fill="#E6E8EB" text-anchor="end">contains</text>
<rect x="12" y="290" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="306" font-size="11.5" fill="#9AA0A8">recursive</text>
<rect x="308" y="296" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="302.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="319" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="335" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="335" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="335" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="335" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="348" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="364" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="364" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="364" font-size="11.5" fill="#9AA0A8">step</text>
<text x="326" y="364" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="377" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="393" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="393" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="393" font-size="11.5" fill="#9AA0A8">repeat_count</text>
<text x="326" y="393" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="406" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="422" font-size="11.5" fill="#9AA0A8">context_folder_names</text>
<text x="22" y="442" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">Nam ⏎ Nu</text>
<rect x="12" y="457" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="473" font-size="11.5" fill="#9AA0A8">source_file_names <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="22" y="493" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">…</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="185" viewBox="0 0 360 185" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Logic Gate (NH) node">
<rect x="0.5" y="0.5" width="359" height="184" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Logic Gate (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LogicGate · Logic</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">a</text>
<rect x="308" y="92" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="98.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="115" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="131" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="131" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="131" font-size="11.5" fill="#9AA0A8">op</text>
<text x="326" y="131" font-size="11.5" fill="#E6E8EB" text-anchor="end">AND</text>
<rect x="12" y="144" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="160" font-size="11.5" fill="#9AA0A8">b <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="150" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="156.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+66
View File
@@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="483" viewBox="0 0 360 483" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load LoRA Clip Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="482" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load LoRA Clip Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoraClipIndex · Loaders</text>
<circle cx="12" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">model</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">clip</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">clip</text>
<circle cx="348" cy="89.0" r="5" fill="#FFD500" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">select_mode</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">lora_count</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">strength_model</text>
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">strength_clip</text>
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="313" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="313" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="313" font-size="11.5" fill="#9AA0A8">lora_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="313" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="326" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="342" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="332" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="338.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="355" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="371" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="371" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="371" font-size="11.5" fill="#9AA0A8">lora_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="371" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="384" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="400" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="390" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="396.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="413" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="429" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">lora_3 … lora_64</text>
<rect x="12" y="442" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="458" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

+57
View File
@@ -0,0 +1,57 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="432" viewBox="0 0 360 432" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Load LoRA Model Index (NH) node">
<rect x="0.5" y="0.5" width="359" height="431" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Load LoRA Model Index (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_LoraModelIndex · Loaders</text>
<circle cx="12" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">model</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">model</text>
<circle cx="348" cy="67.0" r="5" fill="#B39DDB" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">filename</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">index</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">select_mode</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">index</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">index</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">lora_count</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">5</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">strength_model</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.00</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">lora_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">enable_1 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">lora_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">enable_2 <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="345.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="378" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">lora_3 … lora_64</text>
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="407" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">enable_3 … enable_64</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

@@ -0,0 +1,30 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="214" viewBox="0 0 360 214" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Mask Morphology (NH) node">
<rect x="0.5" y="0.5" width="359" height="213" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Mask Morphology (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskAdvancedMorphology · Mask</text>
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">MASK</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="102" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="102" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="102" font-size="11.5" fill="#9AA0A8">horizontal_expand</text>
<text x="326" y="102" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="115" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="131" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="131" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="131" font-size="11.5" fill="#9AA0A8">vertical_expand</text>
<text x="326" y="131" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="144" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="160" font-size="11.5" fill="#9AA0A8">fill_holes</text>
<rect x="308" y="150" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="156.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="173" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="189" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="189" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="189" font-size="11.5" fill="#9AA0A8">blur_radius</text>
<text x="326" y="189" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.0</text>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Mask Aspect Ratio Match node">
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Mask Aspect Ratio Match</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskAspectRatioMatch · Mask</text>
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">target_ratio_mask</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">MASK</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask_to_adjust</text>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">mode</text>
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">stretch</text>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Mask Properties (NH) node">
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Mask Properties (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskProperties · Mask</text>
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">x1</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">y1</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">x2</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">y2</text>
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">bbox</text>
<circle cx="348" cy="199.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+33
View File
@@ -0,0 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="236" viewBox="0 0 360 236" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Mask-Aware Resize node">
<rect x="0.5" y="0.5" width="359" height="235" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Mask-Aware Resize</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskResizeImage · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">mask</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="124" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="124" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="124" font-size="11.5" fill="#9AA0A8">width</text>
<text x="326" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="153" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="153" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="153" font-size="11.5" fill="#9AA0A8">height</text>
<text x="326" y="153" font-size="11.5" fill="#E6E8EB" text-anchor="end">512</text>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="182" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="182" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="182" font-size="11.5" fill="#9AA0A8">mode</text>
<text x="326" y="182" font-size="11.5" fill="#E6E8EB" text-anchor="end">crop</text>
<rect x="12" y="195" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="211" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
<text x="338" y="211" font-size="11.5" fill="#E6E8EB" text-anchor="end">#000000</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="90" viewBox="0 0 360 90" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Create Box Mask node">
<rect x="0.5" y="0.5" width="359" height="89" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Create Box Mask</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MaskToBBox · Mask</text>
<circle cx="12" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">mask</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">box_mask</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1005 B

+41
View File
@@ -0,0 +1,41 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="316" viewBox="0 0 360 316" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Math Eval (NH) node">
<rect x="0.5" y="0.5" width="359" height="315" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Math Eval (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MathEval · Logic</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result_float</text>
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">result_int</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">result_string</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">expression</text>
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">a + b</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">a <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">b <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">c <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">d <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">round_to <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">-1</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="273" viewBox="0 0 360 273" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Multi-Slider (FLOAT) node">
<rect x="0.5" y="0.5" width="359" height="272" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Multi-Slider (FLOAT)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MultiSliderFloat · Utils</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 1</text>
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 2</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 3</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 4</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">Float 5</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="190" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="190" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="190" font-size="11.5" fill="#9AA0A8">float_1</text>
<text x="326" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="219" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="219" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="219" font-size="11.5" fill="#9AA0A8">float_2</text>
<text x="326" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="248" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">float_3 … float_5</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="273" viewBox="0 0 360 273" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Multi-Slider (INT) node">
<rect x="0.5" y="0.5" width="359" height="272" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Multi-Slider (INT)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_MultiSliderInt · Utils</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 1</text>
<circle cx="348" cy="67.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 2</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 3</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 4</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">Int 5</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="174" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="190" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="190" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="190" font-size="11.5" fill="#9AA0A8">int_1</text>
<text x="326" y="190" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="219" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="219" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="219" font-size="11.5" fill="#9AA0A8">int_2</text>
<text x="326" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="248" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">int_3 … int_5</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Pack Universal (NH) node">
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Pack Universal (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PackUniversal · Utils/Pipe</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_0 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">package</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<circle cx="12" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">input_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="146" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">input_3 … input_9</text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+70
View File
@@ -0,0 +1,70 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="519" viewBox="0 0 360 519" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Portrait Segment (NH) node">
<rect x="0.5" y="0.5" width="359" height="518" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Portrait Segment (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PortraitSegment · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">cutout_rgba</text>
<circle cx="348" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">mask_image</text>
<circle cx="348" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">face</text>
<rect x="308" y="136" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="142.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">neck</text>
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="171.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="204" font-size="11.5" fill="#9AA0A8">hair</text>
<rect x="308" y="194" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="200.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="233" font-size="11.5" fill="#9AA0A8">eyes</text>
<rect x="308" y="223" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="229.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">lips</text>
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="258.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">nose</text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="320" font-size="11.5" fill="#9AA0A8">eyebrows</text>
<rect x="308" y="310" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="316.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="349" font-size="11.5" fill="#9AA0A8">ears</text>
<rect x="308" y="339" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="345.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="362" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="378" font-size="11.5" fill="#9AA0A8">mouth_interior</text>
<rect x="308" y="368" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="374.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="391" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="407" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="407" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="407" font-size="11.5" fill="#9AA0A8">mask_expand</text>
<text x="326" y="407" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="420" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="436" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="436" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="436" font-size="11.5" fill="#9AA0A8">mask_blur</text>
<text x="326" y="436" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="449" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="465" font-size="11.5" fill="#9AA0A8">fill_holes</text>
<rect x="308" y="455" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="461.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="478" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="494" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="494" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="494" font-size="11.5" fill="#9AA0A8">device</text>
<text x="326" y="494" font-size="11.5" fill="#E6E8EB" text-anchor="end">cuda</text>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="207" viewBox="0 0 360 207" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Join (NH) node">
<rect x="0.5" y="0.5" width="359" height="206" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Join (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptJoin · Text</text>
<circle cx="12" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">text_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">text_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">separator</text>
<text x="338" y="124" font-size="11.5" fill="#E6E8EB" text-anchor="end">, </text>
<rect x="12" y="137" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="153" font-size="11.5" fill="#9AA0A8">skip_empty</text>
<rect x="308" y="143" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="149.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="166" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="182" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">text_3 … text_5</text>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+36
View File
@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="309" viewBox="0 0 360 309" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Scheduler (NH) node">
<rect x="0.5" y="0.5" width="359" height="308" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Scheduler (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptScheduler · Text</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">current_prompt</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">progress</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">step_index</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">prompts</text>
<text x="22" y="166" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">prompt 1 ⏎ prompt 2 ⏎ prompt 3</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">current_step</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">total_steps</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">10</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">mode</text>
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">sequential</text>
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">seed <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+23
View File
@@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="229" viewBox="0 0 360 229" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Prompt Template (NH) node">
<rect x="0.5" y="0.5" width="359" height="228" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Prompt Template (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_PromptTemplate · Text</text>
<circle cx="12" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">var1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">var2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">missing_vars</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="108" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="124" font-size="11.5" fill="#9AA0A8">template</text>
<text x="22" y="144" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">a {color} {garment}, {style},…</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">var_names</text>
<text x="338" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">color,garment,style</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="204" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">var3 … var6</text>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="229" viewBox="0 0 360 229" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Random Choice (NH) node">
<rect x="0.5" y="0.5" width="359" height="228" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Random Choice (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RandomChoice · Logic</text>
<circle cx="12" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">input_0 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">input_1 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">picked_index</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="111.0" r="5" fill="#CFD8DC" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">input_2 <tspan fill="#7d838c" font-style="italic">(optional)</tspan></text>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">probabilities</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">seed</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">weights</text>
<text x="338" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">1,1,1,1,1,1</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="180.0" y="204" font-size="11" fill="#9AA0A8" text-anchor="middle" font-style="italic">input_3 … input_5</text>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,53 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="426" viewBox="0 0 360 426" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Ratio Preset Image Resize node">
<rect x="0.5" y="0.5" width="359" height="425" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Ratio Preset Image Resize</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RatioPresetImageResize · Resolution</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">info</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">source_aspect_ratio</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">target_aspect_ratio</text>
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">selected_preset</text>
<circle cx="348" cy="199.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="225.0" font-size="12" fill="#D7DADF" text-anchor="end">resize_data</text>
<circle cx="348" cy="221.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="240" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="256" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="256" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="256" font-size="11.5" fill="#9AA0A8">model_family</text>
<text x="326" y="256" font-size="11.5" fill="#E6E8EB" text-anchor="end">FLUX.2 Klein 9B</text>
<rect x="12" y="269" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="285" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="285" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="285" font-size="11.5" fill="#9AA0A8">resolution_level</text>
<text x="326" y="285" font-size="11.5" fill="#E6E8EB" text-anchor="end">1 MP</text>
<rect x="12" y="298" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="314" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="314" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="314" font-size="11.5" fill="#9AA0A8">target_ratio</text>
<text x="326" y="314" font-size="11.5" fill="#E6E8EB" text-anchor="end">1:1</text>
<rect x="12" y="327" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="343" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="343" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="343" font-size="11.5" fill="#9AA0A8">resize_mode</text>
<text x="326" y="343" font-size="11.5" fill="#E6E8EB" text-anchor="end">crop</text>
<rect x="12" y="356" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="372" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="372" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="372" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="372" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
<rect x="12" y="385" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="401" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
<text x="338" y="401" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

+29
View File
@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="302" viewBox="0 0 360 302" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Regex Extract (NH) node">
<rect x="0.5" y="0.5" width="359" height="301" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Regex Extract (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RegexExtract · Text</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">result</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">matched</text>
<circle cx="348" cy="89.0" r="5" fill="#F48FB1" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">groups</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="168" font-size="11.5" fill="#9AA0A8">text</text>
<text x="22" y="188" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">…</text>
<rect x="12" y="203" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="219" font-size="11.5" fill="#9AA0A8">pattern</text>
<text x="338" y="219" font-size="11.5" fill="#E6E8EB" text-anchor="end">(\d+)</text>
<rect x="12" y="232" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="248" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="248" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="248" font-size="11.5" fill="#9AA0A8">mode</text>
<text x="326" y="248" font-size="11.5" fill="#E6E8EB" text-anchor="end">match</text>
<rect x="12" y="261" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="277" font-size="11.5" fill="#9AA0A8">replacement <tspan fill="#6f747c" font-style="italic">opt</tspan></text>
<text x="338" y="277" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="222" viewBox="0 0 360 222" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Remove Resize Padding node">
<rect x="0.5" y="0.5" width="359" height="221" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Remove Resize Padding</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RemoveResizePadding · Resolution</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">resize_data</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">info</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">output_size</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">content_region</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="171" viewBox="0 0 360 171" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Restore Image Geometry node">
<rect x="0.5" y="0.5" width="359" height="170" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Restore Image Geometry</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_RestoreImageGeometry · Resolution</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">original_image</text>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="111.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">resize_data</text>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="146" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="146" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="146" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">bicubic</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+42
View File
@@ -0,0 +1,42 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="338" viewBox="0 0 360 338" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="SAM3 Segment (NH) node">
<rect x="0.5" y="0.5" width="359" height="337" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">SAM3 Segment (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SAM3Segment · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">concepts</text>
<text x="22" y="166" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">person</text>
<rect x="12" y="181" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="197" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="197" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="197" font-size="11.5" fill="#9AA0A8">threshold</text>
<text x="326" y="197" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.50</text>
<rect x="12" y="210" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="226" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="226" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="226" font-size="11.5" fill="#9AA0A8">detail_level</text>
<text x="326" y="226" font-size="11.5" fill="#E6E8EB" text-anchor="end">balanced</text>
<rect x="12" y="239" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="255" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="255" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="255" font-size="11.5" fill="#9AA0A8">max_per_concept</text>
<text x="326" y="255" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="268" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="284" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="284" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="284" font-size="11.5" fill="#9AA0A8">checkpoint</text>
<text x="326" y="284" font-size="11.5" fill="#E6E8EB" text-anchor="end">(choose a file)</text>
<rect x="12" y="297" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="313" font-size="11.5" fill="#9AA0A8">keep_model_loaded</text>
<rect x="308" y="303" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="309.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+46
View File
@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="374" viewBox="0 0 360 374" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Save Image Path (NH) node">
<rect x="0.5" y="0.5" width="359" height="373" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Save Image Path (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SaveImagePath · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">images</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">filenames</text>
<circle cx="348" cy="67.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">path</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">saved_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">folder_path</text>
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="175" font-size="11.5" fill="#9AA0A8">create_new_folder</text>
<rect x="308" y="165" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="171.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="204" font-size="11.5" fill="#9AA0A8">new_folder_name</text>
<text x="338" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="233" font-size="11.5" fill="#9AA0A8">filename_prefix</text>
<text x="338" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="262" font-size="11.5" fill="#9AA0A8">number_suffix</text>
<rect x="308" y="252" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="258.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="291" font-size="11.5" fill="#9AA0A8">preview_image</text>
<rect x="308" y="281" width="30" height="13" rx="6.5" fill="#4a4d53"/>
<circle cx="315" cy="287.5" r="5" fill="#F5F6F7"/>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">file_format</text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">png</text>
<rect x="12" y="333" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="349" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="349" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="349" font-size="11.5" fill="#9AA0A8">dpi</text>
<text x="326" y="349" font-size="11.5" fill="#E6E8EB" text-anchor="end">300</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+32
View File
@@ -0,0 +1,32 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="258" viewBox="0 0 360 258" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Save Large Image node">
<rect x="0.5" y="0.5" width="359" height="257" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#F6AD55"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Save Large Image</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SaveLargeImage · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">file_path</text>
<circle cx="348" cy="89.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">saved_count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">filename_prefix</text>
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end">NH_Large</text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">file_format</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">png</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">quality</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">95</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="233" font-size="11.5" fill="#9AA0A8">embed_metadata</text>
<rect x="308" y="223" width="30" height="13" rx="6.5" fill="#4C8BF5"/>
<circle cx="331" cy="229.5" r="5" fill="#F5F6F7"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="149" viewBox="0 0 360 149" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Compose (NH) node">
<rect x="0.5" y="0.5" width="359" height="148" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Compose (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsCompose · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="46" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="102" font-size="11.5" fill="#9AA0A8">expression</text>
<text x="22" y="122" font-size="11.5" fill="#E6E8EB" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">all</text>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="112" viewBox="0 0 360 112" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Preview (NH) node">
<rect x="0.5" y="0.5" width="359" height="111" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Preview (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsPreview · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">preview</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">segs</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+48
View File
@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="345" viewBox="0 0 360 345" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs Select (NH) node">
<rect x="0.5" y="0.5" width="359" height="344" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs Select (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsSelect · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">mask</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="130" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="146" font-size="11.5" fill="#9AA0A8">labels</text>
<text x="338" y="146" font-size="11.5" fill="#E6E8EB" text-anchor="end"></text>
<rect x="12" y="159" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="175" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="175" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="175" font-size="11.5" fill="#9AA0A8">min_score</text>
<text x="326" y="175" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.00</text>
<rect x="12" y="188" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="204" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="204" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="204" font-size="11.5" fill="#9AA0A8">min_area</text>
<text x="326" y="204" font-size="11.5" fill="#E6E8EB" text-anchor="end">0.0000</text>
<rect x="12" y="217" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="233" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="233" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="233" font-size="11.5" fill="#9AA0A8">max_area</text>
<text x="326" y="233" font-size="11.5" fill="#E6E8EB" text-anchor="end">1.0000</text>
<rect x="12" y="246" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="262" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="262" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="262" font-size="11.5" fill="#9AA0A8">sort_by</text>
<text x="326" y="262" font-size="11.5" fill="#E6E8EB" text-anchor="end">score</text>
<rect x="12" y="275" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="291" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="291" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="291" font-size="11.5" fill="#9AA0A8">start</text>
<text x="326" y="291" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
<rect x="12" y="304" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="320" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="320" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="320" font-size="11.5" fill="#9AA0A8">count</text>
<text x="326" y="320" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+21
View File
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="156" viewBox="0 0 360 156" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs To Impact SEGS (NH) node">
<rect x="0.5" y="0.5" width="359" height="155" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs To Impact SEGS (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsToImpactSEGS · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">segs</text>
<circle cx="348" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="86" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="102" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="102" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="102" font-size="11.5" fill="#9AA0A8">crop_factor</text>
<text x="326" y="102" font-size="11.5" fill="#E6E8EB" text-anchor="end">3.0</text>
<rect x="12" y="115" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="131" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="131" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="131" font-size="11.5" fill="#9AA0A8">batch_index</text>
<text x="326" y="131" font-size="11.5" fill="#E6E8EB" text-anchor="end">0</text>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="156" viewBox="0 0 360 156" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="Segs To Masks (NH) node">
<rect x="0.5" y="0.5" width="359" height="155" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">Segs To Masks (NH)</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SegsToMasks · Vision</text>
<circle cx="12" cy="67.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">segs</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">masks</text>
<circle cx="348" cy="67.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">union</text>
<circle cx="348" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">labels</text>
<circle cx="348" cy="111.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">count</text>
<circle cx="348" cy="133.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="193" viewBox="0 0 360 193" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Simple Face Paste node">
<rect x="0.5" y="0.5" width="359" height="192" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Simple Face Paste</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SimpleFacePaste · Image</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">dest_image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">IMAGE</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<circle cx="12" cy="89.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="93.0" font-size="12" fill="#D7DADF">dest_mask</text>
<circle cx="12" cy="111.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="115.0" font-size="12" fill="#D7DADF">source_image</text>
<circle cx="12" cy="133.0" r="5" fill="#81C784" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="137.0" font-size="12" fill="#D7DADF">source_mask</text>
<rect x="12" y="152" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="168" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="168" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="168" font-size="11.5" fill="#9AA0A8">feathering</text>
<text x="326" y="168" font-size="11.5" fill="#E6E8EB" text-anchor="end">20</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="397" viewBox="0 0 360 397" font-family="Inter, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif" role="img" aria-label="NH Smart Ratio Image Resize node">
<rect x="0.5" y="0.5" width="359" height="396" rx="10" fill="#26282c" stroke="#45484f"/>
<path d="M0.5 10.5 a10 10 0 0 1 10 -10 h339 a10 10 0 0 1 10 10 v36 h-359 z" fill="#34373d"/>
<circle cx="17" cy="17" r="4.5" fill="#8FD18F"/>
<text x="28" y="22" font-size="14" font-weight="600" fill="#EEF0F3">NH Smart Ratio Image Resize</text>
<text x="28" y="38" font-size="10.5" fill="#9AA0A8" font-family="'JetBrains Mono', Consolas, 'Courier New', monospace">NH_SmartRatioImageResize · Resolution</text>
<circle cx="12" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="24" y="71.0" font-size="12" fill="#D7DADF">image</text>
<text x="336" y="71.0" font-size="12" fill="#D7DADF" text-anchor="end">image</text>
<circle cx="348" cy="67.0" r="5" fill="#64B5F6" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="93.0" font-size="12" fill="#D7DADF" text-anchor="end">width</text>
<circle cx="348" cy="89.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="115.0" font-size="12" fill="#D7DADF" text-anchor="end">height</text>
<circle cx="348" cy="111.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="137.0" font-size="12" fill="#D7DADF" text-anchor="end">info</text>
<circle cx="348" cy="133.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="159.0" font-size="12" fill="#D7DADF" text-anchor="end">source_aspect_ratio</text>
<circle cx="348" cy="155.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="181.0" font-size="12" fill="#D7DADF" text-anchor="end">target_aspect_ratio</text>
<circle cx="348" cy="177.0" r="5" fill="#9CC2E5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="203.0" font-size="12" fill="#D7DADF" text-anchor="end">selected_preset</text>
<circle cx="348" cy="199.0" r="5" fill="#C5E1A5" stroke="#1b1c1f" stroke-width="1.5"/>
<text x="336" y="225.0" font-size="12" fill="#D7DADF" text-anchor="end">resize_data</text>
<circle cx="348" cy="221.0" r="5" fill="#E0B272" stroke="#1b1c1f" stroke-width="1.5"/>
<rect x="12" y="240" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="256" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="256" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="256" font-size="11.5" fill="#9AA0A8">model_family</text>
<text x="326" y="256" font-size="11.5" fill="#E6E8EB" text-anchor="end">FLUX.2 Klein 9B</text>
<rect x="12" y="269" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="285" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="285" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="285" font-size="11.5" fill="#9AA0A8">resolution_level</text>
<text x="326" y="285" font-size="11.5" fill="#E6E8EB" text-anchor="end">1 MP</text>
<rect x="12" y="298" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="314" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="314" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="314" font-size="11.5" fill="#9AA0A8">resize_mode</text>
<text x="326" y="314" font-size="11.5" fill="#E6E8EB" text-anchor="end">cover_crop</text>
<rect x="12" y="327" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="20" y="343" font-size="10" fill="#6f747c">◀</text>
<text x="332" y="343" font-size="10" fill="#6f747c">▶</text>
<text x="34" y="343" font-size="11.5" fill="#9AA0A8">resize_method</text>
<text x="326" y="343" font-size="11.5" fill="#E6E8EB" text-anchor="end">lanczos</text>
<rect x="12" y="356" width="336" height="24" rx="7" fill="#1d1f22" stroke="#3a3d43"/>
<text x="22" y="372" font-size="11.5" fill="#9AA0A8">pad_color_hex</text>
<text x="338" y="372" font-size="11.5" fill="#E6E8EB" text-anchor="end">#FFFFFF</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

Some files were not shown because too many files have changed in this diff Show More