Replace comfyui_projects repo with current local version
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.venv*/
|
||||
.DS_Store
|
||||
*.log
|
||||
ComfyUI/models/
|
||||
ComfyUI/input/
|
||||
ComfyUI/output/
|
||||
@@ -7,20 +7,29 @@ Clone this repo into your comfyui folder.
|
||||
## Projects Included
|
||||
|
||||
### 🧩 Custom Nodes: Comfyui_FSL_Nodes
|
||||
- **sampler_config_hub.py**
|
||||
One of two custom nodes to configure sampler and scheduler settings for workflows. Used to feed data to the meta node
|
||||
- **sampler_scheduler_config.py**
|
||||
Two of two custom nodes to configure sampler and scheduler settings for workflows. Used to feed data to the meta node
|
||||
- **manual_alpha_mask_painter.py**
|
||||
Custom node to manually convert black mask areas into alpha transparency. Useful with GRIPTAPE when inpainting and creating the mask by right clicking on the loaded image and select load mask editor. Griptape requires an alpha channel mask.
|
||||
- **8WayImgSwitch**
|
||||
Image switch with eight inputs
|
||||
- **fsl_composite_with_mask_cropped**
|
||||
- **fsl_composite_with_mask_cropped.py**
|
||||
Designed to remove the Alpha Channel But Keep Transparent Parts as Background Color (not currently used)
|
||||
- **fsl_save_and_strip_alpha**
|
||||
Strips alpha channel from RGBA image. (not currently used)
|
||||
- **How to use**
|
||||
place this node folder into the custom_nodes folder - no additional action is necessary. Nodes can be found in the comfyui onscreen nodes folder by searching for FSL
|
||||
- **FSLGeminiGenerateImageV6.py**
|
||||
Allows for the use of Gemini API's to create image. This node also provide for whether or not the preceding image is used for iterative generation.
|
||||
- **fsl_image_memory.py**
|
||||
Provides 4 nodes. Image Memory Store and Image Memory Recall stores an image to the key entered in the 'key' field. Image memory recall recalls the image for subsequent use. Image Memory Clear is used to delete the stored image based on the entered key. Image Memory Clear All clears all keys that have been entered.
|
||||
- **fsl_prompt_compose.py**
|
||||
Node for Positive and Negative prompt as well as 'scene-lock'. When scene-lock is true -
|
||||
When lock_scene: True, the node prepends a short instruction block before your positive text that tells the model to treat the provided image as fixed “scene layout” and only change what you explicitly ask. The included language (summarized) is:
|
||||
- use the provided image as base;
|
||||
- keep composition, subjects, positions, background, lighting, camera angle, clothing, colors unchanged;
|
||||
- change only what you specify;
|
||||
- do not crop, do not isolate a single subject, do not change the background.
|
||||
- **fsl_ensure_nhwc_batch.py**
|
||||
Node guarantees that any incoming image tensor is converted to the [N, H, W, C] (batch–height–width–channels) layout expected by downstream nodes, automatically re-ordering dimensions and normalizing types so all image data conform to a consistent format for safe processing.
|
||||
- **FSLImageSaverWithMetadataV5.py**
|
||||
Node stores metadata with image. Data can be easily read using the LoadImage-w-Metadata.json workflow
|
||||
|
||||
Located in the [`Comfyui_FSL_Nodes/`](https://github.com/fredlef/comfyui_projects/tree/main/custom_nodes/Comfyui_FSL_Nodes) directory.
|
||||
|
||||
@@ -28,13 +37,28 @@ Located in the [`Comfyui_FSL_Nodes/`](https://github.com/fredlef/comfyui_project
|
||||
|
||||
### 🎨 Workflow: T-shirt Designer
|
||||
- **Tshirt-Designer-ver_2.0.json**
|
||||
It is recommended not to use this workflow. A replacement workflow that is less complex will be uploaded as 3.0
|
||||
Workflow designed for generating masked transparent images for T-shirt and object printing.
|
||||
- Many corrections and updates
|
||||
- Full workflow details and notes are available in [`tshirt_designer/`](https://github.com/fredlef/comfyui_projects/tree/main/workflows/tshirt_designer).
|
||||
|
||||
---
|
||||
|
||||
### 🎨 Workflow: Nano Banana
|
||||
- **Nano Banana Iterative-Base.json**
|
||||
Allows the user to create an image using Gemini-2.5 Flash-image-preview or Gemini-2.5 Flash. Once the initial image is created additional iterations of each succeeding image can be created just be entering a new prompt. Setup is included in a Note node in the workflow.
|
||||
- **Nano Banana Iterative-Load 5 Images.json**
|
||||
Provides for using up to 5 nodes to create an image with Nano Banana. The images can they be modified as in the Iterative-Base workflow. Basic instructions are provided in Notes nodes in the workflow.
|
||||
- **Nano Banana w_Griptape w_Upscaler.json**
|
||||
Uses Griptape Nodes as a front end prompt loader to Nano Banana. An excellent upscaler by Alex at ComfyUiStudio is included. An OpenAPI Key is required.
|
||||
|
||||
### Miscellaneous Workflows
|
||||
- **LoadImage-w-Metadata.json**
|
||||
Simple workflow to read the metadata created by the FSLImageSaverWithMetadataV5 node
|
||||
|
||||
## Acknowledgements
|
||||
- **Thanks to Alex and ComfyuiStudio for allowing me to use his excellent upscaler**
|
||||
- **Mycroft Holmes - AKA ChatGPT for excellent assistance and guidance in the creation of Custom Nodes**
|
||||
|
||||
## Author
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
|
||||
import os
|
||||
import time
|
||||
import base64
|
||||
import random
|
||||
from io import BytesIO
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
try:
|
||||
import torch
|
||||
except Exception:
|
||||
torch = None
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
except Exception:
|
||||
Image = None
|
||||
|
||||
|
||||
# ---------------- Helpers ----------------
|
||||
|
||||
def _tensor_to_png_b64(t) -> str:
|
||||
"""Convert torch.Tensor or np.ndarray image to base64 PNG."""
|
||||
if torch is not None and isinstance(t, torch.Tensor):
|
||||
x = t.detach().cpu().float()
|
||||
if x.dim() == 4 and x.shape[0] > 0:
|
||||
x = x[0]
|
||||
if x.dim() == 3 and x.shape[0] in (1, 3, 4) and x.shape[-1] not in (3, 4):
|
||||
x = x.permute(1, 2, 0)
|
||||
arr = x.numpy()
|
||||
elif isinstance(t, np.ndarray):
|
||||
arr = t
|
||||
else:
|
||||
raise TypeError(f"Expected image as torch.Tensor or np.ndarray, got {type(t)}")
|
||||
|
||||
if arr.max() <= 1.5:
|
||||
arr = arr * 255.0
|
||||
arr = np.clip(arr, 0, 255).astype(np.uint8)
|
||||
|
||||
if Image is None:
|
||||
raise RuntimeError("Pillow is required to encode PNG images.")
|
||||
im = Image.fromarray(arr)
|
||||
buf = BytesIO()
|
||||
im.save(buf, format="PNG")
|
||||
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||
|
||||
|
||||
def _extract_base64_image(resp_json: Dict[str, Any]) -> Optional[str]:
|
||||
"""Return first inline image payload from Gemini JSON response."""
|
||||
def _walk(node):
|
||||
if isinstance(node, dict):
|
||||
for k, v in node.items():
|
||||
yield k, v
|
||||
yield from _walk(v)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
yield from _walk(item)
|
||||
|
||||
for k, v in _walk(resp_json):
|
||||
if k in ("inline_data", "inlineData") and isinstance(v, dict):
|
||||
data = v.get("data")
|
||||
mime = v.get("mime_type") or v.get("mimeType") or ""
|
||||
if data and ("image/" in mime or mime == "" or mime is None):
|
||||
return data
|
||||
return None
|
||||
|
||||
|
||||
def _post_with_backoff(url: str, headers: Dict[str, str], params: Dict[str, Any], payload: Dict[str, Any]):
|
||||
delay = 1.0
|
||||
last = None
|
||||
for _ in range(4):
|
||||
r = requests.post(url, headers=headers, params=params, json=payload, timeout=60)
|
||||
last = r
|
||||
if r.status_code != 429:
|
||||
return r
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
return last
|
||||
|
||||
|
||||
MODEL_CHOICES = (
|
||||
"gemini-2.5-flash-image",
|
||||
"gemini-2.5-flash-image-preview",
|
||||
"gemini-2.5-pro",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-pro-exp",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.0-pro-vision",
|
||||
"gemini-2.5-flash-audio-preview",
|
||||
"gemini-2.5-vision-preview",
|
||||
)
|
||||
|
||||
|
||||
class FSLGeminiGenerateImageV6:
|
||||
"""Gemini image generator with iterative + external image toggles."""
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"multiline": True, "default": ""}),
|
||||
"model": (MODEL_CHOICES, {"default": "gemini-2.5-flash-image-preview"}),
|
||||
"width": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
|
||||
"height": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 64}),
|
||||
"temperature": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0x7FFFFFFF}),
|
||||
"use_init_image": ("BOOLEAN", {"default": True}),
|
||||
"use_images": ("BOOLEAN", {"default": False}),
|
||||
"refresh_models": ("BOOLEAN", {"default": False}),
|
||||
"output_prompt": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"init_image": ("IMAGE",),
|
||||
"images": ("IMAGE",),
|
||||
"api_key": ("STRING", {"default": "", "multiline": False, "placeholder": "Leave blank to use GEMINI_API_KEY env var"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT", "INT", "FLOAT", "INT", "STRING", "DICT")
|
||||
RETURN_NAMES = (
|
||||
"image",
|
||||
"prompt",
|
||||
"model",
|
||||
"width",
|
||||
"height",
|
||||
"temperature",
|
||||
"seed",
|
||||
"seed_mode",
|
||||
"metadata",
|
||||
)
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "FSL"
|
||||
|
||||
# ---------- internals ----------
|
||||
|
||||
def _resolve_api_key(self, key_in: str) -> str:
|
||||
k = (key_in or "").strip()
|
||||
if not k:
|
||||
k = os.environ.get("GEMINI_API_KEY", "").strip()
|
||||
if not k:
|
||||
raise RuntimeError("Gemini API key missing. Provide it in the node or set GEMINI_API_KEY env var.")
|
||||
return k
|
||||
|
||||
def _append_tensor_as_part(self, img_like, out: List[Dict[str, Any]]):
|
||||
b64 = _tensor_to_png_b64(img_like)
|
||||
out.append({"inline_data": {"mime_type": "image/png", "data": b64}})
|
||||
|
||||
def _collect_reference_parts(
|
||||
self,
|
||||
use_init_image: bool,
|
||||
init_image,
|
||||
use_images: bool,
|
||||
images,
|
||||
) -> List[Dict[str, Any]]:
|
||||
parts: List[Dict[str, Any]] = []
|
||||
|
||||
# init first (if enabled)
|
||||
if use_init_image and init_image is not None:
|
||||
if isinstance(init_image, (list, tuple)):
|
||||
if len(init_image) > 0:
|
||||
self._append_tensor_as_part(init_image[0], parts)
|
||||
elif torch is not None and isinstance(init_image, torch.Tensor):
|
||||
if init_image.dim() == 4 and init_image.shape[0] > 0:
|
||||
self._append_tensor_as_part(init_image[0], parts)
|
||||
else:
|
||||
self._append_tensor_as_part(init_image, parts)
|
||||
elif isinstance(init_image, np.ndarray):
|
||||
self._append_tensor_as_part(init_image, parts)
|
||||
|
||||
# then all external images (if enabled)
|
||||
if use_images and images is not None:
|
||||
if isinstance(images, (list, tuple)):
|
||||
for t in images:
|
||||
self._append_tensor_as_part(t, parts)
|
||||
elif torch is not None and isinstance(images, torch.Tensor):
|
||||
if images.dim() == 4:
|
||||
for i in range(images.shape[0]):
|
||||
self._append_tensor_as_part(images[i], parts)
|
||||
else:
|
||||
self._append_tensor_as_part(images, parts)
|
||||
elif isinstance(images, np.ndarray):
|
||||
self._append_tensor_as_part(images, parts)
|
||||
|
||||
return parts
|
||||
|
||||
# ---------- main ----------
|
||||
|
||||
def generate(
|
||||
self,
|
||||
prompt: str,
|
||||
model: str,
|
||||
width: int,
|
||||
height: int,
|
||||
temperature: float,
|
||||
seed: int,
|
||||
use_init_image: bool,
|
||||
refresh_models: bool,
|
||||
output_prompt: bool,
|
||||
use_images: bool = False,
|
||||
init_image: Optional["torch.Tensor"] = None,
|
||||
images: Optional["torch.Tensor"] = None,
|
||||
api_key: str = "",
|
||||
) -> Tuple["torch.Tensor", str, str, int, int, float, int, str, Dict[str, Any]]:
|
||||
|
||||
seed_mode = "fixed"
|
||||
if seed == -1:
|
||||
seed = random.randint(0, 0x7FFFFFFF)
|
||||
seed_mode = "randomize"
|
||||
|
||||
api_key = self._resolve_api_key(api_key)
|
||||
endpoint = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
||||
headers = {"Content-Type": "application/json"}
|
||||
|
||||
parts: List[Dict[str, Any]] = [{"text": prompt}]
|
||||
ref_parts = self._collect_reference_parts(use_init_image, init_image, use_images, images)
|
||||
parts.extend(ref_parts)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"contents": [{"role": "user", "parts": parts}],
|
||||
"generationConfig": {
|
||||
"temperature": float(temperature),
|
||||
"seed": int(seed),
|
||||
},
|
||||
}
|
||||
|
||||
_ = refresh_models # UI no-op
|
||||
|
||||
resp = _post_with_backoff(endpoint, headers=headers, params={"key": api_key}, payload=payload)
|
||||
|
||||
# Parse response
|
||||
try:
|
||||
resp_json = resp.json()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Gemini response was not JSON: {e}")
|
||||
|
||||
if isinstance(resp_json, dict) and "error" in resp_json:
|
||||
err = resp_json["error"]
|
||||
raise RuntimeError(f"Gemini error: {err.get('status')} - {err.get('message')}")
|
||||
|
||||
b64_img = _extract_base64_image(resp_json)
|
||||
if not b64_img:
|
||||
raise RuntimeError(f"Failed to find inline image in Gemini response. First 1k of JSON: {str(resp_json)[:1000]}")
|
||||
|
||||
try:
|
||||
img_bytes = base64.b64decode(b64_img)
|
||||
if Image is None:
|
||||
raise RuntimeError("Pillow is required to decode returned image.")
|
||||
pil = Image.open(BytesIO(img_bytes)).convert("RGBA")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to decode inline image: {e}")
|
||||
|
||||
np_img = np.array(pil).astype(np.float32) / 255.0
|
||||
if torch is None:
|
||||
raise RuntimeError("torch is required for IMAGE tensor output.")
|
||||
image_tensor = torch.from_numpy(np_img)[None, ...]
|
||||
|
||||
out_prompt = prompt if output_prompt else ""
|
||||
|
||||
metadata_output = {
|
||||
"prompt": prompt,
|
||||
"model": model,
|
||||
"width": width,
|
||||
"height": height,
|
||||
"temperature": temperature,
|
||||
"seed": seed,
|
||||
"seed_mode": seed_mode,
|
||||
"use_init_image": bool(use_init_image and init_image is not None),
|
||||
"use_images": bool(use_images and images is not None),
|
||||
"init_image_connected": bool(init_image is not None),
|
||||
"batch_images": (
|
||||
int(images.shape[0]) if (torch is not None and isinstance(images, torch.Tensor) and images.dim() == 4)
|
||||
else (len(images) if isinstance(images, (list, tuple)) else (1 if images is not None else 0))
|
||||
),
|
||||
}
|
||||
|
||||
return (
|
||||
image_tensor,
|
||||
out_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
temperature,
|
||||
seed,
|
||||
seed_mode,
|
||||
metadata_output,
|
||||
)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"FSLGeminiGenerateImageV6": FSLGeminiGenerateImageV6}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"FSLGeminiGenerateImageV6": "FSL Gemini Generate Image V6"}
|
||||
@@ -0,0 +1,189 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
from typing import Dict, Any, List, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image, PngImagePlugin
|
||||
import folder_paths
|
||||
from datetime import datetime
|
||||
|
||||
class FSLImageSaverWithMetadataV5:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls) -> Dict[str, Any]:
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filename": ("STRING", {"default": "gemini_out"}),
|
||||
"save_to_subfolder": ("STRING", {"default": ""}),
|
||||
},
|
||||
"optional": {
|
||||
"append_datetime": ("BOOLEAN", {"default": False}),
|
||||
"model_string": ("STRING", {"default": ""}),
|
||||
"modelname": ("STRING", {"default": ""}),
|
||||
"seed_string": ("STRING", {"default": ""}),
|
||||
"width_string": ("STRING", {"default": ""}),
|
||||
"height_string": ("STRING", {"default": ""}),
|
||||
"temperature_string": ("STRING", {"default": ""}),
|
||||
"positive_string": ("STRING", {"default": ""}),
|
||||
"negative_string": ("STRING", {"default": ""}),
|
||||
"cfg_string": ("STRING", {"default": ""}),
|
||||
"steps_string": ("STRING", {"default": ""}),
|
||||
"sampler_string": ("STRING", {"default": ""}),
|
||||
"scheduler_string": ("STRING", {"default": ""}),
|
||||
"guidance_scale_string": ("STRING", {"default": ""}),
|
||||
"watermark_string": ("STRING", {"default": ""}),
|
||||
"external_metadata": ("DICT", {"default": {}}),
|
||||
},
|
||||
}
|
||||
|
||||
# Keep METADATA_RAW as output #1 for compatibility.
|
||||
# Add IMAGE as output #2 so you can optionally preview.
|
||||
RETURN_TYPES = ("METADATA_RAW", "IMAGE",)
|
||||
RETURN_NAMES = ("metadata_raw", "image",)
|
||||
FUNCTION = "save_files"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "FSL/Save"
|
||||
|
||||
def _to_numpy_image(self, t: torch.Tensor) -> np.ndarray:
|
||||
"""Convert a tensor to HWC uint8, preserving alpha if present."""
|
||||
if t.device.type != "cpu":
|
||||
t = t.cpu()
|
||||
t = t.detach().float()
|
||||
# If CHW, convert to HWC
|
||||
if t.dim() == 3 and t.shape[0] in (1, 3, 4) and t.shape[-1] not in (3, 4):
|
||||
t = t.permute(1, 2, 0)
|
||||
arr = (t.numpy() * 255.0).clip(0, 255).astype(np.uint8)
|
||||
return arr
|
||||
|
||||
def save_files(
|
||||
self,
|
||||
images: torch.Tensor,
|
||||
filename: str,
|
||||
save_to_subfolder: str,
|
||||
append_datetime: bool = False,
|
||||
model_string: str = "",
|
||||
modelname: str = "",
|
||||
seed_string: str = "",
|
||||
width_string: str = "",
|
||||
height_string: str = "",
|
||||
temperature_string: str = "",
|
||||
positive_string: str = "",
|
||||
negative_string: str = "",
|
||||
cfg_string: str = "",
|
||||
steps_string: str = "",
|
||||
sampler_string: str = "",
|
||||
scheduler_string: str = "",
|
||||
guidance_scale_string: str = "",
|
||||
watermark_string: str = "",
|
||||
external_metadata: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[Dict[str, Any], torch.Tensor]:
|
||||
|
||||
base = folder_paths.get_output_directory()
|
||||
sub = (save_to_subfolder or "").strip()
|
||||
sub = os.path.normpath(sub).replace("\\", "/").lstrip("/")
|
||||
outdir = os.path.join(base, sub) if sub else base
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
# Normalize batch
|
||||
if images.dim() == 3:
|
||||
images = images.unsqueeze(0)
|
||||
|
||||
# One canonical, timezone-aware timestamp used everywhere
|
||||
now_local = datetime.now().astimezone()
|
||||
ts_file = now_local.strftime("%Y%m%d_%H%M%S") # e.g. 20251014_163733
|
||||
iso_human = now_local.isoformat(timespec="seconds") # e.g. 2025-10-14T16:37:33-04:00
|
||||
|
||||
if append_datetime:
|
||||
filename = f"{filename}_{ts_file}"
|
||||
|
||||
model_value = (model_string or modelname).strip()
|
||||
|
||||
lines: List[str] = []
|
||||
if model_value:
|
||||
lines.append("model: " + model_value)
|
||||
if seed_string:
|
||||
lines.append("seed: " + seed_string)
|
||||
if width_string or height_string:
|
||||
lines.append(f"size: {width_string}x{height_string}")
|
||||
if temperature_string:
|
||||
lines.append("temperature: " + temperature_string)
|
||||
if positive_string:
|
||||
lines.append("positive: " + positive_string)
|
||||
if negative_string:
|
||||
lines.append("negative: " + negative_string)
|
||||
parameters_text = "\n".join(lines)
|
||||
|
||||
pnginfo = PngImagePlugin.PngInfo()
|
||||
if parameters_text:
|
||||
pnginfo.add_text("parameters", parameters_text)
|
||||
if external_metadata:
|
||||
try:
|
||||
pnginfo.add_text("fsl_metadata", json.dumps(external_metadata, ensure_ascii=False))
|
||||
except Exception:
|
||||
pnginfo.add_text("fsl_metadata", str(external_metadata))
|
||||
|
||||
metadata_raw_out: Dict[str, Any] = {
|
||||
"fileinfo": {
|
||||
"filename": "",
|
||||
"resolution": "",
|
||||
"date": "",
|
||||
"size_bytes": 0,
|
||||
"size_human": "",
|
||||
"size": "",
|
||||
"format": "PNG",
|
||||
}
|
||||
}
|
||||
|
||||
preview_tensor: Optional[torch.Tensor] = None
|
||||
|
||||
for i in range(images.shape[0]):
|
||||
np_img = self._to_numpy_image(images[i])
|
||||
h, w = np_img.shape[0], np_img.shape[1]
|
||||
|
||||
# Explicit mode preserves alpha if present
|
||||
mode = "RGBA" if np_img.shape[-1] == 4 else ("RGB" if np_img.shape[-1] == 3 else None)
|
||||
pil = Image.fromarray(np_img, mode=mode) if mode else Image.fromarray(np_img)
|
||||
|
||||
ret = folder_paths.get_save_image_path(filename, outdir, i)
|
||||
# Known return: (full_output_folder, filename_prefix, counter, subfolder, filename_with_subfolder)
|
||||
if isinstance(ret, (list, tuple)) and len(ret) >= 3:
|
||||
full_output_folder, filename_prefix, counter = ret[0], ret[1], ret[2]
|
||||
path = os.path.join(full_output_folder, f"{filename_prefix}_{counter:05}.png")
|
||||
else:
|
||||
path = os.path.join(outdir, f"{filename}_{i:05}.png")
|
||||
|
||||
pil.save(path, format="PNG", pnginfo=pnginfo)
|
||||
|
||||
# Align filesystem mtime with our canonical timestamp so all tools agree
|
||||
epoch = now_local.timestamp()
|
||||
os.utime(path, (epoch, epoch))
|
||||
|
||||
filesize = os.path.getsize(path)
|
||||
size_human = f"{filesize / (1024*1024):.2f} MB"
|
||||
|
||||
metadata_raw_out["fileinfo"] = {
|
||||
"filename": path,
|
||||
"resolution": f"{w}x{h}",
|
||||
"date": iso_human, # unified local, tz-aware
|
||||
"size_bytes": filesize,
|
||||
"size_human": size_human,
|
||||
"size": size_human,
|
||||
"format": "PNG",
|
||||
}
|
||||
|
||||
# Keep first image as preview
|
||||
if preview_tensor is None:
|
||||
# Return normalized [1,H,W,C] float tensor
|
||||
preview_tensor = torch.from_numpy(np_img.astype(np.float32) / 255.0)[None, ...]
|
||||
|
||||
# Safety: if nothing was saved (empty batch), provide a blank preview
|
||||
if preview_tensor is None:
|
||||
preview_tensor = torch.zeros((1, 64, 64, 4), dtype=torch.float32)
|
||||
|
||||
# Return METADATA first (compat), IMAGE second (for optional preview wiring)
|
||||
return (metadata_raw_out, preview_tensor,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"FSLImageSaverWithMetadataV5": FSLImageSaverWithMetadataV5}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"FSLImageSaverWithMetadataV5": "FSL Image Saver w/ Metadata V5"}
|
||||
@@ -1,10 +0,0 @@
|
||||
# Comfyui_FSL_Nodes
|
||||
|
||||
This folder contains custom nodes designed by Fred LeFevre for use with ComfyUI workflows. They are required for the tshirt_designer workflow.
|
||||
|
||||
Included nodes:
|
||||
- `sampler_config_hub.py` — Custom sampler and scheduler configuration node
|
||||
- `sampler_scheduler_config.py` - Custom sampler and scheduler configuration node
|
||||
- `manual_alpha_mask_painter.py` — Manual alpha mask painter for converting black masks to transparency
|
||||
|
||||
These nodes are licensed under the main [repository LICENSE](../LICENSE).
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
title: T-Shirt and Object Masking Workflow Notes
|
||||
author: Fred LeFevre
|
||||
created: 2025-04-27
|
||||
description: Documentation for the T-Shirt and Object Masking Workflow, including custom node usage, image source control, and metadata settings.
|
||||
---
|
||||
|
||||
# T-Shirt and Object Masking Workflow — Testing & Direction Notes
|
||||
|
||||
This workflow was primarily designed to create masked images for T-shirts and other objects. It has grown beyond my initial expectations.
|
||||
|
||||
---
|
||||
|
||||
## 1. Pending Work
|
||||
- **Add an upscaler.**
|
||||
- I currently use **Topaz Photo AI** for upscaling and it does a great job.
|
||||
|
||||
---
|
||||
|
||||
## 2. Custom Nodes
|
||||
- There are **three custom nodes** designed for this workflow.
|
||||
- The **Sampler** and **Scheduler** nodes are included primarily to **feed settings to metadata**.
|
||||
- Available fields (except for **Denoise** and **Control After Generate**) should be entered in the **custom nodes**, **not** the **KSampler**.
|
||||
- **Denoise** and **Control After Generate** should still be entered directly inside the **KSampler**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Image Input Options
|
||||
- There are **four Griptape image nodes** plus a standard **Load Image** node that can feed images into the workflow.
|
||||
- The image that is processed depends on the **5-Way Toggle Image Switch** inside the **Toggle Image Source Group**.
|
||||
- **Griptape nodes** require **both OpenAI** and **Black Forest Labs API keys**.
|
||||
- These keys **used to** be entered in a `.env` file.
|
||||
- They are now entered in the **Griptape Settings window** inside ComfyUI.
|
||||
- Instructions for obtaining the necessary API keys can easily be found **online** or in the **Griptape documentation**.
|
||||
|
||||
---
|
||||
|
||||
## 4. SDXL or Direct Path Group
|
||||
- The **SDXL or Direct Path Group switch** determines:
|
||||
- Whether the image is processed through the full **SDXL workflow**, or
|
||||
- Whether it **goes directly** to the **Transparent Background Node**, bypassing SDXL processing.
|
||||
|
||||
---
|
||||
|
||||
## 5. Prompt vs Image Source Priority
|
||||
- The **Toggle Prompt or Image Src Group** controls **whether the prompt or the image source takes precedence**.
|
||||
- This works in conjunction with setting **Denoise** inside the **KSampler**:
|
||||
- **Denoise = 0.1** → **Prompt** takes precedence.
|
||||
- **Denoise = 0.5** → **Mix** of prompt and image.
|
||||
- **Denoise = 0.9** → **Image** takes precedence.
|
||||
|
||||
### Choices Table:
|
||||
|
||||
| Toggle Prompt or Image Src | SDXL or Direct Path Group | Denoise Setting | Precedence | Notes |
|
||||
|:---------------------------|:--------------------------|:----------------|:-----------|:------|
|
||||
| 0 and 0 | (Doesn't matter) | 0.9 | Manual Prompt (Enter Positive Prompt Here) | |
|
||||
| 0 and 0 | (Doesn't matter) | 0.1 | No Image | If SDXL or Direct Path Group switch is set to 1, image skips SDXL workflow |
|
||||
| 1 and 0 | (Doesn't matter) | 0.9 | Griptape Prompt | |
|
||||
| 1 and 0 | (Doesn't matter) | 0.1 | No Image | |
|
||||
| 0 and 1 | (Doesn't matter) | 0.9 | Manual Prompt | |
|
||||
| 0 and 1 | (Doesn't matter) | 0.5 | Mix of Manual Prompt and Image Source | |
|
||||
| 0 and 1 | (Doesn't matter) | 0.1 | Image Source | |
|
||||
| 1 and 1 | (Doesn't matter) | 0.9 | Manual Prompt | |
|
||||
| 1 and 1 | (Doesn't matter) | 0.5 | Mix of Manual Prompt and Image Source | |
|
||||
| 1 and 1 | (Doesn't matter) | 0.1 | Mix of Manual Prompt and Image Source | |
|
||||
|
||||
---
|
||||
|
||||
# End of Notes
|
||||
@@ -0,0 +1,55 @@
|
||||
# FSL / Ensure NHWC Batch (returns 1xH×W×3 float32)
|
||||
import torch
|
||||
|
||||
class FSLEnsureNHWCBatch:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"image": ("IMAGE",)}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image_nhwc",)
|
||||
FUNCTION = "ensure_nhwc"
|
||||
CATEGORY = "FSL/Utils"
|
||||
|
||||
def _to_nhwc(self, t: torch.Tensor) -> torch.Tensor:
|
||||
if not isinstance(t, torch.Tensor):
|
||||
raise TypeError(f"Expected torch.Tensor, got {type(t)}")
|
||||
if t.dtype != torch.float32:
|
||||
t = t.float()
|
||||
|
||||
# 3D cases: HWC / CHW / HCW
|
||||
if t.ndim == 3:
|
||||
if t.shape[-1] in (3, 4): # HWC already
|
||||
a = t
|
||||
elif t.shape[0] in (3, 4): # CHW -> HWC
|
||||
a = t.permute(1, 2, 0).contiguous()
|
||||
elif t.shape[1] in (3, 4): # HCW -> HWC
|
||||
a = t.permute(0, 2, 1).contiguous()
|
||||
else:
|
||||
raise ValueError(f"3D tensor has unexpected shape {tuple(t.shape)}")
|
||||
return a.unsqueeze(0).clamp(0.0, 1.0) # -> 1 x H x W x C
|
||||
|
||||
# 4D cases: NHWC / NCHW / NHCW
|
||||
if t.ndim == 4:
|
||||
if t.shape[-1] in (3, 4): # NHWC already
|
||||
a = t
|
||||
elif t.shape[1] in (3, 4): # NCHW -> NHWC
|
||||
a = t.permute(0, 2, 3, 1).contiguous()
|
||||
elif t.shape[2] in (3, 4): # NHCW -> NHWC
|
||||
a = t.permute(0, 1, 3, 2).contiguous()
|
||||
else:
|
||||
raise ValueError(f"4D tensor has unexpected shape {tuple(t.shape)}")
|
||||
return a.clamp(0.0, 1.0)
|
||||
|
||||
raise ValueError(f"Expected 3D/4D tensor, got {t.ndim}D")
|
||||
|
||||
def ensure_nhwc(self, image):
|
||||
t = image[0] if isinstance(image, list) else image
|
||||
t = self._to_nhwc(t)
|
||||
# Drop alpha if present so esrgan-like models stay happy
|
||||
if t.shape[-1] == 4:
|
||||
t = t[..., :3]
|
||||
return (t,)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"FSLEnsureNHWCBatch": FSLEnsureNHWCBatch}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"FSLEnsureNHWCBatch": "FSL / Ensure NHWC Batch"}
|
||||
@@ -0,0 +1,130 @@
|
||||
# ComfyUI/custom_nodes/comfyui_fsl_nodes/fsl_image_memory.py
|
||||
# FSL / Image Memory: store & recall IMAGE tensors between runs.
|
||||
|
||||
import torch, time
|
||||
|
||||
# One in-process cache for IMAGEs: key -> list[HWC float tensors]
|
||||
_MEM = {}
|
||||
|
||||
def _ensure_list_of_hwctensors(x):
|
||||
"""Return a flat list of HWC float32 tensors in [0,1]."""
|
||||
imgs = x if isinstance(x, list) else [x]
|
||||
# unwrap single nested list: [[t]] -> [t]
|
||||
if len(imgs) == 1 and isinstance(imgs[0], list):
|
||||
imgs = imgs[0]
|
||||
|
||||
out = []
|
||||
for t in imgs:
|
||||
# normalize to torch tensor
|
||||
if not torch.is_tensor(t):
|
||||
t = torch.tensor(t)
|
||||
|
||||
# common incoming shapes:
|
||||
# (H,W,C) OK; (C,H,W) -> (H,W,C); (1,C,H,W) -> (C,H,W)
|
||||
if t.ndim == 4 and t.shape[0] == 1:
|
||||
t = t.squeeze(0) # (1,C,H,W) -> (C,H,W)
|
||||
if t.ndim == 3 and t.shape[0] in (1, 3) and t.shape[-1] not in (1, 3):
|
||||
# likely CHW -> HWC
|
||||
t = t.movedim(0, -1)
|
||||
if t.ndim == 2:
|
||||
# grayscale H,W -> H,W,1
|
||||
t = t.unsqueeze(-1)
|
||||
|
||||
# dtype/range hygiene
|
||||
if t.dtype != torch.float32:
|
||||
t = t.float()
|
||||
t = t.clamp(0, 1).contiguous()
|
||||
out.append(t)
|
||||
return out
|
||||
|
||||
def _first_shape(lst):
|
||||
try:
|
||||
t = lst[0]
|
||||
return tuple(t.shape)
|
||||
except Exception:
|
||||
return "n/a"
|
||||
|
||||
class FSLImageMemoryStore:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"image": ("IMAGE",), "key": ("STRING", {"default": "last"})}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "store"
|
||||
CATEGORY = "FSL/Memory"
|
||||
|
||||
def store(self, image, key):
|
||||
data = _ensure_list_of_hwctensors(image)
|
||||
_MEM[key] = data
|
||||
print(f"[FSLImageMemoryStore] stored key='{key}' ({len(data)} img) first={_first_shape(data)} mem_id={id(_MEM)}")
|
||||
return (data,) # pass-through as IMAGE
|
||||
|
||||
class FSLImageMemoryRecallSafe:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"key": ("STRING", {"default": "last"})}}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("image",)
|
||||
FUNCTION = "recall"
|
||||
CATEGORY = "FSL/Memory"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return time.time()
|
||||
|
||||
def recall(self, key):
|
||||
imgs = _MEM.get(key, None)
|
||||
if imgs:
|
||||
imgs = _ensure_list_of_hwctensors(imgs)
|
||||
print(f"[FSLImageMemoryRecallSafe] key='{key}' ({len(imgs)} img) first={_first_shape(imgs)} mem_id={id(_MEM)}")
|
||||
return (imgs,)
|
||||
print(f"[FSLImageMemoryRecallSafe] WARNING: key='{key}' missing -> returning 1x1 black mem_id={id(_MEM)}")
|
||||
t = torch.zeros((1, 1, 3), dtype=torch.float32) # HWC in [0,1]
|
||||
return ([t],)
|
||||
|
||||
class FSLImageMemoryClear:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {"required": {"key": ("STRING", {"default": "last"})}}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "clear"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "FSL/Memory"
|
||||
|
||||
def clear(self, key):
|
||||
_MEM.pop(key, None)
|
||||
print(f"[FSLImageMemoryClear] cleared key='{key}' mem_id={id(_MEM)}")
|
||||
return ()
|
||||
|
||||
class FSLImageMemoryClearAll:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "clear_all"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "FSL/Memory"
|
||||
|
||||
def clear_all(self):
|
||||
global _MEM
|
||||
_MEM.clear()
|
||||
print(f"[FSLImageMemoryClearAll] cleared all memory, mem_id={id(_MEM)}")
|
||||
return ()
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FSLImageMemoryStore": FSLImageMemoryStore,
|
||||
"FSLImageMemoryRecallSafe": FSLImageMemoryRecallSafe,
|
||||
"FSLImageMemoryClear": FSLImageMemoryClear,
|
||||
"FSLImageMemoryClearAll": FSLImageMemoryClearAll,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FSLImageMemoryStore": "FSL / Image Memory → Store",
|
||||
"FSLImageMemoryRecallSafe": "FSL / Image Memory → Recall (Safe)",
|
||||
"FSLImageMemoryClear": "FSL / Image Memory → Clear",
|
||||
"FSLImageMemoryClearAll": "FSL / Image Memory → Clear All",
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
class SamplerConfigHub:
|
||||
"""
|
||||
Custom node that emits a unified sampler config dictionary.
|
||||
This allows centralized control over sampler settings when using multiple
|
||||
KSampler nodes in a workflow (e.g., for SDXL base and refiner).
|
||||
"""
|
||||
CATEGORY = "FSL Nodes/Utils" # Changed to match your preferred category
|
||||
RETURN_TYPES = ("SAMPLER_CONFIG",)
|
||||
RETURN_NAMES = ("sampler_config",)
|
||||
FUNCTION = "build_config"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sampler_name": ([
|
||||
"euler", "euler_ancestral", "heun",
|
||||
"dpm_2", "dpm_2_ancestral", "lms",
|
||||
"dpm_fast", "dpm_adaptive",
|
||||
"dpmpp_2s_ancestral", "dpmpp_2m", "dpmpp_sde",
|
||||
"dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde",
|
||||
"ddim", "plms"
|
||||
],),
|
||||
"scheduler": (["normal", "karras", "exponential", "sgm_uniform"],),
|
||||
"steps": ("INT", {"default": 30, "min": 1, "max": 150}),
|
||||
"cfg": ("FLOAT", {"default": 7.5, "min": 0.0, "max": 20.0}),
|
||||
"seed": ("INT", {"default": 42}),
|
||||
}
|
||||
}
|
||||
|
||||
def build_config(self, sampler_name, scheduler, steps, cfg, seed):
|
||||
return ({
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"steps": steps,
|
||||
"cfg": cfg,
|
||||
"seed": seed,
|
||||
},)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FSL SamplerConfigHub": SamplerConfigHub, # Added prefix and comma
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FSL SamplerConfigHub": "🎛️ Sampler Config Hub (FSL)", # Added prefix
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"""
|
||||
FSL Sampler & Scheduler Config Node
|
||||
|
||||
Version: 1.0.0
|
||||
Author: Fred LeFevre
|
||||
Date: 2025-04-27
|
||||
|
||||
Description: Custom Sampler Node to feed settings to metadata.
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
|
||||
class FSL_SamplerSchedulerConfig:
|
||||
CATEGORY = "FSL Nodes/Utils"
|
||||
RETURN_TYPES = ("SAMPLER", "SCHEDULER")
|
||||
RETURN_NAMES = ("sampler", "scheduler")
|
||||
FUNCTION = "bundle"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"sampler_name": ([
|
||||
"euler", "euler_cfg_pp", "euler_ancestral", "euler_ancestral_cfg_pp",
|
||||
"heun", "heunpp2", "dpm_2", "dpm_2_ancestral", "lms", "dpm_fast",
|
||||
"dpm_adaptive", "dpmpp_2s_ancestral", "dpmpp_2s_ancestral_cfg_pp",
|
||||
"dpmpp_sde", "dpmpp_sde_gpu", "dpmpp_2m", "dpmpp_2m_cfg_pp",
|
||||
"dpmpp_2m_sde", "dpmpp_2m_sde_gpu", "dpmpp_3m_sde",
|
||||
"dpmpp_3m_sde_gpu", "ddpm", "lcm", "ipndm", "ipndm_v", "deis",
|
||||
"res_multistep", "res_multistep_cfg_pp", "res_multistep_ancestral",
|
||||
"res_multistep_ancestral_cfg_pp", "gradient_estimation", "er_sde",
|
||||
"ddim", "uni_pc", "uni_pc_bh2"
|
||||
], {"default": "euler"}),
|
||||
"scheduler_name": ([
|
||||
"normal", "karras", "exponential", "sgm_uniform",
|
||||
"simple", "ddim_uniform", "beta", "linear_quadratic", "kl_optimal"
|
||||
], {"default": "karras"})
|
||||
}
|
||||
}
|
||||
|
||||
def bundle(self, sampler_name, scheduler_name):
|
||||
return (sampler_name, scheduler_name)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"FSL Sampler & Scheduler Config": FSL_SamplerSchedulerConfig,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"FSL Sampler & Scheduler Config": "🛠️ Sampler & Scheduler Config (FSL)",
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"id":"c58118a1-db97-4ce0-9bbb-a8a8d39cbdd5","revision":0,"last_node_id":7,"last_link_id":5,"nodes":[{"id":2,"type":"Show any to JSON [Crystools]","pos":[1213.032470703125,-177.2519073486328],"size":[691.7337036132812,430.23626708984375],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"any_value","name":"any_value","shape":7,"type":"*","link":1}],"outputs":[{"localized_name":"string","name":"string","type":"STRING","links":[]}],"properties":{"cnr_id":"comfyui-crystools","ver":"1.27.3","Node name for S&R":"Show any to JSON [Crystools]","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"}},"widgets_values":[]},{"id":3,"type":"LocalImageGalleryNode","pos":[610.5911865234375,399.3844909667969],"size":[800,670],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":[2]},{"localized_name":"paths","name":"paths","type":"LMM_ALL_PATHS","links":null},{"localized_name":"path","name":"path","type":"STRING","links":[3]},{"localized_name":"info","name":"info","type":"STRING","links":[]}],"properties":{"cnr_id":"comfyui_local_image_gallery","ver":"1.1.1","Node name for S&R":"LocalImageGalleryNode","gallery_unique_id":"gallery-nbrbpsxuy","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"},"selection":"[]"},"widgets_values":["gallery-nbrbpsxuy","[]",""]},{"id":5,"type":"Show any to JSON [Crystools]","pos":[1441.543701171875,535.852783203125],"size":[691.7337036132812,430.23626708984375],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"any_value","name":"any_value","shape":7,"type":"*","link":4}],"outputs":[{"localized_name":"string","name":"string","type":"STRING","links":[]}],"properties":{"cnr_id":"comfyui-crystools","ver":"1.27.3","Node name for S&R":"Show any to JSON [Crystools]","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"}},"widgets_values":[]},{"id":4,"type":"FSLMetadataFromImage","pos":[1437.5640869140625,402.2993469238281],"size":[270,82],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":2},{"localized_name":"path","name":"path","type":"STRING","widget":{"name":"path"},"link":3},{"localized_name":"strict","name":"strict","shape":7,"type":"BOOLEAN","widget":{"name":"strict"},"link":null}],"outputs":[{"localized_name":"metadata_raw","name":"metadata_raw","type":"METADATA_RAW","links":[4]}],"properties":{"Node name for S&R":"FSLMetadataFromImage","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"}},"widgets_values":["",false]},{"id":1,"type":"Load image with metadata [Crystools]","pos":[606.4518432617188,-214.61428833007812],"size":[570.5427856445312,497.06268310546875],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"image","name":"image","type":"IMAGE","links":null},{"localized_name":"mask","name":"mask","type":"MASK","links":null},{"localized_name":"prompt","name":"prompt","type":"JSON","links":[]},{"localized_name":"Metadata RAW","name":"Metadata RAW","type":"METADATA_RAW","links":[1]}],"properties":{"cnr_id":"comfyui-crystools","ver":"1.27.3","Node name for S&R":"Load image with metadata [Crystools]","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"}},"widgets_values":["fred_lefevre_closeup_of_a_large_metallic_figure_wears_intrica_287bf5be-7120-4403-9da3-cb0f14b6bd1f_0.png","image"]},{"id":6,"type":"Fast Groups Bypasser (rgthree)","pos":[1980.3636474609375,-217.84588623046875],"size":[236.74765014648438,82],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"OPT_CONNECTION","type":"*","links":null}],"properties":{"matchColors":"","matchTitle":"","showNav":true,"showAllGraphs":true,"sort":"position","customSortAlphabet":"","toggleRestriction":"default","ue_properties":{"widget_ue_connectable":{},"input_ue_unconnectable":{},"version":"7.2.2"}}}],"links":[[1,1,3,2,0,"*"],[2,3,0,4,0,"IMAGE"],[3,3,2,4,1,"STRING"],[4,4,0,5,0,"*"]],"groups":[{"id":1,"title":"Single Image","bounding":[596.4518432617188,-288.21417236328125,1318.3138427734375,580.6626586914062],"color":"#3f789e","font_size":24,"flags":{}},{"id":2,"title":"Batch Loader","bounding":[600.5911865234375,325.78448486328125,1542.685791015625,753.5999755859375],"color":"#3f789e","font_size":24,"flags":{}}],"config":{},"extra":{"ue_links":[],"ds":{"scale":0.5989729227890566,"offset":[427.8159465408812,391.05747207847645]},"links_added_by_ue":[]},"version":0.4}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 379 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 804 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Reference in New Issue
Block a user