Rebrand fork to Hanzo Studio: licensing, branding, Engine nodes, fashion workflows

Licensing (GPL-3.0 preserved):
- NOTICE attributes the ComfyUI upstream and the fork's modifications, GPL-3.0.
- README credits upstream ComfyUI prominently and states GPL-3.0.

Branding (frontend served from the comfyui-frontend-package static dir):
- apply-branding.sh installs the Hanzo favicon (svg + multi-res ico), injects
  icon links, sets <title>Hanzo Studio</title>, and patches display strings.
- make_favicon.py renders the committed favicon.ico from favicon.svg.

Hanzo Engine nodes (custom_nodes/hanzo_engine/, OpenAI-compatible HTTP):
- HanzoChat, HanzoImageGen, HanzoVisionCaption, HanzoSaveText. Graceful node
  errors on connection failure.

Fashion/swimwear starter workflows (user/default/workflows/fashion/):
- fashion_product_shot (FLUX.2 klein t2i), fashion_edit_garment
  (Qwen-Image-Edit i2i), fashion_caption_dataset (Hanzo vision caption loop).
  Each carries a Note node with usage and required files.
This commit is contained in:
hanzo-dev
2026-07-02 23:17:57 -07:00
parent a45a4413dd
commit 1ff6280992
12 changed files with 1677 additions and 41 deletions
+39
View File
@@ -0,0 +1,39 @@
Hanzo Studio
============
Hanzo Studio is a fork of ComfyUI (https://github.com/comfyanonymous/ComfyUI),
Copyright (c) ComfyUI contributors, licensed under the GNU General Public
License v3.0 (GPL-3.0).
Modifications Copyright (c) 2026 Hanzo Industries Inc, also released under the
GNU General Public License v3.0 (GPL-3.0). See the LICENSE file for the full
license text.
As required by the GPL-3.0, this fork remains under the same license as the
upstream project. The original ComfyUI copyright notices are retained.
Modifications made in this fork
-------------------------------
- Branding: user-visible name changed from "ComfyUI" to "Hanzo Studio",
including the served page <title>, favicon, in-app logos, and display
strings. The prebuilt frontend package (comfyui-frontend-package) is patched
at install time by branding/apply-branding.sh; source assets live in
branding/. Backend Python packages are namespaced under studio_* and the
server reports itself as "Hanzo Studio".
- Hanzo Engine integration nodes (custom_nodes/hanzo_engine/): custom nodes
that call a local Hanzo Engine OpenAI-compatible server — HanzoChat
(chat/completions), HanzoImageGen (images/generations), and
HanzoVisionCaption (vision chat/completions). Hanzo Engine is the native-Rust,
all-modality inference and training backend.
- Fashion/swimwear starter workflows (user/default/workflows/fashion/):
product-shot, garment-edit, and caption-dataset workflow templates for a
designer's day-to-day use.
- Deployment/integration middleware for Hanzo infrastructure (IAM auth via
hanzo.id, commerce/billing, metrics), wired through environment variables and
the Dockerfile.
Third-party components retain their own upstream copyrights and licenses.
+6
View File
@@ -35,6 +35,12 @@
Hanzo Studio lets you design and execute advanced stable diffusion pipelines using a graph/nodes/flowchart based interface. Available on Windows, Linux, and macOS.
## Upstream & License
Hanzo Studio is a fork of [**ComfyUI**](https://github.com/comfyanonymous/ComfyUI) by comfyanonymous and the ComfyUI contributors — full credit to the upstream project for the engine this builds on. ComfyUI is licensed under the **GNU General Public License v3.0 (GPL-3.0)**, and this fork stays under the same license. All modifications in this fork are likewise GPL-3.0.
See [`LICENSE`](LICENSE) for the full license text and [`NOTICE`](NOTICE) for the fork's attribution and a summary of what it changes (branding, Hanzo Engine integration nodes, and fashion workflow templates).
## Get Started
#### [Manual Install](#manual-install-windows-linux)
+23 -41
View File
@@ -1,16 +1,17 @@
#!/bin/bash
# Apply Hanzo Studio branding to frontend package
# This runs during Docker build after pip install to patch the pre-built frontend
# Apply Hanzo Studio branding to the prebuilt frontend package (comfyui-frontend-package).
# Runs during the Docker build after pip install, and can be run against a venv
# for local dev. Idempotent: safe to run more than once.
set -e
STATIC_DIR=$(python3 -c "import comfyui_frontend_package, importlib.resources; print(str(importlib.resources.files(comfyui_frontend_package) / 'static'))")
BRANDING_DIR="$(dirname "$0")"
BRANDING_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== Hanzo Studio Branding ==="
echo "Target: $STATIC_DIR"
# --- 1. Replace logo SVG files ---
echo "[1/6] Replacing logos..."
# --- 1. Replace in-app logos ---
echo "[1/5] Replacing logos..."
for logo_file in comfy-logo-single.svg comfy-logo-mono.svg comfy-cloud-logo.svg; do
target="$STATIC_DIR/assets/images/$logo_file"
if [ -f "$target" ]; then
@@ -21,54 +22,35 @@ for logo_file in comfy-logo-single.svg comfy-logo-mono.svg comfy-cloud-logo.svg;
fi
done
# --- 2. Replace favicon ---
echo "[2/6] Replacing favicon..."
python3 -c "
import struct
size = 16
pixels = bytearray(size * size * 4)
for y in range(size):
for x in range(size):
idx = (y * size + x) * 4
pixels[idx] = 0; pixels[idx+1] = 0; pixels[idx+2] = 0; pixels[idx+3] = 0
if 1 <= x <= 4 and 1 <= y <= 14:
pixels[idx] = 255; pixels[idx+1] = 255; pixels[idx+2] = 255; pixels[idx+3] = 255
if 11 <= x <= 14 and 1 <= y <= 14:
pixels[idx] = 255; pixels[idx+1] = 255; pixels[idx+2] = 255; pixels[idx+3] = 255
if 4 <= y <= 11:
expected_x = 1 + (y - 4) * (13.0 / 7.0)
if abs(x - expected_x) <= 2:
pixels[idx] = 255; pixels[idx+1] = 255; pixels[idx+2] = 255; pixels[idx+3] = 255
ico = bytearray()
ico += struct.pack('<HHH', 0, 1, 1)
ico += struct.pack('<BBBBHHII', size, size, 0, 0, 1, 32, len(pixels) + 40, 22)
ico += struct.pack('<IiiHHIIiiII', 40, size, size*2, 1, 32, 0, len(pixels), 0, 0, 0, 0)
for y in range(size-1, -1, -1):
ico += pixels[y*size*4:(y+1)*size*4]
with open('$STATIC_DIR/assets/favicon.ico', 'wb') as f:
f.write(ico)
print(' Generated favicon.ico')
"
# --- 2. Install favicon (Hanzo mark) ---
# The frontend sets the runtime favicon to /assets/favicon.ico via useFavicon();
# the web root copies cover the pre-boot page load and the default /favicon.ico
# request. favicon.ico is a committed artifact rendered from favicon.svg by
# make_favicon.py (the Docker image has no rasterizer).
echo "[2/5] Installing favicon..."
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/assets/favicon.ico"
cp "$BRANDING_DIR/favicon.ico" "$STATIC_DIR/favicon.ico"
cp "$BRANDING_DIR/favicon.svg" "$STATIC_DIR/favicon.svg"
echo " Installed favicon.ico + favicon.svg"
# --- 3. Patch index.html ---
echo "[3/6] Patching index.html..."
# --- 3. Patch index.html (title + icon link + loading text) ---
echo "[3/5] Patching index.html..."
if [ -f "$STATIC_DIR/index.html" ]; then
sed -i 's/<title>ComfyUI<\/title>/<title>Hanzo Studio<\/title>/g' "$STATIC_DIR/index.html"
# Rename the title and inject icon links (the upstream head has none).
sed -i 's#<title>ComfyUI</title>#<title>Hanzo Studio</title><link rel="icon" type="image/svg+xml" href="favicon.svg"/><link rel="alternate icon" href="favicon.ico"/>#g' "$STATIC_DIR/index.html"
sed -i 's/Loading ComfyUI/Loading Hanzo Studio/g' "$STATIC_DIR/index.html"
sed -i 's/content="ComfyUI/content="Hanzo Studio/g' "$STATIC_DIR/index.html"
echo " Patched index.html"
fi
# --- 4. Patch manifest JSON files ---
echo "[4/6] Patching manifests..."
echo "[4/5] Patching manifests..."
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/"ComfyUI"/"Hanzo Studio"/g' {} \;
find "$STATIC_DIR" -name "manifest*.json" -exec sed -i 's/studio\.org/hanzo.ai/g' {} \;
echo " Done"
# --- 5. Smart JS/CSS patching via Python ---
echo "[5/6] Smart JS patching..."
# --- 5. Smart JS/CSS/JSON display-string patching ---
echo "[5/5] Smart JS patching..."
python3 "$BRANDING_DIR/patch_frontend.py" "$STATIC_DIR"
# --- 6. Final verification ---
echo "[6/6] Verification..."
echo "=== Hanzo Studio branding complete ==="
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Render branding/favicon.svg to a multi-resolution favicon.ico.
Source of truth is favicon.svg (the Hanzo mark). The .ico is a committed build
artifact so that apply-branding.sh can install it without needing a rasterizer
in the Docker image. Re-run this whenever favicon.svg changes:
rsvg-convert must be on PATH (librsvg2-bin); Pillow provides ICO packing.
python3 branding/make_favicon.py
"""
import os
import subprocess
import tempfile
from PIL import Image
HERE = os.path.dirname(os.path.abspath(__file__))
SVG = os.path.join(HERE, "favicon.svg")
ICO = os.path.join(HERE, "favicon.ico")
SIZES = [16, 32, 48, 64, 128, 256]
def render_png(size: int, path: str) -> None:
subprocess.run(
["rsvg-convert", "-w", str(size), "-h", str(size), SVG, "-o", path],
check=True,
)
def main() -> None:
with tempfile.TemporaryDirectory() as tmp:
frames = []
for size in SIZES:
png = os.path.join(tmp, f"favicon-{size}.png")
render_png(size, png)
frames.append(Image.open(png).convert("RGBA"))
base = frames[-1]
base.save(ICO, format="ICO", sizes=[(s, s) for s in SIZES])
print(f"Wrote {ICO} ({os.path.getsize(ICO)} bytes) sizes={SIZES}")
if __name__ == "__main__":
main()
+40
View File
@@ -0,0 +1,40 @@
# Hanzo Engine nodes
Custom nodes that call a local **Hanzo Engine** from a Hanzo Studio graph.
Hanzo Engine is the native-Rust, all-modality inference and training backend
(the training loop lives engine-side); these nodes talk to its
OpenAI-compatible HTTP API.
## Configuration
Set the engine base URL via environment variable (default shown):
```bash
export HANZO_ENGINE_URL=http://127.0.0.1:1234/v1
```
Connection or protocol failures surface as a clear error on the failing node —
the server does not crash.
## Nodes (category: `Hanzo/Engine`)
| Node | Route | In → Out |
|------|-------|----------|
| **Hanzo Chat (Engine)** | `POST /v1/chat/completions` | model + prompt + system → `text` (STRING) |
| **Hanzo Image Gen (Engine)** | `POST /v1/images/generations` | prompt + size + steps → `IMAGE` |
| **Hanzo Vision Caption (Engine)** | `POST /v1/chat/completions` (vision) | `IMAGE` + prompt → `caption` (STRING) |
| **Hanzo Save Text** | — | `text` (STRING) → writes `.txt` to `output/` |
- **Hanzo Chat** — prompt engineering and caption/text chains inside a graph.
- **Hanzo Image Gen** — decodes the engine's `b64_json` (or `url`) response into a
Hanzo Studio `IMAGE` tensor. Requests `n` images at the chosen `size`.
- **Hanzo Vision Caption** — encodes the input image as an OpenAI `image_url`
data URL and asks a vision model (e.g. Qwen3-VL) to caption it. Use for
LoRA/dataset prep.
- **Hanzo Save Text** — writes captions to `output/` as `.txt` (image/caption
pairs) and previews them in the node. Completes the caption-dataset loop.
## Requirements
Uses only libraries already present in Hanzo Studio: `requests`, `Pillow`,
`numpy`, `torch`. No extra install.
+4
View File
@@ -0,0 +1,4 @@
"""Hanzo Engine node pack for Hanzo Studio."""
from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+264
View File
@@ -0,0 +1,264 @@
"""Hanzo Engine nodes for Hanzo Studio.
These nodes call a local Hanzo Engine — the native-Rust, all-modality inference
and training backend — over its OpenAI-compatible HTTP API. The base URL comes
from the HANZO_ENGINE_URL environment variable (default http://127.0.0.1:1234/v1).
Connection and protocol errors surface as clear node errors (raised
RuntimeError), which Hanzo Studio reports on the failing node rather than
crashing the server.
"""
import base64
import io
import json
import os
import numpy as np
import requests
import torch
from PIL import Image
import folder_paths
DEFAULT_BASE_URL = "http://127.0.0.1:1234/v1"
def _base_url() -> str:
return os.environ.get("HANZO_ENGINE_URL", DEFAULT_BASE_URL).rstrip("/")
def _post(path: str, payload: dict, timeout: int = 120) -> dict:
url = f"{_base_url()}{path}"
try:
resp = requests.post(url, json=payload, timeout=timeout)
except requests.exceptions.RequestException as exc:
raise RuntimeError(
f"Hanzo Engine request to {url} failed: {exc}. Is the engine running? "
f"Set HANZO_ENGINE_URL (default {DEFAULT_BASE_URL})."
) from exc
if resp.status_code != 200:
raise RuntimeError(
f"Hanzo Engine {url} returned HTTP {resp.status_code}: {resp.text[:500]}"
)
try:
return resp.json()
except ValueError as exc:
raise RuntimeError(
f"Hanzo Engine {url} returned a non-JSON response: {resp.text[:200]}"
) from exc
def _get_bytes(url: str, timeout: int = 120) -> bytes:
try:
resp = requests.get(url, timeout=timeout)
except requests.exceptions.RequestException as exc:
raise RuntimeError(f"Hanzo Engine image fetch from {url} failed: {exc}.") from exc
if resp.status_code != 200:
raise RuntimeError(f"Hanzo Engine image fetch {url} returned HTTP {resp.status_code}.")
return resp.content
def _extract_message_text(data: dict) -> str:
try:
content = (data["choices"][0].get("message") or {}).get("content")
except (KeyError, IndexError, TypeError) as exc:
raise RuntimeError(
f"Unexpected Hanzo Engine response shape: {json.dumps(data)[:300]}"
) from exc
if isinstance(content, list): # OpenAI structured content parts
parts = [p.get("text", "") for p in content if isinstance(p, dict) and p.get("type") == "text"]
return "".join(parts).strip()
return (content or "").strip()
def _tensor_to_pil(image: torch.Tensor) -> Image.Image:
if image.dim() == 4: # [B,H,W,C] -> first frame
image = image[0]
arr = (image.cpu().numpy() * 255.0).clip(0, 255).astype(np.uint8)
return Image.fromarray(arr)
def _pil_to_tensor(img: Image.Image) -> torch.Tensor:
arr = np.array(img.convert("RGB")).astype(np.float32) / 255.0
return torch.from_numpy(arr)[None,] # [1,H,W,C]
def _pil_to_data_url(img: Image.Image) -> str:
buf = io.BytesIO()
img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode("ascii")
return f"data:image/png;base64,{b64}"
def _decode_image_item(item: dict) -> Image.Image:
b64 = item.get("b64_json")
if b64:
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")
url = item.get("url")
if url:
if url.startswith("data:"):
raw = base64.b64decode(url.split(",", 1)[1])
else:
raw = _get_bytes(url)
return Image.open(io.BytesIO(raw)).convert("RGB")
raise RuntimeError("Hanzo Engine image item has neither 'b64_json' nor 'url'.")
class HanzoChat:
"""Text chat via Hanzo Engine /v1/chat/completions. Use for prompt
engineering, caption cleanup, and text chains inside a graph."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("STRING", {"default": "default"}),
"prompt": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"system": ("STRING", {"multiline": True, "default": ""}),
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.05}),
"max_tokens": ("INT", {"default": 512, "min": 1, "max": 32768}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate"
CATEGORY = "Hanzo/Engine"
def generate(self, model, prompt, system="", temperature=0.7, max_tokens=512):
messages = []
if system.strip():
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
data = _post(
"/chat/completions",
{"model": model, "messages": messages, "temperature": temperature, "max_tokens": max_tokens},
)
return (_extract_message_text(data),)
class HanzoImageGen:
"""Text-to-image via Hanzo Engine /v1/images/generations. Returns a
Hanzo Studio IMAGE tensor."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": ""}),
"size": (["1024x1024", "1024x1536", "1536x1024", "768x768", "512x512"], {"default": "1024x1024"}),
},
"optional": {
"model": ("STRING", {"default": "default"}),
"steps": ("INT", {"default": 20, "min": 1, "max": 150}),
"n": ("INT", {"default": 1, "min": 1, "max": 8}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate"
CATEGORY = "Hanzo/Engine"
def generate(self, prompt, size, model="default", steps=20, n=1):
data = _post(
"/images/generations",
{"model": model, "prompt": prompt, "size": size, "n": n, "steps": steps, "response_format": "b64_json"},
timeout=600,
)
items = data.get("data") or []
if not items:
raise RuntimeError("Hanzo Engine /images/generations returned no image data.")
tensors = [_pil_to_tensor(_decode_image_item(it)) for it in items]
if len(tensors) == 1:
batch = tensors[0]
else:
same = len({t.shape for t in tensors}) == 1
batch = torch.cat(tensors, dim=0) if same else tensors[0]
return (batch,)
class HanzoVisionCaption:
"""Caption an IMAGE via Hanzo Engine vision chat (OpenAI image_url format).
Feeds LoRA/dataset prep loops; pair with a vision model such as Qwen3-VL."""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"prompt": ("STRING", {"multiline": True, "default": "Describe this image in detail for a training caption."}),
},
"optional": {
"model": ("STRING", {"default": "default"}),
"system": ("STRING", {"multiline": True, "default": ""}),
"max_tokens": ("INT", {"default": 512, "min": 1, "max": 32768}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("caption",)
FUNCTION = "caption"
CATEGORY = "Hanzo/Engine"
def caption(self, image, prompt, model="default", system="", max_tokens=512):
data_url = _pil_to_data_url(_tensor_to_pil(image))
messages = []
if system.strip():
messages.append({"role": "system", "content": system})
messages.append({
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": data_url}},
],
})
data = _post("/chat/completions", {"model": model, "messages": messages, "max_tokens": max_tokens}, timeout=300)
return (_extract_message_text(data),)
class HanzoSaveText:
"""Write a STRING to a .txt file in the output directory (image/caption
pairs for dataset prep) and preview it in the node."""
def __init__(self):
self.output_dir = folder_paths.get_output_directory()
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"text": ("STRING", {"forceInput": True}),
"filename_prefix": ("STRING", {"default": "caption"}),
},
}
RETURN_TYPES = ()
FUNCTION = "save"
OUTPUT_NODE = True
CATEGORY = "Hanzo/Engine"
def save(self, text, filename_prefix="caption"):
full_output_folder, filename, counter, _subfolder, _prefix = folder_paths.get_save_image_path(
filename_prefix, self.output_dir
)
path = os.path.join(full_output_folder, f"{filename}_{counter:05}_.txt")
with open(path, "w", encoding="utf-8") as fh:
fh.write(text)
return {"ui": {"text": [text]}}
NODE_CLASS_MAPPINGS = {
"HanzoChat": HanzoChat,
"HanzoImageGen": HanzoImageGen,
"HanzoVisionCaption": HanzoVisionCaption,
"HanzoSaveText": HanzoSaveText,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"HanzoChat": "Hanzo Chat (Engine)",
"HanzoImageGen": "Hanzo Image Gen (Engine)",
"HanzoVisionCaption": "Hanzo Vision Caption (Engine)",
"HanzoSaveText": "Hanzo Save Text",
}
+27
View File
@@ -0,0 +1,27 @@
# Fashion / swimwear starter workflows
Three starter templates for a designer's swimwear workflow. They live in the
Hanzo Studio workflow directory, so they appear in the **Workflows** sidebar
(Workflows → Browse). You can also **drag any `.json` here onto the canvas** to
load it.
Each workflow contains a **Note** node describing usage and the model files it
needs. Some required files (Flux.2 text encoder / VAE, Qwen-Image-Edit) may not
be downloaded yet — the graph is prewired so you only fill in filenames.
| File | Pipeline | Purpose |
|------|----------|---------|
| `fashion_product_shot.json` | FLUX.2 klein, text-to-image | Editorial swimwear product photography from a prompt |
| `fashion_edit_garment.json` | Qwen-Image-Edit, image-to-image | Re-scene / edit a garment shot while keeping the garment identical |
| `fashion_caption_dataset.json` | Hanzo Vision Caption → Save Text | Caption images into `.txt` pairs for LoRA dataset prep |
## Notes
- `fashion_product_shot.json``flux-2-klein-4b.safetensors` is already in
`models/diffusion_models/`. Add a Flux.2 text encoder (`type=flux2` on the
CLIPLoader) and a Flux.2 VAE, then queue. Output lands in `output/swimwear/`.
- `fashion_edit_garment.json` — uses the native `TextEncodeQwenImageEdit` path.
Load a product shot, describe the edit, keep the garment unchanged.
- `fashion_caption_dataset.json` — uses the Hanzo Engine nodes
(`custom_nodes/hanzo_engine/`). Set `HANZO_ENGINE_URL` and run a vision model
such as Qwen3-VL. Image/caption pairs land in `output/swimwear/`.
@@ -0,0 +1,169 @@
{
"id": "f6bb36b5-86c3-459e-bc6d-1a4bed73d8e3",
"revision": 0,
"last_node_id": 4,
"last_link_id": 2,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [
40,
40
],
"size": [
300,
320
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
1
],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png"
]
},
{
"id": 2,
"type": "HanzoVisionCaption",
"pos": [
400,
40
],
"size": [
400,
220
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "caption",
"type": "STRING",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "HanzoVisionCaption"
},
"widgets_values": [
"Write a concise training caption for this swimwear fashion photo: garment type, cut, color, pattern, fabric, pose, setting and lighting.",
"default",
"",
384
]
},
{
"id": 3,
"type": "HanzoSaveText",
"pos": [
840,
40
],
"size": [
320,
200
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"link": 2
}
],
"outputs": [],
"properties": {
"Node name for S&R": "HanzoSaveText"
},
"widgets_values": [
"swimwear/caption"
]
},
{
"id": 4,
"type": "Note",
"pos": [
40,
400
],
"size": [
420,
240
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"FASHION CAPTION DATASET \u2014 LoRA dataset prep\n\nLoadImage -> Hanzo Vision Caption -> Hanzo Save Text.\n\nThe Hanzo Vision Caption node calls the local Hanzo Engine\n(HANZO_ENGINE_URL, default http://127.0.0.1:1234/v1) with a vision\nmodel such as Qwen3-VL. Point LoadImage at each garment photo, run,\nand image/caption .txt pairs land in output/swimwear/. Batch by\nswapping LoadImage for a folder loader when preparing a full set."
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
1,
1,
0,
2,
0,
"IMAGE"
],
[
2,
2,
0,
3,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8,
"offset": [
0,
0
]
}
},
"version": 0.4
}
@@ -0,0 +1,558 @@
{
"id": "28aa3f84-589e-45af-963c-f1ee6de49359",
"revision": 0,
"last_node_id": 11,
"last_link_id": 13,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [
40,
40
],
"size": [
300,
320
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4,
5
],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": [],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"example.png"
]
},
{
"id": 2,
"type": "CLIPLoader",
"pos": [
40,
400
],
"size": [
270,
120
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CLIP",
"type": "CLIP",
"links": [
1,
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPLoader"
},
"widgets_values": [
"qwen-image-text-encoder.safetensors",
"qwen_image",
"default"
]
},
{
"id": 3,
"type": "UNETLoader",
"pos": [
40,
560
],
"size": [
270,
120
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "UNETLoader"
},
"widgets_values": [
"qwen-image-edit.safetensors",
"default"
]
},
{
"id": 4,
"type": "VAELoader",
"pos": [
40,
720
],
"size": [
270,
120
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
3,
6,
12
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"qwen-image-vae.safetensors"
]
},
{
"id": 5,
"type": "TextEncodeQwenImageEdit",
"pos": [
400,
40
],
"size": [
400,
200
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 1
},
{
"name": "vae",
"type": "VAE",
"link": 3
},
{
"name": "image",
"type": "IMAGE",
"link": 4
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
8
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "TextEncodeQwenImageEdit"
},
"widgets_values": [
"change the background to a sunlit tropical beach at golden hour; keep the swimwear garment, its color, pattern and fit exactly the same; editorial fashion lighting"
],
"title": "Edit (positive)"
},
{
"id": 6,
"type": "TextEncodeQwenImageEdit",
"pos": [
400,
280
],
"size": [
400,
120
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 2
},
{
"name": "vae",
"type": "VAE",
"link": null
},
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "TextEncodeQwenImageEdit"
},
"widgets_values": [
""
],
"title": "Negative"
},
{
"id": 7,
"type": "VAEEncode",
"pos": [
400,
440
],
"size": [
270,
120
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "pixels",
"type": "IMAGE",
"link": 5
},
{
"name": "vae",
"type": "VAE",
"link": 6
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
10
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEEncode"
},
"widgets_values": []
},
{
"id": 8,
"type": "KSampler",
"pos": [
860,
40
],
"size": [
270,
120
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 7
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 8
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 9
},
{
"name": "latent_image",
"type": "LATENT",
"link": 10
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
11
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
20,
2.5,
"euler",
"simple",
1.0
]
},
{
"id": 9,
"type": "VAEDecode",
"pos": [
1220,
40
],
"size": [
270,
120
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 11
},
{
"name": "vae",
"type": "VAE",
"link": 12
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
13
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 10,
"type": "SaveImage",
"pos": [
1420,
40
],
"size": [
380,
380
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"swimwear/edit"
]
},
{
"id": 11,
"type": "Note",
"pos": [
40,
900
],
"size": [
420,
280
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"FASHION GARMENT / SCENE EDIT \u2014 Qwen-Image-Edit (image-to-image)\n\nRequired model files (place under models/):\n * diffusion_models/<qwen-image-edit unet>.safetensors\n * text_encoders/<qwen_image text encoder>.safetensors\n * vae/<qwen_image vae>.safetensors\n\nThis wires the native Qwen-Image-Edit path: TextEncodeQwenImageEdit\ncarries the reference image + edit instruction; the source image is\nVAE-encoded to seed the sampler. Load a product shot in LoadImage,\ndescribe the edit in the positive prompt, keep the garment identical."
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
1,
2,
0,
5,
0,
"CLIP"
],
[
2,
2,
0,
6,
0,
"CLIP"
],
[
3,
4,
0,
5,
1,
"VAE"
],
[
4,
1,
0,
5,
2,
"IMAGE"
],
[
5,
1,
0,
7,
0,
"IMAGE"
],
[
6,
4,
0,
7,
1,
"VAE"
],
[
7,
3,
0,
8,
0,
"MODEL"
],
[
8,
5,
0,
8,
1,
"CONDITIONING"
],
[
9,
6,
0,
8,
2,
"CONDITIONING"
],
[
10,
7,
0,
8,
3,
"LATENT"
],
[
11,
8,
0,
9,
0,
"LATENT"
],
[
12,
4,
0,
9,
1,
"VAE"
],
[
13,
9,
0,
10,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8,
"offset": [
0,
0
]
}
},
"version": 0.4
}
@@ -0,0 +1,504 @@
{
"id": "ade0e429-67bf-4e6d-adf4-88747094a690",
"revision": 0,
"last_node_id": 11,
"last_link_id": 10,
"nodes": [
{
"id": 1,
"type": "UNETLoader",
"pos": [
40,
40
],
"size": [
270,
120
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "UNETLoader"
},
"widgets_values": [
"flux-2-klein-4b.safetensors",
"default"
]
},
{
"id": 2,
"type": "CLIPLoader",
"pos": [
40,
200
],
"size": [
270,
120
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CLIP",
"type": "CLIP",
"links": [
1,
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPLoader"
},
"widgets_values": [
"flux2-text-encoder.safetensors",
"flux2",
"default"
]
},
{
"id": 3,
"type": "VAELoader",
"pos": [
40,
360
],
"size": [
270,
120
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "VAE",
"type": "VAE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAELoader"
},
"widgets_values": [
"flux2-vae.safetensors"
]
},
{
"id": 4,
"type": "CLIPTextEncode",
"pos": [
360,
40
],
"size": [
400,
200
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 1
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
3
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"professional swimwear product photography, single-piece designer bikini on a model, beach at golden hour, editorial fashion lighting, crisp detail, high-end catalog, 85mm, shallow depth of field"
],
"title": "Positive"
},
{
"id": 5,
"type": "FluxGuidance",
"pos": [
360,
280
],
"size": [
270,
120
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
5
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "FluxGuidance"
},
"widgets_values": [
3.5
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
360,
400
],
"size": [
400,
120
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 2
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
""
],
"title": "Negative"
},
{
"id": 7,
"type": "EmptyFlux2LatentImage",
"pos": [
360,
560
],
"size": [
270,
120
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyFlux2LatentImage"
},
"widgets_values": [
1024,
1536,
1
]
},
{
"id": 8,
"type": "KSampler",
"pos": [
800,
40
],
"size": [
270,
120
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 4
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 5
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 7
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
8
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"fixed",
24,
1.0,
"euler",
"simple",
1.0
]
},
{
"id": 9,
"type": "VAEDecode",
"pos": [
1160,
40
],
"size": [
270,
120
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 8
},
{
"name": "vae",
"type": "VAE",
"link": 9
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
10
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 10,
"type": "SaveImage",
"pos": [
1360,
40
],
"size": [
380,
380
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 10
}
],
"outputs": [],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": [
"swimwear/product"
]
},
{
"id": 11,
"type": "Note",
"pos": [
40,
520
],
"size": [
400,
300
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"FASHION PRODUCT SHOT \u2014 FLUX.2 klein (text-to-image)\n\nRequired model files (place under models/):\n * diffusion_models/flux-2-klein-4b.safetensors (present)\n * text_encoders/<flux2 text encoder>.safetensors (NOT downloaded)\n * vae/<flux2 vae>.safetensors (NOT downloaded)\n\nSet the CLIPLoader (type=flux2) and VAELoader filenames once the\nencoder + VAE are downloaded, then queue. Preset prompt is tuned for\neditorial swimwear product photography."
],
"color": "#432",
"bgcolor": "#653"
}
],
"links": [
[
1,
2,
0,
4,
0,
"CLIP"
],
[
2,
2,
0,
6,
0,
"CLIP"
],
[
3,
4,
0,
5,
0,
"CONDITIONING"
],
[
4,
1,
0,
8,
0,
"MODEL"
],
[
5,
5,
0,
8,
1,
"CONDITIONING"
],
[
6,
6,
0,
8,
2,
"CONDITIONING"
],
[
7,
7,
0,
8,
3,
"LATENT"
],
[
8,
8,
0,
9,
0,
"LATENT"
],
[
9,
3,
0,
9,
1,
"VAE"
],
[
10,
9,
0,
10,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.8,
"offset": [
0,
0
]
}
},
"version": 0.4
}