Files
Xyc2016-Comfyui_Fd_Nodes/src/Comfyui_Fd_Nodes/nodes.py
T

927 lines
36 KiB
Python

import io
import json
import logging
import os
import traceback
from datetime import datetime
from inspect import cleandoc
from io import BytesIO
from typing import Any, Dict, Optional, Tuple
from uuid import uuid4
import numpy as np
import requests
import torch
from comfy.comfy_types.node_typing import IO, ComfyNodeABC, InputTypeDict
from PIL import Image
from .config import (
FD_FLUX2KLEIN_PASSWORD,
FD_FLUX2KLEIN_URL,
FD_FLUX2KLEIN_USERNAME,
FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL,
FD_OSS_URL_PATH_PREFIX_FLUX,
FD_Z_IMAGE_TURBO_PASSWORD,
FD_Z_IMAGE_TURBO_URL,
FD_Z_IMAGE_TURBO_USERNAME,
)
from .old_fd_nodes import FD_BinaryDecisionNormalizer, FD_imgToText_Doubao, FD_Upload
from .old_gemini_api_node import FD_GeminiImage, GenImageServiceError
from .utils.common_util import (
bytes_calculate_hex_md5,
bytesio_to_image_tensor,
downscale_image_tensor,
)
from .utils.error_utils import normalize_error_message
from .utils.oss_client import upload_bytes_to_oss
from .utils.url_utils import normalize_oss_url
from .utils.webhook import webhook_send
from .gpt_image_node import FD_GTPImage
from .gpt_image_combo_node import FD_GPTImageComboNode
from .gpt_multi_image_node import FD_GPTMultiImage
from .seedream_image_combo_node import FD_SeedreamImageComboNode
from .prompt_nodes import EcommercePromptGenerator, PromptListSelector
from .zhiyi_text_node import ZhiYiTextGenNode
from .zhiyi_image_text_node import ZhiYiImageTextNode
from .zhiyi_image_text_combo_node import ZhiYiImageTextComboNode
from .zhiyi_image_to_image_node import ZhiYiImageToImageNode
from .toggle import NodeToggleByID
from .utils.logging_utils import configure_default_logging
from .zhiyi_image_combo_node import ZhiYiImageComboNode
from .zhiyi_image_concat_node import ZhiYiImageConcatNode
from .zhiyi_four_grid_crop_node import ZhiYiFourGridCropNode
from .zhiyi_image_to_image_combo_node import ZhiYiImageToImageComboNode
from .aistudio_image_combo_node import ZhiYiAiStudioImageComboNode
from .remove_bg_by_meitu_node import ZhiYiRemoveBgByMeituNode
from .zhiyi_rmbg_segment_node import ZhiYiBodySegmentNode, ZhiYiClothesSegmentNode, ZhiYiFashionSegmentNode, ZhiYiRMBGNode
from .sam2_segment_node import ZhiYiSAM2SegmentNode
from .zhiyi_controlnet_aux_node import ZhiYiDepthAnythingV2PreprocessorNode, ZhiYiLineArtPreprocessorNode
from .zhiyi_dwpose_detect_node import ZhiYiDWPoseDetectNode
from .v2.controlnet_aux_v2_node import (
ZhiYiDepthAnythingV2PreprocessorNodeV2,
ZhiYiDWPoseDetectNodeV2,
ZhiYiLineArtPreprocessorNodeV2,
)
from .v2.rmbg_segment_v2_node import (
ZhiYiBodySegmentNodeV2,
ZhiYiClothesSegmentNodeV2,
ZhiYiFashionSegmentNodeV2,
ZhiYiRMBGNodeV2,
)
from .v2.sam2_segment_v2_node import ZhiYiSAM2SegmentNodeV2
from .zhiyi_qwen_detect_node import ZhiYiBBoxesToSAM2, ZhiYiQwenDetectNode
from .pattern_extraction_node import PatternApplyAlphaToImage, PatternChooseBackgroundPair, PatternDualBackgroundToRGBA
from .utils.gpt_image_size import resolution_to_image_generation_edit_size
from .utils.seedream_image_size import (
SEEDREAM_ASPECT_RATIOS,
SEEDREAM_IMAGE_SIZES,
)
from .utils.seedream_image_client import get_default_seedream_image_client
configure_default_logging()
logger = logging.getLogger(__name__)
FD_REMOVE_WATERMARK_SERVICE_URL = os.getenv("FD_REMOVE_WATERMARK_SERVICE_URL", "http://localhost:8000/v1/process")
def _resolution_to_edit_size(resolution: str, aspect_ratio: str) -> str:
return resolution_to_image_generation_edit_size(resolution, aspect_ratio)
class FD_RemoveWatermark:
"""
Remove watermark from image using AI inpainting service
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"image": ("IMAGE",),
"service_url": ("STRING", {"default": FD_REMOVE_WATERMARK_SERVICE_URL, "multiline": False}),
"text_prompt": (
"STRING",
{"default": "bottom right watermark. bottom left watermark. top left watermark. top right watermark. corner logo. watermark text.", "multiline": True},
),
"threshold": (
"FLOAT",
{
"default": 0.3,
},
),
"text_threshold": (
"FLOAT",
{
"default": 0.35,
},
),
"max_side": (
"INT",
{
"default": 3000,
},
),
"mask_dilate_ksize": (
"INT",
{
"default": 9,
},
),
"mask_dilate_iters": (
"INT",
{
"default": 2,
},
),
"inpaint_method": (["lama"],),
"enable_quality_check": ("BOOLEAN", {"default": True}),
"fallback_to_original": ("BOOLEAN", {"default": True}),
"jpeg_quality": (
"INT",
{
"default": 95,
},
),
},
"optional": {
"image_delivery": (["multipart", "url"], {
"default": "multipart",
"tooltip": "url 先上传 OSS,再发送 image_url 表单;海外部署强制使用 URL。",
}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "remove_watermark"
CATEGORY = "image/postprocessing"
def remove_watermark(
self,
image: torch.Tensor,
service_url: str,
text_prompt: str,
threshold: float,
text_threshold: float,
max_side: int,
mask_dilate_ksize: int,
mask_dilate_iters: int,
inpaint_method: str,
enable_quality_check: bool,
fallback_to_original: bool,
jpeg_quality: int,
image_delivery: str = "multipart",
) -> Tuple[torch.Tensor]:
from .utils.image_payload import tensor_to_image_payload
from .utils.url_utils import is_url_delivery_required
if image_delivery not in {"multipart", "url"}:
raise ValueError(f"Unknown watermark image_delivery: {image_delivery}")
use_url = is_url_delivery_required() or image_delivery == "url"
call_id = str(uuid4())[:8]
print(
f"FD_RemoveWatermark [{call_id}] Start processing. ",
{
"call_id": call_id,
"image_shape": list(image.shape),
"service_url": service_url,
"text_prompt": text_prompt,
"threshold": threshold,
"text_threshold": text_threshold,
"max_side": max_side,
"mask_dilate_ksize": mask_dilate_ksize,
"mask_dilate_iters": mask_dilate_iters,
"inpaint_method": inpaint_method,
"enable_quality_check": enable_quality_check,
"fallback_to_original": fallback_to_original,
"jpeg_quality": jpeg_quality,
},
)
try:
# Prepare request
data = {
"text_prompt": text_prompt,
"threshold": threshold,
"text_threshold": text_threshold,
"max_side": max_side,
"mask_dilate_ksize": mask_dilate_ksize,
"mask_dilate_iters": mask_dilate_iters,
"inpaint_method": inpaint_method,
"enable_quality_check": enable_quality_check,
"fallback_to_original": fallback_to_original,
"jpeg_quality": jpeg_quality,
}
print(f"FD_RemoveWatermark [{call_id}] Calling service at {service_url} ...")
if use_url:
data["image_url"] = tensor_to_image_payload(image, "url")
response = requests.post(service_url, data=data, timeout=180)
else:
# Preserve the domestic multipart contract and PNG encoding.
img_np = (image[0].cpu().numpy() * 255).astype(np.uint8)
img_bytes = io.BytesIO()
Image.fromarray(img_np).save(img_bytes, format="PNG")
img_bytes.seek(0)
files = {"image": ("image.png", img_bytes, "image/png")}
response = requests.post(service_url, files=files, data=data, timeout=60)
response.raise_for_status()
# Convert response back to ComfyUI tensor
result_img = Image.open(io.BytesIO(response.content)).convert("RGB")
result_np = np.array(result_img).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_np).unsqueeze(0) # [1, H, W, C]
print(f"FD_RemoveWatermark [{call_id}] Successfully processed image.")
return (result_tensor,)
except Exception as e:
if use_url:
raise RuntimeError(f"FD_RemoveWatermark [{call_id}] URL request failed: {e}") from e
# Return original image on any error
print(f"FD_RemoveWatermark [{call_id}] error: {e}, returning original image")
return (image,)
def fd_flux2_klein_send_webhook(service_url: str, flux2_klein_req_body: dict):
now = datetime.now()
now_str = now.strftime("%Y-%m-%d %H:%M:%S")
headers = {"Content-Type": "application/json"}
data = {
"msgtype": "text",
"text": {
"content": json.dumps(
{"datetime": now_str, "service_url": service_url, "flux2_klein_req_body": flux2_klein_req_body},
ensure_ascii=False,
indent=4,
)
},
}
requests.post(FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL, headers=headers, json=data)
class FD_Flux2KleinGenImage(ComfyNodeABC):
"""
Node to generate text and image responses from a Flux2KleinGen model.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls) -> InputTypeDict:
return {
"required": {
"out_request_id": (
IO.STRING,
{
"default": "unknown_request_id",
"tooltip": "FD out_request_id for generation",
},
),
"prompt": (
IO.STRING,
{
"multiline": True,
"default": "",
"tooltip": "Text prompt for generation",
},
),
"aspect_ratio": (
["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "16:9", "9:16", "21:9"],
{
"default": "auto",
"tooltip": "Aspect ratio for generation",
},
),
},
"optional": {
"images": (
IO.IMAGE,
{
"default": None,
"tooltip": "Optional image(s) to use as context for the model. To include multiple images, you can use the Batch Images node.",
},
),
"service_url": (
IO.STRING,
{
"default": FD_FLUX2KLEIN_URL,
"tooltip": "Flux2Klein service URL",
},
),
"seed": (
IO.INT,
{
"default": 0,
"min": 0,
"max": 2 ** 32 - 1,
"tooltip": "Random seed for generation.",
},
),
"resolution": (
IO.COMBO,
{
"options": ["1K", "2K"],
"default": "2K",
},
),
},
"hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = (IO.IMAGE, )
FUNCTION = "api_call"
CATEGORY = "image/generation"
DESCRIPTION = "Edit images synchronously via Flux2Klein API."
API_NODE = True
def api_call(self,
out_request_id: str,
prompt: str,
aspect_ratio: str,
images: Optional[IO.IMAGE] = None,
service_url: str = FD_FLUX2KLEIN_URL,
seed: int = 0,
resolution: str = "2K",
**kwargs,
):
body = {
"out_request_id": out_request_id,
"prompt": prompt,
"seed": seed,
"ratio": aspect_ratio,
"size": resolution,
}
if images is not None:
batch_size = images.shape[0]
image_url_list = []
for i in range(batch_size):
single_image = images[i : i + 1]
original_image = single_image.squeeze()
scaled_image = downscale_image_tensor(single_image, total_pixels=2048 * 2048).squeeze()
logger.info(
"FD_Flux2KleinGenImage Image %s resolution: original=%s scaled=%s",
i,
tuple(original_image.shape),
tuple(scaled_image.shape),
)
image_np = (scaled_image.numpy() * 255).astype(np.uint8)
img = Image.fromarray(image_np)
img_byte_arr = BytesIO()
img.save(img_byte_arr, format="PNG")
img_byte_arr = img_byte_arr.getvalue()
file_oss_path = f"{FD_OSS_URL_PATH_PREFIX_FLUX}/{bytes_calculate_hex_md5(img_byte_arr)}.png"
oss_file_url = upload_bytes_to_oss(file_oss_path, img_byte_arr)
print(f"upload {file_oss_path}")
image_url_list.append(oss_file_url)
body['images'] = image_url_list
if FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL:
try:
print("Sending flux2_klein webhook message...")
fd_flux2_klein_send_webhook(service_url, body)
except Exception:
pass
logger.info(f"Calling Flux2Klein API with {body}")
try:
# example response json {'urls': ['https://zhiyi-image.oss-cn-hangzhou.aliyuncs.com//devops/comfyui/output/20260121/bed973ec3ccb31d49d43a31d9f535b65.png'], 'status': 'success', 'cost_time': 45.2}
response = requests.post(service_url, auth=(FD_FLUX2KLEIN_USERNAME, FD_FLUX2KLEIN_PASSWORD), json=body)
response.raise_for_status()
if response.status_code != 200:
raise Exception(f"Failed to call API: {response.content}")
result = response.json()
logger.info(f"Flux2Klein API response: {result}")
result_url = result["urls"][0] # TODO: 暂时只支持1张图
except Exception:
traceback.print_exc()
raise GenImageServiceError("TIMEOUT")
if FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL:
try:
print("Sending flux2_klein webhook message...")
webhook_send(FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL, {
"flux2_klein_full": {
"request": body,
"response": result,
}
})
except Exception:
pass
image_content = requests.get(normalize_oss_url(result_url)).content
image_bytesio = BytesIO(image_content)
output_image = bytesio_to_image_tensor(image_bytesio)
return (output_image,)
class FD_ZImageTurboGenImage(ComfyNodeABC):
"""
Node to generate text and image responses from a Z-Image-Turbo model.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls) -> InputTypeDict:
return {
"required": {
"out_request_id": (
IO.STRING,
{
"default": "unknown_request_id",
"tooltip": "FD out_request_id for generation",
},
),
"prompt": (
IO.STRING,
{
"multiline": True,
"default": "",
"tooltip": "Text prompt for generation",
},
),
"aspect_ratio": (
["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "16:9", "9:16", "21:9"],
{
"default": "auto",
"tooltip": "Aspect ratio for generation",
},
),
},
"optional": {
"images": (
IO.IMAGE,
{
"default": None,
"tooltip": "Optional image(s) to edit with. To include multiple images, you can use the Batch Images node.",
},
),
"seed": (
IO.INT,
{
"default": 42,
"min": 0,
"max": 2 ** 32 - 1,
"tooltip": "Random seed for generation.",
},
),
"strength": (
"FLOAT",
{
"default": 0.25,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "number",
"tooltip": "How strongly to edit the input image.",
},
),
"guidance_scale": (
"FLOAT",
{
"default": 0.0,
"min": 0.0,
"max": 20.0,
"step": 0.1,
"display": "number",
"tooltip": "Classifier-free guidance scale.",
},
),
"num_inference_steps": (
IO.INT,
{
"default": 9,
"min": 1,
"max": 100,
"tooltip": "Number of denoising steps.",
},
),
"num_images": (
IO.INT,
{
"default": 1,
"min": 1,
"max": 8,
"tooltip": "Number of output images.",
},
),
"resolution": (
IO.COMBO,
{
"options": ["1K", "2K"],
"default": "1K",
"tooltip": "Output image size.",
},
),
},
"hidden": {
"auth_token": "AUTH_TOKEN_COMFY_ORG",
"comfy_api_key": "API_KEY_COMFY_ORG",
"unique_id": "UNIQUE_ID",
},
}
RETURN_TYPES = (IO.IMAGE, IO.STRING)
FUNCTION = "api_call"
CATEGORY = "image/generation"
DESCRIPTION = "Edit images synchronously via Z-Image-Turbo API."
API_NODE = True
def api_call(
self,
out_request_id: str,
prompt: str,
aspect_ratio: str,
images: Optional[IO.IMAGE] = None,
seed: int = 42,
strength: float = 0.25,
guidance_scale: float = 0.0,
num_inference_steps: int = 9,
num_images: int = 1,
resolution: str = "1K",
**kwargs,
):
body = {
"out_request_id": out_request_id,
"prompt": prompt,
"seed": seed,
"ratio": aspect_ratio,
"size": resolution,
"strength": strength,
"guidance_scale": guidance_scale,
"num_inference_steps": num_inference_steps,
"num_images": num_images,
}
if images is not None:
batch_size = images.shape[0]
image_url_list = []
for i in range(batch_size):
single_image = images[i : i + 1]
scaled_image = single_image.squeeze()
image_np = (scaled_image.numpy() * 255).astype(np.uint8)
img = Image.fromarray(image_np)
img_byte_arr = BytesIO()
img.save(img_byte_arr, format="PNG")
img_byte_arr = img_byte_arr.getvalue()
file_oss_path = f"{FD_OSS_URL_PATH_PREFIX_FLUX}/{bytes_calculate_hex_md5(img_byte_arr)}.png"
oss_file_url = upload_bytes_to_oss(file_oss_path, img_byte_arr)
print(f"upload {file_oss_path}")
image_url_list.append(oss_file_url)
body["images"] = image_url_list
if FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL:
try:
print("Sending z_image_turbo webhook message...")
webhook_send(FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL, {
"z_image_turbo_request": {
"service_url": FD_Z_IMAGE_TURBO_URL,
"request": body,
}
})
except Exception:
pass
logger.info(f"Calling Z-Image-Turbo API with {body}")
try:
response = requests.post(FD_Z_IMAGE_TURBO_URL, auth=(FD_Z_IMAGE_TURBO_USERNAME, FD_Z_IMAGE_TURBO_PASSWORD), json=body)
response.raise_for_status()
if response.status_code != 200:
raise Exception(f"Failed to call API: {response.content}")
result = response.json()
logger.info(f"Z-Image-Turbo API response: {result}")
result_url = result["urls"][0]
except Exception:
traceback.print_exc()
raise
if FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL:
try:
print("Sending z_image_turbo webhook message...")
webhook_send(FD_GEN_IMAGE_NOTIFICATION_WEBHOOK_URL, {
"z_image_turbo_full": {
"request": body,
"response": result,
}
})
except Exception:
pass
image_content = requests.get(normalize_oss_url(result_url)).content
image_bytesio = BytesIO(image_content)
output_image = bytesio_to_image_tensor(image_bytesio)
return (output_image, result_url)
class FD_SeedreamImage(ComfyNodeABC):
"""
Node to generate images using Seedream 5.0 models.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls) -> InputTypeDict:
return {
"required": {
"prompt": (
IO.STRING,
{
"multiline": True,
"default": "",
"tooltip": "Text prompt for generation",
},
),
"model": (
["doubao-seedream-5.0-lite", "doubao-seedream-5.0-pro", "doubao-seedream-5.0-flash"],
{
"default": "doubao-seedream-5.0-lite",
"tooltip": "Model to use for generation",
},
),
"size": (
SEEDREAM_IMAGE_SIZES,
{
"default": "2K",
"tooltip": "Output image size preset",
},
),
},
"optional": {
"images": (
IO.IMAGE,
{
"default": None,
"tooltip": "Optional image(s) to use as context. Multiple images supported.",
},
),
"output_format": (
["png", "jpg"],
{
"default": "png",
"tooltip": "Output image format",
},
),
"aspect_ratio": (
SEEDREAM_ASPECT_RATIOS,
{
"default": "1:1",
"tooltip": "Output aspect ratio. Converted to a Seedream pixel size before request.",
},
),
},
}
RETURN_TYPES = (IO.IMAGE, )
FUNCTION = "api_call"
CATEGORY = "image/generation"
DESCRIPTION = "Generate images using Seedream 5.0 models."
API_NODE = True
def api_call(
self,
prompt: str,
model: str,
size: str,
images: Optional[IO.IMAGE] = None,
output_format: str = "png",
aspect_ratio: str = "1:1",
**kwargs,
):
client = get_default_seedream_image_client()
try:
if images is not None:
image_bytesio, _result_url = client.edit_image(
image_tensors=images,
prompt=prompt,
model=model,
size=size,
ratio=aspect_ratio,
resize=True,
)
else:
image_bytesio, _result_url = client.generate_image(
prompt=prompt,
model=model,
size=size,
ratio=aspect_ratio,
resize=True,
)
except RuntimeError as exc:
raise GenImageServiceError(normalize_error_message(exc)) from exc
output_image = bytesio_to_image_tensor(image_bytesio)
return (output_image,)
class Example:
"""
A example node
Class methods
-------------
INPUT_TYPES (dict):
Tell the main program input parameters of nodes.
IS_CHANGED:
optional method to control when the node is re executed.
Attributes
----------
RETURN_TYPES (`tuple`):
The type of each element in the output tulple.
RETURN_NAMES (`tuple`):
Optional: The name of each output in the output tulple.
FUNCTION (`str`):
The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute()
OUTPUT_NODE ([`bool`]):
If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example.
The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected.
Assumed to be False if not present.
CATEGORY (`str`):
The category the node should appear in the UI.
execute(s) -> tuple || None:
The entry point method. The name of this method must be the same as the value of property `FUNCTION`.
For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`.
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
"""
Return a dictionary which contains config for all input fields.
Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT".
Input types "INT", "STRING" or "FLOAT" are special values for fields on the node.
The type can be a list for selection.
Returns: `dict`:
- Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required`
- Value input_fields (`dict`): Contains input fields config:
* Key field_name (`string`): Name of a entry-point method's argument
* Value field_config (`tuple`):
+ First value is a string indicate the type of field or a list for selection.
+ Secound value is a config for type "INT", "STRING" or "FLOAT".
"""
return {
"required": {
"image": ("Image", { "tooltip": "This is an image"}),
"int_field": ("INT", {
"default": 0,
"min": 0, #Minimum value
"max": 4096, #Maximum value
"step": 64, #Slider's step
"display": "number" # Cosmetic only: display as "number" or "slider"
}),
"float_field": ("FLOAT", {
"default": 1.0,
"min": 0.0,
"max": 10.0,
"step": 0.01,
"round": 0.001, #The value represeting the precision to round to, will be set to the step value by default. Can be set to False to disable rounding.
"display": "number"}),
"print_to_screen": (["enable", "disable"],),
"string_field": ("STRING", {
"multiline": False, #True if you want the field to look like the one on the ClipTextEncode node
"default": "Hello World!"
}),
},
}
RETURN_TYPES = ("IMAGE",)
#RETURN_NAMES = ("image_output_name",)
DESCRIPTION = cleandoc(__doc__)
FUNCTION = "test"
#OUTPUT_NODE = False
#OUTPUT_TOOLTIPS = ("",) # Tooltips for the output node
CATEGORY = "Example"
def test(self, image, string_field, int_field, float_field, print_to_screen):
if print_to_screen == "enable":
print(f"""Your input contains:
string_field aka input text: {string_field}
int_field: {int_field}
float_field: {float_field}
""")
#do some processing on the image, in this example I just invert it
image = 1.0 - image
return (image,)
"""
The node will always be re executed if any of the inputs change but
this method can be used to force the node to execute again even when the inputs don't change.
You can make this node return a number or a string. This value will be compared to the one returned the last time the node was
executed, if it is different the node will be executed again.
This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash
changes between executions the LoadImage node is executed again.
"""
#@classmethod
#def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen):
# return ""
# A dictionary that contains all nodes you want to export with their names
# NOTE: names should be globally unique
NODE_CLASS_MAPPINGS = {
"FD_RemoveWatermark": FD_RemoveWatermark,
"FD_Upload": FD_Upload,
"FD_imgToText_Doubao": FD_imgToText_Doubao,
"FD_BinaryDecisionNormalizer": FD_BinaryDecisionNormalizer,
"FD_GeminiImage": FD_GeminiImage,
"FD_GTPImage": FD_GTPImage,
"FD_GPTImageComboNode": FD_GPTImageComboNode,
"FD_GPTMultiImage": FD_GPTMultiImage,
"FD_Flux2KleinGenImage": FD_Flux2KleinGenImage,
"FD_ZImageTurboGenImage": FD_ZImageTurboGenImage,
"FD_SeedreamImage": FD_SeedreamImage,
"FD_SeedreamImageComboNode": FD_SeedreamImageComboNode,
"MaoziEcommercePromptGenerator": EcommercePromptGenerator,
"MaoziPromptListSelector": PromptListSelector,
"ZhiYiImageTextNode": ZhiYiImageTextNode,
"ZhiYiImageTextComboNode": ZhiYiImageTextComboNode,
"ZhiYiImageToImageNode": ZhiYiImageToImageNode,
"ZhiYiTextGenNode": ZhiYiTextGenNode,
"NodeToggleByID": NodeToggleByID,
"ZhiYiImageComboNode": ZhiYiImageComboNode,
"ZhiYiImageConcatNode": ZhiYiImageConcatNode,
"ZhiYiFourGridCropNode": ZhiYiFourGridCropNode,
"ZhiYiImageToImageComboNode": ZhiYiImageToImageComboNode,
"ZhiYiAiStudioImageComboNode": ZhiYiAiStudioImageComboNode,
"ZhiYiRemoveBgByMeituNode": ZhiYiRemoveBgByMeituNode,
"ZhiYiRMBGNode": ZhiYiRMBGNode,
"ZhiYiClothesSegmentNode": ZhiYiClothesSegmentNode,
"ZhiYiFashionSegmentNode": ZhiYiFashionSegmentNode,
"ZhiYiBodySegmentNode": ZhiYiBodySegmentNode,
"ZhiYiSAM2SegmentNode": ZhiYiSAM2SegmentNode,
"ZhiYiDWPoseDetectNode": ZhiYiDWPoseDetectNode,
"ZhiYiLineArtPreprocessorNode": ZhiYiLineArtPreprocessorNode,
"ZhiYiDepthAnythingV2PreprocessorNode": ZhiYiDepthAnythingV2PreprocessorNode,
"ZhiYiRMBGNodeV2": ZhiYiRMBGNodeV2,
"ZhiYiClothesSegmentNodeV2": ZhiYiClothesSegmentNodeV2,
"ZhiYiFashionSegmentNodeV2": ZhiYiFashionSegmentNodeV2,
"ZhiYiBodySegmentNodeV2": ZhiYiBodySegmentNodeV2,
"ZhiYiSAM2SegmentNodeV2": ZhiYiSAM2SegmentNodeV2,
"ZhiYiDWPoseDetectNodeV2": ZhiYiDWPoseDetectNodeV2,
"ZhiYiLineArtPreprocessorNodeV2": ZhiYiLineArtPreprocessorNodeV2,
"ZhiYiDepthAnythingV2PreprocessorNodeV2": ZhiYiDepthAnythingV2PreprocessorNodeV2,
"ZhiYiQwenDetectNode": ZhiYiQwenDetectNode,
"ZhiYiBBoxesToSAM2": ZhiYiBBoxesToSAM2,
"PatternChooseBackgroundPair+": PatternChooseBackgroundPair,
"PatternDualBackgroundToRGBA+": PatternDualBackgroundToRGBA,
"PatternApplyAlphaToImage+": PatternApplyAlphaToImage,
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"FD_RemoveWatermark": "Remove Watermark",
"FD_Upload": "FD Upload to OSS",
"FD_imgToText_Doubao": "FD Image to Text (Doubao)",
"FD_BinaryDecisionNormalizer": "FD Binary Decision Normalizer",
"FD_GeminiImage": "FD Gemini Image",
"FD_GTPImage": "FD GTP Image",
"FD_GPTImageComboNode": "FD GPT Image Combo",
"FD_GPTMultiImage": "FD GPT Multi Image",
"FD_Flux2KleinGenImage": "FD Flux2Klein Gen Image",
"FD_ZImageTurboGenImage": "FD Z-Image-Turbo Gen Image",
"FD_SeedreamImage": "FD Seedream Image",
"FD_SeedreamImageComboNode": "FD Seedream Image Combo",
"MaoziEcommercePromptGenerator": "猫子提示词节点-详情页生成器",
"MaoziPromptListSelector": "猫子提示词节点-列表选择器",
"ZhiYiImageTextNode": "知衣-图生文",
"ZhiYiImageTextComboNode": "知衣-图生文-combo",
"ZhiYiImageToImageNode": "知衣-图生图",
"ZhiYiTextGenNode": "知衣-文生文",
"NodeToggleByID": "节点开关 (按ID)",
"ZhiYiImageComboNode": "fd-输入参数组合",
"ZhiYiImageConcatNode": "知衣-组合图像(最长边)",
"ZhiYiFourGridCropNode": "知衣-四宫格识别拆分",
"ZhiYiImageToImageComboNode": "知衣-图生图-combo",
"ZhiYiAiStudioImageComboNode": "api 图生图节点测试",
"ZhiYiRemoveBgByMeituNode": "知衣-美图服装抠图",
"ZhiYiRMBGNode": "知衣-RMBG2.0背景去除",
"ZhiYiClothesSegmentNode": "知衣-衣物语义分割",
"ZhiYiFashionSegmentNode": "知衣-时尚单品分割",
"ZhiYiBodySegmentNode": "知衣-身体部位分割",
"ZhiYiSAM2SegmentNode": "知衣-SAM2抠图",
"ZhiYiDWPoseDetectNode": "知衣-DWPose姿态检测",
"ZhiYiLineArtPreprocessorNode": "知衣-LineArt线稿预处理",
"ZhiYiDepthAnythingV2PreprocessorNode": "知衣-DepthAnythingV2深度图预处理",
"ZhiYiRMBGNodeV2": "知衣-RMBG2.0背景去除 v2",
"ZhiYiClothesSegmentNodeV2": "知衣-衣物语义分割 v2",
"ZhiYiFashionSegmentNodeV2": "知衣-时尚单品分割 v2",
"ZhiYiBodySegmentNodeV2": "知衣-身体部位分割 v2",
"ZhiYiSAM2SegmentNodeV2": "知衣-SAM2抠图 v2",
"ZhiYiDWPoseDetectNodeV2": "知衣-DWPose姿态检测 v2",
"ZhiYiLineArtPreprocessorNodeV2": "知衣-LineArt线稿预处理 v2",
"ZhiYiDepthAnythingV2PreprocessorNodeV2": "知衣-DepthAnythingV2深度图预处理 v2",
"ZhiYiQwenDetectNode": "知衣-Qwen目标检测",
"ZhiYiBBoxesToSAM2": "知衣-BBox转SAM2格式",
"PatternChooseBackgroundPair+": "🔧 Pattern Choose Background Pair",
"PatternDualBackgroundToRGBA+": "🔧 Pattern Dual Background To RGBA",
"PatternApplyAlphaToImage+": "🔧 Pattern Apply Alpha To Image",
}