diff --git a/comfy_api_nodes/apis/bytedance.py b/comfy_api_nodes/apis/bytedance.py index 6b854631d..87d22034a 100644 --- a/comfy_api_nodes/apis/bytedance.py +++ b/comfy_api_nodes/apis/bytedance.py @@ -18,7 +18,8 @@ class Seedream4Options(BaseModel): class Seedream5OptimizePromptOptions(BaseModel): - thinking: Literal["auto", "enabled", "disabled"] = Field(...) + thinking: Literal["auto", "enabled", "disabled"] | None = Field(None) + mode: Literal["standard", "fast"] | None = Field(None) class Seedream4TaskCreationRequest(BaseModel): diff --git a/comfy_api_nodes/nodes_bytedance.py b/comfy_api_nodes/nodes_bytedance.py index 4d2376136..866507e66 100644 --- a/comfy_api_nodes/nodes_bytedance.py +++ b/comfy_api_nodes/nodes_bytedance.py @@ -753,6 +753,8 @@ def _seedream_model_inputs( max_width: int = 6240, max_height: int = 4992, supports_batch: bool = True, + supports_fast: bool = False, + include_common: bool = False, ): inputs = [ IO.Combo.Input( @@ -813,16 +815,282 @@ def _seedream_model_inputs( advanced=True, ) ) + if supports_fast: + inputs.append( + IO.Combo.Input( + "prompt_optimization", + options=["standard", "fast"], + default="standard", + tooltip="Prompt-optimization mode when reference images are provided: " + "'standard' gives higher quality, 'fast' shorter generation time.", + advanced=True, + ) + ) + if include_common: + inputs.extend( + [ + IO.Int.Input( + "seed", + default=42, + min=0, + max=2147483647, + step=1, + display_mode=IO.NumberDisplay.number, + control_after_generate=True, + tooltip="Seed to use for generation.", + ), + IO.Boolean.Input( + "watermark", + default=False, + tooltip='Whether to add an "AI generated" watermark to the image.', + advanced=True, + ), + IO.Boolean.Input( + "thinking", + default=True, + tooltip=( + "Enable the model's prompt-optimization reasoning ('thinking') for better adherence. " + "Can substantially increase generation time — notably on Seedream 5.0 Pro. " + "Can only be disabled for text-to-image (not when reference images are provided)." + ), + advanced=True, + ), + ] + ) return inputs -class ByteDanceSeedreamNodeV2(IO.ComfyNode): +class ByteDanceSeedreamNodeV3(IO.ComfyNode): + + @classmethod + def define_schema(cls): + return IO.Schema( + node_id="ByteDanceSeedreamNodeV3", + display_name="ByteDance Seedream 4.5 & 5.0", + category="partner/image/ByteDance", + description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", + inputs=[ + IO.String.Input( + "prompt", + multiline=True, + default="", + tooltip="Text prompt for creating or editing an image.", + ), + IO.DynamicCombo.Input( + "model", + options=[ + IO.DynamicCombo.Option( + "seedream 5.0 pro", + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_5_PRO, + max_width=3136, + max_height=2496, + supports_batch=False, + supports_fast=True, + include_common=True, + ), + ), + IO.DynamicCombo.Option( + "seedream 5.0 lite", + _seedream_model_inputs( + max_ref_images=14, + presets=RECOMMENDED_PRESETS_SEEDREAM_5_LITE, + include_common=True, + ), + ), + IO.DynamicCombo.Option( + "seedream-4-5-251128", + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_4_5, + include_common=True, + ), + ), + IO.DynamicCombo.Option( + "seedream-4-0-250828", + _seedream_model_inputs( + max_ref_images=10, + presets=RECOMMENDED_PRESETS_SEEDREAM_4_0, + include_common=True, + ), + ), + ], + ), + ], + outputs=[ + IO.Image.Output(), + ], + hidden=[ + IO.Hidden.auth_token_comfy_org, + IO.Hidden.api_key_comfy_org, + IO.Hidden.unique_id, + ], + is_api_node=True, + price_badge=IO.PriceBadge( + depends_on=IO.PriceBadgeDepends( + widgets=["model", "model.size_preset", "model.width", "model.height"], + input_groups=["model.images"], + ), + expr=""" + ( + $model := $string(widgets.model); + $sp := $string($lookup(widgets, "model.size_preset")); + $w := $lookup(widgets, "model.width"); + $h := $lookup(widgets, "model.height"); + $px := ($type($w) = "number" and $type($h) = "number") ? $w * $h : 0; + $refs := $lookup(inputGroups, "model.images"); + $extra := ($type($refs) = "number" and $refs > 1) ? ($refs - 1) * 0.003 : 0; + $isPro := $contains($model, "5.0 pro"); + $isCustom := $contains($sp, "custom"); + $sizeKnown := $isCustom ? $px > 0 : ($contains($sp, "1k") or $contains($sp, "2k")); + $proPrice := $isCustom + ? ($px < 2610000 ? 0.045 : 0.09) + : ($contains($sp, "1k") ? 0.045 : 0.09); + ($isPro and ($sizeKnown = false)) + ? { + "type": "range_usd", + "min_usd": 0.045 + $extra, + "max_usd": 0.09 + $extra, + "format": { "suffix": "/Image", "approximate": true } + } + : { + "type": "usd", + "usd": $isPro ? $proPrice + $extra + : $contains($model, "5.0 lite") ? 0.035 + : $contains($model, "4-5") ? 0.04 + : 0.03, + "format": { "suffix": $isPro ? "/Image" : " x images/Run", "approximate": true } + } + ) + """, + ), + ) + + @classmethod + async def execute( + cls, + prompt: str, + model: dict, + seed: int = 0, + watermark: bool = False, + thinking: bool = True, + ) -> IO.NodeOutput: + validate_string(prompt, strip_whitespace=True, min_length=1) + model_id = SEEDREAM_MODELS[model["model"]] + presets = SEEDREAM_PRESETS[model_id] + is_pro = "seedream-5-0-pro" in model_id + + size_preset = model.get("size_preset", presets[0][0]) + width = model.get("width", 2048) + height = model.get("height", 2048) + max_images = model.get("max_images", 1) + sequential_image_generation = "disabled" if max_images == 1 else "auto" + images_dict = model.get("images") or {} + fail_on_partial = model.get("fail_on_partial", False) + prompt_optimization = model.get("prompt_optimization", "standard") + seed = model.get("seed", seed) + watermark = model.get("watermark", watermark) + thinking = model.get("thinking", thinking) + + w = h = None + for label, tw, th in presets: + if label == size_preset: + w, h = tw, th + break + if w is None or h is None: + w, h = width, height + + out_num_pixels = w * h + mp_provided = out_num_pixels / 1_000_000.0 + if is_pro: + if out_num_pixels < 921_600: + raise ValueError( + f"Minimum image resolution for the selected model is 0.92MP, but {mp_provided:.2f}MP provided." + ) + if out_num_pixels > 4_194_304: + raise ValueError( + f"Maximum image resolution for the selected model is 4.19MP, but {mp_provided:.2f}MP provided." + ) + else: + if ("seedream-4-5" in model_id or "seedream-5-0" in model_id) and out_num_pixels < 3_686_400: + raise ValueError( + f"Minimum image resolution for the selected model is 3.68MP, but {mp_provided:.2f}MP provided." + ) + if "seedream-4-0" in model_id and out_num_pixels < 921_600: + raise ValueError( + f"Minimum image resolution that the selected model can generate is 0.92MP, " + f"but {mp_provided:.2f}MP provided." + ) + if out_num_pixels > 16_777_216: + raise ValueError( + f"Maximum image resolution for the selected model is 16.78MP, but {mp_provided:.2f}MP provided." + ) + + image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] + n_input_images = sum(get_number_of_images(t) for t in image_tensors) + max_num_of_images = 14 if model_id == "seedream-5-0-260128" else 10 + if n_input_images > max_num_of_images: + raise ValueError( + f"Maximum of {max_num_of_images} reference images are supported, but {n_input_images} received." + ) + if sequential_image_generation == "auto" and n_input_images + max_images > 15: + raise ValueError( + "The maximum number of generated images plus the number of reference images cannot exceed 15." + ) + if not thinking and n_input_images > 0: + raise ValueError( + "'thinking' can only be disabled for text-to-image; enable it when using reference images." + ) + + reference_images_urls: list[str] = [] + if image_tensors: + for tensor in image_tensors: + validate_image_aspect_ratio(tensor, (1, 3), (3, 1)) + reference_images_urls = await upload_images_to_comfyapi( + cls, + image_tensors, + max_images=n_input_images, + mime_type="image/png", + wait_label="Uploading reference images", + ) + + optimize_prompt_options = None + if n_input_images == 0: + optimize_prompt_options = Seedream5OptimizePromptOptions(thinking="enabled" if thinking else "disabled") + elif prompt_optimization == "fast": + optimize_prompt_options = Seedream5OptimizePromptOptions(mode="fast") + response = await sync_op( + cls, + ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"), + response_model=ImageTaskCreationResponse, + data=Seedream4TaskCreationRequest( + model=model_id, + prompt=prompt, + image=reference_images_urls, + size=f"{w}x{h}", + seed=seed, + sequential_image_generation=None if is_pro else sequential_image_generation, + sequential_image_generation_options=None if is_pro else Seedream4Options(max_images=max_images), + watermark=watermark, + optimize_prompt_options=optimize_prompt_options, + ), + ) + if len(response.data) == 1: + return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response))) + urls = [str(d["url"]) for d in response.data if isinstance(d, dict) and "url" in d] + if fail_on_partial and len(urls) < len(response.data): + raise RuntimeError(f"Only {len(urls)} of {len(response.data)} images were generated before error.") + return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) + + +class ByteDanceSeedreamNodeV2(ByteDanceSeedreamNodeV3): @classmethod def define_schema(cls): return IO.Schema( node_id="ByteDanceSeedreamNodeV2", - display_name="ByteDance Seedream 4.5 & 5.0", + display_name="ByteDance Seedream 4.5 & 5.0 (Legacy)", category="partner/image/ByteDance", description="Unified text-to-image generation and precise single-sentence editing at up to 4K resolution.", inputs=[ @@ -896,6 +1164,7 @@ class ByteDanceSeedreamNodeV2(IO.ComfyNode): IO.Hidden.unique_id, ], is_api_node=True, + is_deprecated=True, price_badge=IO.PriceBadge( depends_on=IO.PriceBadgeDepends( widgets=["model", "model.size_preset", "model.width", "model.height"] @@ -924,117 +1193,6 @@ class ByteDanceSeedreamNodeV2(IO.ComfyNode): ), ) - @classmethod - async def execute( - cls, - prompt: str, - model: dict, - seed: int = 0, - watermark: bool = False, - thinking: bool = True, - ) -> IO.NodeOutput: - validate_string(prompt, strip_whitespace=True, min_length=1) - model_id = SEEDREAM_MODELS[model["model"]] - presets = SEEDREAM_PRESETS[model_id] - is_pro = "seedream-5-0-pro" in model_id - - size_preset = model.get("size_preset", presets[0][0]) - width = model.get("width", 2048) - height = model.get("height", 2048) - max_images = model.get("max_images", 1) - sequential_image_generation = "disabled" if max_images == 1 else "auto" - images_dict = model.get("images") or {} - fail_on_partial = model.get("fail_on_partial", False) - - w = h = None - for label, tw, th in presets: - if label == size_preset: - w, h = tw, th - break - if w is None or h is None: - w, h = width, height - - out_num_pixels = w * h - mp_provided = out_num_pixels / 1_000_000.0 - if is_pro: - if out_num_pixels < 921_600: - raise ValueError( - f"Minimum image resolution for the selected model is 0.92MP, but {mp_provided:.2f}MP provided." - ) - if out_num_pixels > 4_194_304: - raise ValueError( - f"Maximum image resolution for the selected model is 4.19MP, but {mp_provided:.2f}MP provided." - ) - else: - if ("seedream-4-5" in model_id or "seedream-5-0" in model_id) and out_num_pixels < 3_686_400: - raise ValueError( - f"Minimum image resolution for the selected model is 3.68MP, but {mp_provided:.2f}MP provided." - ) - if "seedream-4-0" in model_id and out_num_pixels < 921_600: - raise ValueError( - f"Minimum image resolution that the selected model can generate is 0.92MP, " - f"but {mp_provided:.2f}MP provided." - ) - if out_num_pixels > 16_777_216: - raise ValueError( - f"Maximum image resolution for the selected model is 16.78MP, but {mp_provided:.2f}MP provided." - ) - - image_tensors: list[Input.Image] = [t for t in images_dict.values() if t is not None] - n_input_images = sum(get_number_of_images(t) for t in image_tensors) - max_num_of_images = 14 if model_id == "seedream-5-0-260128" else 10 - if n_input_images > max_num_of_images: - raise ValueError( - f"Maximum of {max_num_of_images} reference images are supported, but {n_input_images} received." - ) - if sequential_image_generation == "auto" and n_input_images + max_images > 15: - raise ValueError( - "The maximum number of generated images plus the number of reference images cannot exceed 15." - ) - if not thinking and n_input_images > 0: - raise ValueError( - "'thinking' can only be disabled for text-to-image; enable it when using reference images." - ) - - reference_images_urls: list[str] = [] - if image_tensors: - for tensor in image_tensors: - validate_image_aspect_ratio(tensor, (1, 3), (3, 1)) - reference_images_urls = await upload_images_to_comfyapi( - cls, - image_tensors, - max_images=n_input_images, - mime_type="image/png", - wait_label="Uploading reference images", - ) - - optimize_prompt_options = None - if n_input_images == 0: - optimize_prompt_options = Seedream5OptimizePromptOptions(thinking="enabled" if thinking else "disabled") - response = await sync_op( - cls, - ApiEndpoint(path=BYTEPLUS_IMAGE_ENDPOINT, method="POST"), - response_model=ImageTaskCreationResponse, - data=Seedream4TaskCreationRequest( - model=model_id, - prompt=prompt, - image=reference_images_urls, - size=f"{w}x{h}", - seed=seed, - sequential_image_generation=None if is_pro else sequential_image_generation, - sequential_image_generation_options=None if is_pro else Seedream4Options(max_images=max_images), - watermark=watermark, - optimize_prompt_options=optimize_prompt_options, - ), - ) - if len(response.data) == 1: - return IO.NodeOutput(await download_url_to_image_tensor(get_image_url_from_response(response))) - urls = [str(d["url"]) for d in response.data if isinstance(d, dict) and "url" in d] - if fail_on_partial and len(urls) < len(response.data): - raise RuntimeError(f"Only {len(urls)} of {len(response.data)} images were generated before error.") - return IO.NodeOutput(torch.cat([await download_url_to_image_tensor(i) for i in urls])) - - class ByteDanceSeedreamLayerSeparationNode(IO.ComfyNode): @classmethod @@ -3512,6 +3670,7 @@ class ByteDanceExtension(ComfyExtension): ByteDanceImageNode, ByteDanceSeedreamNode, ByteDanceSeedreamNodeV2, + ByteDanceSeedreamNodeV3, ByteDanceSeedreamLayerSeparationNode, ByteDanceTextToVideoNode, ByteDanceImageToVideoNode,