927 lines
36 KiB
Python
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",
|
|
}
|