initial release - Augment node pack v1.0.0

This commit is contained in:
Aaron Burton
2026-03-13 18:48:59 -07:00
commit b409b782c9
40 changed files with 5006 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
.env
.venv/
*.log
.DS_Store
Thumbs.db
+9
View File
@@ -0,0 +1,9 @@
Copyright 2026 Augment Studio
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+213
View File
@@ -0,0 +1,213 @@
# Augment — ComfyUI Node Pack
A design-focused custom node suite for [ComfyUI](https://github.com/comfyanonymous/ComfyUI), built for digital artists and technical designers who need precision, clean outputs, and scalable workflows.
Augment brings grid systems, construction lines, vector conversion, masking, image transforms, and flow control utilities directly into your ComfyUI graph — so you can build production-ready pipelines without leaving the node canvas.
---
## Node Categories
### Vector
Stop exporting pixel-locked assets. The vector nodes bring true scalable output into your generative workflow. Convert any raster image into a clean, editable SVG, or render an SVG back to a high-quality PNG with full alpha, all without leaving ComfyUI.
**PNG to SVG** is built for logo and brand asset pipelines. Run a generated image straight into vectorization and get a production-ready SVG out the other end. Scalable to any size, ready for Illustrator, Figma, or print. **SVG to PNG** works the other way: load any SVG from your input directory and render it to a crisp PNG with alpha channel intact, feeding directly into the rest of your node graph.
Both nodes are powered by the Augment Studio API. An API key is required and available at [augmentstudio.app/pricing](https://augmentstudio.app/pricing). All other Augment nodes work fully offline.
| Node | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **PNG to SVG** | Vectorizes a raster image (PNG, JPG, WebP) into a clean, scalable SVG via the Augment API. Outputs SVG and JSON metadata |
| **SVG to PNG** | Renders an uploaded SVG to a high-quality PNG with full alpha channel. Outputs image and mask tensors |
---
### Image Processing
Precision image manipulation nodes built around the objects and assets that matter most in design workflows. **Logo Mask** makes isolating logos and graphic elements dead simple: it auto-detects the background, thresholds the foreground, fills internal holes, and smooths contours — giving you a clean usable mask from just an image input, no manual selection required. **Mask Bounding Box** rounds out the masking toolkit by reading any mask and outputting the tight bounding box around its content as pixel coordinates, which can then feed directly into crop, placement, or construction line nodes. The crop nodes make it easy to isolate key subjects. **Face Crop** automatically detects and crops to the largest face in an image with configurable padding and square-crop options, while **Image Crop** handles manual region cropping.
Every node in this section is trigger-aware. Wire them together and your image pipeline runs in exactly the order you designed it, allowing for crazy control.
| Node | Description |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Logo Mask** | Generates a clean filled mask from a logo or graphic element. Auto-detects background, fills holes, smooths contours. Outputs mask, mask image, and masked RGB |
| **Mask Bounding Box** | Reads any mask and outputs the tight bounding box around its content as pixel coordinates, with optional padding and threshold control |
| **Face Crop** | Auto-detects the largest face in an image and crops to it, with configurable padding and optional square crop. Outputs crop coordinates and a trigger |
| **Image Crop** | Crops an image to a defined region by coordinate and size |
| **Image Place** | Composites one image onto another at a specified position |
| **Invert Image** | Inverts pixel values across RGB channels |
| **Image Rotate** | Rotates an image by a given angle |
| **Image Flip** | Flips an image horizontally or vertically |
---
### Grid & Geometric Drawing
The composition toolkit. These nodes are built for designers who think in systems grids, ratios, axes, and geometry. The **Grid Generator** is the centrepiece: a fully configurable grid engine that outputs a grid image, a mask, and all the dimension data you need to wire into other nodes. Everything else in this section is designed to work alongside it.
**Grid Coords** is the companion node. A layout engine that takes a canvas size and a layout preset (golden ratio, rule of thirds, halves, quadrants) and a named region (top-left, center, bottom-right, etc.), then outputs exact pixel coordinates, dimensions, center point, and a mask for that region. Feed those coordinates directly into placement, crop, or construction line nodes to build compositionally precise workflows without guessing.
The drawing nodes (Line Draw, Circle Draw, Fibonacci Spiral, Construction Line Overlay) all output both an image overlay and a mask, and are rendered at 2x supersampling for clean, anti-aliased results at any resolution.
| Node | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Grid Generator** | Generates a grid image with configurable rows, columns, line weight, color, and optional dotted lines. Outputs image, mask, and all dimension values |
| **Grid Coords** | Layout engine. Pick a layout preset (golden ratio, thirds, halves, quadrants) and a region name, get back pixel coordinates, dimensions, center point, and a region mask |
| **Line Draw** | Draws an anti-aliased line with presets (diagonals, center axes, outline) or fully custom coordinates. Supports extending lines to canvas edges |
| **Circle Draw** | Draws an anti-aliased circle or ellipse onto a canvas |
| **Fibonacci Spiral** | Renders a golden ratio spiral with optional subdivision rectangles, configurable iterations and line weight |
| **Construction Line Overlay** | Overlays dotted construction lines (bounding box, center axes, diagonals) onto an existing image from a bounding box input |
| **Shape Edge Detect** | Detects shape edges for contour and silhouette extraction |
---
### Color & Effects
| Node | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **Recolor (Mask)** | Recolors a masked region to a target color |
| **Blur Average (Dominant Color)** | Blurs or averages a region to its dominant color, great for when you need to grab a spefic color from an image, photo, or logo. |
---
### Primitives
Typed value nodes with trigger in and trigger out, unlike ComfyUI's built-in primitives which are evaluated at graph compile time, these are execution-aware and can be wired into a specific point in your workflow's run order.
| Node | Description |
| ---------- | ------------------------------------------ |
| **Int** | Integer primitive with trigger passthrough |
| **Float** | Float primitive with trigger passthrough |
| **String** | String primitive with trigger passthrough |
| **Bool** | Boolean primitive with trigger passthrough |
---
### Variables
Primitives on steroids. Variables are named, persistent values that live across nodes in your workflow. Set them once, read them anywhere, and mutate them through increment, decrement, or reset operations. Built for stateful and iterative workflows where a static primitive isn't enough.
| Node | Description |
| ------------------------------- | ------------------------------------------------------- |
| **Variable Set (Number)** | Assigns a value to a named number variable |
| **Variable Get (Number)** | Reads the current value of a named number variable |
| **Variable Increment (Number)** | Increments a named number variable by a given amount |
| **Variable Decrement (Number)** | Decrements a named number variable by a given amount |
| **Variable Reset (Number)** | Resets a named number variable back to its default |
| **Variable Set (String)** | Assigns a value to a named string variable |
| **Variable Get (String)** | Reads the current value of a named string variable |
| **Variable Delete (String)** | Removes a named string variable from the workflow scope |
---
### Flow Control
By default, ComfyUI evaluates your entire graph at once, everything fires, everything runs. Flow Control breaks that open. These nodes let you sequence, gate, and branch your workflow so that things happen when you decide, not all at the same time. The result is less of a static graph and more of a pipeline you actually control.
**Improved Type Nodes** are the backbone, trigger-aware versions of every major ComfyUI data type that hold their value and only pass it through once a trigger arrives. Chain them together and you get a workflow that moves step by step, each stage waiting on the one before it. **Switch** adds conditional logic: feed it a boolean and it fires down one of two paths, blocking the other entirely, true branching inside a ComfyUI graph.
| Node | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Improved Type Nodes** | Trigger-aware nodes for every major ComfyUI data type (Image, Mask, Latent, Model, Clip, VAE, Conditioning, Int, Float, String, Bool) — holds a value and only passes it through once a trigger arrives |
| **Switch (Improved)** | Takes a boolean and fires down either the true or false path, blocking the other — conditional branching inside your graph |
---
### Data & Intelligence
Every Augment node that does meaningful work outputs structured JSON, coordinates, dimensions, colors, contour counts, layout data, whatever that node knows about what it just did. This section is how you use that data.
**JSON Viewer** lets you inspect the output of any node inline in your graph, no exporting, no guessing. **JSON Extract** lets you pull specific values out by field name and wire them directly into other nodes, prompts, or variables. Today that means tighter, data-driven workflows where one node's output genuinely informs the next. The longer-term direction is feeding this structured data to an LLM so it can make decisions mid-workflow and write values back, closing the loop between generation and design logic in a way ComfyUI doesn't support out of the box.
| Node | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| **JSON Viewer** | Inspects and pretty-prints any Augment JSON output inline in the node canvas |
| **JSON Extract** | Pulls a specific value out of any Augment JSON by field name and outputs it as a string ready to wire forward |
---
### Preview & Save
Improved versions of ComfyUI's core I/O nodes, all trigger-aware and all designed to stay out of your way while giving you more. Every node here passes its value through, so you can drop them anywhere in a chain without breaking it.
**Load Image (Improved)** goes well beyond a standard image loader. Alongside the image it outputs dimensions, alpha mask, and individual R/G/B channel masks, all ready to wire into other nodes without any extra processing. **Preview Any** is particularly useful when building or debugging complex graphs: connect it to any text or numeric output — a JSON string, a variable, a coordinate — and it displays the value inline without interrupting the flow.
| Node | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Load Image (Improved)** | Loads an image and outputs width, height, alpha mask, and individual R/G/B channel masks alongside the image tensor. Supports image override input and trigger passthrough |
| **Preview Image** | Previews an image inline in the node canvas without writing to the output folder. Passes the image through so the chain keeps running |
| **Save Image (Improved)** | Saves an image to the output folder with a custom filename prefix. Passes the image through with trigger out |
| **Preview Any** | Displays any text or numeric value — strings, numbers, JSON — as readable text in the node UI. Useful for inspecting outputs mid-graph without interrupting the workflow |
---
## Installation
Augment is not yet listed on the ComfyUI registry. Install it manually by cloning the repository directly into your `custom_nodes` folder.
### Step 1: Clone the repository
```bash
git clone https://github.com/augmentstudio/augment-ComfyUI augment
cd augment
```
### Step 2: Install dependencies
```bash
cd augment
pip install -r requirements.txt
```
### Step 3: Restart ComfyUI
Restart ComfyUI. Augment nodes will appear under the **Augment** category in the node search.
---
## Vector Nodes: API Key
The **PNG to SVG** and **SVG to PNG** nodes connect to the Augment Studio API for high-quality vectorization. You will need an API key:
1. Visit [augmentstudio.app/pricing](https://augmentstudio.app/pricing)
2. Create an account and obtain your API key
3. Paste the key into the `api_key` field on the vector nodes
All other nodes work fully offline without an API key.
---
## Requirements
- ComfyUI (latest)
- Python 3.10+
- `opencv-python >= 4.5.0`
- `numpy >= 1.19.0`
- `torch >= 1.9.0`
- `Pillow >= 8.0.0`
- `requests >= 2.25.0`
---
## License
MIT License: Copyright 2026 Augment Studio. See [LICENSE.MD](LICENSE.MD) for details.
+186
View File
@@ -0,0 +1,186 @@
"""
Augment - ComfyUI Custom Node Pack
Resilient loader: each node imports independently so one failure won't block the rest.
"""
import traceback
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
_failed = []
def _register(module_path, imports, mappings):
"""
Try to import `imports` from `module_path` and register each into the global dicts.
imports: list of (ClassName,) — names to import from the module
mappings: dict of { "NodeID": ("ClassName", "Display Name") }
"""
try:
mod = __import__(module_path, fromlist=[c for c in imports])
except Exception as e:
for node_id, (cls_name, display) in mappings.items():
_failed.append((node_id, e))
print(f"[augment] ⚠ {node_id} unavailable: {e}")
return
for node_id, (cls_name, display) in mappings.items():
cls = getattr(mod, cls_name, None)
if cls is None:
_failed.append((node_id, f"{cls_name} not found in {module_path}"))
print(f"[augment] ⚠ {node_id} unavailable: {cls_name} not found in {module_path}")
continue
NODE_CLASS_MAPPINGS[node_id] = cls
NODE_DISPLAY_NAME_MAPPINGS[node_id] = display
# ── Executor Override (must import first to patch ComfyUI) ──
try:
__import__("custom_nodes.augment.py.executor")
except Exception as e:
print(f"[augment] ⚠ Executor patch unavailable: {e}")
# ── Image Processing & Cropping ──
_register("custom_nodes.augment.py.face_crop", ["FaceCrop"], {
"AugmentFaceCrop": ("FaceCrop", "Face Crop"),
})
_register("custom_nodes.augment.py.image_crop", ["ImageCrop"], {
"AugmentImageCrop": ("ImageCrop", "Image Crop"),
})
_register("custom_nodes.augment.py.image_place", ["ImagePlaceNode"], {
"AugmentImagePlace": ("ImagePlaceNode", "Image Place"),
})
_register("custom_nodes.augment.py.invert_image", ["InvertImageNode"], {
"AugmentInvertImage": ("InvertImageNode", "Invert Image"),
})
_register("custom_nodes.augment.py.image_rotate", ["ImageRotateNode"], {
"AugmentImageRotate": ("ImageRotateNode", "Image Rotate"),
})
_register("custom_nodes.augment.py.image_flip", ["ImageFlipNode"], {
"AugmentImageFlip": ("ImageFlipNode", "Image Flip"),
})
_register("custom_nodes.augment.py.logo_mask", ["LogoMask"], {
"AugmentLogoMask": ("LogoMask", "Logo Mask"),
})
_register("custom_nodes.augment.py.mask_bounding_box", ["MaskBoundingBox"], {
"AugmentMaskBoundingBox": ("MaskBoundingBox", "Mask Bounding Box"),
})
# ── Grid & Construction Lines ──
_register("custom_nodes.augment.py.grid", ["GridGenerator"], {
"AugmentGridGenerator": ("GridGenerator", "Grid Generator"),
})
_register("custom_nodes.augment.py.grid_coords", ["GridCoordsNode"], {
"AugmentGridCoords": ("GridCoordsNode", "Grid Coords"),
})
_register("custom_nodes.augment.py.draw_line", ["LineDrawNode"], {
"AugmentLineDraw": ("LineDrawNode", "Line Draw"),
})
_register("custom_nodes.augment.py.circle_draw", ["CircleDrawNode"], {
"AugmentCircleDraw": ("CircleDrawNode", "Circle Draw"),
})
_register("custom_nodes.augment.py.fib", ["FibonacciSpiralNode"], {
"AugmentFibonacciSpiral": ("FibonacciSpiralNode", "Fibonacci Spiral"),
})
_register("custom_nodes.augment.py.construction_lines", ["ConstructionLineOverlay"], {
"AugmentConstructionLineOverlay": ("ConstructionLineOverlay", "Construction Line Overlay"),
})
_register("custom_nodes.augment.py.edge_detect", ["ShapeEdgeDetect"], {
"AugmentShapeEdgeDetect": ("ShapeEdgeDetect", "Shape Edge Detect"),
})
# ── Color & Effects ──
_register("custom_nodes.augment.py.recolor", ["RecolorNode"], {
"AugmentRecolor": ("RecolorNode", "Recolor (Mask)"),
})
_register("custom_nodes.augment.py.blur_average", ["BlurAverageNode"], {
"AugmentBlurAverage": ("BlurAverageNode", "Blur Average (Dominant Color)"),
})
# ── Primitives ──
_register("custom_nodes.augment.py.prim_node", ["IntNode", "FloatNode", "StringNode", "BoolNode"], {
"AugmentIntPrim": ("IntNode", "Int"),
"AugmentFloatPrim": ("FloatNode", "Float"),
"AugmentStringPrim": ("StringNode", "String"),
"AugmentBoolPrim": ("BoolNode", "Bool"),
})
# ── Variables ──
_register("custom_nodes.augment.py.variables", [
"NumberVarSetNode", "NumberVarGetNode", "NumberVarIncrementNode",
"NumberVarDecrementNode", "NumberVarResetNode",
"StringVarSetNode", "StringVarGetNode", "StringVarDeleteNode",
], {
"AugmentNumberVarSet": ("NumberVarSetNode", "Variable Set (Number)"),
"AugmentNumberVarGet": ("NumberVarGetNode", "Variable Get (Number)"),
"AugmentNumberVarIncrement": ("NumberVarIncrementNode", "Variable Increment (Number)"),
"AugmentNumberVarDecrement": ("NumberVarDecrementNode", "Variable Decrement (Number)"),
"AugmentNumberVarReset": ("NumberVarResetNode", "Variable Reset (Number)"),
"AugmentStringVarSet": ("StringVarSetNode", "Variable Set (String)"),
"AugmentStringVarGet": ("StringVarGetNode", "Variable Get (String)"),
"AugmentStringVarDelete": ("StringVarDeleteNode", "Variable Delete (String)"),
})
# ── Flow Control ──
try:
from .py.trigger import make_wait_node, WAIT_TYPES
for _name, _type_str in WAIT_TYPES.items():
_cls_name = f"AugmentWait{_name}"
try:
NODE_CLASS_MAPPINGS[_cls_name] = make_wait_node(_name, _type_str)
NODE_DISPLAY_NAME_MAPPINGS[_cls_name] = f"{_name} (Improved)"
except Exception as e:
_failed.append((_cls_name, e))
print(f"[augment] ⚠ {_cls_name} unavailable: {e}")
except Exception as e:
_failed.append(("Wait nodes", e))
print(f"[augment] ⚠ Flow control nodes unavailable: {e}")
# PAID
_register("custom_nodes.augment.py.svg_tools", ["AugmentSVGToPNG", "AugmentPNGToSVG"], {
"AugmentSVGToPNG": ("AugmentSVGToPNG", "Augment SVG to PNG"),
"AugmentPNGToSVG": ("AugmentPNGToSVG", "Augment PNG to SVG"),
})
# ── Crypto & Utilities ──
_register("custom_nodes.augment.py.crypto", ["SHA256Node", "RandomNumberNode", "UUIDNode"], {
"AugmentSHA256": ("SHA256Node", "SHA-256 Hash"),
"AugmentRandomNumber": ("RandomNumberNode", "Random Number"),
"AugmentUUID": ("UUIDNode", "UUID Generator"),
})
_register("custom_nodes.augment.py.json_viewer", ["JsonViewerNode"], {
"AugmentJsonViewer": ("JsonViewerNode", "JSON Viewer"),
})
_register("custom_nodes.augment.py.json_extract", ["JsonExtractNode"], {
"AugmentJsonExtract": ("JsonExtractNode", "JSON Extract"),
})
# ── Preview & Save ──
_register("custom_nodes.augment.py.preview", ["AugmentPreviewImage", "AugmentSaveImage", "AugmentPreviewAny"], {
"AugmentPreviewImage": ("AugmentPreviewImage", "Preview Image"),
"AugmentSaveImage": ("AugmentSaveImage", "Save Image (Improved)"),
"AugmentPreviewAny": ("AugmentPreviewAny", "Preview Any"),
})
_register("custom_nodes.augment.py.load_image", ["AugmentLoadImage"], {
"AugmentLoadImage": ("AugmentLoadImage", "Load Image (Improved)"),
})
_register("custom_nodes.augment.py.switch", ["SwitchNode"], {
"AugmentSwitchNode": ("SwitchNode", "Switch Improved"),
})
# ── Summary ──
WEB_DIRECTORY = "./web/js"
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
if _failed:
print(f"[augment] ✓ Registered {len(NODE_CLASS_MAPPINGS)} nodes ({len(_failed)} failed)")
for node_id, err in _failed:
print(f"[augment] ✗ {node_id}: {err}")
else:
print(f"[augment] ✓ Registered {len(NODE_CLASS_MAPPINGS)} nodes")
+32
View File
@@ -0,0 +1,32 @@
"""
Auto-install dependencies for Augment nodes.
ComfyUI will run this automatically when the node pack is loaded.
"""
import subprocess
import sys
import os
def install():
"""Install required packages."""
requirements_path = os.path.join(os.path.dirname(__file__), "requirements.txt")
if not os.path.exists(requirements_path):
print("[augment] No requirements.txt found, skipping dependency install")
return
print("[augment] Checking dependencies...")
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", "-r", requirements_path, "--quiet"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE
)
print("[augment] ✓ Dependencies installed/verified")
except subprocess.CalledProcessError as e:
print(f"[augment] ⚠ Warning: Could not install dependencies: {e}")
print("[augment] Please manually run: pip install -r requirements.txt")
if __name__ == "__main__":
install()
+96
View File
@@ -0,0 +1,96 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import torch
import numpy as np
from PIL import Image
import folder_paths
import os
class BlurAverageNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "FLOAT", "FLOAT", "FLOAT", "JSON", "TRIGGER")
RETURN_NAMES = ("color_swatch","R", "G", "B", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Augment/Image"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, image, trigger=None):
# image tensor shape: (batch, height, width, channels) with values 0-1
img = image[0]
# Mean across all pixels
r_avg = img[:, :, 0].mean().item()
g_avg = img[:, :, 1].mean().item()
b_avg = img[:, :, 2].mean().item()
# Convert to 0-255
r_255 = round(r_avg * 255)
g_255 = round(g_avg * 255)
b_255 = round(b_avg * 255)
hex_color = "#{:02X}{:02X}{:02X}".format(r_255, g_255, b_255)
# --- Generate swatch as IMAGE output (64x64) ---
swatch = torch.zeros(1, 64, 64, 3)
swatch[:, :, :, 0] = r_avg
swatch[:, :, :, 1] = g_avg
swatch[:, :, :, 2] = b_avg
# --- Save temp image for in-node preview ---
preview_img = Image.new("RGB", (64, 64), (r_255, g_255, b_255))
temp_dir = folder_paths.get_temp_directory()
os.makedirs(temp_dir, exist_ok=True)
preview_filename = f"blur_avg_{hex_color.lstrip('#')}.png"
preview_path = os.path.join(temp_dir, preview_filename)
preview_img.save(preview_path)
results = {
"r": r_255,
"g": g_255,
"b": b_255,
"hex": hex_color,
"r_float": round(r_avg, 6),
"g_float": round(g_avg, 6),
"b_float": round(b_avg, 6),
}
return {
"ui": {
"images": [
{
"filename": preview_filename,
"subfolder": "",
"type": "temp",
}
]
},
"result": (json.dumps(results, indent=2), float(r_255), float(g_255), float(b_255), swatch, "done"),
}
NODE_CLASS_MAPPINGS = {
"BlurAverageNode": BlurAverageNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"BlurAverageNode": "Blur Average (Dominant Color)",
}
+196
View File
@@ -0,0 +1,196 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
from PIL import Image, ImageDraw
import json
import math
PHI = 1.6180339887
class CircleDrawNode:
"""
Draw construction circles. Three modes:
- fibonacci: reads spiral JSON and draws circles at each subdivision step
- single: one circle from manual center_x/center_y/radius
- concentric: multiple circles from center, scaled by golden ratio
"""
MODES = ["fibonacci", "single", "concentric"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"height": ("INT", {"default": 632, "min": 64, "max": 8192, "step": 1}),
"mode": (cls.MODES, {"default": "fibonacci"}),
"center_x": ("INT", {"default": 512, "min": -8192, "max": 8192, "step": 1}),
"center_y": ("INT", {"default": 316, "min": -8192, "max": 8192, "step": 1}),
"radius": ("INT", {"default": 200, "min": 1, "max": 8192, "step": 1}),
"rings": ("INT", {"default": 5, "min": 1, "max": 12, "step": 1}),
"line_weight": ("INT", {"default": 1, "min": 1, "max": 20, "step": 1}),
"line_color": ("STRING", {"default": "#FFFFFF"}),
"dotted": ("BOOLEAN", {"default": False}),
"invert": ("BOOLEAN", {"default": False}),
},
"optional": {
"spiral_json": ("AUGMENT_JSON",),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "mask", "json_result", "trigger")
FUNCTION = "draw"
CATEGORY = "Augment/Grid"
@staticmethod
def _hex_to_rgb(hex_str):
hex_str = hex_str.lstrip("#")
if len(hex_str) == 3:
hex_str = "".join(c * 2 for c in hex_str)
return tuple(int(hex_str[i : i + 2], 16) for i in (0, 2, 4))
@staticmethod
def _draw_circle(draw, cx, cy, r, color, weight, dotted=False):
"""Draw a single circle, optionally dotted."""
if r < 1:
return
bbox = [cx - r, cy - r, cx + r, cy + r]
if not dotted:
draw.ellipse(bbox, outline=color, width=weight)
else:
# Draw dotted circle using small arcs
steps = max(36, int(r * 0.5))
gap = 360 / steps
for i in range(0, steps, 2):
start = i * gap
end = start + gap
draw.arc(bbox, start=start, end=end, fill=color, width=weight)
def draw(
self,
width,
height,
mode,
center_x,
center_y,
radius,
rings,
line_weight,
line_color,
dotted,
invert,
spiral_json=None,
trigger=None,
):
color = self._hex_to_rgb(line_color)
# Supersampling
ss = 2
sw, sh = width * ss, height * ss
slw = line_weight * ss
img = Image.new("RGBA", (sw, sh), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
circles_data = []
if mode == "fibonacci" and spiral_json:
# Parse spiral JSON and draw circles at each step
if isinstance(spiral_json, str):
spiral = json.loads(spiral_json)
else:
spiral = spiral_json
steps = spiral.get("steps", [])
for step in steps:
cx = step["arc_center"]["x"] * ss
cy = step["arc_center"]["y"] * ss
r = step["radius"] * ss
self._draw_circle(draw, cx, cy, r, color + (255,), slw, dotted)
# Also draw small crosshair at center
ch_size = max(4, slw * 2)
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
circles_data.append({
"center": {"x": step["arc_center"]["x"], "y": step["arc_center"]["y"]},
"radius": step["radius"],
"iteration": step["iteration"],
})
elif mode == "concentric":
# Draw rings scaling by golden ratio from center
cx = center_x * ss
cy = center_y * ss
r = radius * ss
for i in range(rings):
current_r = int(r)
self._draw_circle(draw, cx, cy, current_r, color + (255,), slw, dotted)
# Crosshair at center (only on first)
if i == 0:
ch_size = max(6, slw * 3)
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
circles_data.append({
"center": {"x": center_x, "y": center_y},
"radius": int(current_r // ss),
"ring": i,
"scale": round(1 / (PHI ** i), 4),
})
r = r / PHI
else:
# Single circle
cx = center_x * ss
cy = center_y * ss
r = radius * ss
self._draw_circle(draw, cx, cy, int(r), color + (255,), slw, dotted)
# Crosshair
ch_size = max(6, slw * 3)
draw.line([(cx - ch_size, cy), (cx + ch_size, cy)], fill=color + (120,), width=max(1, slw // 2))
draw.line([(cx, cy - ch_size), (cx, cy + ch_size)], fill=color + (120,), width=max(1, slw // 2))
circles_data.append({
"center": {"x": center_x, "y": center_y},
"radius": radius,
})
# Downsample
img = img.resize((width, height), Image.LANCZOS)
# Build output
result = {
"mode": mode,
"canvas": [width, height],
"circles": circles_data,
}
json_result = json.dumps(result, indent=2)
# Convert
img_np = np.array(img)
alpha = img_np[:, :, 3].astype(np.float32) / 255.0
if invert:
alpha = 1.0 - alpha
rgb = img_np[:, :, :3].astype(np.float32) / 255.0
image_tensor = torch.from_numpy(rgb).unsqueeze(0)
mask_tensor = torch.from_numpy(alpha).unsqueeze(0)
return (image_tensor, mask_tensor, json_result, trigger)
+135
View File
@@ -0,0 +1,135 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import numpy as np
import torch
import json
from PIL import Image, ImageDraw
LINE_COLOR = (180, 180, 180)
class ConstructionLineOverlay:
"""Draws construction lines on an image from bounding box."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"x": ("INT", {"default": 0}),
"y": ("INT", {"default": 0}),
"width": ("INT", {"default": 100}),
"height": ("INT", {"default": 100}),
},
"optional": {
"show_bbox": ("BOOLEAN", {"default": True}),
"show_centers": ("BOOLEAN", {"default": True}),
"show_diagonals": ("BOOLEAN", {"default": True}),
"dot_weight": ("INT", {"default": 3, "min": 1, "max": 20, "step": 1}),
"dot_spacing": ("INT", {"default": 10, "min": 1, "max": 60, "step": 1}),
"show_coords": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "json_result", "trigger")
FUNCTION = "draw"
CATEGORY = "Augment/Grid"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def draw(self, image, x, y, width, height,
show_bbox=True, show_centers=True, show_diagonals=True,
dot_weight=3, dot_spacing=10, show_coords=False):
dot_weight = max(1, dot_weight)
dot_spacing = max(3, dot_spacing)
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
img_h, img_w = img_np.shape[:2]
pil_img = Image.fromarray(img_np, "RGB")
draw = ImageDraw.Draw(pil_img)
x0, y0 = x, y
x1, y1 = x + width - 1, y + height - 1
cx, cy = x + width // 2, y + height // 2
r = dot_weight / 2.0
if show_diagonals:
_draw_dotted_line_any(draw, (x0, y0), (x1, y1), LINE_COLOR, r, dot_spacing)
_draw_dotted_line_any(draw, (x1, y0), (x0, y1), LINE_COLOR, r, dot_spacing)
if show_centers:
_draw_dotted_line_h(draw, 0, img_w, cy, LINE_COLOR, r, dot_spacing)
_draw_dotted_line_v(draw, 0, img_h, cx, LINE_COLOR, r, dot_spacing)
if show_bbox:
_draw_dotted_line_h(draw, x0, x1, y0, LINE_COLOR, r, dot_spacing)
_draw_dotted_line_h(draw, x0, x1, y1, LINE_COLOR, r, dot_spacing)
_draw_dotted_line_v(draw, y0, y1, x0, LINE_COLOR, r, dot_spacing)
_draw_dotted_line_v(draw, y0, y1, x1, LINE_COLOR, r, dot_spacing)
if show_coords:
_draw_label(draw, f"{width}px", (cx, y0 - 10), LINE_COLOR, align="center")
_draw_label(draw, f"{height}px", (x1 + 8, cy), LINE_COLOR, align="left")
result_np = np.array(pil_img).astype(np.float32) / 255.0
result = torch.from_numpy(result_np).unsqueeze(0)
json_result = json.dumps({
"node": "ConstructionLineOverlay",
"x": x, "y": y, "width": width, "height": height,
"center": [cx, cy],
})
return (result, json_result, "done")
def _draw_dotted_line_h(draw, x_start, x_end, y_pos, color, r, spacing):
"""Horizontal dotted line using evenly spaced circles (matches grid.py)."""
x = float(x_start)
while x <= x_end:
draw.ellipse([x - r, y_pos - r, x + r, y_pos + r], fill=color)
x += spacing
def _draw_dotted_line_v(draw, y_start, y_end, x_pos, color, r, spacing):
"""Vertical dotted line using evenly spaced circles (matches grid.py)."""
y = float(y_start)
while y <= y_end:
draw.ellipse([x_pos - r, y - r, x_pos + r, y + r], fill=color)
y += spacing
def _draw_dotted_line_any(draw, p1, p2, color, r, spacing):
"""Diagonal dotted line using evenly spaced circles."""
x0, y0 = p1
x1, y1 = p2
dx, dy = x1 - x0, y1 - y0
length = (dx * dx + dy * dy) ** 0.5
if length < 1:
return
ux, uy = dx / length, dy / length
pos = 0.0
while pos <= length:
px = x0 + ux * pos
py = y0 + uy * pos
draw.ellipse([px - r, py - r, px + r, py + r], fill=color)
pos += spacing
def _draw_label(draw, text, position, color, align="left"):
"""Draw a small coordinate label."""
x, y = position
try:
from PIL import ImageFont
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 11)
except Exception:
font = ImageFont.load_default()
anchor = "rm" if align == "right" else ("mm" if align == "center" else "lm")
draw.text((x, y), text, fill=color, font=font, anchor=anchor)
+109
View File
@@ -0,0 +1,109 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import hashlib
import secrets
import time
import json
class SHA256Node:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"encode": ("STRING", {"forceInput": True}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("hash", "json", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, encode, trigger=None):
h = hashlib.sha256(encode.encode("utf-8")).hexdigest()
json_result = json.dumps({"node": "SHA-256 Hash", "input": encode, "hash": h, "algorithm": "sha256"})
return {"ui": {"text": [h]}, "result": (h, json_result, "done")}
class RandomNumberNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
def execute(self, trigger=None):
n = secrets.randbelow(1000000000)
json_result = json.dumps({"node": "Random Number", "number": n, "max": 1000000000})
return {"ui": {"text": [str(n)]}, "result": (n, json_result, "done")}
class UUIDNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("uuid", "json", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
def execute(self, trigger=None):
u = secrets.token_hex(16)
json_result = json.dumps({"node": "UUID Generator", "uuid": u, "length": 32})
return {"ui": {"text": [u]}, "result": (u, json_result, "done")}
NODE_CLASS_MAPPINGS = {
"SHA256Node": SHA256Node,
"RandomNumberNode": RandomNumberNode,
"UUIDNode": UUIDNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SHA256Node": "SHA-256 Hash",
"RandomNumberNode": "Random Number",
"UUIDNode": "UUID Generator",
}
+197
View File
@@ -0,0 +1,197 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
from PIL import Image, ImageDraw
import json
import math
class LineDrawNode:
"""
Draw a line from point A to point B.
Outputs image overlay and mask. Supersampled 2x for clean anti-aliased lines.
"""
PRESETS = [
"custom",
"vertical_center",
"horizontal_center",
"diagonal_left",
"diagonal_right",
"outline",
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"preset": (cls.PRESETS, {"default": "diagonal_left"}),
"x1": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"y1": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"x2": ("INT", {"default": 1024, "min": -8192, "max": 8192, "step": 1}),
"y2": ("INT", {"default": 1024, "min": -8192, "max": 8192, "step": 1}),
"line_weight": ("INT", {"default": 2, "min": 1, "max": 20, "step": 1}),
"line_color": ("STRING", {"default": "#FFFFFF"}),
"extend": ("BOOLEAN", {"default": False}),
"invert": ("BOOLEAN", {"default": False}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "mask", "json_result", "trigger")
FUNCTION = "draw"
CATEGORY = "Augment/Grid"
@staticmethod
def _hex_to_rgb(hex_str):
hex_str = hex_str.lstrip("#")
if len(hex_str) == 3:
hex_str = "".join(c * 2 for c in hex_str)
return tuple(int(hex_str[i : i + 2], 16) for i in (0, 2, 4))
@staticmethod
def _extend_line(x1, y1, x2, y2, w, h):
"""Extend line to canvas edges."""
dx = x2 - x1
dy = y2 - y1
if dx == 0 and dy == 0:
return x1, y1, x2, y2
# Find parameter t where line hits each edge
# line: (x1 + t*dx, y1 + t*dy)
t_values = []
if dx != 0:
# Left edge: x = 0
t = -x1 / dx
y_at = y1 + t * dy
if 0 <= y_at <= h:
t_values.append(t)
# Right edge: x = w
t = (w - x1) / dx
y_at = y1 + t * dy
if 0 <= y_at <= h:
t_values.append(t)
if dy != 0:
# Top edge: y = 0
t = -y1 / dy
x_at = x1 + t * dx
if 0 <= x_at <= w:
t_values.append(t)
# Bottom edge: y = h
t = (h - y1) / dy
x_at = x1 + t * dx
if 0 <= x_at <= w:
t_values.append(t)
if len(t_values) < 2:
return x1, y1, x2, y2
t_min = min(t_values)
t_max = max(t_values)
ex1 = int(x1 + t_min * dx)
ey1 = int(y1 + t_min * dy)
ex2 = int(x1 + t_max * dx)
ey2 = int(y1 + t_max * dy)
return ex1, ey1, ex2, ey2
def draw(self, width, height, preset, x1, y1, x2, y2, line_weight, line_color, extend, invert, trigger=None):
color = self._hex_to_rgb(line_color)
# Resolve preset into coordinates
lines = [] # list of (x1, y1, x2, y2) — outline draws 4 lines
if preset == "vertical_center":
mx = width // 2
lines = [(mx, 0, mx, height)]
elif preset == "horizontal_center":
my = height // 2
lines = [(0, my, width, my)]
elif preset == "diagonal_left":
lines = [(0, 0, width, height)]
elif preset == "diagonal_right":
lines = [(width, 0, 0, height)]
elif preset == "outline":
lines = [
(0, 0, width - 1, 0),
(width - 1, 0, width - 1, height - 1),
(width - 1, height - 1, 0, height - 1),
(0, height - 1, 0, 0),
]
else:
# custom — use raw x1/y1/x2/y2
lines = [(x1, y1, x2, y2)]
# Supersampling
ss = 2
sw, sh = width * ss, height * ss
slw = line_weight * ss
img = Image.new("RGBA", (sw, sh), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
drawn_lines = []
for lx1, ly1, lx2, ly2 in lines:
sx1, sy1, sx2, sy2 = lx1 * ss, ly1 * ss, lx2 * ss, ly2 * ss
if extend and preset == "custom":
sx1, sy1, sx2, sy2 = self._extend_line(sx1, sy1, sx2, sy2, sw, sh)
draw.line([(sx1, sy1), (sx2, sy2)], fill=color + (255,), width=slw)
# Track actual coords for JSON
ax1, ay1, ax2, ay2 = sx1 // ss, sy1 // ss, sx2 // ss, sy2 // ss
dx = ax2 - ax1
dy = ay2 - ay1
length = math.sqrt(dx * dx + dy * dy)
angle = math.degrees(math.atan2(dy, dx))
drawn_lines.append({
"a": {"x": ax1, "y": ay1},
"b": {"x": ax2, "y": ay2},
"midpoint": {"x": (ax1 + ax2) // 2, "y": (ay1 + ay2) // 2},
"length": round(length, 2),
"angle": round(angle, 2),
})
# Downsample
img = img.resize((width, height), Image.LANCZOS)
result = {
"preset": preset,
"canvas": [width, height],
"lines": drawn_lines,
}
if extend and preset == "custom":
ex1, ey1, ex2, ey2 = self._extend_line(x1, y1, x2, y2, width, height)
result["extended"] = {
"a": {"x": ex1, "y": ey1},
"b": {"x": ex2, "y": ey2},
}
json_result = json.dumps(result, indent=2)
# Convert
img_np = np.array(img)
alpha = img_np[:, :, 3].astype(np.float32) / 255.0
if invert:
alpha = 1.0 - alpha
rgb = img_np[:, :, :3].astype(np.float32) / 255.0
image_tensor = torch.from_numpy(rgb).unsqueeze(0)
mask_tensor = torch.from_numpy(alpha).unsqueeze(0)
return (image_tensor, mask_tensor, json_result, trigger)
+46
View File
@@ -0,0 +1,46 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
from .utils import compute_smart_edges
import json
class ShapeEdgeDetect:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
},
"optional": {
"symmetry_threshold": ("FLOAT", {"default": 0.85, "min": 0.5, "max": 0.99, "step": 0.05}),
},
}
RETURN_TYPES = ("EDGE_DATA", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("edges", "json_result", "trigger")
FUNCTION = "detect"
CATEGORY = "Augment/Grid"
def detect(self, mask, symmetry_threshold=0.85):
m = mask[0].cpu().numpy()
edges = compute_smart_edges(m, symmetry_threshold)
h_count = len(edges["h_lines"])
v_count = len(edges["v_lines"])
d45_count = len(edges["d45_lines"])
d135_count = len(edges["d135_lines"])
print(f"[augment] edges: {h_count}H / {v_count}V / {d45_count}d45 / {d135_count}d135")
json_result = json.dumps({
"node": "ShapeEdgeDetect",
"h_lines": h_count,
"v_lines": v_count,
"d45_lines": d45_count,
"d135_lines": d135_count,
})
return (edges, json_result, "done")
+149
View File
@@ -0,0 +1,149 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
"""
Augment Executor Override
─────────────────────────────
Bypasses ComfyUI's caching for trigger-driven workflows.
What it does:
1. Monkeypatches the execute() function in ComfyUI's execution module
2. Any node class with ALWAYS_EXECUTE = True skips the output cache — always re-runs
3. Any node that accepts/outputs TRIGGER type is auto-detected and also skips cache
4. Everything else executes normally — zero impact on other workflows
No fork needed. No CLI flags. Just drop and go.
"""
import logging
import execution as comfy_execution
from comfy_execution.caching import BasicCache, CacheKeySetInputSignature, HierarchicalCache
logger = logging.getLogger("Augment.Executor")
# ── Store reference to the original execute function ─────────────
_original_execute = comfy_execution.execute
def _should_skip_cache(class_def):
"""
Determine if a node class should bypass the output cache.
Returns True if:
- Node has ALWAYS_EXECUTE = True
- Node accepts or returns TRIGGER type
- Node has IS_CHANGED that returns float("NaN") (legacy force-run pattern)
"""
# Explicit opt-in
if getattr(class_def, "ALWAYS_EXECUTE", False):
return True
# Check if node deals with TRIGGER types (input or output)
try:
input_types = class_def.INPUT_TYPES()
for category in ("required", "optional"):
if category in input_types:
for key, val in input_types[category].items():
input_type = val[0] if isinstance(val, tuple) else val
if input_type == "TRIGGER":
return True
except Exception:
pass
# Check return types for TRIGGER
if hasattr(class_def, "RETURN_TYPES"):
for rt in class_def.RETURN_TYPES:
if rt == "TRIGGER":
return True
return False
async def patched_execute(server, dynprompt, caches, current_item, extra_data,
executed, prompt_id, execution_list,
pending_subgraph_results, pending_async_nodes, ui_outputs):
"""
Drop-in replacement for execution.execute().
For nodes that should skip cache:
- Evict any cached output BEFORE the original execute() runs
- This forces the original code past the cache-hit early return (line 419-428)
- The node executes fresh every time
- After execution, evict again so next run also re-executes
For all other nodes: passthrough to original, zero overhead.
"""
import nodes
unique_id = current_item
node_info = dynprompt.get_node(unique_id)
class_type = node_info["class_type"]
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
skip_cache = _should_skip_cache(class_def)
if skip_cache:
# Evict cached output so the original execute() can't short-circuit
try:
caches.outputs.delete(unique_id)
except (AttributeError, KeyError):
# NullCache or key doesn't exist — fine
pass
# Run the original execute
result = await _original_execute(
server, dynprompt, caches, current_item, extra_data,
executed, prompt_id, execution_list,
pending_subgraph_results, pending_async_nodes, ui_outputs
)
if skip_cache:
# Evict AFTER execution too, so next prompt also re-runs this node
try:
caches.outputs.delete(unique_id)
except (AttributeError, KeyError):
pass
return result
# ── Also patch the cache's set_prompt to skip signature checks for our nodes ──
_original_cache_set_prompt = None
def _patch_cache_set_prompt():
"""
Patch CacheSet's set_prompt behavior so IsChangedCache
never marks our trigger nodes as 'unchanged'.
"""
global _original_cache_set_prompt
original_is_changed_get = comfy_execution.IsChangedCache.get
async def patched_is_changed_get(self, node_id):
"""Force trigger/always-execute nodes to always report as changed."""
import nodes
node = self.dynprompt.get_node(node_id)
class_type = node["class_type"]
class_def = nodes.NODE_CLASS_MAPPINGS[class_type]
if _should_skip_cache(class_def):
# Return NaN — ComfyUI treats this as "always changed"
self.is_changed[node_id] = float("NaN")
return float("NaN")
return await original_is_changed_get(self, node_id)
comfy_execution.IsChangedCache.get = patched_is_changed_get
# ── Apply patches on import ─────────────────────────────────────
def _install():
"""Monkeypatch ComfyUI's execution module. Called once on import."""
comfy_execution.execute = patched_execute
_patch_cache_set_prompt()
logger.info("Augment executor override installed — trigger nodes will always re-execute")
_install()
+102
View File
@@ -0,0 +1,102 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import numpy as np
import torch
import cv2
import os
class FaceCrop:
"""Fast face crop using OpenCV. Image in, cropped image + coordinates out."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"padding": ("INT", {"default": 20, "min": 0, "max": 512, "step": 5}),
"square": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("cropped", "x", "y", "width", "height", "json", "trigger")
OUTPUT_NODE = False
FUNCTION = "detect_and_crop"
CATEGORY = "Augment/Image"
def detect_and_crop(self, image, padding=20, square=True):
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
img_h, img_w = img_np.shape[:2]
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
# Try multiple paths to find the Haar cascade file
cascade_file = None
# Try cv2.data path first (if available)
if hasattr(cv2, 'data'):
try:
cascade_file = cv2.data.haarcascades + "haarcascade_frontalface_alt2.xml"
if os.path.exists(cascade_file):
pass # Found it
else:
cascade_file = None
except:
cascade_file = None
# Try other common paths
if not cascade_file:
cascade_paths = [
os.path.join(cv2.__path__[0], "data", "haarcascade_frontalface_alt2.xml"),
"/usr/share/opencv4/haarcascades/haarcascade_frontalface_alt2.xml",
"/usr/local/share/opencv4/haarcascades/haarcascade_frontalface_alt2.xml",
"haarcascade_frontalface_alt2.xml", # System path fallback
]
for path in cascade_paths:
if os.path.exists(path):
cascade_file = path
break
if not cascade_file:
cascade_file = "haarcascade_frontalface_alt2.xml"
cascade = cv2.CascadeClassifier(cascade_file)
faces = cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=6, minSize=(40, 40))
face_count = len(faces) if isinstance(faces, np.ndarray) else 0
if face_count == 0:
print("[augment] FaceCrop: no faces detected")
result_json = json.dumps({"node": "FaceCrop", "x": 0, "y": 0, "width": img_w, "height": img_h})
return (image, 0, 0, img_w, img_h, result_json, "done")
faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)
fx, fy, fw, fh = [int(v) for v in faces[0]]
cx, cy = fx + fw // 2, fy + fh // 2
fw += padding * 2
fh += padding * 2
if square:
side = max(fw, fh)
fw, fh = side, side
x0 = max(0, cx - fw // 2)
y0 = max(0, cy - fh // 2)
x1 = min(img_w, x0 + fw)
y1 = min(img_h, y0 + fh)
out_x, out_y = int(x0), int(y0)
out_w, out_h = int(x1 - x0), int(y1 - y0)
print(f"[augment] FaceCrop: {face_count} face(s), crop x={out_x} y={out_y} w={out_w} h={out_h}")
cropped = image[:, out_y:out_y + out_h, out_x:out_x + out_w, :]
result_json = json.dumps({"node": "FaceCrop", "x": out_x, "y": out_y, "width": out_w, "height": out_h})
return (cropped, out_x, out_y, out_w, out_h, result_json, "done")
+231
View File
@@ -0,0 +1,231 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
from PIL import Image, ImageDraw
import json
import math
PHI = 1.6180339887
class FibonacciSpiralNode:
"""
Draw a Fibonacci/Golden Ratio spiral with optional subdivision rectangles.
Each iteration subdivides the remaining golden rectangle and draws a quarter arc.
Companion to Grid Generator and Grid Coords.
"""
DIRECTIONS = ["top_right", "bottom_right", "bottom_left", "top_left"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"height": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 1}),
"iterations": ("INT", {"default": 8, "min": 2, "max": 16, "step": 1}),
"line_weight": ("INT", {"default": 2, "min": 1, "max": 20, "step": 1}),
"line_color": ("STRING", {"default": "#FFFFFF"}),
"draw_rectangles": ("BOOLEAN", {"default": False}),
"draw_spiral": ("BOOLEAN", {"default": True}),
"invert": ("BOOLEAN", {"default": False}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "mask", "json_result", "trigger")
FUNCTION = "generate"
CATEGORY = "Augment/Grid"
@staticmethod
def _hex_to_rgb(hex_str):
hex_str = hex_str.lstrip("#")
if len(hex_str) == 3:
hex_str = "".join(c * 2 for c in hex_str)
return tuple(int(hex_str[i : i + 2], 16) for i in (0, 2, 4))
def generate(
self,
width,
height,
iterations,
line_weight,
line_color,
draw_rectangles,
draw_spiral,
invert,
trigger=None,
):
color = self._hex_to_rgb(line_color)
# --- Work at 2x resolution for anti-aliased arcs, then downscale ---
ss = 2 # supersampling factor
sw, sh = width * ss, height * ss
slw = line_weight * ss
img = Image.new("RGBA", (sw, sh), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Rectangle colors for subdivision boxes
rect_colors = [
(255, 80, 80, 160), # red
(80, 255, 80, 160), # green
(80, 80, 255, 160), # blue
(255, 200, 60, 160), # yellow
(200, 80, 255, 160), # purple
(80, 220, 220, 160), # cyan
]
# --- Subdivision logic ---
# We track the remaining rectangle and which corner the spiral starts from.
# Direction cycle: the square is carved from a rotating corner each step.
# 0 = carve from RIGHT (square on right side)
# 1 = carve from BOTTOM (square on bottom)
# 2 = carve from LEFT (square on left side)
# 3 = carve from TOP (square on top)
rx, ry, rw, rh = 0.0, 0.0, float(sw), float(sh)
spiral_data = []
arc_points = []
for i in range(iterations):
direction = i % 4
if direction == 0:
# Carve square from RIGHT
sq_size = rh
sq_x = rx + rw - sq_size
sq_y = ry
# Arc: bottom-left quarter (from bottom to left of square)
# Center at bottom-left of square
cx, cy = sq_x, sq_y + sq_size
start_angle = 270
end_angle = 360
# Remaining rect
new_rx, new_ry = rx, ry
new_rw, new_rh = rw - sq_size, rh
elif direction == 1:
# Carve square from BOTTOM
sq_size = rw
sq_x = rx
sq_y = ry + rh - sq_size
# Center at top-left of square
cx, cy = sq_x, sq_y
start_angle = 0
end_angle = 90
# Remaining rect
new_rx, new_ry = rx, ry
new_rw, new_rh = rw, rh - sq_size
elif direction == 2:
# Carve square from LEFT
sq_size = rh
sq_x = rx
sq_y = ry
# Center at top-right of square
cx, cy = sq_x + sq_size, sq_y
start_angle = 90
end_angle = 180
# Remaining rect
new_rx, new_ry = rx + sq_size, ry
new_rw, new_rh = rw - sq_size, rh
else:
# Carve square from TOP
sq_size = rw
sq_x = rx
sq_y = ry
# Center at bottom-right of square
cx, cy = sq_x + sq_size, sq_y + sq_size
start_angle = 180
end_angle = 270
# Remaining rect
new_rx, new_ry = rx, ry + sq_size
new_rw, new_rh = rw, rh - sq_size
radius = sq_size
# Store data
step_data = {
"iteration": i,
"square": {
"x": round(sq_x / ss),
"y": round(sq_y / ss),
"size": round(sq_size / ss),
},
"arc_center": {
"x": round(cx / ss),
"y": round(cy / ss),
},
"radius": round(radius / ss),
"direction": self.DIRECTIONS[direction],
}
spiral_data.append(step_data)
# Draw subdivision rectangle
if draw_rectangles:
rc = rect_colors[i % len(rect_colors)]
draw.rectangle(
[sq_x, sq_y, sq_x + sq_size, sq_y + sq_size],
outline=rc,
width=slw,
)
# Draw arc
if draw_spiral:
bbox = [
cx - radius,
cy - radius,
cx + radius,
cy + radius,
]
draw.arc(
bbox,
start=start_angle,
end=end_angle,
fill=color + (255,),
width=slw,
)
# Move to remaining rectangle
rx, ry, rw, rh = new_rx, new_ry, new_rw, new_rh
if rw < 2 or rh < 2:
break
# --- Downsample to target resolution ---
img = img.resize((width, height), Image.LANCZOS)
# --- Build mask from alpha ---
img_np = np.array(img)
alpha = img_np[:, :, 3].astype(np.float32) / 255.0
if invert:
alpha = 1.0 - alpha
# Convert image to RGB tensor [1, H, W, 3]
rgb = img_np[:, :, :3].astype(np.float32) / 255.0
image_tensor = torch.from_numpy(rgb).unsqueeze(0)
# Mask tensor [1, H, W]
mask_tensor = torch.from_numpy(alpha).unsqueeze(0)
# JSON output
result = {
"canvas": [width, height],
"iterations": len(spiral_data),
"phi": round(PHI, 6),
"steps": spiral_data,
}
json_result = json.dumps(result, indent=2)
return (image_tensor, mask_tensor, json_result, trigger)
+107
View File
@@ -0,0 +1,107 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import numpy as np
import torch
import json
from PIL import Image, ImageDraw
from .utils import compute_all_edges
class GridGenerator:
"""Generates a grid image with configurable rows, columns, and line weight."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"rows": ("INT", {"default": 8, "min": 1, "max": 128, "step": 1}),
"cols": ("INT", {"default": 8, "min": 1, "max": 128, "step": 1}),
"line_weight": ("INT", {"default": 1, "min": 1, "max": 10, "step": 1}),
"width": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
"height": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 8}),
},
"optional": {
"invert": ("BOOLEAN", {"default": False}),
"dotted": ("BOOLEAN", {"default": False}),
"dotted_line_weight": ("INT", {"default": 3, "min": 1, "max": 20, "step": 1}),
"dotted_spacing": ("INT", {"default": 10, "min": 3, "max": 60, "step": 1}),
"line_color": ("STRING", {"default": "#000000"}),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "INT", "INT", "INT", "INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "mask", "rows", "cols", "width", "height", "json_result", "trigger")
FUNCTION = "generate"
CATEGORY = "Augment/Grid"
def generate(self, rows, cols, line_weight, width, height, invert=False, dotted=False, dotted_line_weight=3, dotted_spacing=10, line_color="#000000", trigger=None):
fg = _hex_to_rgb(line_color)
if invert:
bg = fg
fg = (255 - bg[0], 255 - bg[1], 255 - bg[2])
else:
bg = (255, 255, 255)
pil_img = Image.new("RGB", (width, height), bg)
draw = ImageDraw.Draw(pil_img)
if dotted:
r = dotted_line_weight / 2.0
cell_w = width / cols
cell_h = height / rows
dots_per_cell_x = max(1, round(cell_w / dotted_spacing))
dots_per_cell_y = max(1, round(cell_h / dotted_spacing))
sx = cell_w / dots_per_cell_x
sy = cell_h / dots_per_cell_y
for row in range(1, rows):
y_pos = int(round(row * height / rows))
x = 0.0
while x < width:
draw.ellipse([x - r, y_pos - r, x + r, y_pos + r], fill=fg)
x += sx
for col in range(1, cols):
x_pos = int(round(col * width / cols))
y = 0.0
while y < height:
draw.ellipse([x_pos - r, y - r, x_pos + r, y + r], fill=fg)
y += sy
else:
for row in range(1, rows):
y_pos = int(round(row * height / rows))
draw.line([(0, y_pos), (width - 1, y_pos)], fill=fg, width=line_weight)
for col in range(1, cols):
x_pos = int(round(col * width / cols))
draw.line([(x_pos, 0), (x_pos, height - 1)], fill=fg, width=line_weight)
img_np = np.array(pil_img).astype(np.float32) / 255.0
result_image = torch.from_numpy(img_np).unsqueeze(0)
mask_val = img_np[:, :, 0]
result_mask = torch.from_numpy(mask_val).unsqueeze(0)
json_result = json.dumps({
"node": "GridGenerator",
"rows": rows,
"cols": cols,
"width": width,
"height": height,
"line_weight": line_weight,
"dotted": dotted,
"line_color": line_color
})
return (result_image, result_mask, rows, cols, width, height, json_result, "done")
def _hex_to_rgb(hex_str):
"""Convert a hex color string like '#ff0000' to an (R, G, B) tuple."""
h = hex_str.lstrip("#")
if len(h) == 3:
h = h[0]*2 + h[1]*2 + h[2]*2
if len(h) != 6:
return (0, 0, 0)
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16))
+199
View File
@@ -0,0 +1,199 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
import json
PHI = 1.6180339887 # Golden ratio
class GridCoordsNode:
"""
Design layout engine. Pick a layout preset (golden_ratio, thirds, halves, quadrants)
and a region name (top_left, center, etc.) — get back pixel coordinates.
Companion node to Grid Generator.
"""
LAYOUTS = ["golden_ratio", "thirds", "halves", "quadrants"]
REGIONS = [
"top_left", "top_center", "top_right",
"center_left", "center", "center_right",
"bottom_left", "bottom_center", "bottom_right",
]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"canvas_w": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
"canvas_h": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
"layout": (cls.LAYOUTS, {"default": "golden_ratio"}),
"region": (cls.REGIONS, {"default": "center"}),
"padding": ("INT", {"default": 0, "min": 0, "max": 1024, "step": 1}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("INT", "INT", "INT", "INT", "INT", "INT", "AUGMENT_JSON", "MASK", "TRIGGER")
RETURN_NAMES = ("x", "y", "width", "height", "center_x", "center_y", "json_result", "mask", "trigger")
FUNCTION = "calculate"
CATEGORY = "Augment/Grid"
# ------------------------------------------------------------------ #
# Split helpers — return the two split points for each axis
# ------------------------------------------------------------------ #
@staticmethod
def _splits(size, layout, padding):
"""Return (split1, split2) pixel positions for a given axis length."""
inner = size - 2 * padding
p = padding
if layout == "golden_ratio":
s1 = p + int(inner / PHI) # ~61.8 %
s2 = p + int(inner - inner / PHI) # ~38.2 % (mirrored)
# Ensure s1 < s2
if s1 > s2:
s1, s2 = s2, s1
return (s1, s2)
if layout == "thirds":
third = inner / 3
return (p + int(third), p + int(third * 2))
if layout == "halves":
half = inner // 2
mid = p + half
return (mid, mid) # single split
if layout == "quadrants":
half = inner // 2
mid = p + half
return (mid, mid)
return (p, size - p)
# ------------------------------------------------------------------ #
# Region → bounding box
# ------------------------------------------------------------------ #
def _region_box(self, canvas_w, canvas_h, layout, region, padding):
"""
Returns (x, y, w, h) for the named region.
"""
sx1, sx2 = self._splits(canvas_w, layout, padding)
sy1, sy2 = self._splits(canvas_h, layout, padding)
p = padding
cw = canvas_w - padding
ch = canvas_h - padding
# Build column boundaries [left_edge, split1, split2, right_edge]
# For halves/quadrants split1 == split2, giving us 2 zones per axis
cols = sorted(set([p, sx1, sx2, cw]))
rows = sorted(set([p, sy1, sy2, ch]))
# Map region name → (col_index, row_index)
region_map_3x3 = {
"top_left": (0, 0),
"top_center": (1, 0),
"top_right": (2, 0),
"center_left": (0, 1),
"center": (1, 1),
"center_right": (2, 1),
"bottom_left": (0, 2),
"bottom_center": (1, 2),
"bottom_right": (2, 2),
}
# For 2-zone layouts (halves, quadrants) we collapse to 2x2
region_map_2x2 = {
"top_left": (0, 0),
"top_center": (0, 0), # collapse to top_left
"top_right": (1, 0),
"center_left": (0, 0), # collapse to top_left
"center": (0, 0), # collapse to top_left
"center_right": (1, 0), # collapse to top_right
"bottom_left": (0, 1),
"bottom_center": (0, 1), # collapse to bottom_left
"bottom_right": (1, 1),
}
is_2zone = len(cols) <= 3 and len(rows) <= 3
if is_2zone and len(cols) == 2:
# True 2-col layout
ci, ri = region_map_2x2[region]
elif is_2zone and len(cols) == 3:
# 3 boundaries but middle is duplicate — still pick properly
ci, ri = region_map_2x2[region]
else:
ci, ri = region_map_3x3[region]
# Clamp indices
ci = min(ci, len(cols) - 2)
ri = min(ri, len(rows) - 2)
x = int(cols[ci])
y = int(rows[ri])
w = int(cols[ci + 1] - cols[ci])
h = int(rows[ri + 1] - rows[ri])
return x, y, w, h
# ------------------------------------------------------------------ #
# Main
# ------------------------------------------------------------------ #
def calculate(self, canvas_w, canvas_h, layout, region, padding, trigger=None):
x, y, w, h = self._region_box(canvas_w, canvas_h, layout, region, padding)
center_x = x + w // 2
center_y = y + h // 2
# -- Normalized values --
norm_x = round(x / canvas_w, 4) if canvas_w else 0
norm_y = round(y / canvas_h, 4) if canvas_h else 0
norm_w = round(w / canvas_w, 4) if canvas_w else 0
norm_h = round(h / canvas_h, 4) if canvas_h else 0
norm_cx = round(center_x / canvas_w, 4) if canvas_w else 0
norm_cy = round(center_y / canvas_h, 4) if canvas_h else 0
# -- All split lines for this layout (useful for LLM to understand full grid) --
sx1, sx2 = self._splits(canvas_w, layout, padding)
sy1, sy2 = self._splits(canvas_h, layout, padding)
result = {
"layout": layout,
"region": region,
"canvas": [canvas_w, canvas_h],
"padding": padding,
"box": {
"x": x, "y": y, "w": w, "h": h,
"right": x + w, "bottom": y + h,
"center_x": center_x, "center_y": center_y,
},
"normalized": {
"x": norm_x, "y": norm_y,
"w": norm_w, "h": norm_h,
"center_x": norm_cx, "center_y": norm_cy,
},
"splits": {
"x": [sx1, sx2],
"y": [sy1, sy2],
},
}
json_result = json.dumps(result, indent=2)
# -- Mask output: white rectangle at region on black canvas --
mask = np.zeros((canvas_h, canvas_w), dtype=np.float32)
mask[y : y + h, x : x + w] = 1.0
mask_tensor = torch.from_numpy(mask).unsqueeze(0)
return (int(x), int(y), int(w), int(h), int(center_x), int(center_y), json_result, mask_tensor, trigger)
+50
View File
@@ -0,0 +1,50 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import numpy as np
import torch
import cv2
class ImageCrop:
"""Simple image crop. Accepts x, y, width, height — wires to any node outputting INT."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"x": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}),
"y": ("INT", {"default": 0, "min": 0, "max": 8192, "step": 1}),
"width": ("INT", {"default": 256, "min": 1, "max": 8192, "step": 1}),
"height": ("INT", {"default": 256, "min": 1, "max": 8192, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "INT", "INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "x", "y", "width", "height", "json_result", "trigger")
OUTPUT_NODE = False
FUNCTION = "crop"
CATEGORY = "Augment/Image"
def crop(self, image, x, y, width, height):
img_h, img_w = image.shape[1], image.shape[2]
# Clamp to image bounds
x0 = max(0, min(x, img_w - 1))
y0 = max(0, min(y, img_h - 1))
x1 = min(img_w, x0 + width)
y1 = min(img_h, y0 + height)
out_w = int(x1 - x0)
out_h = int(y1 - y0)
out_x = int(x0)
out_y = int(y0)
cropped = image[:, out_y:out_y + out_h, out_x:out_x + out_w, :]
print(f"[augment] ImageCrop: x={out_x} y={out_y} w={out_w} h={out_h}")
result_json = json.dumps({"node": "ImageCrop", "x": out_x, "y": out_y, "width": out_w, "height": out_h})
return (cropped, out_x, out_y, out_w, out_h, result_json, "done")
+40
View File
@@ -0,0 +1,40 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import json
class ImageFlipNode:
"""Flip an image horizontally, vertically, or both."""
MODES = ["horizontal", "vertical", "both"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"direction": (cls.MODES, {"default": "horizontal"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Image"
def execute(self, image, direction, trigger=None):
if direction == "horizontal":
flipped = torch.flip(image, [2])
elif direction == "vertical":
flipped = torch.flip(image, [1])
else:
flipped = torch.flip(image, [1, 2])
json_result = json.dumps({"node": "ImageFlipNode", "direction": direction, "shape": list(image.shape)})
return (flipped, json_result, "done")
+134
View File
@@ -0,0 +1,134 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
import json
from PIL import Image
class ImagePlaceNode:
"""
Place an image onto a canvas at any x/y/w/h coordinates.
Handles resizing, alpha compositing, and fit modes.
Wire from Grid Coords, manual INTs, or any coordinate source.
"""
FIT_MODES = ["fit", "fill", "stretch"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"canvas": ("IMAGE",),
"image": ("IMAGE",),
"x": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"y": ("INT", {"default": 0, "min": -8192, "max": 8192, "step": 1}),
"width": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}),
"height": ("INT", {"default": 512, "min": 1, "max": 8192, "step": 1}),
"fit": (cls.FIT_MODES, {"default": "fit"}),
},
"optional": {
"mask": ("MASK",),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "mask", "json_result", "trigger")
FUNCTION = "place"
CATEGORY = "Augment/Image"
@staticmethod
def _tensor_to_pil(tensor):
"""Convert ComfyUI IMAGE tensor [B,H,W,C] to PIL RGBA."""
img_np = tensor[0].cpu().numpy()
img_np = np.clip(img_np * 255, 0, 255).astype(np.uint8)
if img_np.shape[2] == 4:
return Image.fromarray(img_np, "RGBA")
else:
return Image.fromarray(img_np, "RGB").convert("RGBA")
@staticmethod
def _pil_to_tensor(pil_img):
"""Convert PIL RGB image to ComfyUI IMAGE tensor [1,H,W,3]."""
img_np = np.array(pil_img.convert("RGB")).astype(np.float32) / 255.0
return torch.from_numpy(img_np).unsqueeze(0)
@staticmethod
def _resize_fit(pil_img, target_w, target_h):
"""Resize to fit inside target, preserving aspect ratio (letterbox)."""
src_w, src_h = pil_img.size
scale = min(target_w / src_w, target_h / src_h)
new_w = int(src_w * scale)
new_h = int(src_h * scale)
resized = pil_img.resize((new_w, new_h), Image.LANCZOS)
# Center on transparent canvas
result = Image.new("RGBA", (target_w, target_h), (0, 0, 0, 0))
offset_x = (target_w - new_w) // 2
offset_y = (target_h - new_h) // 2
result.paste(resized, (offset_x, offset_y), resized)
return result
@staticmethod
def _resize_fill(pil_img, target_w, target_h):
"""Resize to fill target, cropping excess (cover)."""
src_w, src_h = pil_img.size
scale = max(target_w / src_w, target_h / src_h)
new_w = int(src_w * scale)
new_h = int(src_h * scale)
resized = pil_img.resize((new_w, new_h), Image.LANCZOS)
# Center crop
left = (new_w - target_w) // 2
top = (new_h - target_h) // 2
return resized.crop((left, top, left + target_w, top + target_h))
@staticmethod
def _resize_stretch(pil_img, target_w, target_h):
"""Stretch to exact target size."""
return pil_img.resize((target_w, target_h), Image.LANCZOS)
def place(self, canvas, image, x, y, width, height, fit, mask=None, trigger=None):
canvas_pil = self._tensor_to_pil(canvas)
image_pil = self._tensor_to_pil(image)
canvas_w, canvas_h = canvas_pil.size
# Resize image to target region
if fit == "fit":
placed = self._resize_fit(image_pil, width, height)
elif fit == "fill":
placed = self._resize_fill(image_pil, width, height)
else:
placed = self._resize_stretch(image_pil, width, height)
# Apply external mask to placed image's alpha if provided
if mask is not None:
mask_pil = Image.fromarray((mask[0].cpu().numpy() * 255).astype(np.uint8), "L")
mask_resized = mask_pil.resize((placed.size[0], placed.size[1]), Image.LANCZOS)
placed_alpha = placed.split()[3]
combined_alpha = Image.fromarray(
np.minimum(np.array(placed_alpha), np.array(mask_resized)).astype(np.uint8), "L"
)
placed.putalpha(combined_alpha)
# Composite onto canvas
result = canvas_pil.copy()
result.paste(placed, (x, y), placed)
# Build placement mask (white where image was placed, including alpha)
mask_img = Image.new("L", (canvas_w, canvas_h), 0)
placed_alpha = placed.split()[3]
mask_img.paste(placed_alpha, (x, y))
# Convert outputs
result_tensor = self._pil_to_tensor(result)
mask_np = np.array(mask_img).astype(np.float32) / 255.0
mask_tensor = torch.from_numpy(mask_np).unsqueeze(0)
json_result = json.dumps({"node": "ImagePlaceNode", "x": x, "y": y, "width": width, "height": height, "fit": fit, "canvas_size": [canvas_w, canvas_h]})
return (result_tensor, mask_tensor, json_result, trigger)
+48
View File
@@ -0,0 +1,48 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import numpy as np
import json
from PIL import Image
class ImageRotateNode:
"""Rotate an image by preset or custom angle."""
PRESETS = ["0", "90", "180", "270", "custom"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"angle": (cls.PRESETS, {"default": "90"}),
"custom_angle": ("FLOAT", {"default": 0.0, "min": -360.0, "max": 360.0, "step": 0.5}),
"expand": ("BOOLEAN", {"default": True}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Image"
def execute(self, image, angle, custom_angle, expand, trigger=None):
deg = custom_angle if angle == "custom" else float(angle)
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
pil_img = Image.fromarray(img_np)
rotated = pil_img.rotate(-deg, resample=Image.BICUBIC, expand=expand, fillcolor=(0, 0, 0))
result_np = np.array(rotated).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_np).unsqueeze(0)
h, w = result_np.shape[:2]
json_result = json.dumps({"node": "ImageRotateNode", "angle": deg, "expand": expand, "size": [w, h]})
return (result_tensor, json_result, "done")
+51
View File
@@ -0,0 +1,51 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import torch
import json
class InvertImageNode:
"""Invert an image's colors (1 - pixel value). Alpha passes through untouched."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"alpha": ("MASK",),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "MASK", "MASK", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "alpha", "red", "green", "blue", "json", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Image"
def execute(self, image, alpha=None, trigger=None):
inverted = 1.0 - image
h, w = image.shape[1], image.shape[2]
if alpha is not None and alpha.shape[-2:] == (h, w) and alpha.any():
has_alpha = True
alpha_out = alpha
else:
has_alpha = False
alpha_out = torch.zeros((1, h, w), dtype=torch.float32)
img_np = inverted[0].cpu().numpy()
red = torch.from_numpy(img_np[:, :, 0]).unsqueeze(0)
green = torch.from_numpy(img_np[:, :, 1]).unsqueeze(0)
blue = torch.from_numpy(img_np[:, :, 2]).unsqueeze(0)
json_result = json.dumps({
"node": "InvertImageNode",
"width": w,
"height": h,
"has_alpha": has_alpha,
})
return (inverted, alpha_out, red, green, blue, json_result, "done")
+61
View File
@@ -0,0 +1,61 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json as json_module
class JsonExtractNode:
"""Extract a specific value from JSON data by name."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json": ("AUGMENT_JSON",),
"field_name": ("STRING", {"default": "value"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("extracted_value", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "extract"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def extract(self, json, field_name, trigger=None):
try:
data = json_module.loads(json)
if "value" in data and field_name not in data:
inner = data["value"]
if isinstance(inner, str):
try:
inner = json_module.loads(inner)
except (json_module.JSONDecodeError, TypeError):
pass
if isinstance(inner, dict) and field_name in inner:
data = inner
fields = field_name.split(".")
value = data
for field in fields:
value = value[field]
result = str(value) if not isinstance(value, str) else value
ui_text = f"Field: {field_name}\nValue: {result}"
except (json_module.JSONDecodeError, KeyError, TypeError):
result = ""
ui_text = f"Field: {field_name}\nError: Could not find field or invalid JSON"
return {
"ui": {"text": [ui_text]},
"result": (result, json, "done")
}
+40
View File
@@ -0,0 +1,40 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json as json_module
class JsonViewerNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"json": ("AUGMENT_JSON", {"forceInput": True}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("json", "trigger")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, json, trigger=None):
try:
parsed = json_module.loads(json)
pretty = json_module.dumps(parsed, indent=2)
except (json_module.JSONDecodeError, TypeError):
pretty = str(json)
return {
"ui": {"text": [pretty]},
"result": (json, "done")
}
+114
View File
@@ -0,0 +1,114 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import hashlib
import json
import os
import numpy as np
import torch
from PIL import Image, ImageOps, ImageSequence
import folder_paths
import node_helpers
class AugmentLoadImage:
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = folder_paths.filter_files_content_types(files, ["image"])
return {
"required": {
"image": (sorted(files), {"image_upload": True}),
},
"optional": {
"image_override": ("IMAGE",),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "INT", "INT", "MASK", "MASK", "MASK", "MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "width", "height", "alpha", "red", "green", "blue", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "load_image"
CATEGORY = "Augment/Image"
def load_image(self, image, image_override=None, trigger=None):
if image_override is not None:
output_image = image_override
h, w = output_image.shape[1], output_image.shape[2]
has_alpha = False
output_mask = torch.zeros((1, h, w), dtype=torch.float32)
source_name = "override"
else:
image_path = folder_paths.get_annotated_filepath(image)
img = node_helpers.pillow(Image.open, image_path)
output_images = []
output_masks = []
for i in ImageSequence.Iterator(img):
i = node_helpers.pillow(ImageOps.exif_transpose, i)
if i.mode == 'I':
i = i.point(lambda i: i * (1 / 255))
has_alpha = 'A' in i.getbands()
rgba = i.convert("RGBA")
rgb = i.convert("RGB")
image_tensor = torch.from_numpy(np.array(rgb).astype(np.float32) / 255.0)[None,]
output_images.append(image_tensor)
if has_alpha:
mask = 1.0 - torch.from_numpy(np.array(rgba.getchannel('A')).astype(np.float32) / 255.0)
else:
mask = torch.zeros((rgb.size[1], rgb.size[0]), dtype=torch.float32)
output_masks.append(mask.unsqueeze(0))
if img.format == "MPO":
break
if len(output_images) > 1:
output_image = torch.cat(output_images, dim=0)
output_mask = torch.cat(output_masks, dim=0)
else:
output_image = output_images[0]
output_mask = output_masks[0]
w = output_image.shape[2]
h = output_image.shape[1]
source_name = image
img_np = output_image[0].numpy()
red = torch.from_numpy(img_np[:, :, 0]).unsqueeze(0)
green = torch.from_numpy(img_np[:, :, 1]).unsqueeze(0)
blue = torch.from_numpy(img_np[:, :, 2]).unsqueeze(0)
w = output_image.shape[2]
h = output_image.shape[1]
json_result = json.dumps({
"node": "Load Image",
"file": source_name,
"width": w,
"height": h,
"has_alpha": has_alpha,
})
return {
"ui": {"text": [f"{source_name}\n{w}x{h}"]},
"result": (output_image, w, h, output_mask, red, green, blue, json_result, "done"),
}
@classmethod
def IS_CHANGED(cls, image, image_override=None, trigger=None):
image_path = folder_paths.get_annotated_filepath(image)
m = hashlib.sha256()
with open(image_path, 'rb') as f:
m.update(f.read())
return m.digest().hex()
@classmethod
def VALIDATE_INPUTS(cls, image, image_override=None, trigger=None):
if not folder_paths.exists_annotated_filepath(image):
return "Invalid image file: {}".format(image)
return True
+140
View File
@@ -0,0 +1,140 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import numpy as np
import torch
import cv2
class LogoMask:
"""Generates a filled mask from edge detection — ideal for logos, icons, and text."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
},
"optional": {
"threshold_low": ("INT", {"default": 50, "min": 0, "max": 255, "step": 5}),
"threshold_high": ("INT", {"default": 150, "min": 0, "max": 255, "step": 5}),
"fill_holes": ("BOOLEAN", {"default": True}),
"smooth": ("INT", {"default": 3, "min": 0, "max": 20, "step": 1}),
"invert": ("BOOLEAN", {"default": False}),
},
}
RETURN_TYPES = ("MASK", "IMAGE", "IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("mask", "mask_image", "masked_rgb", "json_result", "trigger")
OUTPUT_NODE = False
FUNCTION = "generate_mask"
CATEGORY = "Augment/Image"
def generate_mask(self, image, threshold_low=50, threshold_high=150,
fill_holes=True, smooth=3, invert=False):
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
h, w = gray.shape
# Detect background from corner pixels (10px sample from each corner)
s = 10
corners = np.concatenate([
gray[:s, :s].flatten(),
gray[:s, -s:].flatten(),
gray[-s:, :s].flatten(),
gray[-s:, -s:].flatten(),
])
bg_mean = np.mean(corners)
# threshold_low = tolerance (how far from bg a pixel must be to count as foreground)
# Higher = stricter, lower = catches more
if bg_mean > 127:
# Light background — mask anything darker than (bg - tolerance)
cutoff = max(0, bg_mean - threshold_low)
_, mask = cv2.threshold(gray, int(cutoff), 255, cv2.THRESH_BINARY_INV)
else:
# Dark background — mask anything lighter than (bg + tolerance)
cutoff = min(255, bg_mean + threshold_low)
_, mask = cv2.threshold(gray, int(cutoff), 255, cv2.THRESH_BINARY)
# Clean up noise
kernel_clean = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel_clean, iterations=1)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_clean, iterations=1)
if fill_holes:
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if hierarchy is not None:
for i, cnt in enumerate(contours):
cv2.drawContours(mask, [cnt], 0, 255, cv2.FILLED)
else:
# Preserve negative space
contours, hierarchy = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
filled = np.zeros_like(mask)
if hierarchy is not None:
for i, cnt in enumerate(contours):
if hierarchy[0][i][3] == -1:
cv2.drawContours(filled, [cnt], 0, 255, cv2.FILLED)
for i, cnt in enumerate(contours):
if hierarchy[0][i][3] >= 0:
cv2.drawContours(filled, [cnt], 0, 0, cv2.FILLED)
mask = filled
kernel_clean = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel_clean, iterations=1)
if smooth > 0:
# Smooth contour geometry only — no pixel blurring
smooth_mask = np.zeros_like(mask)
contours_s, hierarchy_s = cv2.findContours(mask, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
if hierarchy_s is not None:
for i, cnt in enumerate(contours_s):
epsilon = smooth * 0.5
approx = cv2.approxPolyDP(cnt, epsilon, True)
# Outer contours filled white, inner (holes) filled black
if hierarchy_s[0][i][3] == -1:
cv2.drawContours(smooth_mask, [approx], 0, 255, cv2.FILLED)
if not fill_holes:
for i, cnt in enumerate(contours_s):
if hierarchy_s[0][i][3] >= 0:
epsilon = smooth * 0.5
approx = cv2.approxPolyDP(cnt, epsilon, True)
cv2.drawContours(smooth_mask, [approx], 0, 0, cv2.FILLED)
mask = smooth_mask
if invert:
mask = cv2.bitwise_not(mask)
# Bounding box of the mask content
rows = np.any(mask > 0, axis=1)
cols = np.any(mask > 0, axis=0)
if rows.any():
r = np.where(rows)[0]
c = np.where(cols)[0]
bx, by = int(c[0]), int(r[0])
bw, bh = int(c[-1] - c[0] + 1), int(r[-1] - r[0] + 1)
else:
bx, by, bw, bh = 0, 0, mask.shape[1], mask.shape[0]
total_contours = len(contours) if contours else 0
coverage = float(np.sum(mask > 0) / mask.size * 100)
print(f"[augment] LogoMask: {total_contours} contours, {coverage:.1f}% coverage")
result = torch.from_numpy(mask.astype(np.float32) / 255.0).unsqueeze(0)
# mask_image: mask as RGB (white on black)
mask_rgb = np.stack([mask, mask, mask], axis=-1) # (H, W, 3) 0-255
mask_image = torch.from_numpy(mask_rgb.astype(np.float32) / 255.0).unsqueeze(0) # (1, H, W, 3)
# masked_rgb: original image with mask applied (black where mask is empty)
mask_f = mask.astype(np.float32) / 255.0 # (H, W) 0-1
masked = img_np.astype(np.float32) * mask_f[:, :, np.newaxis] # (H, W, 3)
masked_rgb = torch.from_numpy(masked / 255.0).unsqueeze(0) # (1, H, W, 3)
result_json = json.dumps({
"node": "LogoMask",
"x": bx, "y": by, "width": bw, "height": bh,
"contours": total_contours, "coverage_pct": round(coverage, 1)
})
return (result, mask_image, masked_rgb, result_json, "done")
+69
View File
@@ -0,0 +1,69 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import numpy as np
import torch
import json
class MaskBoundingBox:
"""Finds the bounding box of non-zero mask content and outputs the cropped mask + coordinates."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
"padding": ("INT", {"default": 0, "min": 0, "max": 512, "step": 1}),
"invert": ("BOOLEAN", {"default": False}),
"threshold": ("FLOAT", {"default": 0.10, "min": 0.01, "max": 1.0, "step": 0.01}),
},
}
RETURN_TYPES = ("MASK", "INT", "INT", "INT", "INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("mask", "x", "y", "width", "height", "json_result", "trigger")
FUNCTION = "find_bounds"
CATEGORY = "Augment/Image"
def find_bounds(self, mask, padding, invert, threshold):
m = mask[0].cpu().numpy()
img_h, img_w = m.shape
if invert:
m = 1.0 - m
rows = np.any(m > threshold, axis=1)
cols = np.any(m > threshold, axis=0)
if not rows.any():
x_min, y_min, out_w, out_h = 0, 0, img_w, img_h
else:
row_indices = np.where(rows)[0]
col_indices = np.where(cols)[0]
y_min, y_max = int(row_indices[0]), int(row_indices[-1])
x_min, x_max = int(col_indices[0]), int(col_indices[-1])
x_min = max(x_min - padding, 0)
y_min = max(y_min - padding, 0)
x_max = min(x_max + padding, img_w - 1)
y_max = min(y_max + padding, img_h - 1)
out_w = x_max - x_min + 1
out_h = y_max - y_min + 1
cropped_mask = mask[:, y_min:y_min + out_h, x_min:x_min + out_w]
print(f"[augment] bounds: x={x_min} y={y_min} w={out_w} h={out_h}")
json_result = json.dumps({
"node": "MaskBoundingBox",
"x": x_min,
"y": y_min,
"width": out_w,
"height": out_h,
"padding": padding,
"invert": invert,
})
return (cropped_mask, x_min, y_min, out_w, out_h, json_result, "done")
+197
View File
@@ -0,0 +1,197 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import os
import numpy as np
from PIL import Image
from PIL.PngImagePlugin import PngInfo
import folder_paths
class AugmentPreviewImage:
"""Previews images in the ComfyUI UI without saving to the output folder."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "preview"
CATEGORY = "Augment/Image"
def preview(self, images, trigger=None):
results = []
for batch_idx in range(images.shape[0]):
i = 255.0 * images[batch_idx].cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
filename = f"augment_preview_{batch_idx:05d}.png"
subfolder = "augment_temp"
temp_dir = folder_paths.get_temp_directory()
full_dir = os.path.join(temp_dir, subfolder)
os.makedirs(full_dir, exist_ok=True)
img.save(os.path.join(full_dir, filename), compress_level=1)
results.append({
"filename": filename,
"subfolder": subfolder,
"type": "temp",
})
json_result = json.dumps({"node": "AugmentPreviewImage", "images": results, "count": len(results)})
return {"ui": {"images": results}, "result": (images, json_result, "done")}
class AugmentSaveImage:
"""Saves images to the output folder with a customizable filename prefix."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"filename_prefix": ("STRING", {"default": "Augment"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("image", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "save"
CATEGORY = "Augment/Image"
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
self.type = "output"
self.compress_level = 4
def save(self, images, filename_prefix="Augment", trigger=None):
full_output_folder, filename, counter, subfolder, filename_prefix = (
folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images.shape[2], images.shape[1]
)
)
results = []
for batch_idx in range(images.shape[0]):
i = 255.0 * images[batch_idx].cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = PngInfo()
file = f"{filename}_{counter + batch_idx:05d}.png"
img.save(
os.path.join(full_output_folder, file),
pnginfo=metadata,
compress_level=self.compress_level,
)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type,
})
json_result = json.dumps({"node": "AugmentSaveImage", "images": results, "count": len(results), "prefix": filename_prefix})
return {"ui": {"images": results}, "result": (images, json_result, "done")}
class AugmentPreviewMask:
"""Previews a mask in the ComfyUI UI without saving to the output folder."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mask": ("MASK",),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("MASK", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("mask", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "preview"
CATEGORY = "Augment/Image"
def preview(self, mask, trigger=None):
results = []
for batch_idx in range(mask.shape[0]):
m = 255.0 * mask[batch_idx].cpu().numpy()
img = Image.fromarray(np.clip(m, 0, 255).astype(np.uint8), mode="L")
filename = f"augment_mask_preview_{batch_idx:05d}.png"
subfolder = "augment_temp"
temp_dir = folder_paths.get_temp_directory()
full_dir = os.path.join(temp_dir, subfolder)
os.makedirs(full_dir, exist_ok=True)
img.save(os.path.join(full_dir, filename), compress_level=1)
results.append({
"filename": filename,
"subfolder": subfolder,
"type": "temp",
})
json_result = json.dumps({"node": "Preview Mask", "images": results, "count": len(results)})
return {"ui": {"images": results}, "result": (mask, json_result, "done")}
class AugmentPreviewAny:
"""Displays the string representation of any value in the ComfyUI UI."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"source": ("*", {"forceInput": True}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("*", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "preview"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def preview(self, source=None, trigger=None):
if isinstance(source, str):
text = source
elif isinstance(source, (int, float, bool)):
text = str(source)
elif source is None:
text = "None"
else:
try:
text = json.dumps(source, indent=2, default=str)
except Exception:
try:
text = str(source)
except Exception:
text = "Value exists but could not be serialized"
json_result = json.dumps({"node": "AugmentPreviewAny", "value": text, "type": type(source).__name__})
return {"ui": {"text": [text]}, "result": (source, json_result, "done")}
+132
View File
@@ -0,0 +1,132 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
class IntNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("INT", {"default": 0}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("INT", {"forceInput": True}),
},
}
RETURN_TYPES = ("INT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, value, trigger=None, input=None):
result = input if input is not None else value
json_result = json.dumps({"node": "IntNode", "value": result, "type": "int"})
return (result, json_result, "done")
class FloatNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("FLOAT", {"default": 0.0, "step": 0.01}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("FLOAT", {"forceInput": True}),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, value, trigger=None, input=None):
result = input if input is not None else value
json_result = json.dumps({"node": "FloatNode", "value": result, "type": "float"})
return (result, json_result, "done")
class StringNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("STRING", {"default": ""}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("STRING", {"forceInput": True}),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, value, trigger=None, input=None):
result = input if input is not None else value
json_result = json.dumps({"node": "StringNode", "value": result, "type": "string"})
return (result, json_result, "done")
class BoolNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"value": ("BOOLEAN", {"default": False}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("BOOLEAN", {"forceInput": True}),
},
}
RETURN_TYPES = ("BOOLEAN", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, value, trigger=None, input=None):
result = input if input is not None else value
json_result = json.dumps({"node": "BoolNode", "value": result, "type": "boolean"})
return (result, json_result, "done")
NODE_CLASS_MAPPINGS = {
"IntPrimNode": IntNode,
"FloatPrimNode": FloatNode,
"StringPrimNode": StringNode,
"BoolPrimNode": BoolNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"IntPrimNode": "Int",
"FloatPrimNode": "Float",
"StringPrimNode": "String",
"BoolPrimNode": "Bool",
}
+115
View File
@@ -0,0 +1,115 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
import torch
class RecolorNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"R": ("FLOAT", {"default": 255.0, "min": 0.0, "max": 255.0, "step": 1.0}),
"G": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 255.0, "step": 1.0}),
"B": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 255.0, "step": 1.0}),
},
"optional": {
"hex_color": ("STRING", {"default": ""}),
"preserve_luminance": ("BOOLEAN", {"default": True}),
"blend": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.05}),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("recolored_image", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Image"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, image, mask, R, G, B, hex_color="",
preserve_luminance=True, blend=1.0, trigger=None):
# --- Hex override (same pattern as WCAG node) ---
h = hex_color.strip().lstrip("#")
if len(h) == 6:
try:
R = float(int(h[0:2], 16))
G = float(int(h[2:4], 16))
B = float(int(h[4:6], 16))
except ValueError:
pass
# --- Normalize target color to 0-1 ---
t_r = R / 255.0
t_g = G / 255.0
t_b = B / 255.0
# --- Work on clone ---
img = image.clone() # (batch, H, W, C)
# Mask shape: (batch, H, W) or (H, W)
m = mask.clone()
if m.dim() == 2:
m = m.unsqueeze(0)
for i in range(img.shape[0]):
frame = img[i] # (H, W, C)
frame_mask = m[i] if i < m.shape[0] else m[0] # (H, W)
frame_mask = frame_mask.unsqueeze(-1) # (H, W, 1)
# Clamp mask 0-1
frame_mask = frame_mask.clamp(0.0, 1.0)
if preserve_luminance:
# BT.709 luminance from original pixel
lum = (0.2126 * frame[:, :, 0]
+ 0.7152 * frame[:, :, 1]
+ 0.0722 * frame[:, :, 2]) # (H, W)
# Target color scaled by luminance
new_r = t_r * lum
new_g = t_g * lum
new_b = t_b * lum
colored = torch.stack([new_r, new_g, new_b], dim=-1) # (H, W, 3)
else:
# Flat color fill
colored = torch.zeros_like(frame)
colored[:, :, 0] = t_r
colored[:, :, 1] = t_g
colored[:, :, 2] = t_b
# Blend: original → colored based on blend strength
blended = frame * (1.0 - blend) + colored * blend
# Apply via mask: masked areas get recolor, unmasked stay original
img[i] = frame * (1.0 - frame_mask) + blended * frame_mask
img = img.clamp(0.0, 1.0)
json_result = json.dumps({
"node": "RecolorNode",
"R": int(R),
"G": int(G),
"B": int(B),
"hex_color": hex_color if hex_color else f"#{int(R):02X}{int(G):02X}{int(B):02X}",
"preserve_luminance": preserve_luminance,
"blend": blend
})
return (img, json_result, "done")
NODE_CLASS_MAPPINGS = {
"RecolorNode": RecolorNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"RecolorNode": "Recolor (Mask)",
}
+256
View File
@@ -0,0 +1,256 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Paid Node, credit values may change.
# Augmentstudio.app
import io
import json
import os
import requests
import time
import folder_paths
import numpy as np
import torch
from PIL import Image
NODE_ID = "svg_to_png"
API_URL = "https://augmentstudio.app/api"
UPLOAD_DIR = folder_paths.get_input_directory()
class AugmentSVGToPNG:
@classmethod
def INPUT_TYPES(cls):
files = sorted([
f for f in os.listdir(UPLOAD_DIR)
if f.lower().endswith(".svg")
])
return {
"required": {
"svg_file": (files, {"image_upload": True}),
"api_key": ("STRING", {"default": ""}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "TRIGGER")
RETURN_NAMES = ("image", "alpha", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Vector"
@classmethod
def IS_CHANGED(cls, svg_file="", **kwargs):
path = os.path.join(UPLOAD_DIR, svg_file)
if os.path.isfile(path):
return os.path.getmtime(path)
return float("nan")
@classmethod
def VALIDATE_INPUTS(cls, svg_file="", **kwargs):
path = os.path.join(UPLOAD_DIR, svg_file)
if not os.path.isfile(path):
return f"File not found: {svg_file}"
return True
def execute(self, svg_file, api_key, trigger=None):
api_url = API_URL.rstrip("/")
auth = {"Authorization": f"Bearer {api_key}"}
path = os.path.join(UPLOAD_DIR, svg_file)
with open(path, "rb") as f:
svg_bytes = f.read()
print(f"[Augment API] Submitting SVG: {svg_file} ({len(svg_bytes)} bytes)")
try:
r = requests.post(
f"{api_url}/process",
files={"image": (svg_file, svg_bytes, "image/svg+xml")},
data={"node_id": NODE_ID},
headers=auth,
timeout=30,
)
except requests.exceptions.RequestException as e:
print(f"[Augment API] Submit failed: {e}")
raise
if r.status_code != 200:
raise RuntimeError(f"Submit error {r.status_code}: {r.text[:500]}")
request_id = r.json().get("request_id")
if not request_id:
raise RuntimeError(f"No request_id: {r.text[:500]}")
print(f"[Augment API] Job submitted: {request_id}")
max_wait = 60
elapsed = 0
job_status = "unknown"
while elapsed < max_wait:
time.sleep(1)
elapsed += 1
try:
status_r = requests.get(
f"{api_url}/job/{request_id}/status",
headers=auth, timeout=10,
)
job_status = status_r.json().get("status", "unknown")
except Exception as e:
print(f"[Augment API] Poll error: {e}")
continue
if job_status == "done":
break
elif job_status == "error":
raise RuntimeError(f"Job failed: {status_r.json().get('error')}")
if job_status != "done":
raise RuntimeError(f"Timed out after {max_wait}s")
img_r = requests.get(
f"{api_url}/job/{request_id}/image",
headers=auth, timeout=60,
)
if img_r.status_code != 200:
raise RuntimeError(f"Image fetch error: {img_r.status_code}")
img = Image.open(io.BytesIO(img_r.content)).convert("RGBA")
arr = np.array(img).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(arr[:, :, :3]).unsqueeze(0)
mask_tensor = torch.from_numpy(arr[:, :, 3]).unsqueeze(0)
print(f"[Augment API] Done! {img.size[0]}x{img.size[1]}")
return (image_tensor, mask_tensor, "done")
class AugmentPNGToSVG:
@classmethod
def INPUT_TYPES(cls):
files = sorted([
f for f in os.listdir(UPLOAD_DIR)
if f.lower().endswith((".png", ".jpg", ".jpeg", ".webp"))
])
return {
"required": {
"api_key": ("STRING", {"default": ""}),
},
"optional": {
"image": ("IMAGE",),
"image_file": (files, {"image_upload": True}),
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("SVG", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("svg", "json", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Vector"
OUTPUT_NODE = True
@classmethod
def IS_CHANGED(cls, **kwargs):
return time.time()
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, api_key, image=None, image_file=None, trigger=None):
api_url = API_URL.rstrip("/")
auth = {"Authorization": f"Bearer {api_key}"}
if image is not None:
from PIL import Image as PILImage
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
pil_img = PILImage.fromarray(img_np, "RGB")
buf = io.BytesIO()
pil_img.save(buf, format="PNG")
img_bytes = buf.getvalue()
source_name = "image_input.png"
mime = "image/png"
elif image_file:
path = os.path.join(UPLOAD_DIR, image_file)
with open(path, "rb") as f:
img_bytes = f.read()
source_name = image_file
ext = os.path.splitext(image_file)[1].lower()
mime_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}
mime = mime_types.get(ext, "image/png")
else:
raise RuntimeError("No image provided — connect an image input or select a file")
print(f"[Augment API] Submitting image: {source_name} ({len(img_bytes)} bytes)")
try:
r = requests.post(
f"{api_url}/process",
files={"image": (source_name, img_bytes, mime)},
data={"node_id": "png_to_svg"},
headers=auth,
timeout=30,
)
except requests.exceptions.RequestException as e:
print(f"[Augment API] Submit failed: {e}")
raise
if r.status_code != 200:
raise RuntimeError(f"Submit error {r.status_code}: {r.text[:500]}")
request_id = r.json().get("request_id")
if not request_id:
raise RuntimeError(f"No request_id: {r.text[:500]}")
print(f"[Augment API] Job submitted: {request_id}")
max_wait = 60
elapsed = 0
job_status = "unknown"
while elapsed < max_wait:
time.sleep(1)
elapsed += 1
try:
status_r = requests.get(
f"{api_url}/job/{request_id}/status",
headers=auth, timeout=10,
)
job_status = status_r.json().get("status", "unknown")
except Exception as e:
print(f"[Augment API] Poll error: {e}")
continue
if job_status == "done":
break
elif job_status == "error":
raise RuntimeError(f"Job failed: {status_r.json().get('error')}")
if job_status != "done":
raise RuntimeError(f"Timed out after {max_wait}s")
svg_r = requests.get(
f"{api_url}/job/{request_id}/image",
headers=auth, timeout=60,
)
if svg_r.status_code != 200:
raise RuntimeError(f"SVG fetch error: {svg_r.status_code}")
from comfy_api.latest._util.image_types import SVG
svg_bytes = io.BytesIO(svg_r.content)
size = svg_bytes.getbuffer().nbytes
print(f"[Augment API] Done! Received SVG ({size} bytes)")
json_result = json.dumps({"node": "AugmentPNGToSVG", "size_bytes": size})
return {"ui": {"text": [svg_r.text[:500]]}, "result": (SVG([svg_bytes]), json_result, "done")}
NODE_CLASS_MAPPINGS = {
"AugmentSVGToPNG": AugmentSVGToPNG,
"AugmentPNGToSVG": AugmentPNGToSVG,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AugmentSVGToPNG": "Augment SVG to PNG",
"AugmentPNGToSVG": "Augment PNG to SVG",
}
+31
View File
@@ -0,0 +1,31 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
from comfy_execution.graph_utils import ExecutionBlocker
class SwitchNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("BOOLEAN", {"forceInput": True}),
},
}
RETURN_TYPES = ("TRIGGER", "TRIGGER", "TRIGGER")
RETURN_NAMES = ("on_true", "on_false", "trigger")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, input):
on_true = "done" if input else ExecutionBlocker(None)
on_false = ExecutionBlocker(None) if input else "done"
ui_text = f"Input: {input}\nPath: {'on_true' if input else 'on_false'}"
return {"ui": {"text": [ui_text]}, "result": (on_true, on_false, "done")}
+99
View File
@@ -0,0 +1,99 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import json
class GateNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input": ("*",),
},
}
RETURN_TYPES = ("TRIGGER", "*", "STRING")
RETURN_NAMES = ("trigger", "value", "json")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, input):
json_result = json.dumps({"node": "Convert Any", "type": type(input).__name__, "value": str(input)})
return {"ui": {"text": [str(input)]}, "result": ("done", input, json_result)}
WAIT_DEFAULTS = {
"INT": {"default": 0},
"FLOAT": {"default": 0.0, "step": 0.01},
"STRING": {"default": ""},
"BOOLEAN": {"default": False},
}
def make_wait_node(type_name, type_str):
class WaitNode:
@classmethod
def INPUT_TYPES(cls):
has_widget = type_str in WAIT_DEFAULTS
inputs = {
"required": {},
"optional": {
"trigger": ("TRIGGER",),
},
}
if has_widget:
inputs["required"]["value"] = (type_str, WAIT_DEFAULTS[type_str])
inputs["optional"]["input"] = (type_str, {"forceInput": True})
else:
inputs["required"]["value"] = (type_str,)
return inputs
RETURN_TYPES = (type_str, "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("value", "json", "trigger")
OUTPUT_NODE = True
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, value=None, trigger=None, input=None):
v = input if input is not None else value
json_result = json.dumps({"node": type_str, "type": type_str.lower(), "value": str(v)})
return {"ui": {"text": [str(v)]}, "result": (v, json_result, "done")}
WaitNode.__name__ = f"Wait{type_name}Node"
WaitNode.__qualname__ = f"Wait{type_name}Node"
return WaitNode
WAIT_TYPES = {
"Int": "INT",
"Float": "FLOAT",
"String": "STRING",
"Bool": "BOOLEAN",
"Image": "IMAGE",
"Latent": "LATENT",
"Condition": "CONDITIONING",
"VAE": "VAE",
"Model": "MODEL",
"Clip": "CLIP",
"Mask": "MASK",
}
NODE_CLASS_MAPPINGS = {"GateNode": GateNode}
NODE_DISPLAY_NAME_MAPPINGS = {"GateNode": "Trigger"}
for name, type_str in WAIT_TYPES.items():
cls_name = f"Wait{name}Node"
node_cls = make_wait_node(name, type_str)
NODE_CLASS_MAPPINGS[cls_name] = node_cls
NODE_DISPLAY_NAME_MAPPINGS[cls_name] = f"{name} (Improved)"
+256
View File
@@ -0,0 +1,256 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
"""
Smart shape-edge detection for construction lines.
Extracts meaningful structural lines, not boundary noise.
"""
import numpy as np
try:
from scipy import ndimage
HAS_SCIPY = True
except ImportError:
HAS_SCIPY = False
def compute_all_edges(mask_2d, edge_min=None, merge_tol=None):
"""
Main entry point — replaces the old boundary-tracing approach.
edge_min and merge_tol are kept for API compatibility but ignored.
"""
return compute_smart_edges(mask_2d)
def compute_smart_edges(mask, symmetry_threshold=0.85):
"""
Extract meaningful construction lines from a mask.
Returns structural lines, not boundary noise.
"""
binary = (mask > 0.5).astype(np.uint8)
if binary.sum() == 0:
return {
"h_lines": [],
"v_lines": [],
"d45_lines": [],
"d135_lines": [],
}
ys, xs = np.where(binary)
# === BOUNDING BOX ===
x_min, x_max = int(xs.min()), int(xs.max())
y_min, y_max = int(ys.min()), int(ys.max())
bbox_w = x_max - x_min
bbox_h = y_max - y_min
# === CENTERS ===
center_x = (x_min + x_max) // 2
center_y = (y_min + y_max) // 2
# === CENTER OF MASS ===
if HAS_SCIPY:
com_y, com_x = ndimage.center_of_mass(binary)
com_x, com_y = int(com_x), int(com_y)
else:
com_x, com_y = center_x, center_y
# === BUILD LINES ===
h_lines = [y_min, y_max]
v_lines = [x_min, x_max]
# Add geometric center
if bbox_h > 20:
h_lines.append(center_y)
if bbox_w > 20:
v_lines.append(center_x)
# Add center of mass if different from geometric center
if bbox_h > 40 and abs(com_y - center_y) > bbox_h * 0.05:
h_lines.append(com_y)
if bbox_w > 40 and abs(com_x - center_x) > bbox_w * 0.05:
v_lines.append(com_x)
# === DENSITY-BASED DIVISIONS ===
# Find where the shape has significant internal gaps
h_divisions = _find_density_divisions(binary, axis='horizontal', min_gap=max(8, bbox_h // 20))
v_divisions = _find_density_divisions(binary, axis='vertical', min_gap=max(8, bbox_w // 20))
for y in h_divisions:
if y not in h_lines and y_min < y < y_max:
h_lines.append(y)
for x in v_divisions:
if x not in v_lines and x_min < x < x_max:
v_lines.append(x)
# === DIAGONALS (only for symmetric shapes) ===
d45_lines = []
d135_lines = []
if _is_roughly_square(bbox_w, bbox_h, tolerance=0.25):
# Check for diagonal symmetry
crop = binary[y_min:y_max+1, x_min:x_max+1]
d45_score = _diagonal_symmetry_score(crop, axis='d45')
d135_score = _diagonal_symmetry_score(crop, axis='d135')
if d45_score > symmetry_threshold:
d45_lines.append(center_x - center_y)
if d135_score > symmetry_threshold:
d135_lines.append(center_x + center_y)
# === CORNER DIAGONALS (useful for any shape) ===
# Lines from bbox corners through center
if bbox_w > 50 and bbox_h > 50:
# Only add if the shape has significant diagonal extent
diag_coverage_45 = _check_diagonal_coverage(binary, x_min, y_min, x_max, y_max, direction=45)
diag_coverage_135 = _check_diagonal_coverage(binary, x_min, y_min, x_max, y_max, direction=135)
if diag_coverage_45 > 0.3:
d45_lines.append(x_min - y_min) # top-left to bottom-right
d45_lines.append(x_max - y_max) # if different
if diag_coverage_135 > 0.3:
d135_lines.append(x_min + y_max) # bottom-left to top-right
d135_lines.append(x_max + y_min) # if different
# === CLEANUP ===
h_lines = sorted(set(h_lines))
v_lines = sorted(set(v_lines))
d45_lines = sorted(set(d45_lines))
d135_lines = sorted(set(d135_lines))
# Merge lines that are too close
h_lines = _merge_close(h_lines, min_dist=max(5, bbox_h // 30))
v_lines = _merge_close(v_lines, min_dist=max(5, bbox_w // 30))
d45_lines = _merge_close(d45_lines, min_dist=10)
d135_lines = _merge_close(d135_lines, min_dist=10)
return {
"h_lines": h_lines,
"v_lines": v_lines,
"d45_lines": d45_lines,
"d135_lines": d135_lines,
}
def _find_density_divisions(binary, axis='horizontal', min_gap=8):
"""
Find lines where pixel density drops significantly.
These indicate structural divisions (baselines, gaps between elements, etc.)
"""
divisions = []
if axis == 'horizontal':
density = binary.sum(axis=1).astype(float)
else:
density = binary.sum(axis=0).astype(float)
if len(density) < 10:
return divisions
max_d = density.max()
if max_d == 0:
return divisions
density = density / max_d
# Find valleys (low density regions)
threshold = 0.15
in_gap = False
gap_start = 0
for i, d in enumerate(density):
if d < threshold and not in_gap:
in_gap = True
gap_start = i
elif d >= threshold and in_gap:
in_gap = False
gap_size = i - gap_start
if gap_size >= min_gap:
divisions.append(gap_start + gap_size // 2)
return divisions
def _diagonal_symmetry_score(binary, axis='d45'):
"""Check how symmetric a shape is along a diagonal."""
h, w = binary.shape
if h != w:
# Pad to square for comparison
size = max(h, w)
padded = np.zeros((size, size), dtype=binary.dtype)
padded[:h, :w] = binary
binary = padded
if axis == 'd45':
flipped = binary.T
else: # d135
flipped = np.flipud(np.fliplr(binary)).T
intersection = np.logical_and(binary, flipped).sum()
union = np.logical_or(binary, flipped).sum()
if union == 0:
return 0.0
return intersection / union
def _is_roughly_square(w, h, tolerance=0.25):
"""Check if aspect ratio is close to 1:1"""
if w == 0 or h == 0:
return False
ratio = min(w, h) / max(w, h)
return ratio > (1 - tolerance)
def _check_diagonal_coverage(binary, x_min, y_min, x_max, y_max, direction=45):
"""
Check what fraction of a diagonal line through the bbox is covered by the shape.
"""
samples = 20
hits = 0
for i in range(samples):
t = i / (samples - 1)
if direction == 45:
x = int(x_min + t * (x_max - x_min))
y = int(y_min + t * (y_max - y_min))
else: # 135
x = int(x_min + t * (x_max - x_min))
y = int(y_max - t * (y_max - y_min))
if 0 <= y < binary.shape[0] and 0 <= x < binary.shape[1]:
if binary[y, x]:
hits += 1
return hits / samples
def _merge_close(lines, min_dist=5):
"""Merge lines that are within min_dist of each other."""
if len(lines) <= 1:
return lines
result = [lines[0]]
for line in lines[1:]:
if line - result[-1] >= min_dist:
result.append(line)
else:
# Keep the average? Or just skip? Let's keep first.
pass
return result
# === LEGACY API (kept for compatibility) ===
def build_alpha_grid(mask_2d):
return (mask_2d > 0).astype(np.uint8)
+330
View File
@@ -0,0 +1,330 @@
# "Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
# Augmentstudio.app
import os
import json
VARS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "input")
VARS_FILE = os.path.join(VARS_DIR, "vars.txt")
def load_vars():
if os.path.exists(VARS_FILE):
try:
with open(VARS_FILE, "r") as f:
return json.load(f)
except:
return {}
return {}
def save_vars(data):
os.makedirs(VARS_DIR, exist_ok=True)
with open(VARS_FILE, "w") as f:
json.dump(data, f, indent=2)
def vars_summary():
data = load_vars()
if not data:
return "(no variables saved)"
return "\n".join(f"{k}: {v}" for k, v in data.items())
# ── SET ──────────────────────────────────────────────
class NumberVarSetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_variable"}),
"value": ("FLOAT", {"default": 0.0, "step": 0.01}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("FLOAT", {"forceInput": True}),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, value, trigger=None, input=None):
v = input if input is not None else value
data = load_vars()
data[variable_name] = v
save_vars(data)
json_result = json.dumps({"node": "NumberVarSetNode", "variable": variable_name, "value": v, "type": "number"})
return {"ui": {"text": [vars_summary()]}, "result": (v, json_result, "done")}
# ── GET ──────────────────────────────────────────────
class NumberVarGetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_variable"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, trigger=None):
data = load_vars()
value = data.get(variable_name, 0.0)
json_result = json.dumps({"node": "NumberVarGetNode", "variable": variable_name, "value": value, "type": "number"})
return {"ui": {"text": [vars_summary()]}, "result": (value, json_result, "done")}
# ── INCREMENT ────────────────────────────────────────
class NumberVarIncrementNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_variable"}),
"amount": ("FLOAT", {"default": 1.0, "step": 0.01}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("FLOAT", {"forceInput": True}),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, amount, trigger=None, input=None):
data = load_vars()
current = input if input is not None else data.get(variable_name, 0.0)
new_value = current + amount
data[variable_name] = new_value
save_vars(data)
json_result = json.dumps({"node": "NumberVarIncrementNode", "variable": variable_name, "value": new_value, "amount": amount, "type": "number"})
return {"ui": {"text": [vars_summary()]}, "result": (new_value, json_result, "done")}
# ── DECREMENT ────────────────────────────────────────
class NumberVarDecrementNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_variable"}),
"amount": ("FLOAT", {"default": 1.0, "step": 0.01}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("FLOAT", {"forceInput": True}),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, amount, trigger=None, input=None):
data = load_vars()
current = input if input is not None else data.get(variable_name, 0.0)
new_value = current - amount
data[variable_name] = new_value
save_vars(data)
json_result = json.dumps({"node": "NumberVarDecrementNode", "variable": variable_name, "value": new_value, "amount": amount, "type": "number"})
return {"ui": {"text": [vars_summary()]}, "result": (new_value, json_result, "done")}
# ── RESET ────────────────────────────────────────────
class NumberVarResetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "my_variable"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("FLOAT", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("number", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, trigger=None):
data = load_vars()
data[variable_name] = 0.0
save_vars(data)
json_result = json.dumps({"node": "NumberVarResetNode", "variable": variable_name, "value": 0.0, "type": "number"})
return {"ui": {"text": [vars_summary()]}, "result": (0.0, json_result, "done")}
# ── STRING SET ────────────────────────────────────────
class StringVarSetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "api_key"}),
"value": ("STRING", {"default": ""}),
},
"optional": {
"trigger": ("TRIGGER",),
"input": ("STRING", {"forceInput": True}),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("text", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, value, trigger=None, input=None):
v = input if input is not None else value
data = load_vars()
data[variable_name] = v
save_vars(data)
masked = _masked_summary(load_vars())
json_result = json.dumps({"node": "StringVarSetNode", "variable": variable_name, "value": v, "type": "string"})
return {"ui": {"text": [masked]}, "result": (v, json_result, "done")}
# ── STRING GET ────────────────────────────────────────
class StringVarGetNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "api_key"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("text", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, trigger=None):
data = load_vars()
value = data.get(variable_name, "")
masked = _masked_summary(data)
json_result = json.dumps({"node": "StringVarGetNode", "variable": variable_name, "value": value, "type": "string"})
return {"ui": {"text": [masked]}, "result": (value, json_result, "done")}
# ── STRING DELETE ─────────────────────────────────────
class StringVarDeleteNode:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"variable_name": ("STRING", {"default": "api_key"}),
},
"optional": {
"trigger": ("TRIGGER",),
},
}
RETURN_TYPES = ("STRING", "AUGMENT_JSON", "TRIGGER")
RETURN_NAMES = ("text", "json_result", "trigger")
FUNCTION = "execute"
CATEGORY = "Augment/Utils"
OUTPUT_NODE = True
@classmethod
def VALIDATE_INPUTS(cls, **kwargs):
return True
def execute(self, variable_name, trigger=None):
data = load_vars()
removed = data.pop(variable_name, "")
save_vars(data)
json_result = json.dumps({"node": "StringVarDeleteNode", "variable": variable_name, "deleted_value": removed, "type": "string"})
return {"ui": {"text": [vars_summary()]}, "result": (removed, json_result, "done")}
# ── helpers ──────────────────────────────────────────
def _masked_summary(data):
"""Mask string values that look like keys/secrets in the UI."""
lines = []
for k, v in data.items():
if isinstance(v, str) and len(v) > 12:
lines.append(f"{k}: {v[:4]}...{v[-4:]}")
else:
lines.append(f"{k}: {v}")
return "\n".join(lines) if lines else "(no variables saved)"
NODE_CLASS_MAPPINGS = {
"NumberVarSetNode": NumberVarSetNode,
"NumberVarGetNode": NumberVarGetNode,
"NumberVarIncrementNode": NumberVarIncrementNode,
"NumberVarDecrementNode": NumberVarDecrementNode,
"NumberVarResetNode": NumberVarResetNode,
"StringVarSetNode": StringVarSetNode,
"StringVarGetNode": StringVarGetNode,
"StringVarDeleteNode": StringVarDeleteNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"NumberVarSetNode": "Variable Set (Number)",
"NumberVarGetNode": "Variable Get (Number)",
"NumberVarIncrementNode": "Variable Increment (Number)",
"NumberVarDecrementNode": "Variable Decrement (Number)",
"NumberVarResetNode": "Variable Reset (Number)",
"StringVarSetNode": "Variable Set (String)",
"StringVarGetNode": "Variable Get (String)",
"StringVarDeleteNode": "Variable Delete (String)",
}
+20
View File
@@ -0,0 +1,20 @@
[project]
name = "augment-ComfyUI"
description = "Design-focused node suite for ComfyUI. Convert raster to vector with a built-in PNG to SVG pipeline — bring true vectorization to your generative workflows and stop exporting pixel-locked assets. Includes precision utilities for grid systems, construction lines, edge detection, masking, image transforms, cropping, JSON tools, and flow control. Built for logo design, brand asset pipelines, and anyone who needs clean scalable output from their design based workflows. API tokens for vector nodes available at [augment studio](https://augmentstudio.app/pricing)"
version = "1.0.0"
dependencies = [
"opencv-python>=4.5.0",
"numpy>=1.19.0",
"torch>=1.9.0",
"Pillow>=8.0.0",
"requests>=2.25.0"
]
[project.urls]
Repository = "https://github.com/augmentstudio/augment"
[tool.comfy]
PublisherId = "augment"
DisplayName = "augment-ComfyUI""
Icon = ""
+5
View File
@@ -0,0 +1,5 @@
opencv-python>=4.5.0
numpy>=1.19.0
torch>=1.9.0
Pillow>=8.0.0
requests>=2.25.0
+28
View File
@@ -0,0 +1,28 @@
/*"Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
Augmentstudio.app */
import { app } from "../../../scripts/app.js";
const TITLE_BG = "#443322";
const NODE_BG = "#665533";
const PAID_NODES = [
"AugmentSVGToPNG",
"AugmentPNGToSVG",
];
app.registerExtension({
name: "augment.NodeStyle",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (!PAID_NODES.includes(nodeData.name)) return;
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function (ctx) {
this.color = TITLE_BG;
this.bgcolor = NODE_BG;
onDrawForeground?.apply(this, arguments);
};
},
});
+89
View File
@@ -0,0 +1,89 @@
/*"Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
Augmentstudio.app */
import { app } from "../../../scripts/app.js";
function syntaxHighlight(json) {
json = json.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
return json.replace(
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
function (match) {
let cls = "json-number";
if (/^"/.test(match)) {
if (/:$/.test(match)) {
cls = "json-key";
} else {
cls = "json-string";
}
} else if (/true|false/.test(match)) {
cls = "json-boolean";
} else if (/null/.test(match)) {
cls = "json-null";
}
return '<span class="' + cls + '">' + match + "</span>";
}
);
}
app.registerExtension({
name: "augment.JsonViewer",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "AugmentJsonViewer" || nodeData.name === "AugmentJsonExtract") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
const container = document.createElement("div");
container.style.cssText = `
background:rgb(9, 9, 9);
border-radius: 6px;
padding: 8px 10px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
line-height: 1.4;
overflow: auto;
max-height: 300px;
white-space: pre-wrap;
word-break: break-word;
color: #ccc;
`;
const style = document.createElement("style");
style.textContent = `
.json-key { color:rgb(255, 255, 255); font-weight: 600; }
.json-string { color: #fb971d; }
.json-number { color: #fb971d; }
.json-boolean { color: #fb971d; }
.json-null { color: #666; font-style: italic; }
`;
container.appendChild(style);
const pre = document.createElement("pre");
pre.style.cssText = "margin: 0; font: inherit;";
pre.innerHTML = '<span style="color:#555;">waiting for data...</span>';
container.appendChild(pre);
const widget = this.addDOMWidget("json_display", "customtext", container, {
getValue() { return ""; },
setValue() {},
serialize: false,
});
widget.computeSize = function () {
return [220, Math.min(container.scrollHeight + 16, 320)];
};
this._jsonContainer = pre;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
if (message?.text?.[0] !== undefined && this._jsonContainer) {
this._jsonContainer.innerHTML = syntaxHighlight(message.text[0]);
this.setSize(this.computeSize());
}
};
}
},
});
+34
View File
@@ -0,0 +1,34 @@
/*"Design-focused node suite for ComfyUI."
# Copyright 2026 Augment Studio
Augmentstudio.app */
import { app } from "../../../scripts/app.js";
import { ComfyWidgets } from "../../../scripts/widgets.js";
app.registerExtension({
name: "augment.PreviewAny",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const previewNodes = [
"AugmentPreviewAny", "AugmentSwitch",
"AugmentSHA256", "AugmentRandomNumber", "AugmentUUID",
"AugmentLoadImage",
];
if (previewNodes.includes(nodeData.name)) {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
this.showValueWidget = ComfyWidgets["STRING"](this, "output", ["STRING", { multiline: true }], app).widget;
this.showValueWidget.inputEl.readOnly = true;
this.showValueWidget.serializeValue = async () => "";
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, [message]);
if (message?.text?.[0] !== undefined) {
this.showValueWidget.value = message.text[0];
}
};
}
},
});
+649
View File
@@ -0,0 +1,649 @@
{
"id": "0844657a-3ed3-4e2a-a7c9-54a6f113a60c",
"revision": 0,
"last_node_id": 13,
"last_link_id": 20,
"nodes": [
{
"id": 2,
"type": "AugmentLogoMask",
"pos": [
-100.25392017898614,
1025.3065279520267
],
"size": [
270,
234
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "mask",
"type": "MASK",
"links": [
4
]
},
{
"name": "mask_image",
"type": "IMAGE",
"links": null
},
{
"name": "masked_rgb",
"type": "IMAGE",
"links": null
},
{
"name": "json_result",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"properties": {
"Node name for S&R": "AugmentLogoMask"
},
"widgets_values": [
50,
150,
true,
3,
false
]
},
{
"id": 5,
"type": "AugmentMaskBoundingBox",
"pos": [
232.100684634451,
945.6714392681223
],
"size": [
270,
226
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "mask",
"type": "MASK",
"link": 4
}
],
"outputs": [
{
"name": "mask",
"type": "MASK",
"links": null
},
{
"name": "x",
"type": "INT",
"links": [
5
]
},
{
"name": "y",
"type": "INT",
"links": [
6
]
},
{
"name": "width",
"type": "INT",
"links": [
8
]
},
{
"name": "height",
"type": "INT",
"links": [
9
]
},
{
"name": "json_result",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"properties": {
"Node name for S&R": "AugmentMaskBoundingBox"
},
"widgets_values": [
0,
false,
0.1
]
},
{
"id": 13,
"type": "Reroute",
"pos": [
939.6250943735594,
1024.304073871377
],
"size": [
75,
26
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "",
"type": "*",
"link": 18
}
],
"outputs": [
{
"name": "",
"type": "IMAGE",
"links": [
19,
20
]
}
],
"properties": {
"showOutputText": false,
"horizontal": false
}
},
{
"id": 4,
"type": "AugmentImageCrop",
"pos": [
612.3988632473819,
1026.1191308977811
],
"size": [
270,
250
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 11
},
{
"name": "x",
"type": "INT",
"widget": {
"name": "x"
},
"link": 5
},
{
"name": "y",
"type": "INT",
"widget": {
"name": "y"
},
"link": 6
},
{
"name": "width",
"type": "INT",
"widget": {
"name": "width"
},
"link": 8
},
{
"name": "height",
"type": "INT",
"widget": {
"name": "height"
},
"link": 9
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
18
]
},
{
"name": "x",
"type": "INT",
"links": null
},
{
"name": "y",
"type": "INT",
"links": null
},
{
"name": "width",
"type": "INT",
"links": null
},
{
"name": "height",
"type": "INT",
"links": null
},
{
"name": "json_result",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": []
}
],
"properties": {
"Node name for S&R": "AugmentImageCrop"
},
"widgets_values": [
0,
0,
256,
256
]
},
{
"id": 12,
"type": "AugmentPreviewImage",
"pos": [
1407.4440840935729,
1190.1110326830787
],
"size": [
265.1408536461356,
286
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 20
},
{
"name": "trigger",
"shape": 7,
"type": "TRIGGER",
"link": 16
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": null
},
{
"name": "json",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"properties": {
"Node name for S&R": "AugmentPreviewImage"
},
"widgets_values": []
},
{
"id": 1,
"type": "LoadImage",
"pos": [
-486.9632087162334,
1026.1191308977811
],
"size": [
282.8222198486328,
314.0000000000001
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1,
11
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.17.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"augment_logo.png",
"image"
]
},
{
"id": 11,
"type": "AugmentBoolPrim",
"pos": [
612.3988632473819,
1342.0527501870952
],
"size": [
270,
98
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "trigger",
"shape": 7,
"type": "TRIGGER",
"link": null
},
{
"name": "input",
"shape": 7,
"type": "BOOLEAN",
"link": null
}
],
"outputs": [
{
"name": "value",
"type": "BOOLEAN",
"links": [
14
]
},
{
"name": "json_result",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"title": "SAVE_AS_VECTOR?",
"properties": {
"Node name for S&R": "AugmentBoolPrim"
},
"widgets_values": [
false
]
},
{
"id": 10,
"type": "AugmentSwitchNode",
"pos": [
1170.143532336275,
1048.0987133289495
],
"size": [
150.4999984741211,
66
],
"flags": {
"collapsed": true
},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "input",
"type": "BOOLEAN",
"link": 14
}
],
"outputs": [
{
"name": "on_true",
"type": "TRIGGER",
"links": [
17
]
},
{
"name": "on_false",
"type": "TRIGGER",
"links": [
16
]
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"properties": {
"Node name for S&R": "AugmentSwitchNode"
},
"widgets_values": []
},
{
"id": 7,
"type": "AugmentPNGToSVG",
"pos": [
1407.4440840935729,
793.8371232979359
],
"size": [
270,
122
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"shape": 7,
"type": "IMAGE",
"link": 19
},
{
"name": "trigger",
"shape": 7,
"type": "TRIGGER",
"link": 17
}
],
"outputs": [
{
"name": "svg",
"type": "SVG",
"links": []
},
{
"name": "json",
"type": "AUGMENT_JSON",
"links": null
},
{
"name": "trigger",
"type": "TRIGGER",
"links": null
}
],
"properties": {
"Node name for S&R": "AugmentPNGToSVG"
},
"widgets_values": [
"",
"example.png"
],
"color": "#443322",
"bgcolor": "#665533"
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
4,
2,
0,
5,
0,
"MASK"
],
[
5,
5,
1,
4,
1,
"INT"
],
[
6,
5,
2,
4,
2,
"INT"
],
[
8,
5,
3,
4,
3,
"INT"
],
[
9,
5,
4,
4,
4,
"INT"
],
[
11,
1,
0,
4,
0,
"IMAGE"
],
[
14,
11,
0,
10,
0,
"BOOLEAN"
],
[
16,
10,
1,
12,
1,
"TRIGGER"
],
[
17,
10,
0,
7,
1,
"TRIGGER"
],
[
18,
4,
0,
13,
0,
"IMAGE"
],
[
19,
13,
0,
7,
0,
"IMAGE"
],
[
20,
13,
0,
12,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.573085533011682,
"offset": [
541.1457978789277,
-558.8768228145503
]
},
"frontendVersion": "1.41.18"
},
"version": 0.4
}