Files
fkxianzhou-ComfyUI-Jimeng-API/nodes/nodes_image.py
T

557 lines
18 KiB
Python

import random
import asyncio
import aiohttp
import torch
import comfy.model_management
import json
import time
from comfy_api.latest import io as comfy_io
from volcenginesdkarkruntime.types.images.images import SequentialImageGenerationOptions, ContentGenerationTool
from .nodes_shared import (
GLOBAL_CATEGORY,
_image_to_base64,
get_text,
log_msg,
format_api_error,
JimengClientType,
JimengException,
get_node_count_in_workflow,
create_white_image_tensor,
safe_cat_tensors,
)
from .utils_download import download_url_to_image_tensor_async
from .executor import JimengGenerationExecutor
from .models_config import (
SEEDREAM_4_MODEL_MAP,
SEEDREAM_5_MODEL_MAP,
SEEDREAM_3_MODELS,
)
from .constants import (
MAX_SEED,
MIN_SEED,
MAX_GENERATION_COUNT,
MIN_IMAGE_PIXELS_DEFAULT,
MAX_IMAGE_PIXELS_DEFAULT,
MIN_IMAGE_PIXELS_V4_5,
MAX_IMAGE_PIXELS_V4,
MIN_IMAGE_PIXELS_V5,
MAX_IMAGE_PIXELS_V5,
MIN_ASPECT_RATIO,
MAX_ASPECT_RATIO,
)
from .nodes_image_schema import (
RECOMMENDED_SIZES_V3,
RECOMMENDED_SIZES_V4,
RECOMMENDED_SIZES_V5,
get_image_generation_inputs,
)
def validate_custom_size(width, height, min_pixels, max_pixels):
"""
验证自定义宽高是否符合模型的像素限制和宽高比限制。
"""
total_pixels = width * height
if not (min_pixels <= total_pixels <= max_pixels):
raise JimengException(
get_text("err_pixels_range").format(
min=min_pixels,
max=max_pixels,
current=total_pixels,
)
)
aspect_ratio = width / height
if not (MIN_ASPECT_RATIO <= aspect_ratio <= MAX_ASPECT_RATIO):
raise JimengException(
get_text("err_aspect_ratio").format(
min="1/16", max="16", current=aspect_ratio
)
)
return f"{width}x{height}"
def _get_dynamic_input_order(name):
match = None
try:
import re
match = re.search(r"(\d+)$", str(name))
except Exception:
match = None
if match:
return int(match.group(1))
return 999
def _create_seedream_autogrow_input():
return comfy_io.Autogrow.Input(
"images",
template=comfy_io.Autogrow.TemplateNames(
input=comfy_io.Image.Input("image", optional=True),
names=[f"image_{idx}" for idx in range(1, 15)],
min=1,
),
)
def _prepare_multi_image_inputs(images=None, **kwargs):
input_image_tensors = []
if isinstance(images, dict):
sorted_items = sorted(images.items(), key=lambda item: _get_dynamic_input_order(item[0]))
for _, image_tensor in sorted_items:
if isinstance(image_tensor, torch.Tensor):
input_image_tensors.append(image_tensor)
elif isinstance(images, torch.Tensor):
input_image_tensors.append(images)
for key in sorted([k for k in kwargs.keys() if k.startswith("image_")], key=_get_dynamic_input_order):
image_tensor = kwargs[key]
if isinstance(image_tensor, torch.Tensor):
input_image_tensors.append(image_tensor)
n_input_images = 0
image_b64_list = []
for tensor in input_image_tensors:
batch_size = tensor.shape[0]
n_input_images += batch_size
for i in range(batch_size):
image_b64_list.append(_image_to_base64(tensor[i : i + 1]))
image_param = None
if n_input_images > 0:
if n_input_images == 1:
image_param = f"data:image/jpeg;base64,{image_b64_list[0]}"
else:
image_param = [
f"data:image/jpeg;base64,{image_b64}" for image_b64 in image_b64_list
]
return n_input_images, image_param
class JimengSeedream3(comfy_io.ComfyNode):
"""
Jimeng Seedream 3 图像生成节点。
支持文生图和图生图。
"""
RECOMMENDED_SIZES = RECOMMENDED_SIZES_V3
@classmethod
def define_schema(cls) -> comfy_io.Schema:
return comfy_io.Schema(
node_id="JimengSeedream3",
display_name="Jimeng Seedream 3",
category=GLOBAL_CATEGORY,
is_deprecated=True,
inputs=[
JimengClientType.Input("client"),
comfy_io.String.Input("prompt", multiline=True, default=""),
comfy_io.Float.Input(
"guidance_scale", default=5.0, min=1.0, max=10.0, step=0.1
),
]
+ get_image_generation_inputs(cls.RECOMMENDED_SIZES)
+ [
comfy_io.Image.Input("image", optional=True),
],
hidden=[comfy_io.Hidden.unique_id, comfy_io.Hidden.prompt],
outputs=[
comfy_io.Image.Output(display_name="image"),
comfy_io.String.Output(display_name="response"),
],
)
@classmethod
async def execute(
cls,
client,
prompt,
size,
width,
height,
seed,
generation_count,
watermark,
guidance_scale,
image=None,
) -> comfy_io.NodeOutput:
node_id = cls.hidden.unique_id
ark_client = client.ark
if size == "Custom":
size_param = validate_custom_size(
width,
height,
MIN_IMAGE_PIXELS_DEFAULT,
MAX_IMAGE_PIXELS_DEFAULT,
)
else:
size_param = size.split(" ")[0]
image_param = None
if image is None:
model_id = SEEDREAM_3_MODELS["t2i"]
else:
model_id = SEEDREAM_3_MODELS["i2i"]
image_param = f"data:image/jpeg;base64,{_image_to_base64(image)}"
size_param = "adaptive"
client.check_quota(model_id, generation_count)
if generation_count > 1:
log_msg("batch_submit_start", count=generation_count, model=model_id)
async def _generate_single(idx, session):
current_seed = random.randint(0, MAX_SEED) if seed == -1 else seed + idx
def _call_api():
kwargs = {
"model": model_id,
"prompt": prompt,
"size": size_param,
"response_format": "url",
"watermark": watermark,
"seed": current_seed,
"guidance_scale": guidance_scale,
}
if image_param:
kwargs["image"] = image_param
return ark_client.images.generate(**kwargs)
try:
comfy.model_management.throw_exception_if_processing_interrupted()
resp = await asyncio.to_thread(_call_api)
comfy.model_management.throw_exception_if_processing_interrupted()
image_tensor = await download_url_to_image_tensor_async(
session, resp.data[0].url
)
if image_tensor is None:
raise JimengException(get_text("err_download_img"))
output_response = {
"batch_index": idx,
"model": resp.model,
"created": resp.created,
"url": resp.data[0].url,
"revised_prompt": getattr(resp.data[0], "revised_prompt", None),
}
return image_tensor, output_response
except Exception as e:
if isinstance(e, comfy.model_management.InterruptProcessingException):
raise e
raise JimengException(format_api_error(e))
node_count = get_node_count_in_workflow("JimengSeedream3", prompt=cls.hidden.prompt)
# log_msg("debug_node_count", count=node_count, type="JimengSeedream3")
ignore_errors = node_count > 1
executor = JimengGenerationExecutor(client, node_id, ignore_errors=ignore_errors)
tensors, metadata = await executor.run_parallel_requests(generation_count, _generate_single)
if tensors:
try:
count = tensors[0].shape[0] if isinstance(tensors, list) else tensors.shape[0]
client.update_usage(model_id, count)
except:
pass
if not tensors:
return comfy_io.NodeOutput(create_white_image_tensor(), "[]")
output_tensor = safe_cat_tensors(tensors)
return comfy_io.NodeOutput(
output_tensor, json.dumps(metadata, indent=2)
)
class JimengSeedream4(comfy_io.ComfyNode):
"""
Jimeng Seedream 4 图像生成节点。
支持文生图、组图生成,并使用流式 API 接收结果。
"""
RECOMMENDED_SIZES = RECOMMENDED_SIZES_V4
@classmethod
def define_schema(cls) -> comfy_io.Schema:
return comfy_io.Schema(
node_id="JimengSeedream4",
display_name="Jimeng Seedream 4",
category=GLOBAL_CATEGORY,
inputs=[
JimengClientType.Input("client"),
comfy_io.Combo.Input(
"model_version", options=list(SEEDREAM_4_MODEL_MAP.keys())
),
comfy_io.String.Input("prompt", multiline=True, default=""),
]
+ get_image_generation_inputs(
cls.RECOMMENDED_SIZES,
default_width=2048,
default_height=2048,
enable_group_generation=True,
)
+ [
_create_seedream_autogrow_input(),
],
hidden=[comfy_io.Hidden.unique_id, comfy_io.Hidden.prompt],
outputs=[
comfy_io.Image.Output(display_name="images"),
comfy_io.String.Output(display_name="response"),
],
)
@classmethod
async def execute(
cls,
client,
model_version,
prompt,
enable_group_generation,
max_images,
size,
width,
height,
seed,
generation_count,
watermark,
images=None,
**kwargs,
) -> comfy_io.NodeOutput:
node_id = cls.hidden.unique_id
ark_client = client.ark
model_id = SEEDREAM_4_MODEL_MAP.get(model_version)
if not model_id:
model_id = list(SEEDREAM_4_MODEL_MAP.values())[0]
sequential_param = "auto" if enable_group_generation else "disabled"
n_input_images, image_param = _prepare_multi_image_inputs(images, **kwargs)
if sequential_param == "auto":
total_count = n_input_images + max_images
if total_count > 15:
raise JimengException(
get_text("err_img_limit_group_15").format(
n=n_input_images, max=max_images, total=total_count
)
)
if size == "Custom":
min_pixels = 1280 * 720
if "4.5" in model_version:
min_pixels = MIN_IMAGE_PIXELS_V4_5
size_str = validate_custom_size(
width,
height,
min_pixels,
MAX_IMAGE_PIXELS_V4,
)
else:
size_str = size.split(" ")[0]
seq_options = None
if sequential_param == "auto":
seq_options = SequentialImageGenerationOptions(max_images=max_images)
client.check_quota(model_id, generation_count * max_images if enable_group_generation else generation_count)
if generation_count > 1:
log_msg("batch_submit_start", count=generation_count, model=model_id)
node_count = get_node_count_in_workflow("JimengSeedream4", prompt=cls.hidden.prompt)
# log_msg("debug_node_count", count=node_count, type="JimengSeedream4")
ignore_errors = node_count > 1
executor = JimengGenerationExecutor(client, node_id, ignore_errors=ignore_errors)
async def _generate_single(idx, session):
current_seed = random.randint(0, MAX_SEED) if seed == -1 else seed + idx
kwargs = {
"model": model_id,
"prompt": prompt,
"size": size_str,
"response_format": "b64_json",
"watermark": watermark,
"seed": current_seed,
"sequential_image_generation": sequential_param,
}
if image_param:
kwargs["image"] = image_param
if seq_options:
kwargs["sequential_image_generation_options"] = seq_options
return await executor.stream_generation_helper(
session, ark_client, kwargs, idx, enable_group_generation, generation_count
)
tensors, metadata = await executor.run_parallel_requests(generation_count, _generate_single)
if tensors:
try:
total_imgs = sum([t.shape[0] for t in tensors]) if isinstance(tensors, list) else tensors.shape[0]
client.update_usage(model_id, total_imgs)
except:
pass
if not tensors:
return comfy_io.NodeOutput(create_white_image_tensor(), "[]")
output_tensor = safe_cat_tensors(tensors)
return comfy_io.NodeOutput(
output_tensor, json.dumps(metadata, indent=2)
)
class JimengSeedream5(comfy_io.ComfyNode):
"""
Jimeng Seedream 5 图像生成节点。
支持文生图、组图生成,并使用流式 API 接收结果。
"""
RECOMMENDED_SIZES = RECOMMENDED_SIZES_V5
@classmethod
def define_schema(cls) -> comfy_io.Schema:
return comfy_io.Schema(
node_id="JimengSeedream5",
display_name="Jimeng Seedream 5",
category=GLOBAL_CATEGORY,
inputs=[
JimengClientType.Input("client"),
comfy_io.Combo.Input(
"model_version", options=list(SEEDREAM_5_MODEL_MAP.keys())
),
comfy_io.String.Input("prompt", multiline=True, default=""),
]
+ get_image_generation_inputs(
cls.RECOMMENDED_SIZES,
default_width=2048,
default_height=2048,
enable_group_generation=True,
enable_web_search=True,
)
+ [
_create_seedream_autogrow_input(),
],
hidden=[comfy_io.Hidden.unique_id, comfy_io.Hidden.prompt],
outputs=[
comfy_io.Image.Output(display_name="images"),
comfy_io.String.Output(display_name="response"),
],
)
@classmethod
async def execute(
cls,
client,
model_version,
prompt,
enable_group_generation,
enable_web_search,
max_images,
size,
width,
height,
seed,
generation_count,
watermark,
images=None,
**kwargs,
) -> comfy_io.NodeOutput:
node_id = cls.hidden.unique_id
ark_client = client.ark
model_id = SEEDREAM_5_MODEL_MAP.get(model_version)
if not model_id:
model_id = list(SEEDREAM_5_MODEL_MAP.values())[0]
sequential_param = "auto" if enable_group_generation else "disabled"
n_input_images, image_param = _prepare_multi_image_inputs(images, **kwargs)
if sequential_param == "auto":
total_count = n_input_images + max_images
if total_count > 15:
raise JimengException(
get_text("err_img_limit_group_15").format(
n=n_input_images, max=max_images, total=total_count
)
)
if size == "Custom":
min_pixels = MIN_IMAGE_PIXELS_V5
size_str = validate_custom_size(
width,
height,
min_pixels,
MAX_IMAGE_PIXELS_V5,
)
else:
size_str = size.split(" ")[0]
seq_options = None
if sequential_param == "auto":
seq_options = SequentialImageGenerationOptions(max_images=max_images)
client.check_quota(model_id, generation_count * max_images if enable_group_generation else generation_count)
if generation_count > 1:
log_msg("batch_submit_start", count=generation_count, model=model_id)
node_count = get_node_count_in_workflow("JimengSeedream5", prompt=cls.hidden.prompt)
# log_msg("debug_node_count", count=node_count, type="JimengSeedream5")
ignore_errors = node_count > 1
executor = JimengGenerationExecutor(client, node_id, ignore_errors=ignore_errors)
async def _generate_single(idx, session):
current_seed = random.randint(0, MAX_SEED) if seed == -1 else seed + idx
kwargs = {
"model": model_id,
"prompt": prompt,
"size": size_str,
"response_format": "b64_json",
"watermark": watermark,
"seed": current_seed,
"sequential_image_generation": sequential_param,
}
if enable_web_search:
kwargs["tools"] = [ContentGenerationTool(type="web_search")]
if image_param:
kwargs["image"] = image_param
if seq_options:
kwargs["sequential_image_generation_options"] = seq_options
return await executor.stream_generation_helper(
session, ark_client, kwargs, idx, enable_group_generation, generation_count
)
tensors, metadata = await executor.run_parallel_requests(generation_count, _generate_single)
if tensors:
try:
total_imgs = sum([t.shape[0] for t in tensors]) if isinstance(tensors, list) else tensors.shape[0]
client.update_usage(model_id, total_imgs)
except:
pass
if not tensors:
return comfy_io.NodeOutput(create_white_image_tensor(), "[]")
output_tensor = safe_cat_tensors(tensors)
return comfy_io.NodeOutput(
output_tensor, json.dumps(metadata, indent=2)
)